Files
lobehub/scripts/electronWorkflow/modifiers/appCode.mts

395 lines
12 KiB
TypeScript

import { Lang, parse } from '@ast-grep/napi';
import fs from 'fs-extra';
import path from 'node:path';
import { isDirectRun, runStandalone } from './utils.mjs';
const rewriteFile = async (filePath: string, transformer: (code: string) => string) => {
if (!fs.existsSync(filePath)) return;
const original = await fs.readFile(filePath, 'utf8');
const updated = transformer(original);
if (updated !== original) {
await fs.writeFile(filePath, updated);
}
};
const desktopOnlyVariantsPage = `import { DynamicLayoutProps } from '@/types/next';
import DesktopRouter from './router';
export default async (_props: DynamicLayoutProps) => {
return <DesktopRouter />;
};
`;
const stripDevPanel = (code: string) => {
let result = code.replace(/import DevPanel from ['"]@\/features\/DevPanel['"];\r?\n?/, '');
result = result.replace(
/[\t ]*{process\.env\.NODE_ENV === 'development' && <DevPanel \/>}\s*\r?\n?/,
'',
);
return result;
};
const removeSecurityTab = (code: string) => {
const componentEntryRegex =
/[\t ]*\[SettingsTabs\.Security]: dynamic\(\(\) => import\('\.\.\/security'\), {[\s\S]+?}\),\s*\r?\n/;
const securityTabRegex = /[\t ]*SettingsTabs\.Security,\s*\r?\n/;
return code.replace(componentEntryRegex, '').replace(securityTabRegex, '');
};
const removeSpeedInsightsAndAnalytics = (code: string) => {
const ast = parse(Lang.Tsx, code);
const root = ast.root();
const edits: Array<{ start: number; end: number; text: string }> = [];
// Remove SpeedInsights import
const speedInsightsImport = root.find({
rule: {
pattern: 'import { SpeedInsights } from $SOURCE',
},
});
if (speedInsightsImport) {
const range = speedInsightsImport.range();
edits.push({ start: range.start.index, end: range.end.index, text: '' });
}
// Remove Analytics import
const analyticsImport = root.find({
rule: {
pattern: 'import Analytics from $SOURCE',
},
});
if (analyticsImport) {
const range = analyticsImport.range();
edits.push({ start: range.start.index, end: range.end.index, text: '' });
}
// Remove Suspense block containing Analytics and SpeedInsights
// Find all Suspense blocks and check which one contains Analytics or SpeedInsights
const allSuspenseBlocks = root.findAll({
rule: {
pattern: '<Suspense fallback={null}>$$$</Suspense>',
},
});
for (const suspenseBlock of allSuspenseBlocks) {
const hasAnalytics = suspenseBlock.find({
rule: {
pattern: '<Analytics />',
},
});
const hasSpeedInsights = suspenseBlock.find({
rule: {
pattern: '<SpeedInsights />',
},
});
if (hasAnalytics || hasSpeedInsights) {
const range = suspenseBlock.range();
edits.push({ start: range.start.index, end: range.end.index, text: '' });
break; // Only remove the first matching Suspense block
}
}
// Remove inVercel variable if it's no longer used
const inVercelVar = root.find({
rule: {
pattern: 'const inVercel = process.env.VERCEL === "1";',
},
});
if (inVercelVar) {
// Check if inVercel is still used elsewhere
const allInVercelUsages = root.findAll({
rule: {
regex: 'inVercel',
},
});
// If only the declaration remains, remove it
if (allInVercelUsages.length === 1) {
const range = inVercelVar.range();
edits.push({ start: range.start.index, end: range.end.index, text: '' });
}
}
// Apply edits
if (edits.length === 0) return code;
edits.sort((a, b) => b.start - a.start);
let result = code;
for (const edit of edits) {
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
}
return result;
};
const removeClerkLogic = (code: string) => {
const ast = parse(Lang.Tsx, code);
const root = ast.root();
const edits: Array<{ start: number; end: number; text: string }> = [];
// Remove Clerk import - try multiple patterns
const clerkImportPatterns = [
{ pattern: 'import Clerk from $SOURCE' },
{ pattern: "import Clerk from './Clerk'" },
{ pattern: "import Clerk from './Clerk/index'" },
];
for (const pattern of clerkImportPatterns) {
const clerkImport = root.find({
rule: pattern,
});
if (clerkImport) {
const range = clerkImport.range();
edits.push({ start: range.start.index, end: range.end.index, text: '' });
break;
}
}
const findClerkIfStatement = () => {
const directMatch = root.find({
rule: {
pattern: 'if (authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH) { $$$ }',
},
});
if (directMatch) return directMatch;
const allIfStatements = root.findAll({
rule: {
kind: 'if_statement',
},
});
for (const ifStmt of allIfStatements) {
const condition = ifStmt.find({
rule: {
pattern: 'authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH',
},
});
if (condition) return ifStmt;
}
return null;
};
const clerkIfStatement = findClerkIfStatement();
if (clerkIfStatement) {
const ifRange = clerkIfStatement.range();
const elseClause = clerkIfStatement.find({
rule: {
kind: 'else_clause',
},
});
if (elseClause) {
const elseIfStmt = elseClause.find({
rule: {
kind: 'if_statement',
},
});
if (elseIfStmt) {
// Promote the first else-if to a top-level if and keep the rest of the chain
const elseRange = elseClause.range();
const replacement = code
.slice(elseRange.start.index, elseRange.end.index)
.replace(/^\s*else\s+/, '');
edits.push({
start: ifRange.start.index,
end: ifRange.end.index,
text: replacement,
});
} else {
const elseBlock = elseClause.find({
rule: {
kind: 'statement_block',
},
});
if (elseBlock) {
edits.push({
start: ifRange.start.index,
end: ifRange.end.index,
text: code.slice(elseBlock.range().start.index, elseBlock.range().end.index),
});
} else {
edits.push({ start: ifRange.start.index, end: ifRange.end.index, text: '' });
}
}
} else {
edits.push({ start: ifRange.start.index, end: ifRange.end.index, text: '' });
}
}
// Apply edits
if (edits.length === 0) return code;
edits.sort((a, b) => b.start - a.start);
let result = code;
for (const edit of edits) {
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
}
return result;
};
const removeManifestFromMetadata = (code: string) => {
const ast = parse(Lang.Tsx, code);
const root = ast.root();
const edits: Array<{ start: number; end: number; text: string }> = [];
// Find generateMetadata function
const generateMetadataFunc = root.find({
rule: {
pattern: 'export const generateMetadata = async ($$$) => { $$$ }',
},
});
if (!generateMetadataFunc) return code;
// Find return statement
const returnStatement = generateMetadataFunc.find({
rule: {
kind: 'return_statement',
},
});
if (!returnStatement) return code;
// Find the object in return statement
const returnObject = returnStatement.find({
rule: {
kind: 'object',
},
});
if (!returnObject) return code;
// Find all pair nodes (key-value pairs in the object)
const allPairs = returnObject.findAll({
rule: {
kind: 'pair',
},
});
const keysToRemove = ['manifest', 'metadataBase'];
for (const pair of allPairs) {
// Find the property_identifier or identifier
const key = pair.find({
rule: {
any: [{ kind: 'property_identifier' }, { kind: 'identifier' }],
},
});
if (key && keysToRemove.includes(key.text())) {
const range = pair.range();
// Include the trailing comma if present
const afterPair = code.slice(range.end.index, range.end.index + 10);
const commaMatch = afterPair.match(/^,\s*/);
const endIndex = commaMatch ? range.end.index + commaMatch[0].length : range.end.index;
edits.push({
start: range.start.index,
end: endIndex,
text: '',
});
}
}
// Apply edits
if (edits.length === 0) return code;
edits.sort((a, b) => b.start - a.start);
let result = code;
for (const edit of edits) {
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
}
return result;
};
export const modifyAppCode = async (TEMP_DIR: string) => {
// 1. Replace src/app/[variants]/page.tsx with a desktop-only entry
const variantsPagePath = path.join(TEMP_DIR, 'src/app/[variants]/page.tsx');
if (fs.existsSync(variantsPagePath)) {
console.log(' Processing src/app/[variants]/page.tsx...');
await fs.writeFile(variantsPagePath, desktopOnlyVariantsPage);
}
// 2. Remove DevPanel from src/layout/GlobalProvider/index.tsx
const globalProviderPath = path.join(TEMP_DIR, 'src/layout/GlobalProvider/index.tsx');
if (fs.existsSync(globalProviderPath)) {
console.log(' Processing src/layout/GlobalProvider/index.tsx...');
await rewriteFile(globalProviderPath, stripDevPanel);
}
// 3. Delete src/app/[variants]/(main)/settings/security directory
const securityDirPath = path.join(TEMP_DIR, 'src/app/[variants]/(main)/settings/security');
if (fs.existsSync(securityDirPath)) {
console.log(' Deleting src/app/[variants]/(main)/settings/security directory...');
await fs.remove(securityDirPath);
}
// 4. Remove Security tab wiring from SettingsContent
const settingsContentPath = path.join(
TEMP_DIR,
'src/app/[variants]/(main)/settings/features/SettingsContent.tsx',
);
if (fs.existsSync(settingsContentPath)) {
console.log(' Processing src/app/[variants]/(main)/settings/features/SettingsContent.tsx...');
await rewriteFile(settingsContentPath, removeSecurityTab);
}
// 5. Remove SpeedInsights and Analytics from src/app/[variants]/layout.tsx
const variantsLayoutPath = path.join(TEMP_DIR, 'src/app/[variants]/layout.tsx');
if (fs.existsSync(variantsLayoutPath)) {
console.log(' Processing src/app/[variants]/layout.tsx...');
await rewriteFile(variantsLayoutPath, removeSpeedInsightsAndAnalytics);
}
// 6. Remove Clerk logic from src/layout/AuthProvider/index.tsx
const authProviderPath = path.join(TEMP_DIR, 'src/layout/AuthProvider/index.tsx');
if (fs.existsSync(authProviderPath)) {
console.log(' Processing src/layout/AuthProvider/index.tsx...');
await rewriteFile(authProviderPath, removeClerkLogic);
}
// 7. Replace mdx Image component with next/image export
const mdxImagePath = path.join(TEMP_DIR, 'src/components/mdx/Image.tsx');
if (fs.existsSync(mdxImagePath)) {
console.log(' Processing src/components/mdx/Image.tsx...');
await fs.writeFile(mdxImagePath, "export { default } from 'next/image';\n");
}
// 8. Remove manifest from metadata
const metadataPath = path.join(TEMP_DIR, 'src/app/[variants]/metadata.ts');
if (fs.existsSync(metadataPath)) {
console.log(' Processing src/app/[variants]/metadata.ts...');
await rewriteFile(metadataPath, removeManifestFromMetadata);
}
};
if (isDirectRun(import.meta.url)) {
await runStandalone('modifyAppCode', modifyAppCode, [
{ lang: Lang.Tsx, path: 'src/app/[variants]/page.tsx' },
{ lang: Lang.Tsx, path: 'src/layout/GlobalProvider/index.tsx' },
{ lang: Lang.Tsx, path: 'src/app/[variants]/(main)/settings/features/SettingsContent.tsx' },
{ lang: Lang.Tsx, path: 'src/app/[variants]/layout.tsx' },
{ lang: Lang.Tsx, path: 'src/layout/AuthProvider/index.tsx' },
{ lang: Lang.Tsx, path: 'src/components/mdx/Image.tsx' },
{ lang: Lang.Tsx, path: 'src/app/[variants]/metadata.ts' },
]);
}