fix: build performance

This commit is contained in:
JellyBrick
2025-02-17 02:44:47 +09:00
parent f47262d27b
commit cca493b7d5
4 changed files with 153 additions and 165 deletions

View File

@ -71,7 +71,7 @@ export function throttle<T extends (...params: unknown[]) => unknown>(
}) as T;
}
function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
export function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
const cache = new Map();
return ((...args) => {
@ -84,7 +84,7 @@ function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
}) as T;
}
function retry<T extends (...params: unknown[]) => Promise<unknown>>(
export function retry<T extends (...params: unknown[]) => Promise<unknown>>(
fn: T,
{ retries = 3, delay = 1000 } = {},
) {
@ -102,12 +102,3 @@ function retry<T extends (...params: unknown[]) => Promise<unknown>>(
throw latestError;
};
}
export default {
singleton,
debounce,
cache,
throttle,
memoize,
retry,
};

View File

@ -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();
};

View File

@ -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<Plugin, 'backend' | 'preload' | 'renderer'>
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<Plugin, 'backend' | 'preload' | 'renderer'>
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();
};

View File

@ -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 propertys 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<string, ObjectLiteralElementLike>();
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<string, ObjectLiteralElementLike>();
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 {