cabloy 5.1.207 → 5.1.209

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 (97) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +1 -0
  4. package/package.json +1 -1
  5. package/repo-agent-governance/managed-assets.json +9 -9
  6. package/repo-agent-governance/policies/repository.md +1 -1
  7. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +4 -2
  8. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +13 -1
  9. package/repo-docs/backend/unit-testing.md +1 -1
  10. package/repo-docs/frontend/form-guide.md +17 -1
  11. package/repo-docs/frontend/model-architecture.md +4 -0
  12. package/repo-docs/frontend/model-state-guide.md +23 -2
  13. package/repo-docs/frontend/resource-entry-page-deep-dive.md +119 -0
  14. package/repo-docs/frontend/scripts.md +3 -3
  15. package/repo-docs/frontend/zova-form-under-the-hood.md +5 -0
  16. package/repo-docs/fullstack/parallel-worktree-environment.md +3 -3
  17. package/repo-docs/fullstack/quickstart.md +11 -1
  18. package/repo-docs/reference/repo-scripts.md +19 -5
  19. package/repo-e2e/specs/cabloy-basic.spec.ts +1577 -0
  20. package/vona/packages-vona/vona/package.json +1 -1
  21. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
  22. package/vona/src/suite/a-home/modules/home-user/src/dto/passport.ts +4 -0
  23. package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +172 -0
  24. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +1 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  26. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  27. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
  29. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  30. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  31. package/zova/packages-cli/cli/package.json +2 -2
  32. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  33. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +9 -0
  34. package/zova/packages-cli/cli-set-front/package.json +1 -1
  35. package/zova/packages-zova/zova/package.json +2 -2
  36. package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +17 -17
  37. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +68 -52
  38. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +524 -343
  39. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +45 -11
  40. package/zova/src/suite/a-home/modules/home-passport/test/lib/persistenceIdentity.test.ts +229 -0
  41. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/package.json +9 -1
  42. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/.metadata/index.ts +81 -5
  43. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/bean/bean.draftCoordinator.ts +158 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +11 -3
  45. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +438 -8
  46. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/config.ts +11 -0
  47. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/en-us.ts +9 -0
  48. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/zh-cn.ts +9 -0
  49. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/lib/draftSchema.ts +649 -0
  50. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/model/draft.ts +59 -0
  51. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/monkey.ts +11 -0
  52. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/draft.ts +49 -0
  53. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftDate.test.ts +262 -0
  54. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLifecycle.test.ts +1073 -0
  55. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLoader.mjs +13 -0
  56. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftModel.test.ts +572 -0
  57. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchema.test.ts +427 -0
  58. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchemaSafety.test.ts +200 -0
  59. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  60. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +6 -1
  61. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  62. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  63. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +86 -1
  64. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +88 -13
  65. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +751 -0
  66. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefresh.test.ts +637 -0
  67. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +994 -0
  68. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  69. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +331 -87
  70. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.query.ts +11 -6
  71. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +3 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +17 -1
  73. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/index.ts +1 -0
  74. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/modelPersistence.ts +204 -0
  75. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/persistenceQueue.ts +50 -0
  76. package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +5 -0
  77. package/zova/src/suite-vendor/a-zova/modules/a-model/src/service/storage.ts +47 -2
  78. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -0
  79. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelLoader.mjs +34 -0
  80. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelPersistence.test.ts +727 -0
  81. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceQueue.test.ts +137 -0
  82. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceTransition.test.ts +129 -0
  83. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  84. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/monkey.ts +7 -5
  85. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +2 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  87. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +14 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routeDiscard.ts +26 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +6 -3
  90. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/index.ts +1 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/routeDiscard.ts +24 -0
  92. package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/routeDiscard.test.ts +105 -0
  93. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
  94. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +202 -30
  95. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabs.test.ts +524 -0
  96. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabsLoader.mjs +35 -0
  97. package/zova/src/suite-vendor/a-zova/package.json +6 -6
@@ -0,0 +1,204 @@
1
+ import type { Query, QueryClient, QueryOptions } from '@tanstack/query-core';
2
+
3
+ import { notifyManager } from '@tanstack/query-core';
4
+ import { markRaw, shallowRef } from 'vue';
5
+
6
+ export type ModelPersistenceScope = readonly [string | null, string | null, string | null];
7
+ export interface ModelPersistenceValues {
8
+ instanceId?: string | number | null;
9
+ siteId?: string | number | null;
10
+ userId?: string | number | null;
11
+ }
12
+ export interface ModelPersistenceToken {
13
+ readonly scope: ModelPersistenceScope;
14
+ readonly epoch: number;
15
+ }
16
+
17
+ const normalize = (value: string | number | null | undefined) =>
18
+ value == null ? null : String(value);
19
+
20
+ export function getModelPersistenceStorageKey(
21
+ appName: string,
22
+ scope: ModelPersistenceScope,
23
+ logicalKey: string,
24
+ ): string {
25
+ return JSON.stringify(['zova-model-db', 1, appName, scope, logicalKey]);
26
+ }
27
+
28
+ export function isModelPersistenceDb(options?: unknown): boolean {
29
+ if (options === false) return false;
30
+ const persister =
31
+ options && typeof options === 'object' ? (options as { storage?: string; sync?: boolean }) : {};
32
+ return (persister.storage ?? (persister.sync ? 'local' : 'db')) === 'db';
33
+ }
34
+
35
+ function emptyQueryState() {
36
+ return {
37
+ data: undefined,
38
+ dataUpdateCount: 0,
39
+ dataUpdatedAt: 0,
40
+ error: null,
41
+ errorUpdateCount: 0,
42
+ errorUpdatedAt: 0,
43
+ fetchFailureCount: 0,
44
+ fetchFailureReason: null,
45
+ fetchMeta: null,
46
+ isInvalidated: false,
47
+ status: 'pending' as const,
48
+ fetchStatus: 'idle' as const,
49
+ };
50
+ }
51
+
52
+ /** App-owned identity; independent of the module providing the current Passport. */
53
+ export class ModelPersistenceContext {
54
+ private _epoch = 0;
55
+ private _version = shallowRef(0);
56
+ private _scope: ModelPersistenceScope;
57
+ private _registered = false;
58
+ private _hydrated = new Set<Query>();
59
+ private _listeners = new Set<(token: ModelPersistenceToken) => void>();
60
+
61
+ constructor(
62
+ private _queryClient: QueryClient,
63
+ siteId?: string | number | null,
64
+ ) {
65
+ // IoC owners can be reactive; do not let Vue unwrap this class's private refs
66
+ // or proxy TanStack Query instances retained for hydration adoption.
67
+ markRaw(this);
68
+ this._scope = Object.freeze([null, normalize(siteId), null]);
69
+ }
70
+
71
+ get epoch(): number {
72
+ void this._version.value;
73
+ return this._epoch;
74
+ }
75
+
76
+ capture(): ModelPersistenceToken {
77
+ return Object.freeze({ scope: this._scope, epoch: this.epoch });
78
+ }
79
+
80
+ current(token: ModelPersistenceToken): boolean {
81
+ return token.epoch === this.epoch && token.scope === this._scope;
82
+ }
83
+
84
+ markHydrated(queries: Query[]) {
85
+ if (!this._registered) {
86
+ for (const query of queries) this._hydrated.add(query);
87
+ }
88
+ }
89
+
90
+ touch(query?: Query): void {
91
+ // A transferred Query that has since participated in DB work belongs to the
92
+ // actual invocation scope, not the one-time hydration adoption baseline.
93
+ if (query) this._hydrated.delete(query);
94
+ }
95
+
96
+ subscribe(listener: (token: ModelPersistenceToken) => void): () => void {
97
+ this._listeners.add(listener);
98
+ return () => this._listeners.delete(listener);
99
+ }
100
+
101
+ update(
102
+ values: ModelPersistenceValues,
103
+ options: { initial?: boolean; refetchActive?: boolean } = {},
104
+ ): void {
105
+ const scope: ModelPersistenceScope = Object.freeze([
106
+ normalize(values.instanceId),
107
+ normalize(values.siteId),
108
+ normalize(values.userId),
109
+ ]);
110
+ const adopt = !this._registered && options.initial === true;
111
+ this._registered = true;
112
+ if (scope.every((value, index) => value === this._scope[index])) {
113
+ this._hydrated.clear();
114
+ return;
115
+ }
116
+ const affected = this._queryClient
117
+ .getQueryCache()
118
+ .getAll()
119
+ .filter(
120
+ query =>
121
+ isModelPersistenceDb(query.meta?.persister) && !(adopt && this._hydrated.has(query)),
122
+ );
123
+ this._scope = scope;
124
+ this._hydrated.clear();
125
+ this._invalidate(affected);
126
+ // Outgoing relogin/logout still resets immediately, but must not refetch
127
+ // protected queries after their authentication has been removed.
128
+ if (options.refetchActive === false) return;
129
+ const token = this.capture();
130
+ // The identity owner installs authentication before publishing this transition.
131
+ // Defer refetch until all synchronous reset/listener work has finished.
132
+ void Promise.resolve().then(() => {
133
+ if (this.current(token)) {
134
+ return this._queryClient.refetchQueries({
135
+ type: 'active',
136
+ predicate: query => affected.includes(query),
137
+ });
138
+ }
139
+ });
140
+ }
141
+
142
+ clear(): void {
143
+ this._hydrated.clear();
144
+ this._invalidate(this._queryClient.getQueryCache().getAll());
145
+ }
146
+
147
+ private _invalidate(queries: Query[]): void {
148
+ this._epoch++;
149
+ // Cancel every retryer before resetting any query. Silent + non-reverting avoids
150
+ // the old fetch's asynchronous cancellation handler publishing an error/reset.
151
+ for (const query of queries) void query.cancel({ silent: true, revert: false });
152
+ notifyManager.batch(() => {
153
+ for (const query of queries) {
154
+ query.setOptions({
155
+ ...query.options,
156
+ initialData: undefined,
157
+ initialDataUpdatedAt: undefined,
158
+ });
159
+ for (const observer of query.observers) {
160
+ observer.setOptions({
161
+ ...observer.options,
162
+ initialData: undefined,
163
+ initialDataUpdatedAt: undefined,
164
+ placeholderData: undefined,
165
+ });
166
+ }
167
+ query.setState(emptyQueryState());
168
+ }
169
+ });
170
+ this._version.value++;
171
+ const token = this.capture();
172
+ for (const listener of this._listeners) listener(token);
173
+ }
174
+
175
+ /** Keep later observer option reapplication from reinstalling an old identity. */
176
+ guardOptions<T extends QueryOptions>(options: T): T {
177
+ if (!isModelPersistenceDb(options.meta?.persister)) return options;
178
+ const token = this.capture();
179
+ const initialData = options.initialData;
180
+ const initialDataUpdatedAt = options.initialDataUpdatedAt;
181
+ const placeholderData = (options as T & { placeholderData?: unknown }).placeholderData;
182
+ return {
183
+ ...options,
184
+ initialData: () =>
185
+ this.current(token)
186
+ ? typeof initialData === 'function'
187
+ ? initialData()
188
+ : initialData
189
+ : undefined,
190
+ initialDataUpdatedAt: () =>
191
+ this.current(token)
192
+ ? typeof initialDataUpdatedAt === 'function'
193
+ ? initialDataUpdatedAt()
194
+ : initialDataUpdatedAt
195
+ : undefined,
196
+ placeholderData: (...args: unknown[]) =>
197
+ this.current(token)
198
+ ? typeof placeholderData === 'function'
199
+ ? placeholderData(...args)
200
+ : placeholderData
201
+ : undefined,
202
+ };
203
+ }
204
+ }
@@ -0,0 +1,50 @@
1
+ export class PersistenceQueue {
2
+ private _tail: Promise<unknown> = Promise.resolve();
3
+ private _sequence = 0;
4
+ private _failure?: { sequence: number; error: unknown };
5
+ private _generation = 0;
6
+ private _keyGenerations = new Map<string, number>();
7
+
8
+ generation(key: string): string {
9
+ return `${this._generation}:${this._keyGenerations.get(key) ?? 0}`;
10
+ }
11
+
12
+ invalidate(key: string) {
13
+ this._keyGenerations.set(key, (this._keyGenerations.get(key) ?? 0) + 1);
14
+ }
15
+
16
+ invalidateAll() {
17
+ this._generation++;
18
+ this._keyGenerations.clear();
19
+ }
20
+
21
+ enqueue<T>(operation: () => T | PromiseLike<T>): Promise<T> {
22
+ const sequence = ++this._sequence;
23
+ const result = this._tail.then(operation);
24
+ this._tail = result.catch(error => {
25
+ this._failure ??= { sequence, error };
26
+ });
27
+ return result;
28
+ }
29
+
30
+ async flush(): Promise<void> {
31
+ const sequence = this._sequence;
32
+ await this._tail;
33
+ if (this._failure && this._failure.sequence <= sequence) {
34
+ const { error } = this._failure;
35
+ this._failure = undefined;
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+
41
+ const queues = new WeakMap<object, PersistenceQueue>();
42
+
43
+ export function getPersistenceQueue(storage: object): PersistenceQueue {
44
+ let queue = queues.get(storage);
45
+ if (!queue) {
46
+ queue = new PersistenceQueue();
47
+ queues.set(storage, queue);
48
+ }
49
+ return queue;
50
+ }
@@ -47,6 +47,11 @@ export class Monkey
47
47
  }
48
48
 
49
49
  async beanInit(bean: BeanContainer, beanInstance: BeanBase) {
50
+ bean.defineProperty(beanInstance, '$modelPersistence', {
51
+ enumerable: false,
52
+ configurable: true,
53
+ get: () => this._storage,
54
+ });
50
55
  bean.defineProperty(beanInstance, '$queryClient', {
51
56
  enumerable: false,
52
57
  configurable: true,
@@ -1,12 +1,49 @@
1
- import type { VueQueryPluginOptions } from '@tanstack/vue-query';
1
+ import type { Query, VueQueryPluginOptions } from '@tanstack/vue-query';
2
2
 
3
3
  import { dehydrate, hydrate, QueryCache, QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
4
4
  import { BeanBase, deepExtend } from 'zova';
5
5
  import { Service } from 'zova-module-a-bean';
6
6
 
7
+ import type { ModelPersistenceToken, ModelPersistenceValues } from '../lib/modelPersistence.js';
8
+
9
+ import { ModelPersistenceContext } from '../lib/modelPersistence.js';
10
+
7
11
  @Service()
8
12
  export class ServiceStorage extends BeanBase {
9
13
  private _queryClient: QueryClient;
14
+ private _persistence: ModelPersistenceContext;
15
+
16
+ get epoch() {
17
+ return this._persistence.epoch;
18
+ }
19
+
20
+ capture() {
21
+ return this._persistence.capture();
22
+ }
23
+
24
+ current(token: ModelPersistenceToken) {
25
+ return this._persistence.current(token);
26
+ }
27
+
28
+ update(values: ModelPersistenceValues, options?: { initial?: boolean; refetchActive?: boolean }) {
29
+ this._persistence.update(values, options);
30
+ }
31
+
32
+ subscribe(listener: (token: ModelPersistenceToken) => void) {
33
+ return this._persistence.subscribe(listener);
34
+ }
35
+
36
+ clear() {
37
+ this._persistence.clear();
38
+ }
39
+
40
+ touch(query?: Query) {
41
+ this._persistence.touch(query);
42
+ }
43
+
44
+ guardOptions<T extends Parameters<ModelPersistenceContext['guardOptions']>[0]>(options: T): T {
45
+ return this._persistence.guardOptions(options);
46
+ }
10
47
 
11
48
  async moduleLoaded() {
12
49
  // options
@@ -22,6 +59,7 @@ export class ServiceStorage extends BeanBase {
22
59
  defaultOptions: options,
23
60
  queryCache,
24
61
  }));
62
+ this._persistence = new ModelPersistenceContext(queryClient, this.sys.env.SITE_ID);
25
63
  // use plugin
26
64
  const vueQueryPluginOptions: VueQueryPluginOptions = { queryClient };
27
65
  this.app.vue.use(VueQueryPlugin, vueQueryPluginOptions);
@@ -43,7 +81,14 @@ export class ServiceStorage extends BeanBase {
43
81
  }
44
82
  // client
45
83
  if (process.env.CLIENT && this.ctx.meta.$ssr.isRuntimeSsrPreHydration) {
46
- hydrate(this._queryClient, this.ctx.meta.$ssr.stateDefer.query);
84
+ const state = this.ctx.meta.$ssr.stateDefer.query;
85
+ hydrate(this._queryClient, state);
86
+ this._persistence.markHydrated(
87
+ (state?.queries ?? []).flatMap(item => {
88
+ const query = this._queryClient.getQueryCache().get(item.queryHash);
89
+ return query ? [query] : [];
90
+ }),
91
+ );
47
92
  }
48
93
  }
49
94
  }
@@ -12,11 +12,14 @@ import type {
12
12
  } from '@tanstack/vue-query';
13
13
  import type { DebuggerOptions, UnwrapNestedRefs } from 'vue';
14
14
 
15
+ import type { ServiceStorage } from '../service/storage.js';
16
+
15
17
  import 'zova';
16
18
 
17
19
  declare module 'zova' {
18
20
  export interface BeanBase {
19
21
  $queryClient: ReturnType<typeof useQueryClient>;
22
+ $modelPersistence: ServiceStorage;
20
23
  }
21
24
  }
22
25
 
@@ -0,0 +1,34 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { registerHooks } from 'node:module';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // Keep framework container/decorator bootstrapping out of the Node fixture. All
6
+ // persistence, Query, QueryObserver, queue and state methods are production code.
7
+ const fixtures = {
8
+ 'zova':
9
+ "export class BeanBase {} export class BeanSimple {} export const SymbolBeanFullName = Symbol.for('model-test-bean'); export const cast = value => value; export const deepExtend = (...values) => Object.assign({}, ...values);",
10
+ 'zova-module-a-bean': 'export const Service = () => target => target;',
11
+ 'model-test-metadata': "export const __ThisModule__ = 'a-model';",
12
+ };
13
+ registerHooks({
14
+ resolve(specifier, context, nextResolve) {
15
+ if (specifier in fixtures) return { url: `model-test:${specifier}`, shortCircuit: true };
16
+ if (specifier.endsWith('/.metadata/this.js'))
17
+ return { url: 'model-test:model-test-metadata', shortCircuit: true };
18
+ if (specifier.startsWith('.') && specifier.endsWith('.js') && context.parentURL) {
19
+ const target = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
20
+ if (existsSync(fileURLToPath(target))) return nextResolve(target.href, context);
21
+ }
22
+ return nextResolve(specifier, context);
23
+ },
24
+ load(url, context, nextLoad) {
25
+ if (url.startsWith('model-test:')) {
26
+ return {
27
+ format: 'module',
28
+ source: fixtures[url.slice('model-test:'.length)],
29
+ shortCircuit: true,
30
+ };
31
+ }
32
+ return nextLoad(url, context);
33
+ },
34
+ });