UNPKG

libmodulor

Version:

A TypeScript library to create platform-agnostic applications

282 lines (281 loc) 13 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import { inject, injectable } from 'inversify'; import typescript, { isClassDeclaration, isObjectLiteralExpression, isPropertyAssignment, SyntaxKind, } from 'typescript'; // https://ts-ast-viewer.com // To avoid the following error when used in a consumer : // SyntaxError: Named export 'ModuleKind' not found. The requested module 'typescript' is a CommonJS module, which may not support all module.exports as named exports. // CommonJS modules can always be imported via the default export const { ModuleKind, ModuleResolutionKind, ScriptTarget, createProgram, flattenDiagnosticMessageText, forEachChild, getPreEmitDiagnostics, isIdentifier, isImportDeclaration, isPropertySignature, isStringLiteral, isTypeReferenceNode, isVariableStatement, } = typescript; import { UC_MAIN_CLIENT_SUFFIX, UC_MAIN_SERVER_SUFFIX, UC_MAIN_STEP_PREFIX_REGULAR, UC_MAIN_SUFFIX, UC_POLICY_SUFFIX, } from '../../convention.js'; const ERR_TS_CONFIG_INVALID = (configFileName) => `The root ${configFileName} could not be opened or parsed`; const ERR_TS_CONFIG_COMP_OPTS = (configFileName) => `The root ${configFileName} must contain compilerOptions`; const ERR_UCD_TOO_MANY_GENERICS = () => 'There are more generics than expected in the UCD'; let TypeScriptLibUCDefASTParser = class TypeScriptLibUCDefASTParser { fsManager; logger; compilerOptions; opts; program; typeChecker; constructor(fsManager, logger) { this.fsManager = fsManager; this.logger = logger; } async init(opts, ucdsPaths) { this.opts = opts; this.logger.trace('TypeScriptLibUCDefASTParser Initializing compiler options'); await this.initCompilerOptions(); this.logger.trace('TypeScriptLibUCDefASTParser Creating program'); this.program = createProgram(ucdsPaths, this.compilerOptions); this.logger.trace('TypeScriptLibUCDefASTParser Creating type checker'); this.typeChecker = this.program.getTypeChecker(); } async processFile(path, onImport, onVariable, onInputType, onOPIType, onMainStep, onPolicy, onMetadata) { const srcFile = this.program.getSourceFile(path); if (!srcFile) { return; } forEachChild(srcFile, (node) => { if (isImportDeclaration(node)) { forEachChild(node, (node1) => { if (isStringLiteral(node1)) { onImport(node1.text); } }); } if (isVariableStatement(node)) { this.processVariable(node, onVariable, onInputType, onOPIType, onMainStep, onPolicy, onMetadata); } }); } async transpile() { const diagnostics = getPreEmitDiagnostics(this.program); if (diagnostics.length === 0) { return; } const errors = diagnostics.map((d) => flattenDiagnosticMessageText(d.messageText, '\n')); throw new Error(errors[0]); } async initCompilerOptions() { const { ts: { configFileName, module, moduleResolution, noCheck, skipLibCheck, target, }, } = this.opts.source; const tsConfigPath = this.fsManager.path(configFileName); let tsConfig; try { tsConfig = JSON.parse(await this.fsManager.cat(tsConfigPath)); } catch (_err) { throw new Error(ERR_TS_CONFIG_INVALID(configFileName)); } const compilerOptionsBase = tsConfig.compilerOptions; if (!compilerOptionsBase) { throw new Error(ERR_TS_CONFIG_COMP_OPTS(configFileName)); } this.compilerOptions = { ...compilerOptionsBase, module: ModuleKind[(compilerOptionsBase.module ?? module)], moduleResolution: ModuleResolutionKind[(compilerOptionsBase.moduleResolution ?? moduleResolution)], noCheck: compilerOptionsBase.noCheck ?? noCheck, skipLibCheck: compilerOptionsBase.skipLibCheck ?? skipLibCheck, target: ScriptTarget[(compilerOptionsBase.target ?? target)], }; this.compilerOptions.incremental = false; // Otherwise it triggers the following error : Option '--incremental' can only be specified using tsconfig, emitting to single file or when option '--tsBuildInfoFile' is specified. // @ts-expect-error this.compilerOptions.jsx = undefined; // Otherwise it triggers the following error since TS 5.5 : jsx is a string value; tsconfig JSON must be parsed with parseJsonSourceFileConfigFileContent or getParsedCommandLineOfConfigFile before passing to createProgram // @ts-expect-error this.compilerOptions.lib = undefined; // Otherwise it triggers errors saying it does not find them } getTypeFields(node) { const type = this.typeChecker.getTypeAtLocation(node); const fields = type .getProperties() .map((p) => { const field = { err: null, value: { dataType: null, name: null, raw: null, type: null, }, }; const declarations = p.getDeclarations(); if (!declarations) { return field; } const [first] = declarations; if (first && isPropertySignature(first)) { field.value.raw = first .getText() .replaceAll('\n', '') .replaceAll(';', '') .replaceAll(/\s+/g, ' '); first.forEachChild((node1) => { if (isIdentifier(node1)) { field.value.name = node1.getText(); } else if (isTypeReferenceNode(node1)) { field.value.type = node1.getText(); node1.forEachChild((node2) => { if (isIdentifier(node2) || isTypeReferenceNode(node2) || node2.kind === SyntaxKind.BooleanKeyword || node2.kind === SyntaxKind.NumberKeyword || node2.kind === SyntaxKind.StringKeyword) { field.value.dataType = node2.getText(); } }); } }); } return field; }); return fields; } processConstDeclaration(node, onMainStep, onPolicy, onMetadata) { // TODO : Improve this whole pattern by making it more robust // I think there is a risk of it to break in some cases (e.g. if there are other properties named main, policy, etc.) const metadataPropertyName = 'metadata'; const nodes = []; const populateNodes = (node1) => { // "main: SendClientMain" => "main", ":", "SendClientMain" => 3 if (isPropertyAssignment(node1) && node1.getChildCount() === 3) { const propertyName = node1.getChildAt(0).getText(); if (propertyName === UC_MAIN_SUFFIX.toLocaleLowerCase() || propertyName === UC_POLICY_SUFFIX.toLocaleLowerCase() || propertyName === metadataPropertyName) { nodes.push(node1); } } node1.forEachChild(populateNodes); }; populateNodes(node); for (const node1 of nodes) { const text = node1.getText(); if (text.endsWith(UC_MAIN_CLIENT_SUFFIX)) { this.processLifecycleMain('client', node1, onMainStep); } else if (text.endsWith(UC_MAIN_SERVER_SUFFIX)) { this.processLifecycleMain('server', node1, onMainStep); } else if (text.endsWith(UC_POLICY_SUFFIX)) { const parent = node1.parent.getText(); // { main: X(Client|Server)Main, policy: XUCPolicy } if (parent.includes(UC_MAIN_CLIENT_SUFFIX)) { this.processLifecyclePolicy('client', node1, onPolicy); } else if (parent.includes(UC_MAIN_SERVER_SUFFIX)) { this.processLifecyclePolicy('server', node1, onPolicy); } } else if (text.startsWith(metadataPropertyName)) { this.processMetadata(node1, onMetadata); } } } processConstType(node, onInputType, onOPIType) { let idx = 0; forEachChild(node, (node2) => { if (isTypeReferenceNode(node2)) { forEachChild(node2, (node3) => { if (isIdentifier(node3)) { const typeName = node3.getText(); const fields = this.getTypeFields(node3); if (idx === 0) { onInputType(typeName, fields); } else if (idx === 1 || idx === 2) { onOPIType(typeName, fields, (idx - 1)); } else { throw new Error(ERR_UCD_TOO_MANY_GENERICS()); } } }); idx += 1; } }); } processLifecycleMain(lifecycle, node, onMainStep) { const type = this.typeChecker.getTypeAtLocation(node.getChildAt(2)); const classDeclaration = type .getSymbol() ?.getDeclarations() ?.find((d) => isClassDeclaration(d)); if (!classDeclaration) { return; } const lines = classDeclaration.getFullText().split('\n'); for (const l of lines) { const trimmed = l.trim(); if (!trimmed.startsWith(UC_MAIN_STEP_PREFIX_REGULAR)) { continue; } onMainStep(lifecycle, trimmed); } } processLifecyclePolicy(lifecycle, node, onPolicy) { const node1 = node.getChildAt(2); onPolicy(lifecycle, node1.getText()); } processMetadata(node, onMetadata) { const metadataLike = {}; const type = this.typeChecker.getTypeAtLocation(node); for (const p of type.getProperties()) { const declarations = p.getDeclarations(); if (!declarations) { continue; } for (const d of declarations) { const k = d.getChildAt(0).getText(); const v = d.getChildAt(2).getText(); let value = v; if (value === 'true' || value === 'false') { value = Boolean(value); } else { value = value.substring(1, v.length - 1); // To remove the single quotes } metadataLike[k] = value; } } onMetadata(metadataLike); } processVariable(node, onVariable, onInputType, onOPIType, onMainStep, onPolicy, onMetadata) { const { declarationList: { declarations }, } = node; const [firstDeclaration] = declarations; if (!firstDeclaration) { return; } forEachChild(firstDeclaration, (node1) => { if (isIdentifier(node1)) { onVariable(node1.getText()); } if (isTypeReferenceNode(node1)) { this.processConstType(node1, onInputType, onOPIType); } if (isObjectLiteralExpression(node1)) { this.processConstDeclaration(node1, onMainStep, onPolicy, onMetadata); } }); } }; TypeScriptLibUCDefASTParser = __decorate([ injectable(), __param(0, inject('FSManager')), __param(1, inject('Logger')), __metadata("design:paramtypes", [Object, Object]) ], TypeScriptLibUCDefASTParser); export { TypeScriptLibUCDefASTParser };