gencow 0.1.261 → 0.1.263

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.
@@ -6905,6 +6905,9 @@ function missingNamedImportsIn(ctx, file2) {
6905
6905
  function isProjectSource(file2, projectRoot) {
6906
6906
  return file2.startsWith(`${projectRoot}${sep2}`) && !file2.split(sep2).includes("node_modules") && SCRIPT_FILE.test(file2);
6907
6907
  }
6908
+ function hasProjectTypeScript(projectRootInput) {
6909
+ return loadProjectTypeScript(resolve10(projectRootInput)) !== null;
6910
+ }
6908
6911
  function findMissingNamedImports(inputFiles, projectRootInput) {
6909
6912
  const projectRoot = resolve10(projectRootInput);
6910
6913
  const ts = loadProjectTypeScript(projectRoot);
@@ -7014,34 +7017,39 @@ async function loadEsbuildModule() {
7014
7017
  }
7015
7018
  return { build: mod.build };
7016
7019
  }
7020
+ function esbuildEntryOptions(entryPath) {
7021
+ return {
7022
+ entryPoints: [entryPath],
7023
+ bundle: true,
7024
+ format: "esm",
7025
+ platform: "node",
7026
+ target: "node20",
7027
+ sourcemap: false,
7028
+ logLevel: "silent",
7029
+ packages: "external",
7030
+ external: [...nodeBuiltinExternals, "bun", "bun:*"],
7031
+ metafile: true,
7032
+ banner: {
7033
+ js: [
7034
+ 'import { createRequire as __gencowCreateRequire } from "node:module";',
7035
+ "const require = __gencowCreateRequire(import.meta.url);"
7036
+ ].join("\n")
7037
+ }
7038
+ };
7039
+ }
7040
+ function bundledInputFiles(result) {
7041
+ const inputs = result?.metafile?.inputs;
7042
+ return Object.keys(inputs ?? {}).map((file2) => resolve11(file2));
7043
+ }
7017
7044
  async function transpileTypescriptForNodeDefault(entryPath, context) {
7018
7045
  const workDir = join7(resolve11(context.projectRoot), ".gencow");
7019
7046
  await mkdir(workDir, { recursive: true });
7020
7047
  const tempDir = await mkdtemp(join7(workDir, "codegen-extract-"));
7021
7048
  const outFile = join7(tempDir, "index.mjs");
7022
- let inputFiles = [];
7049
+ let inputFiles;
7023
7050
  try {
7024
7051
  const { build } = await loadEsbuildModule();
7025
- const result = await build({
7026
- entryPoints: [entryPath],
7027
- outfile: outFile,
7028
- bundle: true,
7029
- format: "esm",
7030
- platform: "node",
7031
- target: "node20",
7032
- sourcemap: false,
7033
- logLevel: "silent",
7034
- packages: "external",
7035
- external: [...nodeBuiltinExternals, "bun", "bun:*"],
7036
- metafile: true,
7037
- banner: {
7038
- js: [
7039
- 'import { createRequire as __gencowCreateRequire } from "node:module";',
7040
- "const require = __gencowCreateRequire(import.meta.url);"
7041
- ].join("\n")
7042
- }
7043
- });
7044
- inputFiles = Object.keys(result?.metafile?.inputs ?? {}).map((file2) => resolve11(file2));
7052
+ inputFiles = bundledInputFiles(await build({ ...esbuildEntryOptions(entryPath), outfile: outFile }));
7045
7053
  } catch (error62) {
7046
7054
  const details = error62 instanceof Error ? error62.message : String(error62);
7047
7055
  throw new Error(
@@ -7053,27 +7061,55 @@ async function transpileTypescriptForNodeDefault(entryPath, context) {
7053
7061
  ].join("\n")
7054
7062
  );
7055
7063
  }
7056
- const missingImports = formatMissingNamedImports(
7057
- findMissingNamedImports(inputFiles, context.projectRoot),
7058
- context.projectRoot
7059
- );
7060
- if (missingImports.length > 0) {
7061
- throw new Error(
7062
- [
7063
- "Failed to transpile TypeScript entry for codegen extraction.",
7064
- `Resolved entry: ${entryPath}`,
7065
- `Transpile error: Build failed with ${missingImports.length} error${missingImports.length === 1 ? "" : "s"}:`,
7066
- ...missingImports
7067
- ].join("\n")
7068
- );
7069
- }
7070
7064
  return {
7071
7065
  modulePath: outFile,
7066
+ inputFiles,
7072
7067
  cleanup: async () => {
7073
7068
  await rm(tempDir, { recursive: true, force: true });
7074
7069
  }
7075
7070
  };
7076
7071
  }
7072
+ var MISSING_EXPORT_DIAGNOSTIC = /No matching export in "/u;
7073
+ var NAMED_IMPORT_CHECK_SKIPPED_MESSAGE = "Warning: codegen skipped the named-import check because this project has no typescript package. Install typescript in the project (for example: npm install -D typescript) and rerun codegen.";
7074
+ function defaultWarn(message) {
7075
+ process.stderr.write(`${message}
7076
+ `);
7077
+ }
7078
+ function namedImportLoadFailure(appEntryAbs, runtime, transpileErrorLines) {
7079
+ return new Error(
7080
+ [
7081
+ "Failed to load app module for codegen extraction.",
7082
+ `Resolved entry: ${appEntryAbs}`,
7083
+ `Runtime: ${runtime}`,
7084
+ "Expected export: default defineApi({ procedures: { ... } }) or module object with procedures.",
7085
+ ...transpileErrorLines
7086
+ ].join("\n")
7087
+ );
7088
+ }
7089
+ async function assertNamedImportsResolve(input2) {
7090
+ if (!hasProjectTypeScript(input2.projectRoot)) {
7091
+ input2.warn(NAMED_IMPORT_CHECK_SKIPPED_MESSAGE);
7092
+ return;
7093
+ }
7094
+ let inputFiles = input2.inputFiles;
7095
+ if (!inputFiles) {
7096
+ try {
7097
+ const { build } = await loadEsbuildModule();
7098
+ inputFiles = bundledInputFiles(await build({ ...esbuildEntryOptions(input2.appEntryAbs), write: false }));
7099
+ } catch (error62) {
7100
+ const details = error62 instanceof Error ? error62.message : String(error62);
7101
+ if (input2.runtime === "bun" && !MISSING_EXPORT_DIAGNOSTIC.test(details)) return;
7102
+ throw namedImportLoadFailure(input2.appEntryAbs, input2.runtime, [`Transpile error: ${details}`]);
7103
+ }
7104
+ }
7105
+ const missing = findMissingNamedImports(inputFiles, input2.projectRoot);
7106
+ if (missing.length > 0) {
7107
+ throw namedImportLoadFailure(input2.appEntryAbs, input2.runtime, [
7108
+ `Transpile error: Build failed with ${missing.length} error${missing.length === 1 ? "" : "s"}:`,
7109
+ ...formatMissingNamedImports(missing, input2.projectRoot)
7110
+ ]);
7111
+ }
7112
+ }
7077
7113
  function createCliRegistryExtractorDeps(options = {}) {
7078
7114
  let collectors = null;
7079
7115
  return {
@@ -7085,23 +7121,39 @@ function createCliRegistryExtractorDeps(options = {}) {
7085
7121
  const legacyBeforeImport = coreModule.captureLegacyRealtimeChannels?.() ?? [];
7086
7122
  let moduleImportPath = appEntryAbs;
7087
7123
  let cleanup;
7088
- if (runtime !== "bun" && appEntryAbs.endsWith(".ts")) {
7124
+ if (appEntryAbs.endsWith(".ts")) {
7125
+ let inputFiles;
7126
+ if (runtime !== "bun") {
7127
+ try {
7128
+ const transpile = options.transpileTypescriptForNode ?? transpileTypescriptForNodeDefault;
7129
+ const transpiled = await transpile(appEntryAbs, { projectRoot: input2.projectRoot });
7130
+ moduleImportPath = transpiled.modulePath;
7131
+ cleanup = transpiled.cleanup;
7132
+ inputFiles = transpiled.inputFiles;
7133
+ } catch (error62) {
7134
+ const details = error62 instanceof Error ? error62.message : String(error62);
7135
+ throw new Error(
7136
+ [
7137
+ "Failed to load app module for codegen extraction.",
7138
+ `Resolved entry: ${appEntryAbs}`,
7139
+ `Runtime: ${runtime}`,
7140
+ "Node runtime requires a TypeScript loader adapter for in-process extraction.",
7141
+ `Transpile error: ${details}`
7142
+ ].join("\n")
7143
+ );
7144
+ }
7145
+ }
7089
7146
  try {
7090
- const transpile = options.transpileTypescriptForNode ?? transpileTypescriptForNodeDefault;
7091
- const transpiled = await transpile(appEntryAbs, { projectRoot: input2.projectRoot });
7092
- moduleImportPath = transpiled.modulePath;
7093
- cleanup = transpiled.cleanup;
7147
+ await assertNamedImportsResolve({
7148
+ appEntryAbs,
7149
+ runtime,
7150
+ projectRoot: input2.projectRoot,
7151
+ inputFiles,
7152
+ warn: options.warn ?? defaultWarn
7153
+ });
7094
7154
  } catch (error62) {
7095
- const details = error62 instanceof Error ? error62.message : String(error62);
7096
- throw new Error(
7097
- [
7098
- "Failed to load app module for codegen extraction.",
7099
- `Resolved entry: ${appEntryAbs}`,
7100
- `Runtime: ${runtime}`,
7101
- "Node runtime requires a TypeScript loader adapter for in-process extraction.",
7102
- `Transpile error: ${details}`
7103
- ].join("\n")
7104
- );
7155
+ await cleanup?.();
7156
+ throw error62;
7105
7157
  }
7106
7158
  }
7107
7159
  let appModule;
@@ -3,8 +3,9 @@ import { join, relative, resolve, sep } from "node:path";
3
3
 
4
4
  // esbuild drops an unused import of a missing name without a diagnostic, and it only
5
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
6
+ // export its module does not have. This check runs after esbuild bundles the entry and before
7
+ // the module is imported, on every runtime, over the project's own sources, and only for relative
8
+ // named imports and re-exports. It reads
8
9
  // the ASTs with the user project's `typescript` (the CLI does not depend on it), so the
9
10
  // AST nodes are not statically typed here.
10
11
 
@@ -182,6 +183,11 @@ function isProjectSource(file: string, projectRoot: string): boolean {
182
183
  );
183
184
  }
184
185
 
186
+ // Whether the check can run for this project. Callers warn when it cannot, so a skipped check is visible.
187
+ export function hasProjectTypeScript(projectRootInput: string): boolean {
188
+ return loadProjectTypeScript(resolve(projectRootInput)) !== null;
189
+ }
190
+
185
191
  // `inputFiles` are the files esbuild bundled (absolute paths). Returns every relative named
186
192
  // import whose target module does not export the name. Any internal failure skips the check.
187
193
  export function findMissingNamedImports(
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  findMissingNamedImports,
15
15
  formatMissingNamedImports,
16
+ hasProjectTypeScript,
16
17
  } from "./codegen-named-import-check.ts";
17
18
  import {
18
19
  collectCrudCodegenMeta,
@@ -44,7 +45,13 @@ type CliRegistryRuntimeOptions = {
44
45
  transpileTypescriptForNode?: (
45
46
  entryPath: string,
46
47
  context: { projectRoot: string },
47
- ) => Promise<{ modulePath: string; cleanup?: (() => Promise<void>) | (() => void) }>;
48
+ ) => Promise<{
49
+ modulePath: string;
50
+ inputFiles?: string[];
51
+ cleanup?: (() => Promise<void>) | (() => void);
52
+ }>;
53
+ // Receives the one-line notice when the named-import check is skipped. Defaults to stderr.
54
+ warn?: (message: string) => void;
48
55
  coreModule?: CoreModuleShape;
49
56
  collectors?: Pick<RegistryRuntimeCollectors, "collectCrudCodegenMeta" | "collectProcedureRouteMaps">;
50
57
  };
@@ -105,38 +112,46 @@ async function loadEsbuildModule(): Promise<{ build(options: Record<string, unkn
105
112
  return { build: mod.build };
106
113
  }
107
114
 
115
+ // Bundle options shared by the node transpile and the import check, so both see the same module graph.
116
+ function esbuildEntryOptions(entryPath: string): Record<string, unknown> {
117
+ return {
118
+ entryPoints: [entryPath],
119
+ bundle: true,
120
+ format: "esm",
121
+ platform: "node",
122
+ target: "node20",
123
+ sourcemap: false,
124
+ logLevel: "silent",
125
+ packages: "external",
126
+ external: [...nodeBuiltinExternals, "bun", "bun:*"],
127
+ metafile: true,
128
+ banner: {
129
+ js: [
130
+ 'import { createRequire as __gencowCreateRequire } from "node:module";',
131
+ "const require = __gencowCreateRequire(import.meta.url);",
132
+ ].join("\n"),
133
+ },
134
+ };
135
+ }
136
+
137
+ // esbuild reports inputs relative to the process working directory (its default).
138
+ function bundledInputFiles(result: unknown): string[] {
139
+ const inputs = (result as { metafile?: { inputs?: Record<string, unknown> } } | undefined)?.metafile?.inputs;
140
+ return Object.keys(inputs ?? {}).map((file) => resolve(file));
141
+ }
142
+
108
143
  async function transpileTypescriptForNodeDefault(
109
144
  entryPath: string,
110
145
  context: { projectRoot: string },
111
- ): Promise<{ modulePath: string; cleanup?: () => Promise<void> }> {
146
+ ): Promise<{ modulePath: string; inputFiles: string[]; cleanup?: () => Promise<void> }> {
112
147
  const workDir = join(resolve(context.projectRoot), ".gencow");
113
148
  await mkdir(workDir, { recursive: true });
114
149
  const tempDir = await mkdtemp(join(workDir, "codegen-extract-"));
115
150
  const outFile = join(tempDir, "index.mjs");
116
- let inputFiles: string[] = [];
151
+ let inputFiles: string[];
117
152
  try {
118
153
  const { build } = await loadEsbuildModule();
119
- const result = (await build({
120
- entryPoints: [entryPath],
121
- outfile: outFile,
122
- bundle: true,
123
- format: "esm",
124
- platform: "node",
125
- target: "node20",
126
- sourcemap: false,
127
- logLevel: "silent",
128
- packages: "external",
129
- external: [...nodeBuiltinExternals, "bun", "bun:*"],
130
- metafile: true,
131
- banner: {
132
- js: [
133
- 'import { createRequire as __gencowCreateRequire } from "node:module";',
134
- "const require = __gencowCreateRequire(import.meta.url);",
135
- ].join("\n"),
136
- },
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));
154
+ inputFiles = bundledInputFiles(await build({ ...esbuildEntryOptions(entryPath), outfile: outFile }));
140
155
  } catch (error) {
141
156
  const details = error instanceof Error ? error.message : String(error);
142
157
  throw new Error(
@@ -148,30 +163,76 @@ async function transpileTypescriptForNodeDefault(
148
163
  ].join("\n"),
149
164
  );
150
165
  }
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
- }
167
166
  return {
168
167
  modulePath: outFile,
168
+ inputFiles,
169
169
  cleanup: async () => {
170
170
  await rm(tempDir, { recursive: true, force: true });
171
171
  },
172
172
  };
173
173
  }
174
174
 
175
+ const MISSING_EXPORT_DIAGNOSTIC = /No matching export in "/u;
176
+
177
+ const NAMED_IMPORT_CHECK_SKIPPED_MESSAGE =
178
+ "Warning: codegen skipped the named-import check because this project has no typescript package. " +
179
+ "Install typescript in the project (for example: npm install -D typescript) and rerun codegen.";
180
+
181
+ function defaultWarn(message: string): void {
182
+ process.stderr.write(`${message}\n`);
183
+ }
184
+
185
+ function namedImportLoadFailure(appEntryAbs: string, runtime: string, transpileErrorLines: string[]): Error {
186
+ return new Error(
187
+ [
188
+ "Failed to load app module for codegen extraction.",
189
+ `Resolved entry: ${appEntryAbs}`,
190
+ `Runtime: ${runtime}`,
191
+ "Expected export: default defineApi({ procedures: { ... } }) or module object with procedures.",
192
+ ...transpileErrorLines,
193
+ ].join("\n"),
194
+ );
195
+ }
196
+
197
+ // Runs before the app module is imported, on every runtime. esbuild accepts an unused or
198
+ // type-position import of a missing name, and Bun rejects a used one without a location, so this
199
+ // check decides first. The bundle inputs come from the node transpile when it ran, otherwise from
200
+ // an in-memory esbuild bundle with the same options.
201
+ async function assertNamedImportsResolve(input: {
202
+ appEntryAbs: string;
203
+ runtime: string;
204
+ projectRoot: string;
205
+ inputFiles?: string[];
206
+ warn: (message: string) => void;
207
+ }): Promise<void> {
208
+ if (!hasProjectTypeScript(input.projectRoot)) {
209
+ input.warn(NAMED_IMPORT_CHECK_SKIPPED_MESSAGE);
210
+ return;
211
+ }
212
+ let inputFiles = input.inputFiles;
213
+ if (!inputFiles) {
214
+ try {
215
+ const { build } = await loadEsbuildModule();
216
+ inputFiles = bundledInputFiles(await build({ ...esbuildEntryOptions(input.appEntryAbs), write: false }));
217
+ } catch (error) {
218
+ const details = error instanceof Error ? error.message : String(error);
219
+ // Bun can run inputs esbuild rejects (for example `with { type: "text" }`). On bun those failures
220
+ // skip the check and bun's loader reports any real error. A missing import in such a backend is
221
+ // not caught here. A missing-export diagnostic is still reported, since it is this check's domain.
222
+ // On node the transpile already failed above, so this branch is bun-only in practice.
223
+ if (input.runtime === "bun" && !MISSING_EXPORT_DIAGNOSTIC.test(details)) return;
224
+ throw namedImportLoadFailure(input.appEntryAbs, input.runtime, [`Transpile error: ${details}`]);
225
+ }
226
+ }
227
+ const missing = findMissingNamedImports(inputFiles, input.projectRoot);
228
+ if (missing.length > 0) {
229
+ throw namedImportLoadFailure(input.appEntryAbs, input.runtime, [
230
+ `Transpile error: Build failed with ${missing.length} error${missing.length === 1 ? "" : "s"}:`,
231
+ ...formatMissingNamedImports(missing, input.projectRoot),
232
+ ]);
233
+ }
234
+ }
235
+
175
236
  function createCliRegistryExtractorDeps(options: CliRegistryRuntimeOptions = {}): RegistryExtractorDeps {
176
237
  let collectors: RegistryRuntimeCollectors | null = null;
177
238
  return {
@@ -184,23 +245,39 @@ function createCliRegistryExtractorDeps(options: CliRegistryRuntimeOptions = {})
184
245
  let moduleImportPath = appEntryAbs;
185
246
  let cleanup: (() => Promise<void>) | (() => void) | undefined;
186
247
 
187
- if (runtime !== "bun" && appEntryAbs.endsWith(".ts")) {
248
+ if (appEntryAbs.endsWith(".ts")) {
249
+ let inputFiles: string[] | undefined;
250
+ if (runtime !== "bun") {
251
+ try {
252
+ const transpile = options.transpileTypescriptForNode ?? transpileTypescriptForNodeDefault;
253
+ const transpiled = await transpile(appEntryAbs, { projectRoot: input.projectRoot });
254
+ moduleImportPath = transpiled.modulePath;
255
+ cleanup = transpiled.cleanup;
256
+ inputFiles = transpiled.inputFiles;
257
+ } catch (error) {
258
+ const details = error instanceof Error ? error.message : String(error);
259
+ throw new Error(
260
+ [
261
+ "Failed to load app module for codegen extraction.",
262
+ `Resolved entry: ${appEntryAbs}`,
263
+ `Runtime: ${runtime}`,
264
+ "Node runtime requires a TypeScript loader adapter for in-process extraction.",
265
+ `Transpile error: ${details}`,
266
+ ].join("\n"),
267
+ );
268
+ }
269
+ }
188
270
  try {
189
- const transpile = options.transpileTypescriptForNode ?? transpileTypescriptForNodeDefault;
190
- const transpiled = await transpile(appEntryAbs, { projectRoot: input.projectRoot });
191
- moduleImportPath = transpiled.modulePath;
192
- cleanup = transpiled.cleanup;
271
+ await assertNamedImportsResolve({
272
+ appEntryAbs,
273
+ runtime,
274
+ projectRoot: input.projectRoot,
275
+ inputFiles,
276
+ warn: options.warn ?? defaultWarn,
277
+ });
193
278
  } catch (error) {
194
- const details = error instanceof Error ? error.message : String(error);
195
- throw new Error(
196
- [
197
- "Failed to load app module for codegen extraction.",
198
- `Resolved entry: ${appEntryAbs}`,
199
- `Runtime: ${runtime}`,
200
- "Node runtime requires a TypeScript loader adapter for in-process extraction.",
201
- `Transpile error: ${details}`,
202
- ].join("\n"),
203
- );
279
+ await cleanup?.();
280
+ throw error;
204
281
  }
205
282
  }
206
283