import { readFileSync } from 'node:fs'; import { resolve, basename, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Project, ts, ObjectLiteralExpression, VariableDeclarationKind, Node, type ObjectLiteralElementLike, } from 'ts-morph'; import type { PluginOption } from 'vite'; // Initialize a global project instance to reuse across load calls const __dirname = dirname(fileURLToPath(import.meta.url)); const globalProject = new Project({ tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), skipAddingFilesFromTsConfig: true, skipLoadingLibFiles: true, skipFileDependencyResolution: true, }); // Helper to extract a property’s name from its node const getPropertyName = (prop: Node): string | null => { const kind = prop.getKind(); if ( kind === ts.SyntaxKind.PropertyAssignment || kind === ts.SyntaxKind.ShorthandPropertyAssignment || kind === ts.SyntaxKind.MethodDeclaration ) { return prop.getFirstChildByKindOrThrow(ts.SyntaxKind.Identifier).getText(); } return null; }; export default function ( mode: 'backend' | 'preload' | 'renderer' | 'none', ): PluginOption { return { name: 'ytm-plugin-loader', load: { filter: { id: /(?:\/plugins\/[^/]+\/index\.(?:js|ts|jsx|tsx)|\/plugins\/[^/]+\.(?:js|ts|jsx|tsx))$/, }, handler(id) { const fileContent = readFileSync(id, 'utf8'); // Create or update source file in the global project instance const src = globalProject.createSourceFile( '_pf' + basename(id), fileContent, { overwrite: true }, ); let objExpr: ObjectLiteralExpression | undefined; // Check for `export default ...` const defaultExportAssignment = src.getExportAssignment( (ea) => !ea.isExportEquals(), // Filter out `export = ` ); if (defaultExportAssignment) { const expression = defaultExportAssignment.getExpression(); if (expression.getKind() === ts.SyntaxKind.ObjectLiteralExpression) { objExpr = expression.asKindOrThrow( ts.SyntaxKind.ObjectLiteralExpression, ); } else if (expression.getKind() === ts.SyntaxKind.CallExpression) { const callExpr = expression.asKindOrThrow( ts.SyntaxKind.CallExpression, ); if ( callExpr.getArguments().length === 1 && callExpr.getExpression().getText() === 'createPlugin' ) { const arg = callExpr.getArguments()[0]; if (arg.getKind() === ts.SyntaxKind.ObjectLiteralExpression) { objExpr = arg.asKindOrThrow( ts.SyntaxKind.ObjectLiteralExpression, ); } } } } // If not found via `export default`, check for a named export aliased as 'default' if (!objExpr) { const defaultExportDeclaration = src .getExportedDeclarations() .get('default'); if (defaultExportDeclaration && defaultExportDeclaration.length > 0) { const expr = defaultExportDeclaration[0]; if (expr.getKind() === ts.SyntaxKind.ObjectLiteralExpression) { objExpr = expr.asKindOrThrow( ts.SyntaxKind.ObjectLiteralExpression, ); } else if (expr.getKind() === ts.SyntaxKind.CallExpression) { const callExpr = expr.asKindOrThrow(ts.SyntaxKind.CallExpression); if ( callExpr.getArguments().length === 1 && callExpr.getExpression().getText() === 'createPlugin' ) { const arg = callExpr.getArguments()[0]; if (arg.getKind() === ts.SyntaxKind.ObjectLiteralExpression) { objExpr = arg.asKindOrThrow( ts.SyntaxKind.ObjectLiteralExpression, ); } } } } } if (!objExpr) return null; // Build a map of property names to their AST nodes for fast lookup const propMap = new Map(); for (const prop of objExpr.getProperties()) { const name = getPropertyName(prop); if (name) propMap.set(name, prop); } const contexts = ['backend', 'preload', 'renderer', 'menu']; for (const ctx of contexts) { if (mode === 'none' && propMap.has(ctx)) { propMap.get(ctx)?.remove(); continue; } if (ctx === mode || (ctx === 'menu' && mode === 'backend')) continue; if (propMap.has(ctx)) propMap.get(ctx)?.remove(); } // Add an exported variable 'pluginStub' with the modified object literal's text const varStmt = src.addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { name: 'pluginStub', initializer: (writer) => writer.write(objExpr.getText()), }, ], }); const stubObjExpr = varStmt .getDeclarations()[0] .getInitializerIfKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression); // Similarly build a map for the stub properties const stubMap = new Map(); for (const prop of stubObjExpr.getProperties()) { const name = getPropertyName(prop); if (name) stubMap.set(name, prop); } const stubContexts = mode === 'backend' ? contexts.filter((ctx) => ctx !== 'menu') : contexts; for (const ctx of stubContexts) { if (stubMap.has(ctx)) { stubMap.get(ctx)?.remove(); } } return { code: src.getText(), }; }, }, }; }