diff --git a/README.md b/README.md index b0a1f24..4322e86 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,6 @@ The goal is a framework that is: Configure the `@moduletrace` scope to point at the registry — pick one: -**`.npmrc`** (works with npm, bun, and most tools): - -```ini -@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/ -``` - **`bunfig.toml`** (Bun-native): ```toml @@ -84,6 +78,12 @@ Configure the `@moduletrace` scope to point at the registry — pick one: "@moduletrace" = { registry = "https://git.tben.me/api/packages/moduletrace/npm/" } ``` +**`.npmrc`** (works with npm, bun, and most tools): + +```ini +@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/ +``` + Then install: ```bash diff --git a/dist/functions/bundler/strip-server-side-logic.js b/dist/functions/bundler/strip-server-side-logic.js index bab337c..2280cf6 100644 --- a/dist/functions/bundler/strip-server-side-logic.js +++ b/dist/functions/bundler/strip-server-side-logic.js @@ -1,25 +1,29 @@ import path from "path"; import ts from "typescript"; export default function stripServerSideLogic({ txt_code, file_path }) { + const file_dir_name = path.dirname(file_path); + // 1. Initial Parse of the source const sourceFile = ts.createSourceFile("file.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); const printer = ts.createPrinter(); - const file_fir_name = path.dirname(file_path); - const transformer = (context) => (rootNode) => { + /** + * PASS 1: Remove 'server' export and resolve absolute paths + */ + const stripTransformer = (context) => (rootNode) => { const visitor = (node) => { - // 1. Strip the 'server' export + // Remove 'export const server' if (ts.isVariableStatement(node) && node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) { - const isServer = node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && d.name.text === "server"); - if (isServer) + if (node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && + d.name.text === "server")) { return undefined; + } } - // 2. Convert relative imports to absolute imports + // Convert relative imports to absolute if (ts.isImportDeclaration(node)) { - const moduleSpecifier = node.moduleSpecifier; - if (ts.isStringLiteral(moduleSpecifier) && - moduleSpecifier.text.startsWith(".")) { - // Resolve the relative path to an absolute filesystem path - const absolutePath = path.resolve(file_fir_name, moduleSpecifier.text); + const specifier = node.moduleSpecifier; + if (ts.isStringLiteral(specifier) && + specifier.text.startsWith(".")) { + const absolutePath = path.resolve(file_dir_name, specifier.text); return ts.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts.factory.createStringLiteral(absolutePath), node.attributes); } } @@ -27,36 +31,62 @@ export default function stripServerSideLogic({ txt_code, file_path }) { }; return ts.visitNode(rootNode, visitor); }; - const result = ts.transform(sourceFile, [transformer]); - const intermediate = printer.printFile(result.transformed[0]); - // Pass 2: Cleanup unused imports (Same logic as before) - const cleanSource = ts.createSourceFile("clean.tsx", intermediate, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); - const cleanup = (context) => (rootNode) => { + const step1Result = ts.transform(sourceFile, [stripTransformer]); + const intermediateCode = printer.printFile(step1Result.transformed[0]); + // 2. Re-parse to get a "Clean Slate" AST + const cleanSource = ts.createSourceFile("clean.tsx", intermediateCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + /** + * PASS 2: Collect all used Identifiers and Prune Imports + */ + const usedIdentifiers = new Set(); + function walkAndFindUsages(node) { + if (ts.isIdentifier(node)) { + // We only care about identifiers that AREN'T the names in the import statements themselves + const parent = node.parent; + const isImportName = ts.isImportSpecifier(parent) || + ts.isImportClause(parent) || + ts.isNamespaceImport(parent); + if (!isImportName) { + usedIdentifiers.add(node.text); + } + } + ts.forEachChild(node, walkAndFindUsages); + } + walkAndFindUsages(cleanSource); + const cleanupTransformer = (context) => (rootNode) => { const visitor = (node) => { if (ts.isImportDeclaration(node)) { const clause = node.importClause; if (!clause) return node; + // 1. Clean Named Imports: { A, B } if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { - const used = clause.namedBindings.elements.filter((el) => { - const regex = new RegExp(`\\b${el.name.text}\\b`, "g"); - return ((intermediate.match(regex) || []).length > 1); - }); - if (used.length === 0) + const activeElements = clause.namedBindings.elements.filter((el) => usedIdentifiers.has(el.name.text)); + // If no named imports are used and there is no default import, nix the whole line + if (activeElements.length === 0 && !clause.name) return undefined; - return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(used)), node.moduleSpecifier, node.attributes); + // If we have some named imports left, update the node + return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, + // Only keep default import if it's used + clause.name && + usedIdentifiers.has(clause.name.text) + ? clause.name + : undefined, ts.factory.createNamedImports(activeElements)), node.moduleSpecifier, node.attributes); } - if (clause.name) { - const regex = new RegExp(`\\b${clause.name.text}\\b`, "g"); - if ((intermediate.match(regex) || []).length <= 1) + // 2. Clean Default Imports: import X from '...' + if (clause.name && !usedIdentifiers.has(clause.name.text)) { + // If there are no named bindings attached, nix the whole line + if (!clause.namedBindings) return undefined; + // Otherwise, just strip the default name and keep the named bindings + return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, undefined, clause.namedBindings), node.moduleSpecifier, node.attributes); } } return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(rootNode, visitor); }; - const final = ts.transform(cleanSource, [cleanup]); - return printer.printFile(final.transformed[0]); + const finalResult = ts.transform(cleanSource, [cleanupTransformer]); + return printer.printFile(finalResult.transformed[0]); } diff --git a/dist/utils/rewrite-pages-module.js b/dist/utils/rewrite-pages-module.js index b088c18..3d715fd 100644 --- a/dist/utils/rewrite-pages-module.js +++ b/dist/utils/rewrite-pages-module.js @@ -14,6 +14,9 @@ export default async function rewritePagesModule(params) { for (let i = 0; i < target_pages.length; i++) { const page_path = target_pages[i]; const dst_path = pagePathTransform({ page_path }); + if (page_path.match(/__root\.tsx?/)) { + continue; + } const origin_page_content = await Bun.file(page_path).text(); const dst_page_content = stripServerSideLogic({ txt_code: origin_page_content, diff --git a/package.json b/package.json index ccc6af1..1e8260a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@moduletrace/bunext", "module": "index.ts", "type": "module", - "version": "1.0.13", + "version": "1.0.14", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { diff --git a/src/functions/bundler/strip-server-side-logic.tsx b/src/functions/bundler/strip-server-side-logic.tsx index 1410549..f719e94 100644 --- a/src/functions/bundler/strip-server-side-logic.tsx +++ b/src/functions/bundler/strip-server-side-logic.tsx @@ -7,6 +7,9 @@ type Params = { }; export default function stripServerSideLogic({ txt_code, file_path }: Params) { + const file_dir_name = path.dirname(file_path); + + // 1. Initial Parse of the source const sourceFile = ts.createSourceFile( "file.tsx", txt_code, @@ -15,38 +18,42 @@ export default function stripServerSideLogic({ txt_code, file_path }: Params) { ts.ScriptKind.TSX, ); const printer = ts.createPrinter(); - const file_fir_name = path.dirname(file_path); - const transformer: ts.TransformerFactory = + /** + * PASS 1: Remove 'server' export and resolve absolute paths + */ + const stripTransformer: ts.TransformerFactory = (context) => (rootNode) => { const visitor = (node: ts.Node): ts.Node | undefined => { - // 1. Strip the 'server' export + // Remove 'export const server' if ( ts.isVariableStatement(node) && node.modifiers?.some( (m) => m.kind === ts.SyntaxKind.ExportKeyword, ) ) { - const isServer = node.declarationList.declarations.some( - (d) => - ts.isIdentifier(d.name) && d.name.text === "server", - ); - if (isServer) return undefined; + if ( + node.declarationList.declarations.some( + (d) => + ts.isIdentifier(d.name) && + d.name.text === "server", + ) + ) { + return undefined; + } } - // 2. Convert relative imports to absolute imports + // Convert relative imports to absolute if (ts.isImportDeclaration(node)) { - const moduleSpecifier = node.moduleSpecifier; + const specifier = node.moduleSpecifier; if ( - ts.isStringLiteral(moduleSpecifier) && - moduleSpecifier.text.startsWith(".") + ts.isStringLiteral(specifier) && + specifier.text.startsWith(".") ) { - // Resolve the relative path to an absolute filesystem path const absolutePath = path.resolve( - file_fir_name, - moduleSpecifier.text, + file_dir_name, + specifier.text, ); - return ts.factory.updateImportDeclaration( node, node.modifiers, @@ -56,67 +63,102 @@ export default function stripServerSideLogic({ txt_code, file_path }: Params) { ); } } - return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(rootNode, visitor) as ts.SourceFile; }; - const result = ts.transform(sourceFile, [transformer]); - const intermediate = printer.printFile(result.transformed[0]); + const step1Result = ts.transform(sourceFile, [stripTransformer]); + const intermediateCode = printer.printFile(step1Result.transformed[0]); - // Pass 2: Cleanup unused imports (Same logic as before) + // 2. Re-parse to get a "Clean Slate" AST const cleanSource = ts.createSourceFile( "clean.tsx", - intermediate, + intermediateCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX, ); - const cleanup: ts.TransformerFactory = + + /** + * PASS 2: Collect all used Identifiers and Prune Imports + */ + const usedIdentifiers = new Set(); + + function walkAndFindUsages(node: ts.Node) { + if (ts.isIdentifier(node)) { + // We only care about identifiers that AREN'T the names in the import statements themselves + const parent = node.parent; + const isImportName = + ts.isImportSpecifier(parent) || + ts.isImportClause(parent) || + ts.isNamespaceImport(parent); + if (!isImportName) { + usedIdentifiers.add(node.text); + } + } + ts.forEachChild(node, walkAndFindUsages); + } + walkAndFindUsages(cleanSource); + + const cleanupTransformer: ts.TransformerFactory = (context) => (rootNode) => { const visitor = (node: ts.Node): ts.Node | undefined => { if (ts.isImportDeclaration(node)) { const clause = node.importClause; if (!clause) return node; + // 1. Clean Named Imports: { A, B } if ( clause.namedBindings && ts.isNamedImports(clause.namedBindings) ) { - const used = clause.namedBindings.elements.filter( - (el) => { - const regex = new RegExp( - `\\b${el.name.text}\\b`, - "g", - ); - return ( - (intermediate.match(regex) || []).length > 1 - ); - }, - ); - if (used.length === 0) return undefined; + const activeElements = + clause.namedBindings.elements.filter((el) => + usedIdentifiers.has(el.name.text), + ); + + // If no named imports are used and there is no default import, nix the whole line + if (activeElements.length === 0 && !clause.name) + return undefined; + + // If we have some named imports left, update the node return ts.factory.updateImportDeclaration( node, node.modifiers, ts.factory.updateImportClause( clause, clause.isTypeOnly, - clause.name, - ts.factory.createNamedImports(used), + // Only keep default import if it's used + clause.name && + usedIdentifiers.has(clause.name.text) + ? clause.name + : undefined, + ts.factory.createNamedImports(activeElements), ), node.moduleSpecifier, node.attributes, ); } - if (clause.name) { - const regex = new RegExp( - `\\b${clause.name.text}\\b`, - "g", + // 2. Clean Default Imports: import X from '...' + if (clause.name && !usedIdentifiers.has(clause.name.text)) { + // If there are no named bindings attached, nix the whole line + if (!clause.namedBindings) return undefined; + + // Otherwise, just strip the default name and keep the named bindings + return ts.factory.updateImportDeclaration( + node, + node.modifiers, + ts.factory.updateImportClause( + clause, + clause.isTypeOnly, + undefined, + clause.namedBindings, + ), + node.moduleSpecifier, + node.attributes, ); - if ((intermediate.match(regex) || []).length <= 1) - return undefined; } } return ts.visitEachChild(node, visitor, context); @@ -124,6 +166,6 @@ export default function stripServerSideLogic({ txt_code, file_path }: Params) { return ts.visitNode(rootNode, visitor) as ts.SourceFile; }; - const final = ts.transform(cleanSource, [cleanup]); - return printer.printFile(final.transformed[0]); + const finalResult = ts.transform(cleanSource, [cleanupTransformer]); + return printer.printFile(finalResult.transformed[0]); } diff --git a/src/utils/rewrite-pages-module.ts b/src/utils/rewrite-pages-module.ts index 1ce3175..15a2e1c 100644 --- a/src/utils/rewrite-pages-module.ts +++ b/src/utils/rewrite-pages-module.ts @@ -21,6 +21,10 @@ export default async function rewritePagesModule(params?: Params) { const page_path = target_pages[i]; const dst_path = pagePathTransform({ page_path }); + if (page_path.match(/__root\.tsx?/)) { + continue; + } + const origin_page_content = await Bun.file(page_path).text(); const dst_page_content = stripServerSideLogic({ txt_code: origin_page_content,