import { visit, types, parse, prettyPrint } from 'recast'; import { NodePath } from 'ast-types/lib/node-path'; import { PackageJSON } from '../types/packages'; /** * https://astexplorer.net/ * * Before making changes to this class, it's extremely helpful to check out the astexplorer.net site to understand * the AST objects that get created. * */ type ImportDeclaration = types.namedTypes.ImportDeclaration; type ExpressionStatement = types.namedTypes.ExpressionStatement; type BlockStatement = types.namedTypes.BlockStatement; type MemberExpression = types.namedTypes.MemberExpression; type Statement = types.namedTypes.Statement; const { builders: b } = types; const BASE = ` import StudioFramework from '@movable/studio-framework'; export default class App extends StudioFramework { setup() {} } const app = new App(); app.render(document.getElementById('react-root')).then(() => { window.APP_SUCCESSFULLY_RENDERED = true; }); `; function defaultPackageName(packageName: string): string { return `_${packageName.replace(/[^A-Za-z0-9]/g, '_')}`; } /** * Generates an Import statement * * ex: `import _my_package from "my-package"; * * @param packageName {string} the unique name of the package * @param variableName {string} the variable name of the default package import */ function makePackageImport(packageName: string, variableName: string): ImportDeclaration { const importFrom = b.literal(packageName); const defaultImportSpecifier = b.importDefaultSpecifier(b.identifier(variableName)); return b.importDeclaration([defaultImportSpecifier], importFrom); } /** * Generates a plain import statement. * * ex: `import "my-package/manifest.yml";` * * @param packageName {string} the name of the file to be imported */ function makePlainPackageImport(importName: string): ImportDeclaration { const importFrom = b.literal(importName); return b.importDeclaration([], importFrom); } /** * Creates an expression that looks up the `setGrouping` method: * * ex: `this.setGrouping` */ function setGroupingMember(): MemberExpression { const thisExpression = b.thisExpression(); const setGrouping = b.identifier('setGrouping'); return b.memberExpression(thisExpression, setGrouping); } /** * Creates an initialization function with the given function name * * ex: `packageInitFuncName(this)` * * @param packageInitializationFunctionName {string} the package initialization function */ function makeStandardInitalizeStatement( packageInitializationFunctionName: string ): ExpressionStatement { const packageInitializationFunctionID = b.identifier(packageInitializationFunctionName); const packageInitialization = b.callExpression(packageInitializationFunctionID, [ b.thisExpression() ]); return b.expressionStatement(packageInitialization); } /** * Creates a statement to call `setGrouping` with the package name and initialization function: * * ex: `this.setGrouping("my-package", () => { initilizationFn(this); })` * * @param packageName {string} the unique name of the package * @param packageInitializationFunctionName {string} the package initialization function */ function makeSetGroupingStatement( packageName: string, packageInitializationFunctionName: string ): ExpressionStatement { const packageNameArg = b.literal(packageName); const initStatement = makeStandardInitalizeStatement(packageInitializationFunctionName); const anonymousFn = b.arrowFunctionExpression([], b.blockStatement([initStatement])); const memberExpression = setGroupingMember(); const callExpression = b.callExpression(memberExpression, [packageNameArg, anonymousFn]); return b.expressionStatement(callExpression); } function makeRelativeImport(path: string): string { return path.startsWith('./') ? path : `./${path}`; } function generateImports(pkg: PackageJSON, entryPath: string): [Statement[], Statement[]] { const { name, ['studio-package']: studioPackage } = pkg; const setups: Statement[] = []; const imports: Statement[] = []; const variableName = defaultPackageName(name); const relativeEntry = makeRelativeImport(entryPath); imports.push(makePackageImport(relativeEntry.replace(/\.js$/, ''), variableName)); if (studioPackage && studioPackage.manifest) { const relativeManifest = makeRelativeImport(studioPackage.manifest); imports.push(makePlainPackageImport(relativeManifest)); } setups.push(makeSetGroupingStatement(name, variableName)); return [imports, setups]; } export default function generateApp(pkg: PackageJSON, entryPath: string): string { const app = parse(BASE); const [imports, setups] = generateImports(pkg, entryPath); const BODY = app.program.body; // Add imports after the StudioFrameworkImport BODY.splice(1, 0, ...imports); // Insert the "setups" into the setup method: visit(app, { visitBlockStatement(path: NodePath): unknown { const parentValue = path.parent.parent.value; if (parentValue.type === 'MethodDefinition' && parentValue.key.name === 'setup') { path.value.body.push(...setups); return false; } this.traverse(path); } }); return prettyPrint(app, { tabWidth: 2 }).code; }