mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-17 05:02:06 +00:00
fix: build performance
This commit is contained in:
@ -71,7 +71,7 @@ export function throttle<T extends (...params: unknown[]) => unknown>(
|
|||||||
}) as T;
|
}) 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();
|
const cache = new Map();
|
||||||
|
|
||||||
return ((...args) => {
|
return ((...args) => {
|
||||||
@ -84,7 +84,7 @@ function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
|
|||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
function retry<T extends (...params: unknown[]) => Promise<unknown>>(
|
export function retry<T extends (...params: unknown[]) => Promise<unknown>>(
|
||||||
fn: T,
|
fn: T,
|
||||||
{ retries = 3, delay = 1000 } = {},
|
{ retries = 3, delay = 1000 } = {},
|
||||||
) {
|
) {
|
||||||
@ -102,12 +102,3 @@ function retry<T extends (...params: unknown[]) => Promise<unknown>>(
|
|||||||
throw latestError;
|
throw latestError;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
singleton,
|
|
||||||
debounce,
|
|
||||||
cache,
|
|
||||||
throttle,
|
|
||||||
memoize,
|
|
||||||
retry,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -7,15 +7,15 @@ import { Project } from 'ts-morph';
|
|||||||
const snakeToCamel = (text: string) =>
|
const snakeToCamel = (text: string) =>
|
||||||
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
||||||
|
|
||||||
export const i18nImporter = () => {
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const globalProject = new Project({
|
||||||
const project = new Project({
|
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
||||||
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
skipAddingFilesFromTsConfig: true,
|
||||||
skipAddingFilesFromTsConfig: true,
|
skipLoadingLibFiles: true,
|
||||||
skipLoadingLibFiles: true,
|
skipFileDependencyResolution: true,
|
||||||
skipFileDependencyResolution: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
export const i18nImporter = () => {
|
||||||
const srcPath = resolve(__dirname, '..', 'src');
|
const srcPath = resolve(__dirname, '..', 'src');
|
||||||
const plugins = globSync(['src/i18n/resources/*.json']).map((path) => {
|
const plugins = globSync(['src/i18n/resources/*.json']).map((path) => {
|
||||||
const nameWithExt = basename(path);
|
const nameWithExt = basename(path);
|
||||||
@ -24,24 +24,28 @@ export const i18nImporter = () => {
|
|||||||
return { name, path };
|
return { name, path };
|
||||||
});
|
});
|
||||||
|
|
||||||
const src = project.createSourceFile('vm:i18n', (writer) => {
|
const src = globalProject.createSourceFile(
|
||||||
// prettier-ignore
|
'vm:i18n',
|
||||||
for (const { name, path } of plugins) {
|
(writer) => {
|
||||||
|
// prettier-ignore
|
||||||
|
for (const { name, path } of plugins) {
|
||||||
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
||||||
writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`);
|
writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
|
|
||||||
writer.writeLine('export const languageResources = {');
|
writer.writeLine('export const languageResources = {');
|
||||||
for (const { name } of plugins) {
|
for (const { name } of plugins) {
|
||||||
writer.writeLine(` "${name}": {`);
|
writer.writeLine(` "${name}": {`);
|
||||||
writer.writeLine(` translation: ${snakeToCamel(name)}Json,`);
|
writer.writeLine(` translation: ${snakeToCamel(name)}Json,`);
|
||||||
writer.writeLine(' },');
|
writer.writeLine(' },');
|
||||||
}
|
}
|
||||||
writer.writeLine('};');
|
writer.writeLine('};');
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
});
|
},
|
||||||
|
{ overwrite: true },
|
||||||
|
);
|
||||||
|
|
||||||
return src.getText();
|
return src.getText();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,17 +7,17 @@ import { Project } from 'ts-morph';
|
|||||||
const snakeToCamel = (text: string) =>
|
const snakeToCamel = (text: string) =>
|
||||||
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
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 = (
|
export const pluginVirtualModuleGenerator = (
|
||||||
mode: 'main' | 'preload' | 'renderer',
|
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 srcPath = resolve(__dirname, '..', 'src');
|
||||||
const plugins = globSync([
|
const plugins = globSync([
|
||||||
'src/plugins/*/index.{js,ts}',
|
'src/plugins/*/index.{js,ts}',
|
||||||
@ -35,35 +35,39 @@ export const pluginVirtualModuleGenerator = (
|
|||||||
return { name, path };
|
return { name, path };
|
||||||
});
|
});
|
||||||
|
|
||||||
const src = project.createSourceFile('vm:pluginIndexes', (writer) => {
|
const src = globalProject.createSourceFile(
|
||||||
// prettier-ignore
|
'vm:pluginIndexes',
|
||||||
for (const { name, path } of plugins) {
|
(writer) => {
|
||||||
|
// prettier-ignore
|
||||||
|
for (const { name, path } of plugins) {
|
||||||
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
||||||
writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`);
|
writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
|
|
||||||
// Context-specific exports
|
// Context-specific exports
|
||||||
writer.writeLine(`export const ${mode}Plugins = {`);
|
writer.writeLine(`export const ${mode}Plugins = {`);
|
||||||
for (const { name } of plugins) {
|
for (const { name } of plugins) {
|
||||||
const checkMode = mode === 'main' ? 'backend' : mode;
|
const checkMode = mode === 'main' ? 'backend' : mode;
|
||||||
// HACK: To avoid situation like importing renderer plugins in main
|
// HACK: To avoid situation like importing renderer plugins in main
|
||||||
writer.writeLine(
|
writer.writeLine(
|
||||||
` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`,
|
` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
writer.writeLine('};');
|
writer.writeLine('};');
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
|
|
||||||
// All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'>
|
// All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'>
|
||||||
writer.writeLine('export const allPlugins = {');
|
writer.writeLine('export const allPlugins = {');
|
||||||
for (const { name } of plugins) {
|
for (const { name } of plugins) {
|
||||||
writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`);
|
writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`);
|
||||||
}
|
}
|
||||||
writer.writeLine('};');
|
writer.writeLine('};');
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
});
|
},
|
||||||
|
{ overwrite: true },
|
||||||
|
);
|
||||||
|
|
||||||
return src.getText();
|
return src.getText();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFileSync } from 'node:fs';
|
||||||
import { resolve, basename, dirname } from 'node:path';
|
import { resolve, basename, dirname } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
@ -8,10 +8,34 @@ import {
|
|||||||
ts,
|
ts,
|
||||||
ObjectLiteralExpression,
|
ObjectLiteralExpression,
|
||||||
VariableDeclarationKind,
|
VariableDeclarationKind,
|
||||||
|
Node,
|
||||||
|
type ObjectLiteralElementLike,
|
||||||
} from 'ts-morph';
|
} from 'ts-morph';
|
||||||
|
|
||||||
import type { PluginOption } from 'vite';
|
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 (
|
export default function (
|
||||||
mode: 'backend' | 'preload' | 'renderer' | 'none',
|
mode: 'backend' | 'preload' | 'renderer' | 'none',
|
||||||
): PluginOption {
|
): PluginOption {
|
||||||
@ -22,131 +46,96 @@ export default function (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'ytm-plugin-loader',
|
name: 'ytm-plugin-loader',
|
||||||
async load(id) {
|
load(id) {
|
||||||
if (!pluginFilter(id)) return null;
|
if (!pluginFilter(id)) return null;
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
// Read file asynchronously
|
||||||
|
const fileContent = readFileSync(id, 'utf8');
|
||||||
const project = new Project({
|
// Create or update source file in the global project instance
|
||||||
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
const src = globalProject.createSourceFile(
|
||||||
skipAddingFilesFromTsConfig: true,
|
|
||||||
skipLoadingLibFiles: true,
|
|
||||||
skipFileDependencyResolution: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const src = project.createSourceFile(
|
|
||||||
'_pf' + basename(id),
|
'_pf' + basename(id),
|
||||||
await readFile(id, 'utf8'),
|
fileContent,
|
||||||
|
{ overwrite: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const exports = src.getExportedDeclarations();
|
const exports = src.getExportedDeclarations();
|
||||||
let objExpr: ObjectLiteralExpression | undefined = undefined;
|
let objExpr: ObjectLiteralExpression | undefined;
|
||||||
|
|
||||||
for (const [name, [expr]] of exports) {
|
// Identify the default export as an object literal, or via a 'createPlugin' call
|
||||||
if (name !== 'default') continue;
|
for (const [exportName, declarations] of exports) {
|
||||||
|
if (exportName !== 'default') continue;
|
||||||
switch (expr.getKind()) {
|
const expr = declarations[0];
|
||||||
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;
|
|
||||||
|
|
||||||
|
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];
|
const arg = callExpr.getArguments()[0];
|
||||||
if (arg.getKind() !== ts.SyntaxKind.ObjectLiteralExpression)
|
if (arg.getKind() === ts.SyntaxKind.ObjectLiteralExpression) {
|
||||||
continue;
|
objExpr = arg.asKindOrThrow(
|
||||||
|
ts.SyntaxKind.ObjectLiteralExpression,
|
||||||
objExpr = arg.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!objExpr) return null;
|
if (!objExpr) return null;
|
||||||
|
|
||||||
const properties = objExpr.getProperties();
|
// Build a map of property names to their AST nodes for fast lookup
|
||||||
const propertyNames = properties.map((prop) => {
|
const propMap = new Map<string, ObjectLiteralElementLike>();
|
||||||
switch (prop.getKind()) {
|
for (const prop of objExpr.getProperties()) {
|
||||||
case ts.SyntaxKind.PropertyAssignment:
|
const name = getPropertyName(prop);
|
||||||
return prop
|
if (name) propMap.set(name, 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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const contexts = ['backend', 'preload', 'renderer', 'menu'];
|
const contexts = ['backend', 'preload', 'renderer', 'menu'];
|
||||||
for (const ctx of contexts) {
|
for (const ctx of contexts) {
|
||||||
if (mode === 'none') {
|
if (mode === 'none' && propMap.has(ctx)) {
|
||||||
const index = propertyNames.indexOf(ctx);
|
propMap.get(ctx)?.remove();
|
||||||
if (index === -1) continue;
|
|
||||||
|
|
||||||
objExpr.getProperty(propertyNames[index])?.remove();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ctx === mode || (ctx === 'menu' && mode === 'backend')) continue;
|
||||||
if (ctx === mode) continue;
|
if (propMap.has(ctx)) propMap.get(ctx)?.remove();
|
||||||
if (ctx === 'menu' && mode === 'backend') continue;
|
|
||||||
|
|
||||||
const index = propertyNames.indexOf(ctx);
|
|
||||||
if (index === -1) continue;
|
|
||||||
|
|
||||||
objExpr.getProperty(propertyNames[index])?.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stubObjExpr = src
|
// Add an exported variable 'pluginStub' with the modified object literal's text
|
||||||
.addVariableStatement({
|
const varStmt = src.addVariableStatement({
|
||||||
isExported: true,
|
isExported: true,
|
||||||
declarationKind: VariableDeclarationKind.Const,
|
declarationKind: VariableDeclarationKind.Const,
|
||||||
declarations: [
|
declarations: [
|
||||||
{
|
{
|
||||||
name: 'pluginStub',
|
name: 'pluginStub',
|
||||||
initializer: (writer) => writer.write(objExpr.getText()),
|
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');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const stubObjExpr = varStmt
|
||||||
|
.getDeclarations()[0]
|
||||||
|
.getInitializerIfKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
||||||
|
|
||||||
if (mode === 'backend') contexts.pop();
|
// Similarly build a map for the stub properties
|
||||||
for (const ctx of contexts) {
|
const stubMap = new Map<string, ObjectLiteralElementLike>();
|
||||||
const index = stubPropertyNames.indexOf(ctx);
|
for (const prop of stubObjExpr.getProperties()) {
|
||||||
if (index === -1) continue;
|
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 {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user