diff --git a/src/providers/decorators.ts b/src/providers/decorators.ts index a838594e..98394f77 100644 --- a/src/providers/decorators.ts +++ b/src/providers/decorators.ts @@ -71,7 +71,7 @@ export function throttle unknown>( }) as T; } -function memoize unknown>(fn: T): T { +export function memoize unknown>(fn: T): T { const cache = new Map(); return ((...args) => { @@ -84,7 +84,7 @@ function memoize unknown>(fn: T): T { }) as T; } -function retry Promise>( +export function retry Promise>( fn: T, { retries = 3, delay = 1000 } = {}, ) { @@ -102,12 +102,3 @@ function retry Promise>( throw latestError; }; } - -export default { - singleton, - debounce, - cache, - throttle, - memoize, - retry, -}; diff --git a/vite-plugins/i18n-importer.mts b/vite-plugins/i18n-importer.mts index 9650d5ae..1efe3736 100644 --- a/vite-plugins/i18n-importer.mts +++ b/vite-plugins/i18n-importer.mts @@ -7,15 +7,15 @@ import { Project } from 'ts-morph'; const snakeToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); -export const i18nImporter = () => { - const __dirname = dirname(fileURLToPath(import.meta.url)); - const project = new Project({ - tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), - skipAddingFilesFromTsConfig: true, - skipLoadingLibFiles: true, - skipFileDependencyResolution: true, - }); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const globalProject = new Project({ + tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), + skipAddingFilesFromTsConfig: true, + skipLoadingLibFiles: true, + skipFileDependencyResolution: true, +}); +export const i18nImporter = () => { const srcPath = resolve(__dirname, '..', 'src'); const plugins = globSync(['src/i18n/resources/*.json']).map((path) => { const nameWithExt = basename(path); @@ -24,24 +24,28 @@ export const i18nImporter = () => { return { name, path }; }); - const src = project.createSourceFile('vm:i18n', (writer) => { - // prettier-ignore - for (const { name, path } of plugins) { + const src = globalProject.createSourceFile( + 'vm:i18n', + (writer) => { + // prettier-ignore + for (const { name, path } of plugins) { const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`); } - writer.blankLine(); + writer.blankLine(); - writer.writeLine('export const languageResources = {'); - for (const { name } of plugins) { - writer.writeLine(` "${name}": {`); - writer.writeLine(` translation: ${snakeToCamel(name)}Json,`); - writer.writeLine(' },'); - } - writer.writeLine('};'); - writer.blankLine(); - }); + writer.writeLine('export const languageResources = {'); + for (const { name } of plugins) { + writer.writeLine(` "${name}": {`); + writer.writeLine(` translation: ${snakeToCamel(name)}Json,`); + writer.writeLine(' },'); + } + writer.writeLine('};'); + writer.blankLine(); + }, + { overwrite: true }, + ); return src.getText(); }; diff --git a/vite-plugins/plugin-importer.mts b/vite-plugins/plugin-importer.mts index 5b675f55..dfe7907b 100644 --- a/vite-plugins/plugin-importer.mts +++ b/vite-plugins/plugin-importer.mts @@ -7,17 +7,17 @@ import { Project } from 'ts-morph'; const snakeToCamel = (text: string) => text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const globalProject = new Project({ + tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), + skipAddingFilesFromTsConfig: true, + skipLoadingLibFiles: true, + skipFileDependencyResolution: true, +}); + export const pluginVirtualModuleGenerator = ( mode: 'main' | 'preload' | 'renderer', ) => { - const __dirname = dirname(fileURLToPath(import.meta.url)); - const project = new Project({ - tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), - skipAddingFilesFromTsConfig: true, - skipLoadingLibFiles: true, - skipFileDependencyResolution: true, - }); - const srcPath = resolve(__dirname, '..', 'src'); const plugins = globSync([ 'src/plugins/*/index.{js,ts}', @@ -35,35 +35,39 @@ export const pluginVirtualModuleGenerator = ( return { name, path }; }); - const src = project.createSourceFile('vm:pluginIndexes', (writer) => { - // prettier-ignore - for (const { name, path } of plugins) { + const src = globalProject.createSourceFile( + 'vm:pluginIndexes', + (writer) => { + // prettier-ignore + for (const { name, path } of plugins) { const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`); } - writer.blankLine(); + writer.blankLine(); - // Context-specific exports - writer.writeLine(`export const ${mode}Plugins = {`); - for (const { name } of plugins) { - const checkMode = mode === 'main' ? 'backend' : mode; - // HACK: To avoid situation like importing renderer plugins in main - writer.writeLine( - ` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`, - ); - } - writer.writeLine('};'); - writer.blankLine(); + // Context-specific exports + writer.writeLine(`export const ${mode}Plugins = {`); + for (const { name } of plugins) { + const checkMode = mode === 'main' ? 'backend' : mode; + // HACK: To avoid situation like importing renderer plugins in main + writer.writeLine( + ` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`, + ); + } + writer.writeLine('};'); + writer.blankLine(); - // All plugins export (stub only) // Omit - writer.writeLine('export const allPlugins = {'); - for (const { name } of plugins) { - writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`); - } - writer.writeLine('};'); - writer.blankLine(); - }); + // All plugins export (stub only) // Omit + writer.writeLine('export const allPlugins = {'); + for (const { name } of plugins) { + writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`); + } + writer.writeLine('};'); + writer.blankLine(); + }, + { overwrite: true }, + ); return src.getText(); }; diff --git a/vite-plugins/plugin-loader.mts b/vite-plugins/plugin-loader.mts index e17949af..5f410d3f 100644 --- a/vite-plugins/plugin-loader.mts +++ b/vite-plugins/plugin-loader.mts @@ -1,4 +1,4 @@ -import { readFile } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; import { resolve, basename, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -8,10 +8,34 @@ import { 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 { @@ -22,131 +46,96 @@ export default function ( return { name: 'ytm-plugin-loader', - async load(id) { + load(id) { if (!pluginFilter(id)) return null; - const __dirname = dirname(fileURLToPath(import.meta.url)); - - const project = new Project({ - tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), - skipAddingFilesFromTsConfig: true, - skipLoadingLibFiles: true, - skipFileDependencyResolution: true, - }); - - const src = project.createSourceFile( + // Read file asynchronously + const fileContent = readFileSync(id, 'utf8'); + // Create or update source file in the global project instance + const src = globalProject.createSourceFile( '_pf' + basename(id), - await readFile(id, 'utf8'), + fileContent, + { overwrite: true }, ); + const exports = src.getExportedDeclarations(); - let objExpr: ObjectLiteralExpression | undefined = undefined; + let objExpr: ObjectLiteralExpression | undefined; - for (const [name, [expr]] of exports) { - if (name !== 'default') continue; - - switch (expr.getKind()) { - case ts.SyntaxKind.ObjectLiteralExpression: { - objExpr = expr.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression); - break; - } - case ts.SyntaxKind.CallExpression: { - const callExpr = expr.asKindOrThrow(ts.SyntaxKind.CallExpression); - if (callExpr.getArguments().length !== 1) continue; - - const name = callExpr.getExpression().getText(); - if (name !== 'createPlugin') continue; + // Identify the default export as an object literal, or via a 'createPlugin' call + for (const [exportName, declarations] of exports) { + if (exportName !== 'default') continue; + const expr = declarations[0]; + const exprKind = expr.getKind(); + if (exprKind === ts.SyntaxKind.ObjectLiteralExpression) { + objExpr = expr.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression); + break; + } else if (exprKind === 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) - continue; - - objExpr = arg.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression); - break; + if (arg.getKind() === ts.SyntaxKind.ObjectLiteralExpression) { + objExpr = arg.asKindOrThrow( + ts.SyntaxKind.ObjectLiteralExpression, + ); + break; + } } } } if (!objExpr) return null; - const properties = objExpr.getProperties(); - const propertyNames = properties.map((prop) => { - switch (prop.getKind()) { - case ts.SyntaxKind.PropertyAssignment: - return prop - .asKindOrThrow(ts.SyntaxKind.PropertyAssignment) - .getName(); - case ts.SyntaxKind.ShorthandPropertyAssignment: - return prop - .asKindOrThrow(ts.SyntaxKind.ShorthandPropertyAssignment) - .getName(); - case ts.SyntaxKind.MethodDeclaration: - return prop - .asKindOrThrow(ts.SyntaxKind.MethodDeclaration) - .getName(); - default: - throw new Error('Not implemented'); - } - }); + // 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') { - const index = propertyNames.indexOf(ctx); - if (index === -1) continue; - - objExpr.getProperty(propertyNames[index])?.remove(); + if (mode === 'none' && propMap.has(ctx)) { + propMap.get(ctx)?.remove(); continue; } - - if (ctx === mode) continue; - if (ctx === 'menu' && mode === 'backend') continue; - - const index = propertyNames.indexOf(ctx); - if (index === -1) continue; - - objExpr.getProperty(propertyNames[index])?.remove(); + if (ctx === mode || (ctx === 'menu' && mode === 'backend')) continue; + if (propMap.has(ctx)) propMap.get(ctx)?.remove(); } - const stubObjExpr = src - .addVariableStatement({ - isExported: true, - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: 'pluginStub', - initializer: (writer) => writer.write(objExpr.getText()), - }, - ], - }) - .getDeclarations()[0] - .getInitializer() as ObjectLiteralExpression; - - const stubProperties = stubObjExpr.getProperties(); - const stubPropertyNames = stubProperties.map((prop) => { - switch (prop.getKind()) { - case ts.SyntaxKind.PropertyAssignment: - return prop - .asKindOrThrow(ts.SyntaxKind.PropertyAssignment) - .getName(); - case ts.SyntaxKind.ShorthandPropertyAssignment: - return prop - .asKindOrThrow(ts.SyntaxKind.ShorthandPropertyAssignment) - .getName(); - case ts.SyntaxKind.MethodDeclaration: - return prop - .asKindOrThrow(ts.SyntaxKind.MethodDeclaration) - .getName(); - default: - throw new Error('Not implemented'); - } + // 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); - if (mode === 'backend') contexts.pop(); - for (const ctx of contexts) { - const index = stubPropertyNames.indexOf(ctx); - if (index === -1) continue; + // 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); + } - stubObjExpr.getProperty(stubPropertyNames[index])?.remove(); + const stubContexts = + mode === 'backend' + ? contexts.filter((ctx) => ctx !== 'backend') + : contexts; + for (const ctx of stubContexts) { + if (stubMap.has(ctx)) { + stubMap.get(ctx)?.remove(); + } } return {