@esmx/core 3.0.0-rc.116 → 3.0.0-rc.118
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/LICENSE +21 -0
- package/README.md +17 -3
- package/README.zh-CN.md +20 -6
- package/dist/app.mjs +4 -2
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.mjs +28 -1
- package/dist/cli/validate.d.ts +15 -0
- package/dist/cli/validate.mjs +139 -0
- package/dist/cli/validate.test.d.ts +1 -0
- package/dist/cli/validate.test.mjs +216 -0
- package/dist/core.d.ts +31 -0
- package/dist/core.mjs +71 -5
- package/dist/declaration/index.d.ts +47 -0
- package/dist/declaration/index.mjs +63 -0
- package/dist/declaration/index.test.d.ts +1 -0
- package/dist/declaration/index.test.mjs +202 -0
- package/dist/declaration/lower.d.ts +11 -0
- package/dist/declaration/lower.mjs +78 -0
- package/dist/declaration/lower.test.d.ts +1 -0
- package/dist/declaration/lower.test.mjs +333 -0
- package/dist/declaration/reader.d.ts +28 -0
- package/dist/declaration/reader.mjs +55 -0
- package/dist/declaration/reinit.e2e.test.d.ts +1 -0
- package/dist/declaration/reinit.e2e.test.mjs +117 -0
- package/dist/declaration/resolver.d.ts +59 -0
- package/dist/declaration/resolver.mjs +430 -0
- package/dist/declaration/resolver.test.d.ts +1 -0
- package/dist/declaration/resolver.test.mjs +1005 -0
- package/dist/declaration/schema.d.ts +89 -0
- package/dist/declaration/schema.mjs +282 -0
- package/dist/declaration/schema.test.d.ts +1 -0
- package/dist/declaration/schema.test.mjs +101 -0
- package/dist/declaration/semver.d.ts +22 -0
- package/dist/declaration/semver.mjs +229 -0
- package/dist/declaration/semver.test.d.ts +1 -0
- package/dist/declaration/semver.test.mjs +87 -0
- package/dist/declaration/test-fixtures.d.ts +18 -0
- package/dist/declaration/test-fixtures.mjs +69 -0
- package/dist/declaration/types.d.ts +58 -0
- package/dist/declaration/types.mjs +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +10 -0
- package/dist/manifest-json.d.ts +61 -0
- package/dist/manifest-json.mjs +68 -5
- package/dist/manifest-json.test.d.ts +1 -0
- package/dist/manifest-json.test.mjs +196 -0
- package/dist/module-config.d.ts +45 -5
- package/dist/module-config.mjs +60 -49
- package/dist/module-config.test.mjs +227 -91
- package/dist/pack-config.d.ts +2 -9
- package/dist/render-context.mjs +22 -5
- package/dist/utils/import-map.d.ts +23 -3
- package/dist/utils/import-map.mjs +38 -1
- package/dist/utils/import-map.test.mjs +228 -0
- package/package.json +9 -6
- package/src/app.ts +5 -2
- package/src/cli/cli.ts +44 -1
- package/src/cli/validate.test.ts +251 -0
- package/src/cli/validate.ts +196 -0
- package/src/core.ts +84 -5
- package/src/declaration/index.test.ts +223 -0
- package/src/declaration/index.ts +135 -0
- package/src/declaration/lower.test.ts +372 -0
- package/src/declaration/lower.ts +135 -0
- package/src/declaration/reader.ts +93 -0
- package/src/declaration/reinit.e2e.test.ts +148 -0
- package/src/declaration/resolver.test.ts +1111 -0
- package/src/declaration/resolver.ts +638 -0
- package/src/declaration/schema.test.ts +118 -0
- package/src/declaration/schema.ts +339 -0
- package/src/declaration/semver.test.ts +101 -0
- package/src/declaration/semver.ts +278 -0
- package/src/declaration/test-fixtures.ts +96 -0
- package/src/declaration/types.ts +71 -0
- package/src/index.ts +28 -1
- package/src/manifest-json.test.ts +236 -0
- package/src/manifest-json.ts +166 -5
- package/src/module-config.test.ts +266 -105
- package/src/module-config.ts +130 -58
- package/src/pack-config.ts +2 -9
- package/src/render-context.ts +34 -6
- package/src/utils/import-map.test.ts +261 -0
- package/src/utils/import-map.ts +92 -6
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import type { Diagnostic, ResolvedMount, SupplyEntry } from '../declaration';
|
|
3
|
+
import { resolveDeclaration } from '../declaration';
|
|
4
|
+
|
|
5
|
+
export const VALIDATE_HELP = `Usage: esmx validate [--json]
|
|
6
|
+
|
|
7
|
+
Build-free dry run of the RFC 0001 module protocol: reads the package.json
|
|
8
|
+
"esmx" declaration in the current directory and runs the resolution layer
|
|
9
|
+
(phases 1-2: consumption graph + supply merge) without building anything.
|
|
10
|
+
|
|
11
|
+
Exit code is non-zero only when an error-severity diagnostic is found;
|
|
12
|
+
warnings alone exit 0. A package without an "esmx" field uses the legacy
|
|
13
|
+
entry.node.ts config and is not an error.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--json Emit a machine-readable envelope on stdout and nothing else:
|
|
17
|
+
{
|
|
18
|
+
"diagnostics": [ { "code", "check"?, "module", "package"?,
|
|
19
|
+
"found"?, "required"?, "message", "fix" } ],
|
|
20
|
+
"supply": { "<package>": { "groups": [ { "major",
|
|
21
|
+
"provider", "version" } ] } },
|
|
22
|
+
"mounts": { "<module>": { "name", "root",
|
|
23
|
+
"artifactDir", "built" } }
|
|
24
|
+
}
|
|
25
|
+
Two providers of one (package, major) is an E_DUP_PROVIDER error
|
|
26
|
+
(a shared dependency must have a single owner).
|
|
27
|
+
Legacy packages emit { "protocol": "legacy", "diagnostics": [] }.
|
|
28
|
+
--help Show this message.`;
|
|
29
|
+
|
|
30
|
+
export interface RunValidateOptions {
|
|
31
|
+
json?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RunValidateResult {
|
|
35
|
+
exitCode: number;
|
|
36
|
+
/** Full stdout payload (JSON envelope or human report). */
|
|
37
|
+
output: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ValidateEnvelopeEntry {
|
|
41
|
+
code: string;
|
|
42
|
+
check?: string;
|
|
43
|
+
module: string;
|
|
44
|
+
package?: string;
|
|
45
|
+
found?: string;
|
|
46
|
+
required?: string;
|
|
47
|
+
message: string;
|
|
48
|
+
fix: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toEnvelopeEntry(diagnostic: Diagnostic): ValidateEnvelopeEntry {
|
|
52
|
+
const entry: ValidateEnvelopeEntry = {
|
|
53
|
+
code: diagnostic.code,
|
|
54
|
+
module: diagnostic.module,
|
|
55
|
+
message: diagnostic.message,
|
|
56
|
+
fix: diagnostic.fix
|
|
57
|
+
};
|
|
58
|
+
if (diagnostic.check !== undefined) {
|
|
59
|
+
entry.check = diagnostic.check;
|
|
60
|
+
}
|
|
61
|
+
if (diagnostic.package !== undefined) {
|
|
62
|
+
entry.package = diagnostic.package;
|
|
63
|
+
}
|
|
64
|
+
if (diagnostic.found !== undefined) {
|
|
65
|
+
entry.found = diagnostic.found;
|
|
66
|
+
}
|
|
67
|
+
if (diagnostic.required !== undefined) {
|
|
68
|
+
entry.required = diagnostic.required;
|
|
69
|
+
}
|
|
70
|
+
return entry;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatHumanDiagnostic(diagnostic: Diagnostic): string {
|
|
74
|
+
const color = diagnostic.severity === 'error' ? 'red' : 'yellow';
|
|
75
|
+
const location = diagnostic.package
|
|
76
|
+
? `${diagnostic.module} → ${diagnostic.package}`
|
|
77
|
+
: diagnostic.module;
|
|
78
|
+
const lines = [styleText(color, `[${diagnostic.code}]`) + ` ${location}`];
|
|
79
|
+
if (diagnostic.check) {
|
|
80
|
+
lines.push(` check: ${diagnostic.check}`);
|
|
81
|
+
}
|
|
82
|
+
if (diagnostic.found !== undefined || diagnostic.required !== undefined) {
|
|
83
|
+
lines.push(
|
|
84
|
+
` found: ${diagnostic.found ?? '—'} / required: ${diagnostic.required ?? '—'}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
lines.push(` ${diagnostic.message}`);
|
|
88
|
+
lines.push(` fix: ${diagnostic.fix}`);
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatHumanReport(
|
|
93
|
+
diagnostics: Diagnostic[],
|
|
94
|
+
supply: Record<string, SupplyEntry>,
|
|
95
|
+
mounts: Record<string, ResolvedMount>
|
|
96
|
+
): string {
|
|
97
|
+
const sections: string[] = [];
|
|
98
|
+
const errorCount = diagnostics.filter((d) => d.severity === 'error').length;
|
|
99
|
+
const warningCount = diagnostics.length - errorCount;
|
|
100
|
+
|
|
101
|
+
if (diagnostics.length === 0) {
|
|
102
|
+
sections.push(styleText('green', '✓ Declaration is valid.'));
|
|
103
|
+
} else {
|
|
104
|
+
sections.push(diagnostics.map(formatHumanDiagnostic).join('\n'));
|
|
105
|
+
sections.push(`${errorCount} error(s), ${warningCount} warning(s).`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const supplyEntries = Object.entries(supply);
|
|
109
|
+
sections.push(
|
|
110
|
+
supplyEntries.length === 0
|
|
111
|
+
? 'Supply: (empty)'
|
|
112
|
+
: [
|
|
113
|
+
'Supply:',
|
|
114
|
+
...supplyEntries.flatMap(([pkg, entry]) =>
|
|
115
|
+
entry.groups.map(
|
|
116
|
+
(group) =>
|
|
117
|
+
` ${pkg} → ${group.provider}@${group.version ?? 'unresolved'}${
|
|
118
|
+
entry.groups.length > 1
|
|
119
|
+
? ` (major ${group.major})`
|
|
120
|
+
: ''
|
|
121
|
+
}`
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
].join('\n')
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const mountEntries = Object.entries(mounts);
|
|
128
|
+
sections.push(
|
|
129
|
+
mountEntries.length === 0
|
|
130
|
+
? 'Mounts: (empty)'
|
|
131
|
+
: [
|
|
132
|
+
'Mounts:',
|
|
133
|
+
...mountEntries.map(
|
|
134
|
+
([name, mount]) =>
|
|
135
|
+
` ${name} → ${mount.artifactDir} (${mount.built ? 'built' : 'not built'})`
|
|
136
|
+
)
|
|
137
|
+
].join('\n')
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return sections.join('\n\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `esmx validate` core (RFC 0001 §10 Phase 5, the agent verification loop).
|
|
145
|
+
* Pure with respect to process state: never writes to stdout/stderr and
|
|
146
|
+
* never exits — the CLI case owns printing and the exit code.
|
|
147
|
+
*/
|
|
148
|
+
export async function runValidate(
|
|
149
|
+
rootDir: string,
|
|
150
|
+
options: RunValidateOptions = {}
|
|
151
|
+
): Promise<RunValidateResult> {
|
|
152
|
+
const resolved = resolveDeclaration(rootDir);
|
|
153
|
+
if (!resolved) {
|
|
154
|
+
if (options.json) {
|
|
155
|
+
return {
|
|
156
|
+
exitCode: 0,
|
|
157
|
+
output: JSON.stringify(
|
|
158
|
+
{ protocol: 'legacy', diagnostics: [] },
|
|
159
|
+
null,
|
|
160
|
+
4
|
|
161
|
+
)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
exitCode: 0,
|
|
166
|
+
output: [
|
|
167
|
+
'No "esmx" field in package.json — this module uses the legacy entry.node.ts config.',
|
|
168
|
+
'That is fine: the new module protocol is opt-in. To adopt it, add an "esmx" field to package.json (see the module protocol docs).'
|
|
169
|
+
].join('\n')
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { diagnostics, supply } = resolved;
|
|
174
|
+
const mounts = resolved.mounts;
|
|
175
|
+
const hasError = diagnostics.some((d) => d.severity === 'error');
|
|
176
|
+
const exitCode = hasError ? 1 : 0;
|
|
177
|
+
|
|
178
|
+
if (options.json) {
|
|
179
|
+
return {
|
|
180
|
+
exitCode,
|
|
181
|
+
output: JSON.stringify(
|
|
182
|
+
{
|
|
183
|
+
diagnostics: diagnostics.map(toEnvelopeEntry),
|
|
184
|
+
supply,
|
|
185
|
+
mounts
|
|
186
|
+
},
|
|
187
|
+
null,
|
|
188
|
+
4
|
|
189
|
+
)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
exitCode,
|
|
194
|
+
output: formatHumanReport(diagnostics, supply, mounts)
|
|
195
|
+
};
|
|
196
|
+
}
|
package/src/core.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ImportMap, ScopesMap, SpecifierMap } from '@esmx/import';
|
|
|
8
8
|
|
|
9
9
|
import serialize from 'serialize-javascript';
|
|
10
10
|
import { type App, createApp } from './app';
|
|
11
|
+
import { resolveModuleOptions } from './declaration';
|
|
11
12
|
import { getManifestList, type ManifestJson } from './manifest-json';
|
|
12
13
|
import {
|
|
13
14
|
type ModuleConfig,
|
|
@@ -396,13 +397,14 @@ export class Esmx {
|
|
|
396
397
|
throw new Error('Cannot be initialized repeatedly');
|
|
397
398
|
}
|
|
398
399
|
|
|
399
|
-
const
|
|
400
|
+
const packageJson = await this.readJson<Record<string, unknown>>(
|
|
400
401
|
path.resolve(this.root, 'package.json')
|
|
401
402
|
);
|
|
403
|
+
const name = String(packageJson.name);
|
|
402
404
|
const moduleConfig = parseModuleConfig(
|
|
403
405
|
name,
|
|
404
406
|
this.root,
|
|
405
|
-
this._options.modules
|
|
407
|
+
resolveModuleOptions(this.root, packageJson, this._options.modules)
|
|
406
408
|
);
|
|
407
409
|
const packConfig = parsePackConfig(this._options.packs);
|
|
408
410
|
this._readied = {
|
|
@@ -472,6 +474,78 @@ export class Esmx {
|
|
|
472
474
|
return true;
|
|
473
475
|
}
|
|
474
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Atomic generational relink (RFC 0001 §9).
|
|
479
|
+
*
|
|
480
|
+
* In-process remotes are not hot-swappable: the server import map and the
|
|
481
|
+
* module loader are captured once when the app is created. When a mounted
|
|
482
|
+
* remote republishes (new resolved versions, new wiring), the composer
|
|
483
|
+
* adopts it by relinking — building a NEW generation (fresh cache,
|
|
484
|
+
* re-resolved module config, rebuilt app capturing a new import map +
|
|
485
|
+
* loader) and switching to it atomically.
|
|
486
|
+
*
|
|
487
|
+
* The previous generation keeps serving throughout the rebuild; the new
|
|
488
|
+
* app is installed only once it is fully built, then the previous app's
|
|
489
|
+
* resources are released. If the rebuild fails, the previous generation
|
|
490
|
+
* is fully restored (rollback) and the error propagates — a failed relink
|
|
491
|
+
* never takes the running server down.
|
|
492
|
+
*
|
|
493
|
+
* Scope: the production render path (`start`/`preview`). Dev rebuilds its
|
|
494
|
+
* own app. Cross-compiler dev-watch invalidation remains future work.
|
|
495
|
+
*
|
|
496
|
+
* @returns true once the new generation is live.
|
|
497
|
+
* @throws {NotReadyError} when called before {@link init}.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```ts
|
|
501
|
+
* // After a mounted remote republishes:
|
|
502
|
+
* await esmx.reinit(); // new generation live, old requests drained
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
public async reinit(): Promise<boolean> {
|
|
506
|
+
const previous = this.readied;
|
|
507
|
+
|
|
508
|
+
const packageJson = await this.readJson<Record<string, unknown>>(
|
|
509
|
+
path.resolve(this.root, 'package.json')
|
|
510
|
+
);
|
|
511
|
+
const name = String(packageJson.name);
|
|
512
|
+
const moduleConfig = parseModuleConfig(
|
|
513
|
+
name,
|
|
514
|
+
this.root,
|
|
515
|
+
resolveModuleOptions(this.root, packageJson, this._options.modules)
|
|
516
|
+
);
|
|
517
|
+
const packConfig = parsePackConfig(this._options.packs);
|
|
518
|
+
|
|
519
|
+
// Install the new generation's config + fresh cache, but keep serving
|
|
520
|
+
// on the previous app until the new one is built. App-creation reads
|
|
521
|
+
// `this.readied`, so the rebuilt app captures the new import map.
|
|
522
|
+
this._readied = {
|
|
523
|
+
command: previous.command,
|
|
524
|
+
app: previous.app,
|
|
525
|
+
moduleConfig,
|
|
526
|
+
packConfig,
|
|
527
|
+
cache: createCache(this.isProd)
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const command = previous.command;
|
|
532
|
+
const devApp = this._options.devApp || defaultDevApp;
|
|
533
|
+
const app: App = [COMMAND.dev, COMMAND.build].includes(command)
|
|
534
|
+
? await devApp(this)
|
|
535
|
+
: await createApp(this, command);
|
|
536
|
+
this.readied.app = app;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
// Rollback: restore the previous generation untouched.
|
|
539
|
+
this._readied = previous;
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (previous.app?.destroy) {
|
|
544
|
+
await previous.app.destroy();
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
|
|
475
549
|
/**
|
|
476
550
|
* Execute the application's build process.
|
|
477
551
|
*
|
|
@@ -632,7 +706,7 @@ export class Esmx {
|
|
|
632
706
|
await this._options.postBuild?.(this);
|
|
633
707
|
return true;
|
|
634
708
|
} catch (e) {
|
|
635
|
-
console.error(e);
|
|
709
|
+
console.error('[@esmx/core] postBuild hook failed:', e);
|
|
636
710
|
return false;
|
|
637
711
|
}
|
|
638
712
|
}
|
|
@@ -965,17 +1039,20 @@ export class Esmx {
|
|
|
965
1039
|
src: string;
|
|
966
1040
|
filepath: string;
|
|
967
1041
|
code: string;
|
|
1042
|
+
integrity: Record<string, string> | null;
|
|
968
1043
|
}
|
|
969
1044
|
: {
|
|
970
1045
|
src: null;
|
|
971
1046
|
filepath: null;
|
|
972
1047
|
code: string;
|
|
1048
|
+
integrity: Record<string, string> | null;
|
|
973
1049
|
}
|
|
974
1050
|
> {
|
|
975
1051
|
return this.readied.cache(
|
|
976
1052
|
`getImportMap-${mode}`,
|
|
977
1053
|
async (): Promise<any> => {
|
|
978
1054
|
const importmap = await this.getImportMap('client');
|
|
1055
|
+
const { integrity } = importmap;
|
|
979
1056
|
const { basePathPlaceholder } = this;
|
|
980
1057
|
let filepath: string | null = null;
|
|
981
1058
|
if (this._importmapHash === null) {
|
|
@@ -1023,7 +1100,8 @@ document.head.appendChild(script);
|
|
|
1023
1100
|
return {
|
|
1024
1101
|
src,
|
|
1025
1102
|
filepath,
|
|
1026
|
-
code: `<script data-base="${basePathPlaceholder}" src="${src}"></script
|
|
1103
|
+
code: `<script data-base="${basePathPlaceholder}" src="${src}"></script>`,
|
|
1104
|
+
integrity: integrity ?? null
|
|
1027
1105
|
};
|
|
1028
1106
|
}
|
|
1029
1107
|
if (basePathPlaceholder) {
|
|
@@ -1041,7 +1119,8 @@ document.head.appendChild(script);
|
|
|
1041
1119
|
return {
|
|
1042
1120
|
src: null,
|
|
1043
1121
|
filepath: null,
|
|
1044
|
-
code: `<script type="importmap">${serialize(importmap, { isJSON: true, unsafe: true })}</script
|
|
1122
|
+
code: `<script type="importmap">${serialize(importmap, { isJSON: true, unsafe: true })}</script>`,
|
|
1123
|
+
integrity: integrity ?? null
|
|
1045
1124
|
};
|
|
1046
1125
|
}
|
|
1047
1126
|
);
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { ModuleConfig } from '../module-config';
|
|
4
|
+
import { resolveDeclaration, resolveModuleOptions } from './index';
|
|
5
|
+
import {
|
|
6
|
+
createFixtureRoot,
|
|
7
|
+
removeFixtureRoot,
|
|
8
|
+
writeFixturePackage
|
|
9
|
+
} from './test-fixtures';
|
|
10
|
+
|
|
11
|
+
const fixtureRoots: string[] = [];
|
|
12
|
+
|
|
13
|
+
async function fixtureRoot(): Promise<string> {
|
|
14
|
+
const root = await createFixtureRoot();
|
|
15
|
+
fixtureRoots.push(root);
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('resolveDeclaration', () => {
|
|
25
|
+
it('should return null when the package has no esmx field', async () => {
|
|
26
|
+
const root = await fixtureRoot();
|
|
27
|
+
const appDir = writeFixturePackage(root, {
|
|
28
|
+
dir: 'legacy',
|
|
29
|
+
packageJson: { name: 'legacy', version: '1.0.0' }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(resolveDeclaration(appDir)).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should compose reader, resolver and lowering', async () => {
|
|
36
|
+
const root = await fixtureRoot();
|
|
37
|
+
writeFixturePackage(root, {
|
|
38
|
+
dir: 'app/node_modules/shared',
|
|
39
|
+
packageJson: {
|
|
40
|
+
name: 'shared',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
esmx: { provides: ['vue'] }
|
|
43
|
+
},
|
|
44
|
+
built: true
|
|
45
|
+
});
|
|
46
|
+
writeFixturePackage(root, {
|
|
47
|
+
dir: 'app/node_modules/shared/node_modules/vue',
|
|
48
|
+
packageJson: { name: 'vue', version: '3.4.21' }
|
|
49
|
+
});
|
|
50
|
+
const appDir = writeFixturePackage(root, {
|
|
51
|
+
dir: 'app',
|
|
52
|
+
packageJson: {
|
|
53
|
+
name: 'app',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
dependencies: { vue: '^3.4.0' },
|
|
56
|
+
esmx: {
|
|
57
|
+
entry: {
|
|
58
|
+
client: './src/entry.client.ts',
|
|
59
|
+
server: './src/entry.server.ts'
|
|
60
|
+
},
|
|
61
|
+
uses: ['shared']
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = resolveDeclaration(appDir);
|
|
67
|
+
|
|
68
|
+
expect(result).not.toBeNull();
|
|
69
|
+
expect(result?.supply.vue).toEqual({
|
|
70
|
+
groups: [{ major: 3, provider: 'shared', version: '3.4.21' }]
|
|
71
|
+
});
|
|
72
|
+
expect(result?.config).toEqual({
|
|
73
|
+
links: {
|
|
74
|
+
shared: path.join(appDir, 'node_modules/shared/dist')
|
|
75
|
+
},
|
|
76
|
+
imports: { vue: 'shared/vue' }
|
|
77
|
+
});
|
|
78
|
+
expect(
|
|
79
|
+
result?.diagnostics.filter((d) => d.severity === 'error')
|
|
80
|
+
).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('resolveModuleOptions', () => {
|
|
85
|
+
it('should keep the legacy path untouched when there is no esmx field', async () => {
|
|
86
|
+
const root = await fixtureRoot();
|
|
87
|
+
const appDir = writeFixturePackage(root, {
|
|
88
|
+
dir: 'legacy',
|
|
89
|
+
packageJson: { name: 'legacy', version: '1.0.0' }
|
|
90
|
+
});
|
|
91
|
+
const modules: ModuleConfig = {
|
|
92
|
+
imports: { vue: 'shared/vue' },
|
|
93
|
+
exports: ['pkg:vue']
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = resolveModuleOptions(
|
|
97
|
+
appDir,
|
|
98
|
+
{ name: 'legacy' },
|
|
99
|
+
modules
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result).toBe(modules);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw E_PROTOCOL_IN_BEHAVIOR when esmx coexists with protocol fields in options.modules', async () => {
|
|
106
|
+
const root = await fixtureRoot();
|
|
107
|
+
const appDir = writeFixturePackage(root, {
|
|
108
|
+
dir: 'app',
|
|
109
|
+
packageJson: {
|
|
110
|
+
name: 'app',
|
|
111
|
+
version: '1.0.0',
|
|
112
|
+
esmx: { provides: ['vue'] }
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(() =>
|
|
117
|
+
resolveModuleOptions(
|
|
118
|
+
appDir,
|
|
119
|
+
{ name: 'app', esmx: { provides: ['vue'] } },
|
|
120
|
+
{ imports: { vue: 'shared/vue' } }
|
|
121
|
+
)
|
|
122
|
+
).toThrowError(/E_PROTOCOL_IN_BEHAVIOR/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should allow links-only options.modules as environment overrides', async () => {
|
|
126
|
+
const root = await fixtureRoot();
|
|
127
|
+
writeFixturePackage(root, {
|
|
128
|
+
dir: 'shared',
|
|
129
|
+
packageJson: {
|
|
130
|
+
name: 'shared',
|
|
131
|
+
version: '1.0.0',
|
|
132
|
+
esmx: { provides: ['vue'] }
|
|
133
|
+
},
|
|
134
|
+
built: true
|
|
135
|
+
});
|
|
136
|
+
writeFixturePackage(root, {
|
|
137
|
+
dir: 'shared/node_modules/vue',
|
|
138
|
+
packageJson: { name: 'vue', version: '3.4.21' }
|
|
139
|
+
});
|
|
140
|
+
const appDir = writeFixturePackage(root, {
|
|
141
|
+
dir: 'app',
|
|
142
|
+
packageJson: {
|
|
143
|
+
name: 'app',
|
|
144
|
+
version: '1.0.0',
|
|
145
|
+
dependencies: { vue: '^3.4.0' },
|
|
146
|
+
esmx: {
|
|
147
|
+
entry: {
|
|
148
|
+
client: './src/entry.client.ts',
|
|
149
|
+
server: './src/entry.server.ts'
|
|
150
|
+
},
|
|
151
|
+
uses: ['shared']
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const result = resolveModuleOptions(
|
|
157
|
+
appDir,
|
|
158
|
+
{ name: 'app', esmx: {} },
|
|
159
|
+
{ links: { shared: '../shared/dist' } }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(result).toEqual({
|
|
163
|
+
links: { shared: path.join(root, 'shared/dist') },
|
|
164
|
+
imports: { vue: 'shared/vue' }
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should throw a clear error listing error diagnostics', async () => {
|
|
169
|
+
const root = await fixtureRoot();
|
|
170
|
+
const appDir = writeFixturePackage(root, {
|
|
171
|
+
dir: 'app',
|
|
172
|
+
packageJson: {
|
|
173
|
+
name: 'app',
|
|
174
|
+
version: '1.0.0',
|
|
175
|
+
esmx: {
|
|
176
|
+
entry: { client: './src/entry.client.ts' },
|
|
177
|
+
uses: ['ghost']
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(() =>
|
|
183
|
+
resolveModuleOptions(appDir, { name: 'app', esmx: {} })
|
|
184
|
+
).toThrowError(/E_NOT_LINKED[\s\S]*ghost/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should print warnings instead of throwing', async () => {
|
|
188
|
+
const root = await fixtureRoot();
|
|
189
|
+
writeFixturePackage(root, {
|
|
190
|
+
dir: 'app/node_modules/base',
|
|
191
|
+
packageJson: {
|
|
192
|
+
name: 'base',
|
|
193
|
+
version: '1.0.0',
|
|
194
|
+
dependencies: { vue: '^3.4.0' },
|
|
195
|
+
esmx: { provides: ['vue'] }
|
|
196
|
+
},
|
|
197
|
+
built: true
|
|
198
|
+
});
|
|
199
|
+
writeFixturePackage(root, {
|
|
200
|
+
dir: 'app/node_modules/base/node_modules/vue',
|
|
201
|
+
packageJson: { name: 'vue', version: '3.4.21' }
|
|
202
|
+
});
|
|
203
|
+
const appDir = writeFixturePackage(root, {
|
|
204
|
+
dir: 'app',
|
|
205
|
+
packageJson: {
|
|
206
|
+
name: 'app',
|
|
207
|
+
version: '1.0.0',
|
|
208
|
+
esmx: {
|
|
209
|
+
entry: { client: './src/entry.client.ts' },
|
|
210
|
+
uses: ['base']
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
215
|
+
|
|
216
|
+
const result = resolveModuleOptions(appDir, { name: 'app', esmx: {} });
|
|
217
|
+
|
|
218
|
+
expect(result?.imports).toEqual({ vue: 'base/vue' });
|
|
219
|
+
expect(warn).toHaveBeenCalledWith(
|
|
220
|
+
expect.stringContaining('W_NO_RANGE')
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
});
|