diff --git a/.gitignore b/.gitignore index 3a15c7b..5f7af7b 100644 --- a/.gitignore +++ b/.gitignore @@ -178,4 +178,6 @@ out /build __fixtures__ /public -/.* \ No newline at end of file +/.data +/.dump +/.vscode \ No newline at end of file diff --git a/dist/commands/build/index.js b/dist/commands/build/index.js index 0381fda..5f18717 100644 --- a/dist/commands/build/index.js +++ b/dist/commands/build/index.js @@ -2,12 +2,14 @@ import { Command } from "commander"; import allPagesBundler from "../../functions/bundler/all-pages-bundler"; import { log } from "../../utils/log"; import init from "../../functions/init"; +import rewritePagesModule from "../../utils/rewrite-pages-module"; export default function () { return new Command("build") .description("Build Project") .action(async () => { process.env.NODE_ENV = "production"; process.env.BUILD = "true"; + await rewritePagesModule(); await init(); log.banner(); log.build("Building Project ..."); diff --git a/dist/commands/dev/index.js b/dist/commands/dev/index.js index 4cf18d0..7232638 100644 --- a/dist/commands/dev/index.js +++ b/dist/commands/dev/index.js @@ -2,12 +2,14 @@ import { Command } from "commander"; import startServer from "../../functions/server/start-server"; import { log } from "../../utils/log"; import bunextInit from "../../functions/bunext-init"; +import rewritePagesModule from "../../utils/rewrite-pages-module"; export default function () { return new Command("dev") .description("Run development server") .action(async () => { process.env.NODE_ENV == "development"; log.info("Running development server ..."); + await rewritePagesModule(); await bunextInit(); await startServer(); }); diff --git a/dist/functions/bundler/all-pages-bundler.js b/dist/functions/bundler/all-pages-bundler.js index bc8d99a..653171d 100644 --- a/dist/functions/bundler/all-pages-bundler.js +++ b/dist/functions/bundler/all-pages-bundler.js @@ -44,7 +44,10 @@ export default async function allPagesBundler(params) { if (!source.includes("server")) { return { contents: source, loader: "tsx" }; } - const strippedCode = stripServerSideLogic({ txt_code: source }); + const strippedCode = stripServerSideLogic({ + txt_code: source, + file_path: args.path, + }); return { contents: strippedCode, loader: "tsx", diff --git a/dist/functions/bundler/strip-server-side-logic.d.ts b/dist/functions/bundler/strip-server-side-logic.d.ts index 8398d8b..a82ba00 100644 --- a/dist/functions/bundler/strip-server-side-logic.d.ts +++ b/dist/functions/bundler/strip-server-side-logic.d.ts @@ -1,5 +1,6 @@ type Params = { txt_code: string; + file_path: string; }; -export default function stripServerSideLogic({ txt_code }: Params): string; +export default function stripServerSideLogic({ txt_code, file_path }: Params): string; export {}; diff --git a/dist/functions/bundler/strip-server-side-logic.js b/dist/functions/bundler/strip-server-side-logic.js index 97469d9..bab337c 100644 --- a/dist/functions/bundler/strip-server-side-logic.js +++ b/dist/functions/bundler/strip-server-side-logic.js @@ -1,61 +1,62 @@ +import path from "path"; import ts from "typescript"; -export default function stripServerSideLogic({ txt_code }) { - const sourceFile = ts.createSourceFile("temp.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); - const transformer = (context) => { - return (rootNode) => { - const visitor = (node) => { - if (ts.isVariableStatement(node) && - node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) { - const isServerExport = node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && - d.name.text === "server"); - if (isServerExport) - return undefined; // Remove it +export default function stripServerSideLogic({ txt_code, file_path }) { + 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) => { + const visitor = (node) => { + // 1. Strip the 'server' export + 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; + } + // 2. Convert relative imports to absolute imports + 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); + return ts.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts.factory.createStringLiteral(absolutePath), node.attributes); } - return ts.visitEachChild(node, visitor, context); - }; - return ts.visitNode(rootNode, visitor); + } + return ts.visitEachChild(node, visitor, context); }; + return ts.visitNode(rootNode, visitor); }; const result = ts.transform(sourceFile, [transformer]); - const printer = ts.createPrinter(); - const strippedCode = printer.printFile(result.transformed[0]); - const cleanSourceFile = ts.createSourceFile("clean.tsx", strippedCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); - // Simple reference check: if a named import isn't found in the text, drop it - const cleanupTransformer = (context) => { - return (rootNode) => { - const visitor = (node) => { - if (ts.isImportDeclaration(node)) { - const clause = node.importClause; - if (!clause) - return node; - // Handle named imports like { BunextPageProps, BunextPageServerFn } - if (clause.namedBindings && - ts.isNamedImports(clause.namedBindings)) { - const activeElements = clause.namedBindings.elements.filter((el) => { - const name = el.name.text; - // Check if the name appears anywhere else in the file - const regex = new RegExp(`\\b${name}\\b`, "g"); - const matches = strippedCode.match(regex); - return matches && matches.length > 1; // 1 for the import itself, >1 for usage - }); - if (activeElements.length === 0) - return undefined; - return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(activeElements)), node.moduleSpecifier, node.attributes); - } - // Handle default imports like 'import BunSQLite' - if (clause.name) { - const name = clause.name.text; - const regex = new RegExp(`\\b${name}\\b`, "g"); - const matches = strippedCode.match(regex); - if (!matches || matches.length <= 1) - return undefined; - } + 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 visitor = (node) => { + if (ts.isImportDeclaration(node)) { + const clause = node.importClause; + if (!clause) + return node; + 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; + return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(used)), node.moduleSpecifier, node.attributes); } - return ts.visitEachChild(node, visitor, context); - }; - return ts.visitNode(rootNode, visitor); + if (clause.name) { + const regex = new RegExp(`\\b${clause.name.text}\\b`, "g"); + if ((intermediate.match(regex) || []).length <= 1) + return undefined; + } + } + return ts.visitEachChild(node, visitor, context); }; + return ts.visitNode(rootNode, visitor); }; - const finalResult = ts.transform(cleanSourceFile, [cleanupTransformer]); - return printer.printFile(finalResult.transformed[0]); + const final = ts.transform(cleanSource, [cleanup]); + return printer.printFile(final.transformed[0]); } diff --git a/dist/utils/rewrite-pages-module.js b/dist/utils/rewrite-pages-module.js index a6a72cf..b088c18 100644 --- a/dist/utils/rewrite-pages-module.js +++ b/dist/utils/rewrite-pages-module.js @@ -17,6 +17,7 @@ export default async function rewritePagesModule(params) { const origin_page_content = await Bun.file(page_path).text(); const dst_page_content = stripServerSideLogic({ txt_code: origin_page_content, + file_path: page_path, }); await Bun.write(dst_path, dst_page_content, { createPath: true, diff --git a/package.json b/package.json index a553a60..d354972 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@moduletrace/bunext", "module": "index.ts", "type": "module", - "version": "1.0.10", + "version": "1.0.11", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index b1daccb..b1b5b50 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; import allPagesBundler from "../../functions/bundler/all-pages-bundler"; import { log } from "../../utils/log"; import init from "../../functions/init"; +import rewritePagesModule from "../../utils/rewrite-pages-module"; export default function () { return new Command("build") @@ -10,6 +11,7 @@ export default function () { process.env.NODE_ENV = "production"; process.env.BUILD = "true"; + await rewritePagesModule(); await init(); log.banner(); diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index 34a97c3..173da9e 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; import startServer from "../../functions/server/start-server"; import { log } from "../../utils/log"; import bunextInit from "../../functions/bunext-init"; +import rewritePagesModule from "../../utils/rewrite-pages-module"; export default function () { return new Command("dev") @@ -11,6 +12,7 @@ export default function () { log.info("Running development server ..."); + await rewritePagesModule(); await bunextInit(); await startServer(); diff --git a/src/functions/bundler/all-pages-bundler.ts b/src/functions/bundler/all-pages-bundler.ts index 401bbba..4bbc375 100644 --- a/src/functions/bundler/all-pages-bundler.ts +++ b/src/functions/bundler/all-pages-bundler.ts @@ -64,7 +64,10 @@ export default async function allPagesBundler(params?: Params) { return { contents: source, loader: "tsx" }; } - const strippedCode = stripServerSideLogic({ txt_code: source }); + const strippedCode = stripServerSideLogic({ + txt_code: source, + file_path: args.path, + }); return { contents: strippedCode, diff --git a/src/functions/bundler/strip-server-side-logic.tsx b/src/functions/bundler/strip-server-side-logic.tsx index 6353271..1410549 100644 --- a/src/functions/bundler/strip-server-side-logic.tsx +++ b/src/functions/bundler/strip-server-side-logic.tsx @@ -1,78 +1,101 @@ +import path from "path"; import ts from "typescript"; type Params = { txt_code: string; + file_path: string; }; -export default function stripServerSideLogic({ txt_code }: Params) { +export default function stripServerSideLogic({ txt_code, file_path }: Params) { const sourceFile = ts.createSourceFile( - "temp.tsx", + "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: ts.TransformerFactory = (context) => { - return (rootNode) => { + const transformer: ts.TransformerFactory = + (context) => (rootNode) => { const visitor = (node: ts.Node): ts.Node | undefined => { + // 1. Strip the 'server' export if ( ts.isVariableStatement(node) && node.modifiers?.some( (m) => m.kind === ts.SyntaxKind.ExportKeyword, ) ) { - const isServerExport = - node.declarationList.declarations.some( - (d) => - ts.isIdentifier(d.name) && - d.name.text === "server", - ); - if (isServerExport) return undefined; // Remove it + const isServer = node.declarationList.declarations.some( + (d) => + ts.isIdentifier(d.name) && d.name.text === "server", + ); + if (isServer) return undefined; } + + // 2. Convert relative imports to absolute imports + 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, + ); + + return ts.factory.updateImportDeclaration( + node, + node.modifiers, + node.importClause, + ts.factory.createStringLiteral(absolutePath), + node.attributes, + ); + } + } + return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(rootNode, visitor) as ts.SourceFile; }; - }; const result = ts.transform(sourceFile, [transformer]); - const printer = ts.createPrinter(); - const strippedCode = printer.printFile(result.transformed[0]); + const intermediate = printer.printFile(result.transformed[0]); - const cleanSourceFile = ts.createSourceFile( + // Pass 2: Cleanup unused imports (Same logic as before) + const cleanSource = ts.createSourceFile( "clean.tsx", - strippedCode, + intermediate, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX, ); - - // Simple reference check: if a named import isn't found in the text, drop it - const cleanupTransformer: ts.TransformerFactory = ( - context, - ) => { - return (rootNode) => { + const cleanup: ts.TransformerFactory = + (context) => (rootNode) => { const visitor = (node: ts.Node): ts.Node | undefined => { if (ts.isImportDeclaration(node)) { const clause = node.importClause; if (!clause) return node; - // Handle named imports like { BunextPageProps, BunextPageServerFn } if ( clause.namedBindings && ts.isNamedImports(clause.namedBindings) ) { - const activeElements = - clause.namedBindings.elements.filter((el) => { - const name = el.name.text; - // Check if the name appears anywhere else in the file - const regex = new RegExp(`\\b${name}\\b`, "g"); - const matches = strippedCode.match(regex); - return matches && matches.length > 1; // 1 for the import itself, >1 for usage - }); - - if (activeElements.length === 0) return undefined; + 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; return ts.factory.updateImportDeclaration( node, node.modifiers, @@ -80,27 +103,27 @@ export default function stripServerSideLogic({ txt_code }: Params) { clause, clause.isTypeOnly, clause.name, - ts.factory.createNamedImports(activeElements), + ts.factory.createNamedImports(used), ), node.moduleSpecifier, node.attributes, ); } - // Handle default imports like 'import BunSQLite' if (clause.name) { - const name = clause.name.text; - const regex = new RegExp(`\\b${name}\\b`, "g"); - const matches = strippedCode.match(regex); - if (!matches || matches.length <= 1) return undefined; + const regex = new RegExp( + `\\b${clause.name.text}\\b`, + "g", + ); + if ((intermediate.match(regex) || []).length <= 1) + return undefined; } } return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(rootNode, visitor) as ts.SourceFile; }; - }; - const finalResult = ts.transform(cleanSourceFile, [cleanupTransformer]); - return printer.printFile(finalResult.transformed[0]); + const final = ts.transform(cleanSource, [cleanup]); + return printer.printFile(final.transformed[0]); } diff --git a/src/utils/rewrite-pages-module.ts b/src/utils/rewrite-pages-module.ts index 1eb187f..1ce3175 100644 --- a/src/utils/rewrite-pages-module.ts +++ b/src/utils/rewrite-pages-module.ts @@ -24,6 +24,7 @@ export default async function rewritePagesModule(params?: Params) { const origin_page_content = await Bun.file(page_path).text(); const dst_page_content = stripServerSideLogic({ txt_code: origin_page_content, + file_path: page_path, }); await Bun.write(dst_path, dst_page_content, {