gencow 0.1.259 → 0.1.261
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli-command-runner.mjs +12 -0
- package/lib/cloud-targets.mjs +14 -4
- package/lib/db-command.mjs +16 -2
- package/lib/deploy-codegen-failure.mjs +21 -4
- package/lib/deploy-prepared-source.mjs +37 -0
- package/lib/deploy-runtime.mjs +85 -69
- package/lib/deploy-static-fullstack-runtime.mjs +18 -15
- package/lib/static-command.mjs +13 -3
- package/package.json +4 -4
- package/runtime/server.mjs +135 -140
- package/runtime/tooling.mjs +242 -65
- package/scripts/codegen-named-import-check.ts +224 -0
- package/scripts/tooling-codegen-runtime.ts +26 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
// esbuild drops an unused import of a missing name without a diagnostic, and it only
|
|
5
|
+
// warns for namespace member access. Codegen would then load a backend that names an
|
|
6
|
+
// export its module does not have. This check runs after a successful build, over the
|
|
7
|
+
// project's own sources, and only for relative named imports and re-exports. It reads
|
|
8
|
+
// the ASTs with the user project's `typescript` (the CLI does not depend on it), so the
|
|
9
|
+
// AST nodes are not statically typed here.
|
|
10
|
+
|
|
11
|
+
const SCRIPT_FILE = /\.(?:[cm]?[jt]sx?)$/u;
|
|
12
|
+
const TYPESCRIPT_FILE = /\.(?:[cm]?tsx?)$/u;
|
|
13
|
+
const RELATIVE_SPECIFIER = /^\.{1,2}\//u;
|
|
14
|
+
const DECLARATION_FILE = /\.d\.[cm]?ts$/u;
|
|
15
|
+
|
|
16
|
+
export type MissingNamedImport = {
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
name: string;
|
|
21
|
+
target: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Context = {
|
|
25
|
+
ts: any;
|
|
26
|
+
compilerOptions: Record<string, unknown>;
|
|
27
|
+
// `null` marks a module whose export names cannot be known (or a cycle in progress).
|
|
28
|
+
exportCache: Map<string, Set<string> | null>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// The project's typescript is the one the user compiles with. A missing or unusable copy
|
|
32
|
+
// skips the check, so an unusual project is never blocked by the CLI.
|
|
33
|
+
function loadProjectTypeScript(projectRoot: string): any | null {
|
|
34
|
+
try {
|
|
35
|
+
const requireFromProject = createRequire(join(projectRoot, "package.json"));
|
|
36
|
+
const ts = requireFromProject(requireFromProject.resolve("typescript"));
|
|
37
|
+
return typeof ts?.createSourceFile === "function" && typeof ts?.resolveModuleName === "function"
|
|
38
|
+
? ts
|
|
39
|
+
: null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A `.d.ts` target is skipped: TypeScript prefers it over a sibling `.js` that esbuild
|
|
46
|
+
// bundles, so a stale declaration file would report names the real module does export.
|
|
47
|
+
function resolveRelativeModule(ctx: Context, fromFile: string, specifier: string): string | null {
|
|
48
|
+
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
49
|
+
const resolved = ctx.ts.resolveModuleName(
|
|
50
|
+
specifier,
|
|
51
|
+
fromFile,
|
|
52
|
+
ctx.compilerOptions,
|
|
53
|
+
ctx.ts.sys,
|
|
54
|
+
).resolvedModule;
|
|
55
|
+
if (!resolved || resolved.isExternalLibraryImport) return null;
|
|
56
|
+
if (DECLARATION_FILE.test(resolved.resolvedFileName)) return null;
|
|
57
|
+
return resolved.resolvedFileName;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// `null` when the file cannot be read, so the caller skips it instead of parsing empty text.
|
|
61
|
+
function parseSource(ctx: Context, file: string): any | null {
|
|
62
|
+
const text = ctx.ts.sys.readFile(file);
|
|
63
|
+
if (typeof text !== "string") return null;
|
|
64
|
+
return ctx.ts.createSourceFile(file, text, ctx.ts.ScriptTarget.Latest, true);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasExportModifier(ts: any, statement: any): boolean {
|
|
68
|
+
return (statement.modifiers ?? []).some((modifier: any) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasDefaultModifier(ts: any, statement: any): boolean {
|
|
72
|
+
return (statement.modifiers ?? []).some((modifier: any) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Identifiers carry `text`; destructuring patterns carry `elements` (holes have no name).
|
|
76
|
+
function bindingNames(binding: any, names: string[]): void {
|
|
77
|
+
if (!binding) return;
|
|
78
|
+
if (typeof binding.text === "string") {
|
|
79
|
+
names.push(binding.text);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const element of binding.elements ?? []) bindingNames(element.name, names);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Export names of a module, following `export *` chains. Types count as exports, so a
|
|
86
|
+
// plain import of a type is accepted. Returns null when the names cannot be known.
|
|
87
|
+
function exportNamesOf(ctx: Context, file: string): Set<string> | null {
|
|
88
|
+
if (ctx.exportCache.has(file)) return ctx.exportCache.get(file) ?? null;
|
|
89
|
+
ctx.exportCache.set(file, null);
|
|
90
|
+
const names = collectExportNames(ctx, file);
|
|
91
|
+
ctx.exportCache.set(file, names);
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function collectExportNames(ctx: Context, file: string): Set<string> | null {
|
|
96
|
+
const { ts } = ctx;
|
|
97
|
+
if (!TYPESCRIPT_FILE.test(file)) return null;
|
|
98
|
+
const source = parseSource(ctx, file);
|
|
99
|
+
if (!source) return null;
|
|
100
|
+
const names = new Set<string>();
|
|
101
|
+
for (const statement of source.statements) {
|
|
102
|
+
if (ts.isExportDeclaration(statement)) {
|
|
103
|
+
const clause = statement.exportClause;
|
|
104
|
+
if (!clause) {
|
|
105
|
+
// `export * from "pkg"` or an unresolved path leaves the names unknown.
|
|
106
|
+
const target = resolveRelativeModule(ctx, file, statement.moduleSpecifier.text);
|
|
107
|
+
const inner = target ? exportNamesOf(ctx, target) : null;
|
|
108
|
+
if (!inner) return null;
|
|
109
|
+
for (const name of inner) if (name !== "default") names.add(name);
|
|
110
|
+
} else if (ts.isNamespaceExport(clause)) {
|
|
111
|
+
names.add(clause.name.text);
|
|
112
|
+
} else {
|
|
113
|
+
for (const element of clause.elements) names.add(element.name.text);
|
|
114
|
+
}
|
|
115
|
+
} else if (ts.isExportAssignment(statement)) {
|
|
116
|
+
if (statement.isExportEquals) return null;
|
|
117
|
+
names.add("default");
|
|
118
|
+
} else if (hasExportModifier(ts, statement)) {
|
|
119
|
+
if (hasDefaultModifier(ts, statement)) {
|
|
120
|
+
names.add("default");
|
|
121
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
122
|
+
const declared: string[] = [];
|
|
123
|
+
for (const declaration of statement.declarationList.declarations)
|
|
124
|
+
bindingNames(declaration.name, declared);
|
|
125
|
+
for (const name of declared) names.add(name);
|
|
126
|
+
} else if (statement.name?.text) {
|
|
127
|
+
names.add(statement.name.text);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return names;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function missingNamedImportsIn(ctx: Context, file: string): MissingNamedImport[] {
|
|
135
|
+
const { ts } = ctx;
|
|
136
|
+
const sourceFile = parseSource(ctx, file);
|
|
137
|
+
if (!sourceFile) return [];
|
|
138
|
+
const missing: MissingNamedImport[] = [];
|
|
139
|
+
for (const statement of sourceFile.statements) {
|
|
140
|
+
let specifiers: any[] = [];
|
|
141
|
+
let specifier: string | null = null;
|
|
142
|
+
if (ts.isImportDeclaration(statement)) {
|
|
143
|
+
const clause = statement.importClause;
|
|
144
|
+
const bindings = clause && !clause.isTypeOnly ? clause.namedBindings : undefined;
|
|
145
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
146
|
+
specifiers = bindings.elements;
|
|
147
|
+
specifier = statement.moduleSpecifier.text;
|
|
148
|
+
}
|
|
149
|
+
} else if (
|
|
150
|
+
ts.isExportDeclaration(statement) &&
|
|
151
|
+
statement.moduleSpecifier &&
|
|
152
|
+
!statement.isTypeOnly &&
|
|
153
|
+
statement.exportClause &&
|
|
154
|
+
ts.isNamedExports(statement.exportClause)
|
|
155
|
+
) {
|
|
156
|
+
specifiers = statement.exportClause.elements;
|
|
157
|
+
specifier = statement.moduleSpecifier.text;
|
|
158
|
+
}
|
|
159
|
+
if (!specifier) continue;
|
|
160
|
+
const target = resolveRelativeModule(ctx, file, specifier);
|
|
161
|
+
if (!target) continue;
|
|
162
|
+
const exported = exportNamesOf(ctx, target);
|
|
163
|
+
if (!exported) continue;
|
|
164
|
+
for (const element of specifiers) {
|
|
165
|
+
if (element.isTypeOnly) continue;
|
|
166
|
+
const nameNode = element.propertyName ?? element.name;
|
|
167
|
+
const name = nameNode.text;
|
|
168
|
+
if (exported.has(name)) continue;
|
|
169
|
+
const position = sourceFile.getLineAndCharacterOfPosition(nameNode.getStart(sourceFile));
|
|
170
|
+
// Column is 0-based like esbuild's, so both diagnostics in one output use the same convention.
|
|
171
|
+
missing.push({ file, line: position.line + 1, column: position.character, name, target });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return missing;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isProjectSource(file: string, projectRoot: string): boolean {
|
|
178
|
+
return (
|
|
179
|
+
file.startsWith(`${projectRoot}${sep}`) &&
|
|
180
|
+
!file.split(sep).includes("node_modules") &&
|
|
181
|
+
SCRIPT_FILE.test(file)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// `inputFiles` are the files esbuild bundled (absolute paths). Returns every relative named
|
|
186
|
+
// import whose target module does not export the name. Any internal failure skips the check.
|
|
187
|
+
export function findMissingNamedImports(
|
|
188
|
+
inputFiles: readonly string[],
|
|
189
|
+
projectRootInput: string,
|
|
190
|
+
): MissingNamedImport[] {
|
|
191
|
+
const projectRoot = resolve(projectRootInput);
|
|
192
|
+
const ts = loadProjectTypeScript(projectRoot);
|
|
193
|
+
if (!ts) return [];
|
|
194
|
+
const ctx: Context = {
|
|
195
|
+
ts,
|
|
196
|
+
compilerOptions: {
|
|
197
|
+
moduleResolution: ts.ModuleResolutionKind?.Bundler ?? ts.ModuleResolutionKind?.NodeJs ?? 2,
|
|
198
|
+
module: ts.ModuleKind?.ESNext,
|
|
199
|
+
allowJs: true,
|
|
200
|
+
resolveJsonModule: true,
|
|
201
|
+
},
|
|
202
|
+
exportCache: new Map(),
|
|
203
|
+
};
|
|
204
|
+
try {
|
|
205
|
+
const missing: MissingNamedImport[] = [];
|
|
206
|
+
for (const file of inputFiles) {
|
|
207
|
+
if (isProjectSource(file, projectRoot)) missing.push(...missingNamedImportsIn(ctx, file));
|
|
208
|
+
}
|
|
209
|
+
return missing;
|
|
210
|
+
} catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Formats each finding like an esbuild diagnostic, so the classifier reads it the same way.
|
|
216
|
+
export function formatMissingNamedImports(
|
|
217
|
+
missing: readonly MissingNamedImport[],
|
|
218
|
+
projectRoot: string,
|
|
219
|
+
): string[] {
|
|
220
|
+
return missing.map((item) => {
|
|
221
|
+
const target = relative(resolve(projectRoot), item.target).split(sep).join("/");
|
|
222
|
+
return `${item.file}:${item.line}:${item.column}: ERROR: No matching export in "${target}" for import "${item.name}"`;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
createNodeProcessRunner,
|
|
11
11
|
extractTables,
|
|
12
12
|
} from "../../shared/src/codegen/index.ts";
|
|
13
|
+
import {
|
|
14
|
+
findMissingNamedImports,
|
|
15
|
+
formatMissingNamedImports,
|
|
16
|
+
} from "./codegen-named-import-check.ts";
|
|
13
17
|
import {
|
|
14
18
|
collectCrudCodegenMeta,
|
|
15
19
|
collectProcedureRouteMaps,
|
|
@@ -109,9 +113,10 @@ async function transpileTypescriptForNodeDefault(
|
|
|
109
113
|
await mkdir(workDir, { recursive: true });
|
|
110
114
|
const tempDir = await mkdtemp(join(workDir, "codegen-extract-"));
|
|
111
115
|
const outFile = join(tempDir, "index.mjs");
|
|
116
|
+
let inputFiles: string[] = [];
|
|
112
117
|
try {
|
|
113
118
|
const { build } = await loadEsbuildModule();
|
|
114
|
-
await build({
|
|
119
|
+
const result = (await build({
|
|
115
120
|
entryPoints: [entryPath],
|
|
116
121
|
outfile: outFile,
|
|
117
122
|
bundle: true,
|
|
@@ -122,13 +127,16 @@ async function transpileTypescriptForNodeDefault(
|
|
|
122
127
|
logLevel: "silent",
|
|
123
128
|
packages: "external",
|
|
124
129
|
external: [...nodeBuiltinExternals, "bun", "bun:*"],
|
|
130
|
+
metafile: true,
|
|
125
131
|
banner: {
|
|
126
132
|
js: [
|
|
127
133
|
'import { createRequire as __gencowCreateRequire } from "node:module";',
|
|
128
134
|
"const require = __gencowCreateRequire(import.meta.url);",
|
|
129
135
|
].join("\n"),
|
|
130
136
|
},
|
|
131
|
-
});
|
|
137
|
+
})) as { metafile?: { inputs?: Record<string, unknown> } } | undefined;
|
|
138
|
+
// esbuild reports inputs relative to the process working directory (its default).
|
|
139
|
+
inputFiles = Object.keys(result?.metafile?.inputs ?? {}).map((file) => resolve(file));
|
|
132
140
|
} catch (error) {
|
|
133
141
|
const details = error instanceof Error ? error.message : String(error);
|
|
134
142
|
throw new Error(
|
|
@@ -140,6 +148,22 @@ async function transpileTypescriptForNodeDefault(
|
|
|
140
148
|
].join("\n"),
|
|
141
149
|
);
|
|
142
150
|
}
|
|
151
|
+
// esbuild accepts an unused or type-position import of a missing name; reject it here
|
|
152
|
+
// before codegen loads the module, in the same diagnostic shape as an esbuild error.
|
|
153
|
+
const missingImports = formatMissingNamedImports(
|
|
154
|
+
findMissingNamedImports(inputFiles, context.projectRoot),
|
|
155
|
+
context.projectRoot,
|
|
156
|
+
);
|
|
157
|
+
if (missingImports.length > 0) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
[
|
|
160
|
+
"Failed to transpile TypeScript entry for codegen extraction.",
|
|
161
|
+
`Resolved entry: ${entryPath}`,
|
|
162
|
+
`Transpile error: Build failed with ${missingImports.length} error${missingImports.length === 1 ? "" : "s"}:`,
|
|
163
|
+
...missingImports,
|
|
164
|
+
].join("\n"),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
143
167
|
return {
|
|
144
168
|
modulePath: outFile,
|
|
145
169
|
cleanup: async () => {
|