cabloy 5.1.219 → 5.1.221

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 (89) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/agent-governance.yml +2 -2
  3. package/.github/workflows/create-cabloy.yml +3 -3
  4. package/.github/workflows/docs-pages.yml +2 -2
  5. package/.github/workflows/playwright-e2e.yml +4 -4
  6. package/.github/workflows/vona-cov-pg.yml +2 -2
  7. package/.github/workflows/vona-test-crud.yml +2 -2
  8. package/.github/workflows/vona-test-mysql.yml +2 -2
  9. package/.github/workflows/vona-test-pg.yml +2 -2
  10. package/.github/workflows/vona-test-sqlite3.yml +2 -2
  11. package/.github/workflows/vona-tsc.yml +2 -2
  12. package/.github/workflows/zova-ui.yml +2 -2
  13. package/CHANGELOG.md +23 -0
  14. package/README.md +1 -1
  15. package/package.json +4 -3
  16. package/repo-docs/frontend/openapi-sdk-guide.md +2 -0
  17. package/repo-docs/fullstack/quickstart.md +3 -3
  18. package/repo-docs/reference/repo-scripts.md +16 -0
  19. package/scripts/generateOpenapiIsolated.test.ts +25 -0
  20. package/scripts/generateOpenapiIsolated.ts +46 -12
  21. package/scripts/runIsolatedTest.test.ts +165 -45
  22. package/scripts/runIsolatedTest.ts +111 -24
  23. package/scripts/strictGenerationDiagnostics.test.ts +1009 -0
  24. package/scripts/strictGenerationDiagnostics.ts +61 -0
  25. package/scripts/strictGenerationStartup.test.ts +212 -0
  26. package/vona/package.original.json +3 -2
  27. package/vona/packages-cli/cli/package.json +1 -1
  28. package/vona/packages-cli/cli-set-api/package.json +1 -1
  29. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +14 -4
  30. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateEntryFiles.ts +6 -1
  31. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +2 -10
  32. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/validateGenerationPaths.ts +27 -0
  33. package/vona/packages-cli/cli-set-api/test/binDevLifecycle.test.ts +1 -1
  34. package/vona/packages-cli/cli-set-api/test/strictGenerationPrivacy.test.ts +439 -0
  35. package/vona/packages-vona/vona/package.json +1 -1
  36. package/vona/packages-vona/vona-core/package.json +1 -1
  37. package/vona/packages-vona/vona-core/src/lib/bean/beanBase.ts +4 -0
  38. package/vona/packages-vona/vona-core/src/lib/core/asyncLocalStorage.ts +84 -12
  39. package/vona/packages-vona/vona-core/src/lib/core/index.ts +14 -0
  40. package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +19 -3
  41. package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +346 -0
  42. package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.ts +138 -0
  43. package/vona/packages-vona/vona-core/src/lib/core/logger/index.ts +1 -0
  44. package/vona/packages-vona/vona-core/src/lib/core/logger/utils.ts +25 -5
  45. package/vona/packages-vona/vona-core/src/lib/core/requestBoundary.ts +89 -0
  46. package/vona/packages-vona/vona-core/src/lib/core/testFreshness.test.ts +379 -0
  47. package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +232 -0
  48. package/vona/packages-vona/vona-core/src/lib/core/testStrictMode.ts +9 -0
  49. package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +4 -3
  50. package/vona/packages-vona/vona-core/src/lib/framework/start.ts +2 -0
  51. package/vona/packages-vona/vona-mock/package.json +1 -1
  52. package/vona/pnpm-lock.yaml +691 -356
  53. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  54. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  55. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +111 -60
  56. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +6 -6
  57. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +13 -1
  58. package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
  59. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/lib/jwtClientLogging.test.ts +182 -0
  60. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +2 -8
  61. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  62. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/aopMethod.log.ts +4 -1
  63. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/bean.logger.ts +9 -0
  64. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +27 -14
  65. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.test.ts +207 -0
  66. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.ts +30 -0
  67. package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +33 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +1 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/index.ts +11 -0
  70. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/lib/protectedSqliteRegistry.ts +356 -0
  71. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +6 -0
  73. package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +2 -2
  74. package/vona/src/suite-vendor/a-vona/modules/a-telemetry/package.json +1 -1
  75. package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/bean.telemetry.ts +8 -1
  76. package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/middlewareSystem.trace.ts +9 -14
  77. package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/lib/spanDiagnostics.ts +70 -0
  78. package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/service/telemetry.ts +37 -23
  79. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  80. package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +31 -4
  81. package/vona/src/suite-vendor/a-vona/package.json +2 -2
  82. package/zova/package.original.json +3 -2
  83. package/zova/packages-cli/cli/package.json +2 -2
  84. package/zova/packages-cli/cli-set-front/package.json +1 -1
  85. package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +11 -101
  86. package/zova/packages-cli/cli-set-front/src/lib/common/restDependencies.ts +78 -0
  87. package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +252 -0
  88. package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +568 -0
  89. package/zova/pnpm-lock.yaml +442 -564
@@ -1,6 +1,13 @@
1
1
  import type { Context, Span, SpanOptions } from '@opentelemetry/api';
2
2
 
3
- import { context, propagation, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
3
+ import {
4
+ context,
5
+ INVALID_SPAN_CONTEXT,
6
+ propagation,
7
+ ROOT_CONTEXT,
8
+ SpanKind,
9
+ trace,
10
+ } from '@opentelemetry/api';
4
11
  import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
5
12
  import { W3CTraceContextPropagator } from '@opentelemetry/core';
6
13
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
@@ -11,12 +18,15 @@ import {
11
18
  TraceIdRatioBasedSampler,
12
19
  } from '@opentelemetry/sdk-trace-base';
13
20
  import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
14
- import { BeanBase } from 'vona';
21
+ import { BeanBase, captureRequestBoundary, getRequestBoundary } from 'vona';
15
22
  import { Service } from 'vona-module-a-bean';
16
23
 
17
24
  import type { ITelemetryCarrier } from '../types/telemetry.ts';
18
25
 
19
26
  import { createIngressTrustChecker } from '../lib/ingress.ts';
27
+ import { endHttpSpan, recordSpanException } from '../lib/spanDiagnostics.ts';
28
+
29
+ const nonRecordingSpan = trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
20
30
 
21
31
  const carrierGetter = {
22
32
  get(carrier: ITelemetryCarrier, key: string) {
@@ -44,10 +54,12 @@ export class ServiceTelemetry extends BeanBase {
44
54
  private _enabled = false;
45
55
 
46
56
  get enabled() {
57
+ if (getRequestBoundary()) return false;
47
58
  return this._enabled;
48
59
  }
49
60
 
50
61
  init() {
62
+ if (getRequestBoundary()) return;
51
63
  if (this._provider || this._enabled) return;
52
64
  const config = this.scope.config;
53
65
  this._isTrustedIngress = createIngressTrustChecker(config.ingress);
@@ -96,64 +108,64 @@ export class ServiceTelemetry extends BeanBase {
96
108
  }
97
109
 
98
110
  get activeContext() {
111
+ if (getRequestBoundary()) return ROOT_CONTEXT;
99
112
  return this.ctx?.state.telemetry?.context ?? context.active();
100
113
  }
101
114
 
102
115
  extractCarrier(carrier?: ITelemetryCarrier) {
116
+ if (getRequestBoundary()) return ROOT_CONTEXT;
103
117
  if (!carrier) return context.active();
104
118
  return propagation.extract(context.active(), carrier, carrierGetter);
105
119
  }
106
120
 
107
121
  isTrustedIngress(peerAddress?: string, internalHeaderValue?: string) {
122
+ if (getRequestBoundary()) return false;
108
123
  this._isTrustedIngress ??= createIngressTrustChecker(this.scope.config.ingress);
109
124
  return this._isTrustedIngress(peerAddress, internalHeaderValue);
110
125
  }
111
126
 
112
127
  extractHttpIngressCarrier(trusted: boolean, carrier: ITelemetryCarrier) {
128
+ if (getRequestBoundary()) return ROOT_CONTEXT;
113
129
  return trusted ? this.extractCarrier(carrier) : this.extractCarrier();
114
130
  }
115
131
 
116
- injectCarrier(source = this.activeContext): ITelemetryCarrier {
132
+ injectCarrier(source?: Context): ITelemetryCarrier {
133
+ if (getRequestBoundary()) return { version: 1 };
117
134
  const carrier: ITelemetryCarrier = { version: 1 };
118
- propagation.inject(source, carrier, carrierSetter);
135
+ propagation.inject(source === undefined ? this.activeContext : source, carrier, carrierSetter);
119
136
  return carrier;
120
137
  }
121
138
 
122
- startSpan(name: string, options?: SpanOptions, parent = this.activeContext) {
123
- return trace.getTracer('vona-module-a-telemetry').startSpan(name, options, parent);
139
+ startSpan(name: string, options?: SpanOptions, parent?: Context) {
140
+ if (getRequestBoundary()) return nonRecordingSpan;
141
+ return trace
142
+ .getTracer('vona-module-a-telemetry')
143
+ .startSpan(name, options, parent === undefined ? this.activeContext : parent);
124
144
  }
125
145
 
126
146
  withSpan<RESULT>(span: Span, fn: () => RESULT): RESULT {
147
+ if (getRequestBoundary()) return fn();
127
148
  const active = trace.setSpan(this.activeContext, span);
128
149
  return context.with(active, fn);
129
150
  }
130
151
 
131
- createContext(span: Span, parent = this.activeContext) {
132
- return trace.setSpan(parent, span);
152
+ createContext(span: Span, parent?: Context) {
153
+ if (getRequestBoundary()) return ROOT_CONTEXT;
154
+ return trace.setSpan(parent === undefined ? this.activeContext : parent, span);
133
155
  }
134
156
 
135
157
  recordException(span: Span, error: unknown) {
136
- if (error instanceof Error) {
137
- span.recordException(error);
138
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
139
- return;
140
- }
141
- span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
158
+ if (getRequestBoundary()) return;
159
+ recordSpanException(span, error, captureRequestBoundary());
142
160
  }
143
161
 
144
162
  endHttpSpan(span: Span, statusCode: number, aborted = false) {
145
- if (aborted || statusCode >= 500) {
146
- span.setStatus({
147
- code: SpanStatusCode.ERROR,
148
- message: aborted ? 'client aborted' : undefined,
149
- });
150
- }
151
- span.setAttribute('http.response.status_code', statusCode);
152
- if (aborted) span.setAttribute('vona.http.aborted', true);
153
- span.end();
163
+ if (getRequestBoundary()) return;
164
+ endHttpSpan(span, statusCode, aborted);
154
165
  }
155
166
 
156
167
  getLogContext() {
168
+ if (getRequestBoundary()) return;
157
169
  if (!this._enabled) return;
158
170
  const spanContext = trace.getSpanContext(this.activeContext);
159
171
  if (!spanContext || !trace.isSpanContextValid(spanContext)) return;
@@ -166,6 +178,7 @@ export class ServiceTelemetry extends BeanBase {
166
178
  }
167
179
 
168
180
  createServerSpan(method: string, parent: Context) {
181
+ if (getRequestBoundary()) return nonRecordingSpan;
169
182
  return this.startSpan(`HTTP ${method}`, { kind: SpanKind.SERVER }, parent);
170
183
  }
171
184
 
@@ -175,6 +188,7 @@ export class ServiceTelemetry extends BeanBase {
175
188
  controllerBeanFullName: string,
176
189
  action: string,
177
190
  ) {
191
+ if (getRequestBoundary()) return;
178
192
  const span = this.ctx.state.telemetry?.serverSpan;
179
193
  if (!span) return;
180
194
  span.updateName(`HTTP ${method.toUpperCase()} ${routePath}`);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-version",
3
- "version": "5.1.21",
3
+ "version": "5.1.22",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona-module-a-version",
6
6
  "keywords": [
@@ -9,6 +9,8 @@ import {
9
9
  BeanBase,
10
10
  assertDisposableDatabaseName as assertDisposableDatabaseNameCore,
11
11
  assertIsolatedTestPath,
12
+ assertFreshTestResourcesReady,
13
+ requiresFreshTestResources,
12
14
  assertTestRunOwner,
13
15
  resolveVonaLocalPaths,
14
16
  withIsolatedTestDatabaseDiscovery,
@@ -39,6 +41,7 @@ interface IDevDatabasePreparation {
39
41
  }
40
42
 
41
43
  const devDatabasePreparations = new WeakMap<VonaApplication, IDevDatabasePreparation>();
44
+ const freshDatabaseCreations = new WeakMap<VonaApplication, Map<string, string>>();
42
45
 
43
46
  export function assertDisposableDatabaseName(
44
47
  databaseName: string,
@@ -318,7 +321,7 @@ export class ServiceDatabase extends BeanBase {
318
321
  // client
319
322
  const client = this.bean.database.current.client;
320
323
  // get current database name
321
- const databaseName = client.getDatabaseName();
324
+ const currentDatabaseName = client.getDatabaseName();
322
325
  const sqliteRoot =
323
326
  client.db.dialectName === 'better-sqlite3'
324
327
  ? resolveVonaLocalPaths(
@@ -327,9 +330,12 @@ export class ServiceDatabase extends BeanBase {
327
330
  this.app.meta.env.VONA_TEST_RUN_ID,
328
331
  ).sqliteRoot
329
332
  : undefined;
330
- const currentFilename = sqliteRoot ? path.basename(databaseName, '.db') : databaseName;
333
+ const currentFilename = sqliteRoot
334
+ ? path.basename(currentDatabaseName, '.db')
335
+ : currentDatabaseName;
331
336
  const isTestDatabase = currentFilename.startsWith(databasePrefix);
332
- if (isTestDatabase) assertDisposableDatabaseName(databaseName, databasePrefix, sqliteRoot);
337
+ if (isTestDatabase)
338
+ assertDisposableDatabaseName(currentDatabaseName, databasePrefix, sqliteRoot);
333
339
  if (
334
340
  this.app.meta.isTest &&
335
341
  this.app.meta.env.VONA_TEST_RUN_ID &&
@@ -337,11 +343,32 @@ export class ServiceDatabase extends BeanBase {
337
343
  !isTestDatabase
338
344
  ) {
339
345
  throw new Error(
340
- `Isolated test database is outside its disposable namespace: ${databaseName}`,
346
+ `Isolated test database is outside its disposable namespace: ${currentDatabaseName}`,
341
347
  );
342
348
  }
343
349
  // test db
344
350
  if (this.app.meta.isTest) {
351
+ if (requiresFreshTestResources()) {
352
+ assertFreshTestResourcesReady(this.app);
353
+ const key = `${client.clientName}\0${databasePrefix}`;
354
+ const created = freshDatabaseCreations.get(this.app);
355
+ if (isTestDatabase) {
356
+ if (created?.get(key) !== currentDatabaseName) {
357
+ throw new Error('Strict test cannot reuse an unowned database');
358
+ }
359
+ return;
360
+ }
361
+ if ((await this.__fetchDatabases(client, instanceName, configInstanceBase)).length !== 0) {
362
+ throw new Error('Strict test database namespace is no longer fresh');
363
+ }
364
+ const name = await this.__createDatabase(client, instanceName, configInstanceBase);
365
+ assertDisposableDatabaseName(name, databasePrefix, sqliteRoot);
366
+ const creations = created ?? new Map<string, string>();
367
+ creations.set(key, name);
368
+ freshDatabaseCreations.set(this.app, creations);
369
+ await client.changeConfigConnectionAndReloadWorker(name);
370
+ return;
371
+ }
345
372
  // check
346
373
  if (isTestDatabase) {
347
374
  // donothing
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.101",
3
+ "version": "5.1.103",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -54,7 +54,7 @@
54
54
  "vona-module-a-static": "workspace:^",
55
55
  "vona-module-a-summer": "workspace:^",
56
56
  "vona-module-a-swagger": "workspace:^",
57
- "vona-module-a-telemetry": "^5.0.2",
57
+ "vona-module-a-telemetry": "^5.0.3",
58
58
  "vona-module-a-upload": "workspace:^",
59
59
  "vona-module-a-user": "workspace:^",
60
60
  "vona-module-a-validation": "workspace:^",
@@ -14,7 +14,7 @@
14
14
  "url": "git+https://github.com/zovajs/zova.git"
15
15
  },
16
16
  "type": "module",
17
- "packageManager": "pnpm@11.5.2",
17
+ "packageManager": "pnpm@12.14.0",
18
18
  "scripts": {
19
19
  "zova": "node ./packages-cli/cli/src/bin/zova.ts",
20
20
  "dev": "npm run dev:ssr:admin",
@@ -96,6 +96,7 @@
96
96
  "oxlint": "^1.77.0"
97
97
  },
98
98
  "engines": {
99
- "node": ">=22.12.0"
99
+ "node": ">=24.4.0",
100
+ "pnpm": ">=11.5.2"
100
101
  }
101
102
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli",
3
- "version": "1.2.115",
3
+ "version": "1.2.117",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "@cabloy/process-helper": "^3.1.8",
45
45
  "fs-extra": "^11.3.5",
46
46
  "semver": "^7.6.2",
47
- "zova-cli-set-front": "^1.2.113"
47
+ "zova-cli-set-front": "^1.2.115"
48
48
  },
49
49
  "devDependencies": {
50
50
  "clean-package": "^2.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli-set-front",
3
- "version": "1.2.113",
3
+ "version": "1.2.115",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli-set-front",
6
6
  "keywords": [
@@ -12,9 +12,10 @@ import fse from 'fs-extra';
12
12
  import path from 'node:path';
13
13
  import { rimraf } from 'rimraf';
14
14
  import { build } from 'tsdown';
15
- import yaml from 'yaml';
16
15
  import { createConfigUtils, saveJSONFile } from 'zova-vite';
17
16
 
17
+ import { extractRestDependencies } from '../common/restDependencies.ts';
18
+ import { extractRestDependencyVersions } from '../common/restDependencyVersions.ts';
18
19
  import { loadJSONFile } from '../common/utils.ts';
19
20
  import { __ThisSetName__ } from '../this.ts';
20
21
 
@@ -61,6 +62,7 @@ interface IBinBuildRestContext {
61
62
  outDir: string;
62
63
  bundleModules?: string[];
63
64
  sideEffectImportModuleNames?: string[];
65
+ moduleRoots?: string[];
64
66
  packageJson?: IBinBuildRestPackageJson;
65
67
  }
66
68
 
@@ -116,11 +118,13 @@ export class CliBinBuildRest extends BeanCliBase {
116
118
  const modules: string[] = [];
117
119
  const bundleModules: string[] = [];
118
120
  const sideEffectImportModuleNames: string[] = [];
121
+ const moduleRoots: string[] = [];
119
122
  for (const module of modulesMeta.modulesArray) {
120
123
  const moduleName = module.info.fullName;
121
124
  modules.push(`import '${moduleName}';`);
122
125
  modules.push(`export * from '${moduleName}';`);
123
126
  const moduleRoot = module.root.replaceAll('\\', '/');
127
+ moduleRoots.push(moduleRoot);
124
128
  if (moduleRoot.includes('/src/module/') || moduleRoot.includes('/src/suite/')) {
125
129
  bundleModules.push(moduleName);
126
130
  } else {
@@ -129,6 +133,7 @@ export class CliBinBuildRest extends BeanCliBase {
129
133
  }
130
134
  context.bundleModules = bundleModules;
131
135
  context.sideEffectImportModuleNames = sideEffectImportModuleNames;
136
+ context.moduleRoots = moduleRoots;
132
137
  argv.Modules = modules.join('\n');
133
138
  // Name/Version
134
139
  argv.Name = bundleName;
@@ -181,8 +186,7 @@ export class CliBinBuildRest extends BeanCliBase {
181
186
  tsconfig: 'tsconfig.rest.json',
182
187
  clean: false,
183
188
  dts: {
184
- // resolve: true,
185
- resolver: 'tsc',
189
+ resolver: 'oxc',
186
190
  tsgo: true,
187
191
  eager: true,
188
192
  tsconfig: 'tsconfig.rest.json',
@@ -213,13 +217,13 @@ export class CliBinBuildRest extends BeanCliBase {
213
217
  }
214
218
 
215
219
  async _build(buildContext: IBinBuildRestContext) {
216
- const { projectPath, flavor, bundleNameCopy, outDir, packageJson } = buildContext;
220
+ const { projectPath, flavor, bundleNameCopy, outDir, packageJson, moduleRoots } = buildContext;
217
221
  // build
218
222
  await this._buildTs(buildContext);
219
223
  await this._buildDts(buildContext);
220
224
  // deps
221
225
  const deps = await _extractDeps(path.join(outDir, 'index.d.mts'));
222
- const depsVersion = await _extractDepsVersion(projectPath, deps);
226
+ const depsVersion = await extractRestDependencyVersions(projectPath, deps, moduleRoots);
223
227
  // package.json
224
228
  const pkgContent = {
225
229
  ...packageJson!,
@@ -256,100 +260,6 @@ function _copyToTarget(
256
260
  }
257
261
 
258
262
  async function _extractDeps(filePath: string): Promise<string[]> {
259
- let content = (await fse.readFile(filePath)).toString();
260
- const pos = content.indexOf('//#region');
261
- content = content.substring(0, pos);
262
- const packageNames = new Set();
263
- const re = /import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
264
-
265
- let match: any;
266
- while (true) {
267
- match = re.exec(content);
268
- if (!match) break;
269
- let pkg: string = match[1];
270
- if (pkg.startsWith('.') || pkg.startsWith('/')) continue;
271
- if (pkg.startsWith('@')) {
272
- if (pkg.split('/').length > 2) {
273
- pkg = pkg.split('/').slice(0, 2).join('/');
274
- }
275
- } else {
276
- if (pkg.split('/').length > 1) {
277
- pkg = pkg.split('/').slice(0, 1).join('/');
278
- }
279
- }
280
- packageNames.add(pkg);
281
- }
282
- return Array.from(packageNames).sort() as string[];
283
- }
284
-
285
- async function _extractDepsVersion(
286
- projectPath: string,
287
- deps: string[],
288
- ): Promise<Record<string, string>> {
289
- const lockfilePath = path.join(projectPath, 'pnpm-lock.yaml');
290
- const lockfileContent = (await fse.readFile(lockfilePath)).toString();
291
- const parsedLockfile = yaml.parse(lockfileContent);
292
- const depsVersion: Record<string, string> = {};
293
- for (const dep of deps) {
294
- let dependencyRange = getPackageDependencyRangeFromLock(parsedLockfile, dep);
295
- if (!dependencyRange) {
296
- const version = await getPackageVersionFromNodeModules(projectPath, dep);
297
- dependencyRange = version ? `^${version}` : null;
298
- }
299
- if (!dependencyRange) {
300
- console.warn('dep version not found: ', dep);
301
- continue;
302
- }
303
- depsVersion[dep] = dependencyRange;
304
- }
305
- return depsVersion;
306
- }
307
-
308
- function getPackageDependencyRangeFromLock(
309
- parsedLockfile: any,
310
- packageName: string,
311
- ): string | null {
312
- const importer = parsedLockfile.importers?.['.'];
313
- const sections = ['dependencies', 'devDependencies', 'optionalDependencies'];
314
- for (const section of sections) {
315
- const dependencyRange = getImporterDependencyRange(importer?.[section]?.[packageName]);
316
- if (dependencyRange) return dependencyRange;
317
- }
318
- return null;
319
- }
320
-
321
- function getImporterDependencyRange(
322
- importerDependency: { specifier?: string; version?: string } | undefined,
323
- ): string | null {
324
- if (!importerDependency) return null;
325
-
326
- const specifier = importerDependency.specifier;
327
- if (specifier?.startsWith('npm:')) return specifier;
328
-
329
- const version = normalizeLockfileVersion(importerDependency.version);
330
- if (!version) return null;
331
- return `^${version}`;
332
- }
333
-
334
- function normalizeLockfileVersion(version: string | undefined): string | null {
335
- if (!version) return null;
336
- if (version.startsWith('link:') || version.startsWith('file:')) return null;
337
-
338
- const versionWithoutPeers = version.split('(')[0];
339
- if (!versionWithoutPeers) return null;
340
- if (/^\d/.test(versionWithoutPeers)) return versionWithoutPeers;
341
-
342
- const aliasSeparator = versionWithoutPeers.lastIndexOf('@');
343
- if (aliasSeparator === -1) return null;
344
-
345
- const aliasedVersion = versionWithoutPeers.slice(aliasSeparator + 1);
346
- if (!aliasedVersion || !/^\d/.test(aliasedVersion)) return null;
347
- return aliasedVersion;
348
- }
349
-
350
- async function getPackageVersionFromNodeModules(projectPath: string, packageName: string) {
351
- const pkgFile = path.join(projectPath, 'node_modules', packageName, 'package.json');
352
- if (!fse.existsSync(pkgFile)) return null;
353
- const pkgContent = await loadJSONFile(pkgFile);
354
- return pkgContent.version;
263
+ const content = await fse.readFile(filePath, 'utf8');
264
+ return extractRestDependencies(content);
355
265
  }
@@ -0,0 +1,78 @@
1
+ import { isBuiltin } from 'node:module';
2
+ import path from 'node:path';
3
+ import ts from 'typescript';
4
+
5
+ export function extractRestDependencies(content: string): string[] {
6
+ const source = ts.createSourceFile('index.d.mts', content, ts.ScriptTarget.Latest);
7
+ // createSourceFile exposes syntax diagnostics at runtime, but not in its public return type.
8
+ const { parseDiagnostics } = source as ts.SourceFile & {
9
+ parseDiagnostics: readonly ts.Diagnostic[];
10
+ };
11
+ if (parseDiagnostics.length) {
12
+ const diagnostic = parseDiagnostics[0];
13
+ const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
14
+ throw new Error(
15
+ `Invalid REST declaration at ${source.fileName}:${line + 1}:${character + 1}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`,
16
+ );
17
+ }
18
+ for (const kind of ['path', 'types'] as const) {
19
+ const references = kind === 'path' ? source.referencedFiles : source.typeReferenceDirectives;
20
+ if (references.length) {
21
+ throw new Error(
22
+ `Unsupported REST declaration reference directive: ${kind} ${JSON.stringify(references[0].fileName)}. ` +
23
+ 'The current REST bundling workflow does not support this directive.',
24
+ );
25
+ }
26
+ }
27
+ // lib references are compiler-owned, not package dependencies.
28
+ const dependencies = new Set<string>();
29
+
30
+ function addDependency(specifier: ts.Node | undefined) {
31
+ if (!specifier) return;
32
+ if (!ts.isStringLiteral(specifier)) {
33
+ throw new Error('Unsupported REST declaration module specifier');
34
+ }
35
+ const id = specifier.text;
36
+ if (
37
+ id.startsWith('.') ||
38
+ id.startsWith('#') ||
39
+ id.startsWith('\0') ||
40
+ path.isAbsolute(id) ||
41
+ path.win32.isAbsolute(id) ||
42
+ /^[a-z][a-z\d+.-]*:/i.test(id) ||
43
+ isBuiltin(id)
44
+ ) {
45
+ return;
46
+ }
47
+ if (!id || id.includes(':')) {
48
+ throw new Error(`Unsupported REST declaration package reference: ${id}`);
49
+ }
50
+ const parts = id.split('/');
51
+ if (id.startsWith('@') && (!parts[0].slice(1) || !parts[1])) {
52
+ throw new Error(`Invalid REST declaration package reference: ${id}`);
53
+ }
54
+ dependencies.add(parts.slice(0, id.startsWith('@') ? 2 : 1).join('/'));
55
+ }
56
+
57
+ function visit(node: ts.Node) {
58
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
59
+ addDependency(node.moduleSpecifier);
60
+ } else if (ts.isImportTypeNode(node)) {
61
+ if (!ts.isLiteralTypeNode(node.argument)) {
62
+ throw new Error('Unsupported REST declaration import type');
63
+ }
64
+ addDependency(node.argument.literal);
65
+ } else if (
66
+ ts.isImportEqualsDeclaration(node) &&
67
+ ts.isExternalModuleReference(node.moduleReference)
68
+ ) {
69
+ addDependency(node.moduleReference.expression);
70
+ } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
71
+ throw new Error('Unsupported REST declaration dynamic import');
72
+ }
73
+ ts.forEachChild(node, visit);
74
+ }
75
+
76
+ visit(source);
77
+ return Array.from(dependencies).sort();
78
+ }