Skip to content

Commit

Permalink
Merge 3d35a2c into 2994a96
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpschaaf committed Sep 22, 2022
2 parents 2994a96 + 3d35a2c commit a245525
Show file tree
Hide file tree
Showing 27 changed files with 745 additions and 735 deletions.
8 changes: 8 additions & 0 deletions .changeset/famous-dolls-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@lit-labs/analyzer': minor
'@lit-labs/gen-wrapper-angular': minor
'@lit-labs/gen-wrapper-react': minor
'@lit-labs/gen-wrapper-vue': minor
---

Refactored Analyzer into better fit for use in plugins. Analyzer class now takes a ts.Program, and PackageAnalyzer takes a package path and creates a program to analyze a package on the filesystem.
2 changes: 1 addition & 1 deletion packages/labs/analyzer/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"request": "launch",
"name": "Test",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/node_modules/.bin/uvu",
"program": "${workspaceFolder}/../../../node_modules/.bin/uvu",
"args": ["test", "\\_test\\.js$"],
"outFiles": ["${workspaceFolder}/**/*.js"],
"console": "integratedTerminal"
Expand Down
1 change: 1 addition & 0 deletions packages/labs/analyzer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

export {Analyzer} from './lib/analyzer.js';
export {createPackageAnalyzer} from './lib/analyze-package.js';

export type {
Package,
Expand Down
45 changes: 45 additions & 0 deletions packages/labs/analyzer/src/lib/analyze-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ts from 'typescript';
import {AbsolutePath} from './paths.js';
import * as path from 'path';
import {DiagnosticsError} from './errors.js';
import {Analyzer} from './analyzer.js';

/**
* Returns an analyzer for a Lit npm package based on a filesystem path.
*/
export const createPackageAnalyzer = (packageRoot: AbsolutePath) => {
const configFileName = ts.findConfigFile(
packageRoot,
ts.sys.fileExists,
'tsconfig.json'
);
if (configFileName === undefined) {
// TODO: use a hard-coded tsconfig for JS projects.
throw new Error(`tsconfig.json not found in ${packageRoot}`);
}
const configFile = ts.readConfigFile(configFileName, ts.sys.readFile);
// Note `configFileName` is optional but must be set for
// `getOutputFileNames` to work correctly; however, it must be relative to
// `packageRoot`
const commandLine = ts.parseJsonConfigFileContent(
configFile.config /* json */,
ts.sys /* host */,
packageRoot /* basePath */,
undefined /* existingOptions */,
path.relative(packageRoot, configFileName) /* configFileName */
);

const program = ts.createProgram(commandLine.fileNames, commandLine.options);

const analyzer = new Analyzer({getProgram: () => program, fs: ts.sys, path});

const diagnostics = program.getSemanticDiagnostics();
if (diagnostics.length > 0) {
throw new DiagnosticsError(
diagnostics,
`Error analyzing package '${packageRoot}': Please fix errors first`
);
}

return analyzer;
};
130 changes: 64 additions & 66 deletions packages/labs/analyzer/src/lib/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,91 +5,89 @@
*/

import ts from 'typescript';
import {Package, PackageJson} from './model.js';
import {ProgramContext} from './program-context.js';
import {Package, PackageJson, AnalyzerInterface} from './model.js';
import {AbsolutePath} from './paths.js';
import * as fs from 'fs';
import * as path from 'path';
import {getModule} from './javascript/modules.js';
export {PackageJson};
import {getPackageInfo} from './javascript/packages.js';

export interface AnalyzerInit {
getProgram: () => ts.Program;
fs: AnalyzerInterface['fs'];
path: AnalyzerInterface['path'];
}

/**
* An analyzer for Lit npm packages
* An analyzer for Lit typescript modules.
*/
export class Analyzer {
readonly packageRoot: AbsolutePath;
readonly programContext: ProgramContext;
export class Analyzer implements AnalyzerInterface {
private readonly _getProgram: () => ts.Program;
readonly fs: AnalyzerInterface['fs'];
readonly path: AnalyzerInterface['path'];
private _commandLine: ts.ParsedCommandLine | undefined = undefined;

/**
* @param packageRoot The root directory of the package to analyze. Currently
* this directory must have a tsconfig.json and package.json.
*/
constructor(packageRoot: AbsolutePath) {
this.packageRoot = packageRoot;
constructor(init: AnalyzerInit) {
this._getProgram = init.getProgram;
this.fs = init.fs;
this.path = init.path;
}

// TODO(kschaaf): Consider moving the package.json and tsconfig.json
// to analyzePackage() or move it to an async factory function that
// passes these to the constructor as arguments.
const packageJsonFilename = path.join(packageRoot, 'package.json');
let packageJsonText;
try {
packageJsonText = fs.readFileSync(packageJsonFilename, 'utf8');
} catch (e) {
throw new Error(`package.json not found at ${packageJsonFilename}`);
}
let packageJson;
try {
packageJson = JSON.parse(packageJsonText);
} catch (e) {
throw new Error(`Malformed package.json found at ${packageJsonFilename}`);
}
if (packageJson.name === undefined) {
throw new Error(
`package.json in ${packageJsonFilename} did not have a name.`
);
}
get program() {
return this._getProgram();
}

const configFileName = ts.findConfigFile(
packageRoot,
ts.sys.fileExists,
'tsconfig.json'
);
if (configFileName === undefined) {
// TODO: use a hard-coded tsconfig for JS projects.
throw new Error(`tsconfig.json not found in ${packageRoot}`);
}
const configFile = ts.readConfigFile(configFileName, ts.sys.readFile);
// Note `configFileName` is optional but must be set for
// `getOutputFileNames` to work correctly; however, it must be relative to
// `packageRoot`
const commandLine = ts.parseJsonConfigFileContent(
configFile.config /* json */,
ts.sys /* host */,
packageRoot /* basePath */,
undefined /* existingOptions */,
path.relative(packageRoot, configFileName) /* configFileName */
);
get commandLine() {
return (this._commandLine ??= getCommandLineFromProgram(this));
}

this.programContext = new ProgramContext(
packageRoot,
commandLine,
packageJson
getModule(modulePath: AbsolutePath) {
return getModule(
this.program.getSourceFile(this.path.normalize(modulePath))!,
this
);
}

analyzePackage() {
const rootFileNames = this.programContext.program.getRootFileNames();
getPackage() {
const rootFileNames = this.program.getRootFileNames();

// Find the package.json for this package based on the first root filename
// in the program (we assume all root files in a program belong to the same
// package)
const packageInfo = getPackageInfo(rootFileNames[0] as AbsolutePath, this);

return new Package({
rootDir: this.packageRoot,
...packageInfo,
modules: rootFileNames.map((fileName) =>
getModule(
this.programContext.program.getSourceFile(path.normalize(fileName))!,
this.programContext
this.program.getSourceFile(this.path.normalize(fileName))!,
this,
packageInfo
)
),
tsConfig: this.programContext.commandLine,
packageJson: this.programContext.packageJson,
});
}
}

/**
* Extracts a `ts.ParsedCommandLine` (essentially, the key bits of a
* `tsconfig.json`) from the analyzer's `ts.Program`.
*
* The `ts.getOutputFileNames()` function must be passed a
* `ts.ParsedCommandLine`; since not all usages of the analyzer create the
* program directly from a tsconfig (plugins get passed the program only),
* this allows backing the `ParsedCommandLine` out of an existing program.
*/
export const getCommandLineFromProgram = (analyzer: Analyzer) => {
const compilerOptions = analyzer.program.getCompilerOptions();
const commandLine = ts.parseJsonConfigFileContent(
{
files: analyzer.program.getRootFileNames(),
compilerOptions,
},
ts.sys,
analyzer.path.basename(compilerOptions.configFilePath as string),
undefined,
compilerOptions.configFilePath as string
);
return commandLine;
};
9 changes: 6 additions & 3 deletions packages/labs/analyzer/src/lib/javascript/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
*/

import ts from 'typescript';
import {ClassDeclaration} from '../model.js';
import {ProgramContext} from '../program-context.js';
import {ClassDeclaration, AnalyzerInterface} from '../model.js';

/**
* Returns an analyzer `ClassDeclaration` model for the given
* ts.ClassDeclaration.
*/
export const getClassDeclaration = (
declaration: ts.ClassDeclaration,
_programContext: ProgramContext
_analyzer: AnalyzerInterface
): ClassDeclaration => {
return new ClassDeclaration({
// TODO(kschaaf): support anonymous class expressions when assigned to a const
Expand Down
43 changes: 27 additions & 16 deletions packages/labs/analyzer/src/lib/javascript/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,41 @@
*/

import ts from 'typescript';
import {Module} from '../model.js';
import {Module, AnalyzerInterface, PackageInfo} from '../model.js';
import {
isLitElement,
getLitElementDeclaration,
} from '../lit-element/lit-element.js';
import * as path from 'path';
import {getClassDeclaration} from './classes.js';
import {getVariableDeclarations} from './variables.js';
import {ProgramContext} from '../program-context.js';
import {AbsolutePath, absoluteToPackage} from '../paths.js';
import {getPackageInfo} from './packages.js';

/**
* Returns an analyzer `Module` model for the given ts.SourceFile.
*/
export const getModule = (
sourceFile: ts.SourceFile,
programContext: ProgramContext
analyzer: AnalyzerInterface,
packageInfo: PackageInfo = getPackageInfo(
sourceFile.fileName as AbsolutePath,
analyzer
)
) => {
// Find and load the package.json associated with this module; this both gives
// us the packageRoot for this module (needed for translating the source file
// path to a package relative path), as well as the packageName (needed for
// generating references to any symbols in this module). This will need
// caching/invalidation.
const {rootDir, packageJson} = packageInfo;
const sourcePath = absoluteToPackage(
path.normalize(sourceFile.fileName) as AbsolutePath,
programContext.packageRoot
analyzer.path.normalize(sourceFile.fileName) as AbsolutePath,
rootDir
);
const fullSourcePath = path.join(programContext.packageRoot, sourcePath);
const fullSourcePath = path.join(rootDir, sourcePath);
const jsPath = ts
.getOutputFileNames(programContext.commandLine, fullSourcePath, false)
.getOutputFileNames(analyzer.commandLine, fullSourcePath, false)
.filter((f) => f.endsWith('.js'))[0];
// TODO(kschaaf): this could happen if someone imported only a .d.ts file;
// we might need to handle this differently
Expand All @@ -40,29 +53,27 @@ export const getModule = (
// separators; since sourcePath uses OS separators, normalize
// this so that all our model paths are OS-native
jsPath: absoluteToPackage(
path.normalize(jsPath) as AbsolutePath,
programContext.packageRoot as AbsolutePath
analyzer.path.normalize(jsPath) as AbsolutePath,
rootDir
),
sourceFile,
packageJson,
});

programContext.currentModule = module;

for (const statement of sourceFile.statements) {
if (ts.isClassDeclaration(statement)) {
module.declarations.push(
isLitElement(statement, programContext)
? getLitElementDeclaration(statement, programContext)
: getClassDeclaration(statement, programContext)
isLitElement(statement, analyzer)
? getLitElementDeclaration(statement, analyzer)
: getClassDeclaration(statement, analyzer)
);
} else if (ts.isVariableStatement(statement)) {
module.declarations.push(
...statement.declarationList.declarations
.map((dec) => getVariableDeclarations(dec, dec.name, programContext))
.map((dec) => getVariableDeclarations(dec, dec.name, analyzer))
.flat()
);
}
}
programContext.currentModule = undefined;
return module;
};

0 comments on commit a245525

Please sign in to comment.