Bugfix. Update Tsx server stripping function to have absolute paths. Run the rewrite function on dev and build commands.

This commit is contained in:
Benjamin Toby 2026-03-22 10:53:27 +01:00
parent a9af20a8b2
commit 3d044ee79e
13 changed files with 142 additions and 99 deletions

4
.gitignore vendored
View File

@ -178,4 +178,6 @@ out
/build /build
__fixtures__ __fixtures__
/public /public
/.* /.data
/.dump
/.vscode

View File

@ -2,12 +2,14 @@ import { Command } from "commander";
import allPagesBundler from "../../functions/bundler/all-pages-bundler"; import allPagesBundler from "../../functions/bundler/all-pages-bundler";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import init from "../../functions/init"; import init from "../../functions/init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
export default function () { export default function () {
return new Command("build") return new Command("build")
.description("Build Project") .description("Build Project")
.action(async () => { .action(async () => {
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
process.env.BUILD = "true"; process.env.BUILD = "true";
await rewritePagesModule();
await init(); await init();
log.banner(); log.banner();
log.build("Building Project ..."); log.build("Building Project ...");

View File

@ -2,12 +2,14 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server"; import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init"; import bunextInit from "../../functions/bunext-init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
export default function () { export default function () {
return new Command("dev") return new Command("dev")
.description("Run development server") .description("Run development server")
.action(async () => { .action(async () => {
process.env.NODE_ENV == "development"; process.env.NODE_ENV == "development";
log.info("Running development server ..."); log.info("Running development server ...");
await rewritePagesModule();
await bunextInit(); await bunextInit();
await startServer(); await startServer();
}); });

View File

@ -44,7 +44,10 @@ export default async function allPagesBundler(params) {
if (!source.includes("server")) { if (!source.includes("server")) {
return { contents: source, loader: "tsx" }; return { contents: source, loader: "tsx" };
} }
const strippedCode = stripServerSideLogic({ txt_code: source }); const strippedCode = stripServerSideLogic({
txt_code: source,
file_path: args.path,
});
return { return {
contents: strippedCode, contents: strippedCode,
loader: "tsx", loader: "tsx",

View File

@ -1,5 +1,6 @@
type Params = { type Params = {
txt_code: string; 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 {}; export {};

View File

@ -1,61 +1,62 @@
import path from "path";
import ts from "typescript"; import ts from "typescript";
export default function stripServerSideLogic({ txt_code }) { export default function stripServerSideLogic({ txt_code, file_path }) {
const sourceFile = ts.createSourceFile("temp.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); const sourceFile = ts.createSourceFile("file.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const transformer = (context) => { const printer = ts.createPrinter();
return (rootNode) => { const file_fir_name = path.dirname(file_path);
const visitor = (node) => { const transformer = (context) => (rootNode) => {
if (ts.isVariableStatement(node) && const visitor = (node) => {
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) { // 1. Strip the 'server' export
const isServerExport = node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && if (ts.isVariableStatement(node) &&
d.name.text === "server"); node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
if (isServerExport) const isServer = node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && d.name.text === "server");
return undefined; // Remove it 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.visitEachChild(node, visitor, context);
return ts.visitNode(rootNode, visitor);
}; };
return ts.visitNode(rootNode, visitor);
}; };
const result = ts.transform(sourceFile, [transformer]); const result = ts.transform(sourceFile, [transformer]);
const printer = ts.createPrinter(); const intermediate = printer.printFile(result.transformed[0]);
const strippedCode = printer.printFile(result.transformed[0]); // Pass 2: Cleanup unused imports (Same logic as before)
const cleanSourceFile = ts.createSourceFile("clean.tsx", strippedCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); const cleanSource = ts.createSourceFile("clean.tsx", intermediate, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
// Simple reference check: if a named import isn't found in the text, drop it const cleanup = (context) => (rootNode) => {
const cleanupTransformer = (context) => { const visitor = (node) => {
return (rootNode) => { if (ts.isImportDeclaration(node)) {
const visitor = (node) => { const clause = node.importClause;
if (ts.isImportDeclaration(node)) { if (!clause)
const clause = node.importClause; return node;
if (!clause) if (clause.namedBindings &&
return node; ts.isNamedImports(clause.namedBindings)) {
// Handle named imports like { BunextPageProps, BunextPageServerFn } const used = clause.namedBindings.elements.filter((el) => {
if (clause.namedBindings && const regex = new RegExp(`\\b${el.name.text}\\b`, "g");
ts.isNamedImports(clause.namedBindings)) { return ((intermediate.match(regex) || []).length > 1);
const activeElements = clause.namedBindings.elements.filter((el) => { });
const name = el.name.text; if (used.length === 0)
// Check if the name appears anywhere else in the file return undefined;
const regex = new RegExp(`\\b${name}\\b`, "g"); return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(used)), node.moduleSpecifier, node.attributes);
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;
}
} }
return ts.visitEachChild(node, visitor, context); if (clause.name) {
}; const regex = new RegExp(`\\b${clause.name.text}\\b`, "g");
return ts.visitNode(rootNode, visitor); 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]); const final = ts.transform(cleanSource, [cleanup]);
return printer.printFile(finalResult.transformed[0]); return printer.printFile(final.transformed[0]);
} }

View File

@ -17,6 +17,7 @@ export default async function rewritePagesModule(params) {
const origin_page_content = await Bun.file(page_path).text(); const origin_page_content = await Bun.file(page_path).text();
const dst_page_content = stripServerSideLogic({ const dst_page_content = stripServerSideLogic({
txt_code: origin_page_content, txt_code: origin_page_content,
file_path: page_path,
}); });
await Bun.write(dst_path, dst_page_content, { await Bun.write(dst_path, dst_page_content, {
createPath: true, createPath: true,

View File

@ -2,7 +2,7 @@
"name": "@moduletrace/bunext", "name": "@moduletrace/bunext",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "1.0.10", "version": "1.0.11",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@ -2,6 +2,7 @@ import { Command } from "commander";
import allPagesBundler from "../../functions/bundler/all-pages-bundler"; import allPagesBundler from "../../functions/bundler/all-pages-bundler";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import init from "../../functions/init"; import init from "../../functions/init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
export default function () { export default function () {
return new Command("build") return new Command("build")
@ -10,6 +11,7 @@ export default function () {
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
process.env.BUILD = "true"; process.env.BUILD = "true";
await rewritePagesModule();
await init(); await init();
log.banner(); log.banner();

View File

@ -2,6 +2,7 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server"; import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init"; import bunextInit from "../../functions/bunext-init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
export default function () { export default function () {
return new Command("dev") return new Command("dev")
@ -11,6 +12,7 @@ export default function () {
log.info("Running development server ..."); log.info("Running development server ...");
await rewritePagesModule();
await bunextInit(); await bunextInit();
await startServer(); await startServer();

View File

@ -64,7 +64,10 @@ export default async function allPagesBundler(params?: Params) {
return { contents: source, loader: "tsx" }; return { contents: source, loader: "tsx" };
} }
const strippedCode = stripServerSideLogic({ txt_code: source }); const strippedCode = stripServerSideLogic({
txt_code: source,
file_path: args.path,
});
return { return {
contents: strippedCode, contents: strippedCode,

View File

@ -1,78 +1,101 @@
import path from "path";
import ts from "typescript"; import ts from "typescript";
type Params = { type Params = {
txt_code: string; 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( const sourceFile = ts.createSourceFile(
"temp.tsx", "file.tsx",
txt_code, txt_code,
ts.ScriptTarget.Latest, ts.ScriptTarget.Latest,
true, true,
ts.ScriptKind.TSX, ts.ScriptKind.TSX,
); );
const printer = ts.createPrinter();
const file_fir_name = path.dirname(file_path);
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => { const transformer: ts.TransformerFactory<ts.SourceFile> =
return (rootNode) => { (context) => (rootNode) => {
const visitor = (node: ts.Node): ts.Node | undefined => { const visitor = (node: ts.Node): ts.Node | undefined => {
// 1. Strip the 'server' export
if ( if (
ts.isVariableStatement(node) && ts.isVariableStatement(node) &&
node.modifiers?.some( node.modifiers?.some(
(m) => m.kind === ts.SyntaxKind.ExportKeyword, (m) => m.kind === ts.SyntaxKind.ExportKeyword,
) )
) { ) {
const isServerExport = const isServer = node.declarationList.declarations.some(
node.declarationList.declarations.some( (d) =>
(d) => ts.isIdentifier(d.name) && d.name.text === "server",
ts.isIdentifier(d.name) && );
d.name.text === "server", if (isServer) return undefined;
);
if (isServerExport) return undefined; // Remove it
} }
// 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.visitEachChild(node, visitor, context);
}; };
return ts.visitNode(rootNode, visitor) as ts.SourceFile; return ts.visitNode(rootNode, visitor) as ts.SourceFile;
}; };
};
const result = ts.transform(sourceFile, [transformer]); const result = ts.transform(sourceFile, [transformer]);
const printer = ts.createPrinter(); const intermediate = printer.printFile(result.transformed[0]);
const strippedCode = printer.printFile(result.transformed[0]);
const cleanSourceFile = ts.createSourceFile( // Pass 2: Cleanup unused imports (Same logic as before)
const cleanSource = ts.createSourceFile(
"clean.tsx", "clean.tsx",
strippedCode, intermediate,
ts.ScriptTarget.Latest, ts.ScriptTarget.Latest,
true, true,
ts.ScriptKind.TSX, ts.ScriptKind.TSX,
); );
const cleanup: ts.TransformerFactory<ts.SourceFile> =
// Simple reference check: if a named import isn't found in the text, drop it (context) => (rootNode) => {
const cleanupTransformer: ts.TransformerFactory<ts.SourceFile> = (
context,
) => {
return (rootNode) => {
const visitor = (node: ts.Node): ts.Node | undefined => { const visitor = (node: ts.Node): ts.Node | undefined => {
if (ts.isImportDeclaration(node)) { if (ts.isImportDeclaration(node)) {
const clause = node.importClause; const clause = node.importClause;
if (!clause) return node; if (!clause) return node;
// Handle named imports like { BunextPageProps, BunextPageServerFn }
if ( if (
clause.namedBindings && clause.namedBindings &&
ts.isNamedImports(clause.namedBindings) ts.isNamedImports(clause.namedBindings)
) { ) {
const activeElements = const used = clause.namedBindings.elements.filter(
clause.namedBindings.elements.filter((el) => { (el) => {
const name = el.name.text; const regex = new RegExp(
// Check if the name appears anywhere else in the file `\\b${el.name.text}\\b`,
const regex = new RegExp(`\\b${name}\\b`, "g"); "g",
const matches = strippedCode.match(regex); );
return matches && matches.length > 1; // 1 for the import itself, >1 for usage return (
}); (intermediate.match(regex) || []).length > 1
);
if (activeElements.length === 0) return undefined; },
);
if (used.length === 0) return undefined;
return ts.factory.updateImportDeclaration( return ts.factory.updateImportDeclaration(
node, node,
node.modifiers, node.modifiers,
@ -80,27 +103,27 @@ export default function stripServerSideLogic({ txt_code }: Params) {
clause, clause,
clause.isTypeOnly, clause.isTypeOnly,
clause.name, clause.name,
ts.factory.createNamedImports(activeElements), ts.factory.createNamedImports(used),
), ),
node.moduleSpecifier, node.moduleSpecifier,
node.attributes, node.attributes,
); );
} }
// Handle default imports like 'import BunSQLite'
if (clause.name) { if (clause.name) {
const name = clause.name.text; const regex = new RegExp(
const regex = new RegExp(`\\b${name}\\b`, "g"); `\\b${clause.name.text}\\b`,
const matches = strippedCode.match(regex); "g",
if (!matches || matches.length <= 1) return undefined; );
if ((intermediate.match(regex) || []).length <= 1)
return undefined;
} }
} }
return ts.visitEachChild(node, visitor, context); return ts.visitEachChild(node, visitor, context);
}; };
return ts.visitNode(rootNode, visitor) as ts.SourceFile; return ts.visitNode(rootNode, visitor) as ts.SourceFile;
}; };
};
const finalResult = ts.transform(cleanSourceFile, [cleanupTransformer]); const final = ts.transform(cleanSource, [cleanup]);
return printer.printFile(finalResult.transformed[0]); return printer.printFile(final.transformed[0]);
} }

View File

@ -24,6 +24,7 @@ export default async function rewritePagesModule(params?: Params) {
const origin_page_content = await Bun.file(page_path).text(); const origin_page_content = await Bun.file(page_path).text();
const dst_page_content = stripServerSideLogic({ const dst_page_content = stripServerSideLogic({
txt_code: origin_page_content, txt_code: origin_page_content,
file_path: page_path,
}); });
await Bun.write(dst_path, dst_page_content, { await Bun.write(dst_path, dst_page_content, {