Bogfix: update server module stripping function.
This commit is contained in:
parent
c3e1341ff8
commit
e2df3256f4
12
README.md
12
README.md
@ -71,12 +71,6 @@ The goal is a framework that is:
|
|||||||
|
|
||||||
Configure the `@moduletrace` scope to point at the registry — pick one:
|
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):
|
**`bunfig.toml`** (Bun-native):
|
||||||
|
|
||||||
```toml
|
```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/" }
|
"@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:
|
Then install:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -1,25 +1,29 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
export default function stripServerSideLogic({ txt_code, file_path }) {
|
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 sourceFile = ts.createSourceFile("file.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||||
const printer = ts.createPrinter();
|
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) => {
|
const visitor = (node) => {
|
||||||
// 1. Strip the 'server' export
|
// Remove 'export const server'
|
||||||
if (ts.isVariableStatement(node) &&
|
if (ts.isVariableStatement(node) &&
|
||||||
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
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 (node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) &&
|
||||||
if (isServer)
|
d.name.text === "server")) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
// 2. Convert relative imports to absolute imports
|
}
|
||||||
|
// Convert relative imports to absolute
|
||||||
if (ts.isImportDeclaration(node)) {
|
if (ts.isImportDeclaration(node)) {
|
||||||
const moduleSpecifier = node.moduleSpecifier;
|
const specifier = node.moduleSpecifier;
|
||||||
if (ts.isStringLiteral(moduleSpecifier) &&
|
if (ts.isStringLiteral(specifier) &&
|
||||||
moduleSpecifier.text.startsWith(".")) {
|
specifier.text.startsWith(".")) {
|
||||||
// Resolve the relative path to an absolute filesystem path
|
const absolutePath = path.resolve(file_dir_name, specifier.text);
|
||||||
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.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);
|
return ts.visitNode(rootNode, visitor);
|
||||||
};
|
};
|
||||||
const result = ts.transform(sourceFile, [transformer]);
|
const step1Result = ts.transform(sourceFile, [stripTransformer]);
|
||||||
const intermediate = printer.printFile(result.transformed[0]);
|
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, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
const cleanSource = ts.createSourceFile("clean.tsx", intermediateCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||||
const cleanup = (context) => (rootNode) => {
|
/**
|
||||||
|
* 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) => {
|
const visitor = (node) => {
|
||||||
if (ts.isImportDeclaration(node)) {
|
if (ts.isImportDeclaration(node)) {
|
||||||
const clause = node.importClause;
|
const clause = node.importClause;
|
||||||
if (!clause)
|
if (!clause)
|
||||||
return node;
|
return node;
|
||||||
|
// 1. Clean Named Imports: { A, B }
|
||||||
if (clause.namedBindings &&
|
if (clause.namedBindings &&
|
||||||
ts.isNamedImports(clause.namedBindings)) {
|
ts.isNamedImports(clause.namedBindings)) {
|
||||||
const used = clause.namedBindings.elements.filter((el) => {
|
const activeElements = clause.namedBindings.elements.filter((el) => usedIdentifiers.has(el.name.text));
|
||||||
const regex = new RegExp(`\\b${el.name.text}\\b`, "g");
|
// If no named imports are used and there is no default import, nix the whole line
|
||||||
return ((intermediate.match(regex) || []).length > 1);
|
if (activeElements.length === 0 && !clause.name)
|
||||||
});
|
|
||||||
if (used.length === 0)
|
|
||||||
return undefined;
|
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) {
|
// 2. Clean Default Imports: import X from '...'
|
||||||
const regex = new RegExp(`\\b${clause.name.text}\\b`, "g");
|
if (clause.name && !usedIdentifiers.has(clause.name.text)) {
|
||||||
if ((intermediate.match(regex) || []).length <= 1)
|
// If there are no named bindings attached, nix the whole line
|
||||||
|
if (!clause.namedBindings)
|
||||||
return undefined;
|
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.visitEachChild(node, visitor, context);
|
||||||
};
|
};
|
||||||
return ts.visitNode(rootNode, visitor);
|
return ts.visitNode(rootNode, visitor);
|
||||||
};
|
};
|
||||||
const final = ts.transform(cleanSource, [cleanup]);
|
const finalResult = ts.transform(cleanSource, [cleanupTransformer]);
|
||||||
return printer.printFile(final.transformed[0]);
|
return printer.printFile(finalResult.transformed[0]);
|
||||||
}
|
}
|
||||||
|
|||||||
3
dist/utils/rewrite-pages-module.js
vendored
3
dist/utils/rewrite-pages-module.js
vendored
@ -14,6 +14,9 @@ export default async function rewritePagesModule(params) {
|
|||||||
for (let i = 0; i < target_pages.length; i++) {
|
for (let i = 0; i < target_pages.length; i++) {
|
||||||
const page_path = target_pages[i];
|
const page_path = target_pages[i];
|
||||||
const dst_path = pagePathTransform({ page_path });
|
const dst_path = pagePathTransform({ page_path });
|
||||||
|
if (page_path.match(/__root\.tsx?/)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
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,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "@moduletrace/bunext",
|
"name": "@moduletrace/bunext",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.13",
|
"version": "1.0.14",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@ -7,6 +7,9 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function stripServerSideLogic({ txt_code, file_path }: 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(
|
const sourceFile = ts.createSourceFile(
|
||||||
"file.tsx",
|
"file.tsx",
|
||||||
txt_code,
|
txt_code,
|
||||||
@ -15,38 +18,42 @@ export default function stripServerSideLogic({ txt_code, file_path }: Params) {
|
|||||||
ts.ScriptKind.TSX,
|
ts.ScriptKind.TSX,
|
||||||
);
|
);
|
||||||
const printer = ts.createPrinter();
|
const printer = ts.createPrinter();
|
||||||
const file_fir_name = path.dirname(file_path);
|
|
||||||
|
|
||||||
const transformer: ts.TransformerFactory<ts.SourceFile> =
|
/**
|
||||||
|
* PASS 1: Remove 'server' export and resolve absolute paths
|
||||||
|
*/
|
||||||
|
const stripTransformer: ts.TransformerFactory<ts.SourceFile> =
|
||||||
(context) => (rootNode) => {
|
(context) => (rootNode) => {
|
||||||
const visitor = (node: ts.Node): ts.Node | undefined => {
|
const visitor = (node: ts.Node): ts.Node | undefined => {
|
||||||
// 1. Strip the 'server' export
|
// Remove 'export const server'
|
||||||
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 isServer = node.declarationList.declarations.some(
|
if (
|
||||||
|
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;
|
)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Convert relative imports to absolute imports
|
// Convert relative imports to absolute
|
||||||
if (ts.isImportDeclaration(node)) {
|
if (ts.isImportDeclaration(node)) {
|
||||||
const moduleSpecifier = node.moduleSpecifier;
|
const specifier = node.moduleSpecifier;
|
||||||
if (
|
if (
|
||||||
ts.isStringLiteral(moduleSpecifier) &&
|
ts.isStringLiteral(specifier) &&
|
||||||
moduleSpecifier.text.startsWith(".")
|
specifier.text.startsWith(".")
|
||||||
) {
|
) {
|
||||||
// Resolve the relative path to an absolute filesystem path
|
|
||||||
const absolutePath = path.resolve(
|
const absolutePath = path.resolve(
|
||||||
file_fir_name,
|
file_dir_name,
|
||||||
moduleSpecifier.text,
|
specifier.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
return ts.factory.updateImportDeclaration(
|
return ts.factory.updateImportDeclaration(
|
||||||
node,
|
node,
|
||||||
node.modifiers,
|
node.modifiers,
|
||||||
@ -56,67 +63,102 @@ export default function stripServerSideLogic({ txt_code, file_path }: Params) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 step1Result = ts.transform(sourceFile, [stripTransformer]);
|
||||||
const intermediate = printer.printFile(result.transformed[0]);
|
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(
|
const cleanSource = ts.createSourceFile(
|
||||||
"clean.tsx",
|
"clean.tsx",
|
||||||
intermediate,
|
intermediateCode,
|
||||||
ts.ScriptTarget.Latest,
|
ts.ScriptTarget.Latest,
|
||||||
true,
|
true,
|
||||||
ts.ScriptKind.TSX,
|
ts.ScriptKind.TSX,
|
||||||
);
|
);
|
||||||
const cleanup: ts.TransformerFactory<ts.SourceFile> =
|
|
||||||
|
/**
|
||||||
|
* PASS 2: Collect all used Identifiers and Prune Imports
|
||||||
|
*/
|
||||||
|
const usedIdentifiers = new Set<string>();
|
||||||
|
|
||||||
|
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<ts.SourceFile> =
|
||||||
(context) => (rootNode) => {
|
(context) => (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;
|
||||||
|
|
||||||
|
// 1. Clean Named Imports: { A, B }
|
||||||
if (
|
if (
|
||||||
clause.namedBindings &&
|
clause.namedBindings &&
|
||||||
ts.isNamedImports(clause.namedBindings)
|
ts.isNamedImports(clause.namedBindings)
|
||||||
) {
|
) {
|
||||||
const used = clause.namedBindings.elements.filter(
|
const activeElements =
|
||||||
(el) => {
|
clause.namedBindings.elements.filter((el) =>
|
||||||
const regex = new RegExp(
|
usedIdentifiers.has(el.name.text),
|
||||||
`\\b${el.name.text}\\b`,
|
|
||||||
"g",
|
|
||||||
);
|
);
|
||||||
return (
|
|
||||||
(intermediate.match(regex) || []).length > 1
|
// 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 (used.length === 0) return undefined;
|
// If we have some named imports left, update the node
|
||||||
return ts.factory.updateImportDeclaration(
|
return ts.factory.updateImportDeclaration(
|
||||||
node,
|
node,
|
||||||
node.modifiers,
|
node.modifiers,
|
||||||
ts.factory.updateImportClause(
|
ts.factory.updateImportClause(
|
||||||
clause,
|
clause,
|
||||||
clause.isTypeOnly,
|
clause.isTypeOnly,
|
||||||
clause.name,
|
// Only keep default import if it's used
|
||||||
ts.factory.createNamedImports(used),
|
clause.name &&
|
||||||
|
usedIdentifiers.has(clause.name.text)
|
||||||
|
? clause.name
|
||||||
|
: undefined,
|
||||||
|
ts.factory.createNamedImports(activeElements),
|
||||||
),
|
),
|
||||||
node.moduleSpecifier,
|
node.moduleSpecifier,
|
||||||
node.attributes,
|
node.attributes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clause.name) {
|
// 2. Clean Default Imports: import X from '...'
|
||||||
const regex = new RegExp(
|
if (clause.name && !usedIdentifiers.has(clause.name.text)) {
|
||||||
`\\b${clause.name.text}\\b`,
|
// If there are no named bindings attached, nix the whole line
|
||||||
"g",
|
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);
|
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;
|
return ts.visitNode(rootNode, visitor) as ts.SourceFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
const final = ts.transform(cleanSource, [cleanup]);
|
const finalResult = ts.transform(cleanSource, [cleanupTransformer]);
|
||||||
return printer.printFile(final.transformed[0]);
|
return printer.printFile(finalResult.transformed[0]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,10 @@ export default async function rewritePagesModule(params?: Params) {
|
|||||||
const page_path = target_pages[i];
|
const page_path = target_pages[i];
|
||||||
const dst_path = pagePathTransform({ page_path });
|
const dst_path = pagePathTransform({ page_path });
|
||||||
|
|
||||||
|
if (page_path.match(/__root\.tsx?/)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user