@bleedingdev/modern-js-app-tools-extensions 3.9.3-ultramodern.1 → 3.9.3-ultramodern.10

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.
Files changed (34) hide show
  1. package/dist/cjs/build-config/build-environment.js +7 -5
  2. package/dist/cjs/build-config/internal-effect-discovery.js +94 -6
  3. package/dist/cjs/native-type-checker.js +66 -20
  4. package/dist/cjs/release-identity.js +1 -1
  5. package/dist/cjs/renderer-build-identity.js +13 -3
  6. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +18 -1
  7. package/dist/esm/build-config/build-environment.mjs +7 -5
  8. package/dist/esm/build-config/internal-effect-discovery.mjs +90 -5
  9. package/dist/esm/native-type-checker.mjs +68 -22
  10. package/dist/esm/release-identity.mjs +1 -1
  11. package/dist/esm/renderer-build-identity.mjs +13 -3
  12. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +18 -1
  13. package/dist/esm-node/build-config/build-environment.mjs +7 -5
  14. package/dist/esm-node/build-config/internal-effect-discovery.mjs +90 -5
  15. package/dist/esm-node/native-type-checker.mjs +68 -22
  16. package/dist/esm-node/release-identity.mjs +1 -1
  17. package/dist/esm-node/renderer-build-identity.mjs +13 -3
  18. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +18 -1
  19. package/dist/types/build-config/build-environment.d.ts +3 -1
  20. package/dist/types/build-config/internal-effect-discovery.d.ts +3 -1
  21. package/dist/types/cloudflare/worker-manifest.d.ts +3 -2
  22. package/dist/types/config.d.ts +4 -0
  23. package/dist/types/native-type-checker.d.ts +4 -2
  24. package/dist/types/release-identity.d.ts +10 -0
  25. package/dist/types/renderer-build-identity.d.ts +12 -1
  26. package/package.json +27 -27
  27. package/src/build-config/build-environment.ts +11 -9
  28. package/src/build-config/internal-effect-discovery.ts +238 -19
  29. package/src/cloudflare/worker-manifest.ts +7 -3
  30. package/src/config.ts +4 -0
  31. package/src/native-type-checker.ts +87 -43
  32. package/src/release-identity.ts +11 -2
  33. package/src/renderer-build-identity.ts +51 -11
  34. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +18 -1
@@ -1,18 +1,19 @@
1
- import { execFile } from 'node:child_process';
1
+ import { execFile, execFileSync } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import {
4
4
  existsSync,
5
5
  readFileSync,
6
6
  realpathSync,
7
- statSync,
8
7
  unlinkSync,
9
8
  writeFileSync,
10
9
  } from 'node:fs';
10
+ import { createRequire } from 'node:module';
11
11
  import path from 'node:path';
12
12
  import { promisify } from 'node:util';
13
13
  import { refreshTsgoCheckerConfig } from '@modern-js/builder';
14
14
  import type { Rspack, RspackChain } from '@rsbuild/core';
15
15
  import { type ParseError, parse, printParseErrorCode } from 'jsonc-parser';
16
+ import { resolveEffectTsgoCompiler } from './build-config/build-environment';
16
17
 
17
18
  const execute = promisify(execFile);
18
19
  const name = 'UltramodernNativeTypeChecker';
@@ -20,44 +21,63 @@ const name = 'UltramodernNativeTypeChecker';
20
21
  export interface NativeTypeCheckerCommand {
21
22
  executable: string;
22
23
  args: readonly string[];
24
+ /** The selected package's native config API. tsrs has no --showConfig CLI. */
25
+ configApi?: string;
23
26
  }
24
27
 
25
- /** Launch the selected stable package through its declared public compiler bin. */
28
+ /** Resolve the Effect-enabled native compiler from the application's origin. */
26
29
  export function resolveNativeTypeCheckerCommand(
27
- packageJsonPath: string,
30
+ from: string,
28
31
  ): NativeTypeCheckerCommand {
29
- const manifestFile = realpathSync(packageJsonPath);
30
- const directory = path.dirname(manifestFile);
31
- const manifest = JSON.parse(readFileSync(manifestFile, 'utf8')) as {
32
- name?: unknown;
33
- version?: unknown;
34
- bin?: { tsc?: unknown };
35
- };
36
- if (manifest.name !== 'typescript' || manifest.version !== '7.0.2') {
37
- throw new Error(
38
- `${name} requires typescript@7.0.2; found ${String(manifest.name)}@${String(manifest.version)} at ${manifestFile}.`,
39
- );
40
- }
41
- const bin = manifest.bin?.tsc;
42
- if (typeof bin !== 'string' || !bin || path.isAbsolute(bin)) {
43
- throw new Error(`${name} requires the selected package's public tsc bin.`);
44
- }
45
- const launcher = realpathSync(path.resolve(directory, bin));
46
- const relative = path.relative(directory, launcher);
47
- if (
48
- !relative ||
49
- path.isAbsolute(relative) ||
50
- relative === '..' ||
51
- relative.startsWith(`..${path.sep}`) ||
52
- !statSync(launcher).isFile()
53
- ) {
54
- throw new Error(
55
- `${name} public tsc bin must belong to its selected package.`,
56
- );
32
+ const executable = resolveEffectTsgoCompiler({ from });
33
+ const version = execFileSync(executable, ['--version'], {
34
+ encoding: 'utf8',
35
+ env: {
36
+ ...process.env,
37
+ TSRS_CHECKER_ASSIGNMENT: process.env.TSRS_CHECKER_ASSIGNMENT || 'go',
38
+ },
39
+ timeout: 10_000,
40
+ });
41
+ if (version.includes('(tsrs ')) {
42
+ return {
43
+ executable,
44
+ args: [],
45
+ configApi: createRequire(from).resolve('tsrs/sync'),
46
+ };
57
47
  }
58
- return { executable: process.execPath, args: [launcher] };
48
+ // TypeScript-Go supports --showConfig, including project references. Its
49
+ // older native config API lacks tsrs's diagnostics and reference fields.
50
+ return {
51
+ executable,
52
+ args: [],
53
+ };
59
54
  }
60
55
 
56
+ // Use the real native parser, not a handwritten tsconfig/extends interpreter.
57
+ // Running the client in a child also applies assignment to its native API
58
+ // process without changing the build/dev server's environment.
59
+ const inspectNativeConfig = `
60
+ import { pathToFileURL } from 'node:url';
61
+ const [modulePath, compiler, configFile] = process.argv.slice(1);
62
+ const { API, formatDiagnostics } = await import(pathToFileURL(modulePath).href);
63
+ const api = new API({ tsserverPath: compiler });
64
+ try {
65
+ const config = api.parseConfigFile(configFile);
66
+ if (config.errors.length) {
67
+ process.stdout.write(formatDiagnostics(config.errors, api));
68
+ process.exitCode = 1;
69
+ } else {
70
+ process.stdout.write(JSON.stringify({
71
+ compilerOptions: config.options,
72
+ files: config.fileNames,
73
+ references: config.projectReferences,
74
+ }));
75
+ }
76
+ } finally {
77
+ api.close();
78
+ }
79
+ `;
80
+
61
81
  interface TypeCheckerConfig {
62
82
  compilerOptions?: Record<string, unknown>;
63
83
  references?: Array<{ path: string }>;
@@ -230,25 +250,51 @@ export class UltramodernNativeTypeChecker {
230
250
  private async run(args: string[]): Promise<string> {
231
251
  try {
232
252
  const selected = this.options.compiler();
233
- const command =
253
+ const command: NativeTypeCheckerCommand =
234
254
  typeof selected === 'string'
235
255
  ? { executable: selected, args: [] }
236
256
  : selected;
257
+ const inspect = args[0] === '--showConfig' && command.configApi;
237
258
  const result = await execute(
238
- command.executable,
239
- [...command.args, ...args],
259
+ inspect ? process.execPath : command.executable,
260
+ inspect
261
+ ? [
262
+ '--input-type=module',
263
+ '--eval',
264
+ inspectNativeConfig,
265
+ command.configApi!,
266
+ command.executable,
267
+ args[args.indexOf('--project') + 1],
268
+ ]
269
+ : [...command.args, ...args],
240
270
  {
241
271
  cwd: path.dirname(this.options.configFile),
272
+ // Apply to every command, including explicit paths and TSRS_BINARY.
273
+ // Keep caller selection; do not change the dev server's environment.
274
+ env: {
275
+ ...process.env,
276
+ TSRS_CHECKER_ASSIGNMENT:
277
+ process.env.TSRS_CHECKER_ASSIGNMENT || 'go',
278
+ },
242
279
  maxBuffer: 16 * 1024 * 1024,
280
+ ...(inspect ? { timeout: 10_000 } : {}),
243
281
  },
244
282
  );
245
283
  return result.stdout;
246
284
  } catch (cause) {
247
- const output = cause as { stdout?: string; stderr?: string };
285
+ const output = cause as {
286
+ stdout?: string;
287
+ stderr?: string;
288
+ code?: number | string;
289
+ signal?: NodeJS.Signals;
290
+ };
248
291
  const diagnostics = `${output.stdout ?? ''}${output.stderr ?? ''}`;
249
- throw new Error(
250
- `${name} failed:\n${diagnostics || (cause instanceof Error ? cause.message : String(cause))}${missingJsxRuntimeHint(diagnostics)}`,
251
- { cause },
292
+ throw Object.assign(
293
+ new Error(
294
+ `${name} failed:\n${diagnostics || (cause instanceof Error ? cause.message : String(cause))}${missingJsxRuntimeHint(diagnostics)}`,
295
+ { cause },
296
+ ),
297
+ { code: output.code, signal: output.signal },
252
298
  );
253
299
  }
254
300
  }
@@ -333,9 +379,7 @@ export class UltramodernNativeTypeChecker {
333
379
  }
334
380
  } catch (cause) {
335
381
  compilation.errors.push(
336
- new Error(cause instanceof Error ? cause.message : String(cause), {
337
- cause,
338
- }),
382
+ cause instanceof Error ? cause : new Error(String(cause)),
339
383
  );
340
384
  }
341
385
  });
@@ -51,8 +51,7 @@ export const createUltramodernReleaseBuildMarker = ({
51
51
  .update(
52
52
  `${BUILD_MARKER_NAMESPACE}:${unitId}:${generationBuildMarker}:${sourceRevision}`,
53
53
  )
54
- .digest('hex')
55
- .slice(0, 16);
54
+ .digest('hex');
56
55
 
57
56
  export const resolveUltramodernSourceRevision = (
58
57
  workspaceRoot = process.cwd(),
@@ -119,6 +118,16 @@ export const resolveUltramodernSourceRevision = (
119
118
  return revision;
120
119
  };
121
120
 
121
+ /**
122
+ * The one release identity of a delivery unit, resolvable before its build.
123
+ *
124
+ * For a clean source revision the build marker is a 64-hex digest of the unit
125
+ * id, its generation marker and the revision. API bundles carry it, and a UI
126
+ * unit's renderer adopts it as its `buildId`, so contracts, release plans and
127
+ * Worker names derived from it before the build match the built
128
+ * `ultramodern-build.json`, `renderer-build.json`, chunk banners and envelope.
129
+ * A dirty or non-Git checkout yields the non-promotable `workspace` identity.
130
+ */
122
131
  export const resolveUltramodernReleaseIdentity = ({
123
132
  generationBuildMarker,
124
133
  sourceRevision: configuredSourceRevision,
@@ -25,7 +25,18 @@ export interface RendererBuildIdentityOptions {
25
25
  mode: 'development' | 'production';
26
26
  /** Delivery-unit app id; defaults to the application package name. */
27
27
  appId?: string;
28
- /** Externally authenticated source revision (delivery unit or CI). */
28
+ /**
29
+ * The declared delivery unit's release identity, resolved before the build
30
+ * by `resolveUltramodernReleaseIdentity`. A promotable production build
31
+ * adopts its buildMarker as the buildId, so consumers that resolve the unit
32
+ * identity before compiling see the marker the built artifacts carry.
33
+ */
34
+ deliveryUnit?: {
35
+ readonly unitId: string;
36
+ readonly buildMarker: string;
37
+ readonly sourceRevision: string;
38
+ };
39
+ /** Externally authenticated source revision (CI) for an undeclared app. */
29
40
  sourceRevision?: string;
30
41
  /** Actual final entry ownership, supplied by the selected renderer composition. */
31
42
  routerBindings: RendererRouterBindings;
@@ -49,6 +60,8 @@ const LOCKFILES = [
49
60
  'bun.lockb',
50
61
  ];
51
62
 
63
+ const RELEASE_BUILD_MARKER_PATTERN = /^[a-f0-9]{64}$/u;
64
+
52
65
  /** Stable per process: one dev session or dirty build keeps one buildId. */
53
66
  const processNonce = randomUUID();
54
67
 
@@ -132,20 +145,47 @@ export function resolveRendererBuildIdentities(
132
145
  throw new Error(
133
146
  'Renderer application identity requires a delivery artifact appId or a package name.',
134
147
  );
148
+ const { deliveryUnit } = options;
149
+ if (
150
+ deliveryUnit !== undefined &&
151
+ (typeof deliveryUnit.unitId !== 'string' ||
152
+ !deliveryUnit.unitId ||
153
+ deliveryUnit.unitId.trim() !== deliveryUnit.unitId)
154
+ )
155
+ throw new Error(
156
+ 'Renderer delivery-unit identity requires a nonempty trimmed unitId.',
157
+ );
135
158
  const profileKey = rendererProfileKey(options.profile);
136
159
  const sourceRevision = resolveUltramodernSourceRevision(
137
160
  projectRoot,
138
- options.sourceRevision,
139
- );
140
- const buildId = sha256(
141
- JSON.stringify([
142
- profileKey,
143
- lockfileHash(projectRoot),
144
- options.mode === 'development' || sourceRevision === 'workspace'
145
- ? processNonce
146
- : sourceRevision,
147
- ]),
161
+ deliveryUnit ? deliveryUnit.sourceRevision : options.sourceRevision,
148
162
  );
163
+ const promotable =
164
+ options.mode === 'production' && sourceRevision !== 'workspace';
165
+ let buildId: string;
166
+ if (promotable && deliveryUnit) {
167
+ // One identity per delivery unit, known before compilation.
168
+ if (
169
+ deliveryUnit.sourceRevision !== sourceRevision ||
170
+ !RELEASE_BUILD_MARKER_PATTERN.test(deliveryUnit.buildMarker)
171
+ )
172
+ throw new Error(
173
+ `Renderer delivery unit ${deliveryUnit.unitId} must carry the release identity of source revision ${sourceRevision}.`,
174
+ );
175
+ buildId = deliveryUnit.buildMarker;
176
+ } else {
177
+ // Undeclared apps and non-promotable builds (development, dirty trees)
178
+ // keep a per-app id; non-promotable ids are unique per process.
179
+ buildId = sha256(
180
+ JSON.stringify([
181
+ appId,
182
+ deliveryUnit?.unitId ?? null,
183
+ profileKey,
184
+ lockfileHash(projectRoot),
185
+ promotable ? sourceRevision : processNonce,
186
+ ]),
187
+ );
188
+ }
149
189
  const identities = Object.fromEntries(
150
190
  entries.map(entryName => [
151
191
  entryName,
@@ -306,7 +306,24 @@ async function invokeNativeRouteWorker(route, identity, request, env, ctx) {
306
306
  const resources =
307
307
  nativeRenderer?.renderer === identity.renderer &&
308
308
  Object.hasOwn(nativeRenderer.entries, route.entryName)
309
- ? nativeRenderer.entries[route.entryName]
309
+ ? {
310
+ ...nativeRenderer.entries[route.entryName],
311
+ ...(Object.hasOwn(nativeRenderer, 'workerFederation')
312
+ ? { workerFederation: nativeRenderer.workerFederation }
313
+ : {}),
314
+ ...(Object.hasOwn(nativeRenderer, 'workerFederationManifestUrl')
315
+ ? {
316
+ workerFederationManifestUrl:
317
+ nativeRenderer.workerFederationManifestUrl,
318
+ }
319
+ : {}),
320
+ ...(Object.hasOwn(nativeRenderer, 'workerFederationServices')
321
+ ? {
322
+ workerFederationServices:
323
+ nativeRenderer.workerFederationServices,
324
+ }
325
+ : {}),
326
+ }
310
327
  : undefined;
311
328
  // Native renderers have no Flight transport. Reject before the bundle loads.
312
329
  if (