@lit-labs/analyzer
Advanced tools
Comparing version 0.2.2 to 0.3.0
@@ -7,2 +7,6 @@ /** | ||
export { Analyzer } from './lib/analyzer.js'; | ||
export { createPackageAnalyzer } from './lib/analyze-package.js'; | ||
// Any non-type exports below must be safe to use on objects between multiple | ||
// versions of the analyzer library | ||
export { getImportsStringForReferences } from './lib/model.js'; | ||
//# sourceMappingURL=index.js.map |
@@ -6,19 +6,36 @@ /** | ||
*/ | ||
import { Package, PackageJson } from './model.js'; | ||
import { ProgramContext } from './program-context.js'; | ||
import ts from 'typescript'; | ||
import { Package, PackageJson, AnalyzerInterface } from './model.js'; | ||
import { AbsolutePath } from './paths.js'; | ||
export { PackageJson }; | ||
export interface AnalyzerInit { | ||
getProgram: () => ts.Program; | ||
fs: AnalyzerInterface['fs']; | ||
path: AnalyzerInterface['path']; | ||
basePath?: AbsolutePath; | ||
} | ||
/** | ||
* An analyzer for Lit npm packages | ||
* An analyzer for Lit typescript modules. | ||
*/ | ||
export declare class Analyzer { | ||
readonly packageRoot: AbsolutePath; | ||
readonly programContext: ProgramContext; | ||
/** | ||
* @param packageRoot The root directory of the package to analyze. Currently | ||
* this directory must have a tsconfig.json and package.json. | ||
*/ | ||
constructor(packageRoot: AbsolutePath); | ||
analyzePackage(): Package; | ||
export declare class Analyzer implements AnalyzerInterface { | ||
private readonly _getProgram; | ||
readonly fs: AnalyzerInterface['fs']; | ||
readonly path: AnalyzerInterface['path']; | ||
private _commandLine; | ||
constructor(init: AnalyzerInit); | ||
get program(): ts.Program; | ||
get commandLine(): ts.ParsedCommandLine; | ||
getModule(modulePath: AbsolutePath): import("./model.js").Module; | ||
getPackage(): Package; | ||
} | ||
/** | ||
* 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 declare const getCommandLineFromProgram: (analyzer: Analyzer) => ts.ParsedCommandLine; | ||
//# sourceMappingURL=analyzer.d.ts.map |
@@ -8,59 +8,71 @@ /** | ||
import { Package } from './model.js'; | ||
import { ProgramContext } from './program-context.js'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import { getModule } from './javascript/modules.js'; | ||
import { getPackageInfo, getPackageRootForModulePath, } from './javascript/packages.js'; | ||
/** | ||
* An analyzer for Lit npm packages | ||
* An analyzer for Lit typescript modules. | ||
*/ | ||
export class Analyzer { | ||
/** | ||
* @param packageRoot The root directory of the package to analyze. Currently | ||
* this directory must have a tsconfig.json and package.json. | ||
*/ | ||
constructor(packageRoot) { | ||
this.packageRoot = packageRoot; | ||
// 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'); | ||
constructor(init) { | ||
this._commandLine = undefined; | ||
this._getProgram = init.getProgram; | ||
this.fs = init.fs; | ||
this.path = init.path; | ||
} | ||
get program() { | ||
return this._getProgram(); | ||
} | ||
get commandLine() { | ||
return (this._commandLine ?? (this._commandLine = getCommandLineFromProgram(this))); | ||
} | ||
getModule(modulePath) { | ||
return getModule(this.program.getSourceFile(this.path.normalize(modulePath)), this); | ||
} | ||
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) | ||
if (rootFileNames.length === 0) { | ||
throw new Error('No source files found in package.'); | ||
} | ||
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.`); | ||
} | ||
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 */); | ||
this.programContext = new ProgramContext(packageRoot, commandLine, packageJson); | ||
} | ||
analyzePackage() { | ||
const rootFileNames = this.programContext.program.getRootFileNames(); | ||
const packageInfo = getPackageInfo(rootFileNames[0], this); | ||
return new Package({ | ||
rootDir: this.packageRoot, | ||
modules: rootFileNames.map((fileName) => getModule(this.programContext.program.getSourceFile(path.normalize(fileName)), this.programContext)), | ||
tsConfig: this.programContext.commandLine, | ||
packageJson: this.programContext.packageJson, | ||
...packageInfo, | ||
modules: rootFileNames.map((fileName) => getModule(this.program.getSourceFile(this.path.normalize(fileName)), this, packageInfo)), | ||
}); | ||
} | ||
} | ||
/** | ||
* 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) => { | ||
const compilerOptions = analyzer.program.getCompilerOptions(); | ||
const files = analyzer.program.getRootFileNames(); | ||
const json = { | ||
files, | ||
compilerOptions, | ||
}; | ||
if (compilerOptions.configFilePath !== undefined) { | ||
// For a TS project, derive the package root from the config file path | ||
const packageRoot = analyzer.path.basename(compilerOptions.configFilePath); | ||
return ts.parseJsonConfigFileContent(json, ts.sys, packageRoot, undefined, compilerOptions.configFilePath); | ||
} | ||
else { | ||
// Otherwise, this is a JS project; we can determine the package root | ||
// based on the package.json location; we can look that up based on | ||
// the first root file | ||
const packageRoot = getPackageRootForModulePath(files[0], analyzer | ||
// Note we don't pass a configFilePath since we don't have one; This just | ||
// means we can't use ts.getOutputFileNames(), which we isn't needed in | ||
// JS program | ||
); | ||
return ts.parseJsonConfigFileContent(json, ts.sys, packageRoot); | ||
} | ||
}; | ||
//# sourceMappingURL=analyzer.js.map |
@@ -12,5 +12,8 @@ /** | ||
import ts from 'typescript'; | ||
import { ClassDeclaration } from '../model.js'; | ||
import { ProgramContext } from '../program-context.js'; | ||
export declare const getClassDeclaration: (declaration: ts.ClassDeclaration, _programContext: ProgramContext) => ClassDeclaration; | ||
import { ClassDeclaration, AnalyzerInterface } from '../model.js'; | ||
/** | ||
* Returns an analyzer `ClassDeclaration` model for the given | ||
* ts.ClassDeclaration. | ||
*/ | ||
export declare const getClassDeclaration: (declaration: ts.ClassDeclaration, _analyzer: AnalyzerInterface) => ClassDeclaration; | ||
//# sourceMappingURL=classes.d.ts.map |
@@ -7,5 +7,10 @@ /** | ||
import { ClassDeclaration } from '../model.js'; | ||
export const getClassDeclaration = (declaration, _programContext) => { | ||
/** | ||
* Returns an analyzer `ClassDeclaration` model for the given | ||
* ts.ClassDeclaration. | ||
*/ | ||
export const getClassDeclaration = (declaration, _analyzer) => { | ||
return new ClassDeclaration({ | ||
name: declaration.name?.text, | ||
// TODO(kschaaf): support anonymous class expressions when assigned to a const | ||
name: declaration.name?.text ?? '', | ||
node: declaration, | ||
@@ -12,0 +17,0 @@ }); |
@@ -7,5 +7,7 @@ /** | ||
import ts from 'typescript'; | ||
import { Module } from '../model.js'; | ||
import { ProgramContext } from '../program-context.js'; | ||
export declare const getModule: (sourceFile: ts.SourceFile, programContext: ProgramContext) => Module; | ||
import { Module, AnalyzerInterface, PackageInfo } from '../model.js'; | ||
/** | ||
* Returns an analyzer `Module` model for the given ts.SourceFile. | ||
*/ | ||
export declare const getModule: (sourceFile: ts.SourceFile, analyzer: AnalyzerInterface, packageInfo?: PackageInfo) => Module; | ||
//# sourceMappingURL=modules.d.ts.map |
@@ -13,8 +13,20 @@ /** | ||
import { absoluteToPackage } from '../paths.js'; | ||
export const getModule = (sourceFile, programContext) => { | ||
const sourcePath = absoluteToPackage(path.normalize(sourceFile.fileName), programContext.packageRoot); | ||
const fullSourcePath = path.join(programContext.packageRoot, sourcePath); | ||
const jsPath = ts | ||
.getOutputFileNames(programContext.commandLine, fullSourcePath, false) | ||
.filter((f) => f.endsWith('.js'))[0]; | ||
import { getPackageInfo } from './packages.js'; | ||
/** | ||
* Returns an analyzer `Module` model for the given ts.SourceFile. | ||
*/ | ||
export const getModule = (sourceFile, analyzer, packageInfo = getPackageInfo(sourceFile.fileName, 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(analyzer.path.normalize(sourceFile.fileName), rootDir); | ||
const fullSourcePath = path.join(rootDir, sourcePath); | ||
const jsPath = fullSourcePath.endsWith('.js') | ||
? fullSourcePath | ||
: ts | ||
.getOutputFileNames(analyzer.commandLine, fullSourcePath, false) | ||
.filter((f) => f.endsWith('.js'))[0]; | ||
// TODO(kschaaf): this could happen if someone imported only a .d.ts file; | ||
@@ -30,21 +42,20 @@ // we might need to handle this differently | ||
// this so that all our model paths are OS-native | ||
jsPath: absoluteToPackage(path.normalize(jsPath), programContext.packageRoot), | ||
jsPath: absoluteToPackage(analyzer.path.normalize(jsPath), 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)); | ||
module.declarations.push(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; | ||
}; | ||
//# sourceMappingURL=modules.js.map |
@@ -12,7 +12,10 @@ /** | ||
import ts from 'typescript'; | ||
import { VariableDeclaration } from '../model.js'; | ||
import { ProgramContext } from '../program-context.js'; | ||
import { VariableDeclaration, AnalyzerInterface } from '../model.js'; | ||
declare type VariableName = ts.Identifier | ts.ObjectBindingPattern | ts.ArrayBindingPattern; | ||
export declare const getVariableDeclarations: (dec: ts.VariableDeclaration, name: VariableName, programContext: ProgramContext) => VariableDeclaration[]; | ||
/** | ||
* Returns an array of analyzer `VariableDeclaration` models for the given | ||
* ts.VariableDeclaration. | ||
*/ | ||
export declare const getVariableDeclarations: (dec: ts.VariableDeclaration, name: VariableName, analyzer: AnalyzerInterface) => VariableDeclaration[]; | ||
export {}; | ||
//# sourceMappingURL=variables.d.ts.map |
@@ -14,3 +14,8 @@ /** | ||
import { DiagnosticsError } from '../errors.js'; | ||
export const getVariableDeclarations = (dec, name, programContext) => { | ||
import { getTypeForNode } from '../types.js'; | ||
/** | ||
* Returns an array of analyzer `VariableDeclaration` models for the given | ||
* ts.VariableDeclaration. | ||
*/ | ||
export const getVariableDeclarations = (dec, name, analyzer) => { | ||
if (ts.isIdentifier(name)) { | ||
@@ -21,3 +26,3 @@ return [ | ||
node: dec, | ||
type: programContext.getTypeForNode(name), | ||
type: getTypeForNode(name, analyzer), | ||
}), | ||
@@ -33,3 +38,3 @@ ]; | ||
return els | ||
.map((el) => getVariableDeclarations(dec, el.name, programContext)) | ||
.map((el) => getVariableDeclarations(dec, el.name, analyzer)) | ||
.flat(); | ||
@@ -36,0 +41,0 @@ } |
@@ -7,5 +7,9 @@ /** | ||
import { Event } from '../model.js'; | ||
import { ProgramContext } from '../program-context.js'; | ||
import { AnalyzerInterface } from '../model.js'; | ||
import { LitClassDeclaration } from './lit-element.js'; | ||
export declare const getEvents: (node: LitClassDeclaration, programContext: ProgramContext) => Map<string, Event>; | ||
/** | ||
* Returns an array of analyzer `Event` models for the given | ||
* ts.ClassDeclaration. | ||
*/ | ||
export declare const getEvents: (node: LitClassDeclaration, analyzer: AnalyzerInterface) => Map<string, Event>; | ||
//# sourceMappingURL=events.d.ts.map |
@@ -13,3 +13,8 @@ /** | ||
import { DiagnosticsError } from '../errors.js'; | ||
export const getEvents = (node, programContext) => { | ||
import { getTypeForJSDocTag } from '../types.js'; | ||
/** | ||
* Returns an array of analyzer `Event` models for the given | ||
* ts.ClassDeclaration. | ||
*/ | ||
export const getEvents = (node, analyzer) => { | ||
const events = new Map(); | ||
@@ -33,3 +38,3 @@ const jsDocTags = ts.getJSDocTags(node); | ||
name, | ||
type: type ? programContext.getTypeForJSDocTag(tag) : undefined, | ||
type: type ? getTypeForJSDocTag(tag, analyzer) : undefined, | ||
description, | ||
@@ -36,0 +41,0 @@ }); |
@@ -12,4 +12,3 @@ /** | ||
import ts from 'typescript'; | ||
import { LitElementDeclaration } from '../model.js'; | ||
import { ProgramContext } from '../program-context.js'; | ||
import { LitElementDeclaration, AnalyzerInterface } from '../model.js'; | ||
/** | ||
@@ -19,3 +18,3 @@ * Gets an analyzer LitElementDeclaration object from a ts.ClassDeclaration | ||
*/ | ||
export declare const getLitElementDeclaration: (node: LitClassDeclaration, programContext: ProgramContext) => LitElementDeclaration; | ||
export declare const getLitElementDeclaration: (node: LitClassDeclaration, analyzer: AnalyzerInterface) => LitElementDeclaration; | ||
/** | ||
@@ -34,5 +33,5 @@ * This type identifies a ClassDeclaration as one that inherits from LitElement. | ||
*/ | ||
export declare const isLitElement: (node: ts.Node, programContext: ProgramContext) => node is LitClassDeclaration; | ||
export declare const isLitElement: (node: ts.Node, analyzer: AnalyzerInterface) => node is LitClassDeclaration; | ||
/** | ||
* Returns the tagname associated with a | ||
* Returns the tagname associated with a LitClassDeclaration | ||
* @param declaration | ||
@@ -39,0 +38,0 @@ * @returns |
@@ -20,9 +20,10 @@ /** | ||
*/ | ||
export const getLitElementDeclaration = (node, programContext) => { | ||
export const getLitElementDeclaration = (node, analyzer) => { | ||
return new LitElementDeclaration({ | ||
tagname: getTagName(node), | ||
name: node.name?.text, | ||
// TODO(kschaaf): support anonymous class expressions when assigned to a const | ||
name: node.name?.text ?? '', | ||
node, | ||
reactiveProperties: getProperties(node, programContext), | ||
events: getEvents(node, programContext), | ||
reactiveProperties: getProperties(node, analyzer), | ||
events: getEvents(node, analyzer), | ||
}); | ||
@@ -52,8 +53,9 @@ }; | ||
*/ | ||
export const isLitElement = (node, programContext) => { | ||
export const isLitElement = (node, analyzer) => { | ||
if (!ts.isClassLike(node)) { | ||
return false; | ||
} | ||
const type = programContext.checker.getTypeAtLocation(node); | ||
const baseTypes = programContext.checker.getBaseTypes(type); | ||
const checker = analyzer.program.getTypeChecker(); | ||
const type = checker.getTypeAtLocation(node); | ||
const baseTypes = checker.getBaseTypes(type); | ||
for (const t of baseTypes) { | ||
@@ -67,3 +69,3 @@ if (_isLitElementClassDeclaration(t)) { | ||
/** | ||
* Returns the tagname associated with a | ||
* Returns the tagname associated with a LitClassDeclaration | ||
* @param declaration | ||
@@ -73,4 +75,3 @@ * @returns | ||
export const getTagName = (declaration) => { | ||
// TODO (justinfagnani): support customElements.define() | ||
let tagname = undefined; | ||
let tagName = undefined; | ||
const customElementDecorator = declaration.decorators?.find(isCustomElementDecorator); | ||
@@ -80,6 +81,29 @@ if (customElementDecorator !== undefined && | ||
ts.isStringLiteral(customElementDecorator.expression.arguments[0])) { | ||
tagname = customElementDecorator.expression.arguments[0].text; | ||
// Get tag from decorator: `@customElement('x-foo')` | ||
tagName = customElementDecorator.expression.arguments[0].text; | ||
} | ||
return tagname; | ||
else { | ||
// Otherwise, look for imperative define in the form of: | ||
// `customElements.define('x-foo', XFoo);` | ||
declaration.parent.forEachChild((child) => { | ||
if (ts.isExpressionStatement(child) && | ||
ts.isCallExpression(child.expression) && | ||
ts.isPropertyAccessExpression(child.expression.expression) && | ||
child.expression.arguments.length >= 2) { | ||
const [tagNameArg, ctorArg] = child.expression.arguments; | ||
const { expression, name } = child.expression.expression; | ||
if (ts.isIdentifier(expression) && | ||
expression.text === 'customElements' && | ||
ts.isIdentifier(name) && | ||
name.text === 'define' && | ||
ts.isStringLiteralLike(tagNameArg) && | ||
ts.isIdentifier(ctorArg) && | ||
ctorArg.text === declaration.name?.text) { | ||
tagName = tagNameArg.text; | ||
} | ||
} | ||
}); | ||
} | ||
return tagName; | ||
}; | ||
//# sourceMappingURL=lit-element.js.map |
@@ -13,5 +13,4 @@ /** | ||
import { LitClassDeclaration } from './lit-element.js'; | ||
import { ReactiveProperty } from '../model.js'; | ||
import { ProgramContext } from '../program-context.js'; | ||
export declare const getProperties: (node: LitClassDeclaration, programContext: ProgramContext) => Map<string, ReactiveProperty>; | ||
import { ReactiveProperty, AnalyzerInterface } from '../model.js'; | ||
export declare const getProperties: (classDeclaration: LitClassDeclaration, analyzer: AnalyzerInterface) => Map<string, ReactiveProperty>; | ||
/** | ||
@@ -18,0 +17,0 @@ * Gets the `attribute` property of a property options object as a string. |
@@ -12,10 +12,15 @@ /** | ||
import ts from 'typescript'; | ||
import { getTypeForNode } from '../types.js'; | ||
import { getPropertyDecorator, getPropertyOptions } from './decorators.js'; | ||
export const getProperties = (node, programContext) => { | ||
import { DiagnosticsError } from '../errors.js'; | ||
const isStatic = (prop) => prop.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword); | ||
export const getProperties = (classDeclaration, analyzer) => { | ||
const reactiveProperties = new Map(); | ||
const propertyDeclarations = node.members.filter((m) => ts.isPropertyDeclaration(m)); | ||
const undecoratedProperties = new Map(); | ||
// Filter down to just the property and getter declarations | ||
const propertyDeclarations = classDeclaration.members.filter((m) => ts.isPropertyDeclaration(m) || ts.isGetAccessorDeclaration(m)); | ||
let staticProperties; | ||
for (const prop of propertyDeclarations) { | ||
if (!ts.isIdentifier(prop.name)) { | ||
// TODO(justinfagnani): emit error instead | ||
throw new Error('unsupported property name'); | ||
throw new DiagnosticsError(prop, 'Unsupported property name'); | ||
} | ||
@@ -25,7 +30,8 @@ const name = prop.name.text; | ||
if (propertyDecorator !== undefined) { | ||
// Decorated property; get property options from the decorator and add | ||
// them to the reactiveProperties map | ||
const options = getPropertyOptions(propertyDecorator); | ||
reactiveProperties.set(name, { | ||
name, | ||
type: programContext.getTypeForNode(prop), | ||
node: prop, | ||
type: getTypeForNode(prop, analyzer), | ||
attribute: getPropertyAttribute(options, name), | ||
@@ -37,6 +43,123 @@ typeOption: getPropertyType(options), | ||
} | ||
else if (name === 'properties' && isStatic(prop)) { | ||
// This field has the static properties block (initializer or getter). | ||
// Note we will process this after the loop so that the | ||
// `undecoratedProperties` map is complete before processing the static | ||
// properties block. | ||
staticProperties = prop; | ||
} | ||
else if (!isStatic(prop)) { | ||
// Store the declaration node for any undecorated properties. In a TS | ||
// program that happens to use a static properties block along with | ||
// the `declare` keyword to type the field, we can use this node to | ||
// get/infer the TS type of the field from | ||
undecoratedProperties.set(name, prop); | ||
} | ||
} | ||
// Handle static properties block (initializer or getter). | ||
if (staticProperties !== undefined) { | ||
addPropertiesFromStaticBlock(classDeclaration, staticProperties, undecoratedProperties, reactiveProperties, analyzer); | ||
} | ||
return reactiveProperties; | ||
}; | ||
/** | ||
* Given a static properties declaration (field or getter), add property | ||
* options to the provided `reactiveProperties` map. | ||
*/ | ||
const addPropertiesFromStaticBlock = (classDeclaration, properties, undecoratedProperties, reactiveProperties, analyzer) => { | ||
// Add any constructor initializers to the undecorated properties node map | ||
// from which we can infer types from. This is the primary path that JS source | ||
// can get their inferred types (in TS, types will come from the undecorated | ||
// fields passed in, since you need to declare the field to assign it in the | ||
// constructor). | ||
addConstructorInitializers(classDeclaration, undecoratedProperties); | ||
// Find the object literal from the initializer or getter return value | ||
const object = getStaticPropertiesObjectLiteral(properties); | ||
// Loop over each key/value in the object and add them to the map | ||
for (const prop of object.properties) { | ||
if (ts.isPropertyAssignment(prop) && | ||
ts.isIdentifier(prop.name) && | ||
ts.isObjectLiteralExpression(prop.initializer)) { | ||
const name = prop.name.text; | ||
const options = prop.initializer; | ||
const nodeForType = undecoratedProperties.get(name); | ||
reactiveProperties.set(name, { | ||
name, | ||
type: nodeForType !== undefined | ||
? getTypeForNode(nodeForType, analyzer) | ||
: undefined, | ||
attribute: getPropertyAttribute(options, name), | ||
typeOption: getPropertyType(options), | ||
reflect: getPropertyReflect(options), | ||
converter: getPropertyConverter(options), | ||
}); | ||
} | ||
else { | ||
throw new DiagnosticsError(prop, 'Unsupported static properties entry. Expected a string identifier key and object literal value.'); | ||
} | ||
} | ||
}; | ||
/** | ||
* Find the object literal for a static properties block. | ||
* | ||
* If a ts.PropertyDeclaration, it will look like: | ||
* | ||
* static properties = { ... }; | ||
* | ||
* If a ts.GetAccessorDeclaration, it will look like: | ||
* | ||
* static get properties() { | ||
* return {... } | ||
* } | ||
*/ | ||
const getStaticPropertiesObjectLiteral = (properties) => { | ||
let object = undefined; | ||
if (ts.isPropertyDeclaration(properties) && | ||
properties.initializer !== undefined && | ||
ts.isObjectLiteralExpression(properties.initializer)) { | ||
// `properties` has a static initializer; get the object from there | ||
object = properties.initializer; | ||
} | ||
else if (ts.isGetAccessorDeclaration(properties)) { | ||
// Object was in a static getter: find the object in the return value | ||
const statements = properties.body?.statements; | ||
const statement = statements?.[statements.length - 1]; | ||
if (statement !== undefined && | ||
ts.isReturnStatement(statement) && | ||
statement.expression !== undefined && | ||
ts.isObjectLiteralExpression(statement.expression)) { | ||
object = statement.expression; | ||
} | ||
} | ||
if (object === undefined) { | ||
throw new DiagnosticsError(properties, `Unsupported static properties format. Expected an object literal assigned in a static initializer or returned from a static getter.`); | ||
} | ||
return object; | ||
}; | ||
/** | ||
* Adds any field initializers in the given class's constructor to the provided | ||
* map. This will be used for inferring the type of fields in JS programs. | ||
*/ | ||
const addConstructorInitializers = (classDeclaration, undecoratedProperties) => { | ||
const ctor = classDeclaration.forEachChild((node) => ts.isConstructorDeclaration(node) ? node : undefined); | ||
if (ctor !== undefined) { | ||
ctor.body?.statements.forEach((stmt) => { | ||
// Look for initializers in the form of `this.foo = xxxx` | ||
if (ts.isExpressionStatement(stmt) && | ||
ts.isBinaryExpression(stmt.expression) && | ||
ts.isPropertyAccessExpression(stmt.expression.left) && | ||
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword && | ||
ts.isIdentifier(stmt.expression.left.name) && | ||
!undecoratedProperties.has(stmt.expression.left.name.text)) { | ||
// Add the initializer expression to the map | ||
undecoratedProperties.set( | ||
// Property name | ||
stmt.expression.left.name.text, | ||
// Expression from which we can infer a type | ||
stmt.expression.right); | ||
} | ||
}); | ||
} | ||
}; | ||
/** | ||
* Gets the `attribute` property of a property options object as a string. | ||
@@ -43,0 +166,0 @@ */ |
@@ -10,14 +10,35 @@ /** | ||
export { PackageJson }; | ||
export interface PackageInit { | ||
/** | ||
* Return type of `getLitElementModules`: contains a module and filtered list of | ||
* LitElementDeclarations contained within it. | ||
*/ | ||
export declare type ModuleWithLitElementDeclarations = { | ||
module: Module; | ||
declarations: LitElementDeclaration[]; | ||
}; | ||
export interface PackageInfoInit { | ||
name: string; | ||
rootDir: AbsolutePath; | ||
packageJson: PackageJson; | ||
tsConfig: ts.ParsedCommandLine; | ||
} | ||
export declare class PackageInfo { | ||
readonly name: string; | ||
readonly rootDir: AbsolutePath; | ||
readonly packageJson: PackageJson; | ||
constructor(init: PackageInfoInit); | ||
} | ||
export interface PackageInit extends PackageInfo { | ||
modules: ReadonlyArray<Module>; | ||
} | ||
export declare class Package { | ||
readonly rootDir: AbsolutePath; | ||
export declare class Package extends PackageInfo { | ||
readonly modules: ReadonlyArray<Module>; | ||
readonly tsConfig: ts.ParsedCommandLine; | ||
readonly packageJson: PackageJson; | ||
constructor(init: PackageInit); | ||
/** | ||
* Returns a list of modules in this package containing LitElement | ||
* declarations, along with the filtered list of LitElementDeclarartions. | ||
*/ | ||
getLitElementModules(): { | ||
module: Module; | ||
declarations: LitElementDeclaration[]; | ||
}[]; | ||
} | ||
@@ -28,2 +49,3 @@ export interface ModuleInit { | ||
jsPath: PackagePath; | ||
packageJson: PackageJson; | ||
} | ||
@@ -47,12 +69,20 @@ export declare class Module { | ||
readonly declarations: Array<Declaration>; | ||
readonly packageJson: PackageJson; | ||
constructor(init: ModuleInit); | ||
} | ||
export declare type Declaration = ClassDeclaration | VariableDeclaration; | ||
export interface VariableDeclarationInit { | ||
interface DeclarationInit { | ||
name: string; | ||
} | ||
export declare abstract class Declaration { | ||
name: string; | ||
constructor(init: DeclarationInit); | ||
isVariableDeclaration(): this is VariableDeclaration; | ||
isClassDeclaration(): this is ClassDeclaration; | ||
isLitElementDeclaration(): this is LitElementDeclaration; | ||
} | ||
export interface VariableDeclarationInit extends DeclarationInit { | ||
node: ts.VariableDeclaration; | ||
type: Type | undefined; | ||
} | ||
export declare class VariableDeclaration { | ||
readonly name: string; | ||
export declare class VariableDeclaration extends Declaration { | ||
readonly node: ts.VariableDeclaration; | ||
@@ -62,8 +92,6 @@ readonly type: Type | undefined; | ||
} | ||
export interface ClassDeclarationInit { | ||
name: string | undefined; | ||
export interface ClassDeclarationInit extends DeclarationInit { | ||
node: ts.ClassDeclaration; | ||
} | ||
export declare class ClassDeclaration { | ||
readonly name: string | undefined; | ||
export declare class ClassDeclaration extends Declaration { | ||
readonly node: ts.ClassDeclaration; | ||
@@ -78,3 +106,2 @@ constructor(init: ClassDeclarationInit); | ||
export declare class LitElementDeclaration extends ClassDeclaration { | ||
readonly isLitElement = true; | ||
/** | ||
@@ -96,4 +123,3 @@ * The element's tag name, if one is associated with this class declaration, | ||
name: string; | ||
node: ts.PropertyDeclaration; | ||
type: Type; | ||
type: Type | undefined; | ||
reflect: boolean; | ||
@@ -121,3 +147,2 @@ attribute: boolean | string | undefined; | ||
} | ||
export declare const isLitElementDeclaration: (dec: Declaration) => dec is LitElementDeclaration; | ||
export interface LitModule { | ||
@@ -127,7 +152,6 @@ module: Module; | ||
} | ||
export declare const getLitModules: (analysis: Package) => LitModule[]; | ||
export interface ReferenceInit { | ||
name: string; | ||
package?: string; | ||
module?: string; | ||
package?: string | undefined; | ||
module?: string | undefined; | ||
isGlobal?: boolean; | ||
@@ -143,7 +167,14 @@ } | ||
} | ||
export interface TypeInit { | ||
type: ts.Type; | ||
text: string; | ||
getReferences: () => Reference[]; | ||
} | ||
export declare class Type { | ||
type: ts.Type; | ||
text: string; | ||
references: Reference[]; | ||
constructor(type: ts.Type, text: string, references: Reference[]); | ||
private _getReferences; | ||
private _references; | ||
constructor(init: TypeInit); | ||
get references(): Reference[]; | ||
} | ||
@@ -157,2 +188,8 @@ /** | ||
export declare const getImportsStringForReferences: (references: Reference[]) => string; | ||
export interface AnalyzerInterface { | ||
program: ts.Program; | ||
commandLine: ts.ParsedCommandLine; | ||
fs: Pick<ts.System, 'readDirectory' | 'readFile' | 'realpath' | 'fileExists' | 'useCaseSensitiveFileNames'>; | ||
path: Pick<typeof import('path'), 'join' | 'relative' | 'dirname' | 'basename' | 'dirname' | 'parse' | 'normalize'>; | ||
} | ||
//# sourceMappingURL=model.d.ts.map |
@@ -6,9 +6,31 @@ /** | ||
*/ | ||
export class Package { | ||
export class PackageInfo { | ||
constructor(init) { | ||
this.name = init.name; | ||
this.rootDir = init.rootDir; | ||
this.packageJson = init.packageJson; | ||
this.tsConfig = init.tsConfig; | ||
} | ||
} | ||
export class Package extends PackageInfo { | ||
constructor(init) { | ||
super(init); | ||
this.modules = init.modules; | ||
} | ||
/** | ||
* Returns a list of modules in this package containing LitElement | ||
* declarations, along with the filtered list of LitElementDeclarartions. | ||
*/ | ||
getLitElementModules() { | ||
const modules = []; | ||
for (const module of this.modules) { | ||
const declarations = module.declarations.filter((d) => d.isLitElementDeclaration()); | ||
if (declarations.length > 0) { | ||
modules.push({ | ||
module, | ||
declarations, | ||
}); | ||
} | ||
} | ||
return modules; | ||
} | ||
} | ||
@@ -21,7 +43,22 @@ export class Module { | ||
this.jsPath = init.jsPath; | ||
this.packageJson = init.packageJson; | ||
} | ||
} | ||
export class VariableDeclaration { | ||
export class Declaration { | ||
constructor(init) { | ||
this.name = init.name; | ||
} | ||
isVariableDeclaration() { | ||
return this instanceof VariableDeclaration; | ||
} | ||
isClassDeclaration() { | ||
return this instanceof ClassDeclaration; | ||
} | ||
isLitElementDeclaration() { | ||
return this instanceof LitElementDeclaration; | ||
} | ||
} | ||
export class VariableDeclaration extends Declaration { | ||
constructor(init) { | ||
super(init); | ||
this.node = init.node; | ||
@@ -31,5 +68,5 @@ this.type = init.type; | ||
} | ||
export class ClassDeclaration { | ||
export class ClassDeclaration extends Declaration { | ||
constructor(init) { | ||
this.name = init.name; | ||
super(init); | ||
this.node = init.node; | ||
@@ -41,3 +78,2 @@ } | ||
super(init); | ||
this.isLitElement = true; | ||
this.tagname = init.tagname; | ||
@@ -48,20 +84,2 @@ this.reactiveProperties = init.reactiveProperties; | ||
} | ||
// TODO(justinfagnani): Move helpers into a Lit-specific module | ||
export const isLitElementDeclaration = (dec) => { | ||
return (dec instanceof ClassDeclaration && | ||
dec.isLitElement); | ||
}; | ||
export const getLitModules = (analysis) => { | ||
const modules = []; | ||
for (const module of analysis.modules) { | ||
const elements = module.declarations.filter(isLitElementDeclaration); | ||
if (elements.length > 0) { | ||
modules.push({ | ||
module, | ||
elements, | ||
}); | ||
} | ||
} | ||
return modules; | ||
}; | ||
export class Reference { | ||
@@ -82,7 +100,11 @@ constructor(init) { | ||
export class Type { | ||
constructor(type, text, references) { | ||
this.type = type; | ||
this.text = text; | ||
this.references = references; | ||
constructor(init) { | ||
this._references = undefined; | ||
this.type = init.type; | ||
this.text = init.text; | ||
this._getReferences = init.getReferences; | ||
} | ||
get references() { | ||
return (this._references ?? (this._references = this._getReferences())); | ||
} | ||
} | ||
@@ -89,0 +111,0 @@ /** |
{ | ||
"name": "@lit-labs/analyzer", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"publishConfig": { | ||
@@ -51,7 +51,9 @@ "access": "public" | ||
"files": [ | ||
"/src/", | ||
"!/src/test/", | ||
"index.*", | ||
"/lib/", | ||
"!/lib/.tsbuildinfo" | ||
], | ||
"exports": { | ||
".": "./index.js" | ||
}, | ||
"dependencies": { | ||
@@ -58,0 +60,0 @@ "package-json-type": "^1.0.3", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
67
0
173193
1747
1