@bleedingdev/modern-js-create 3.4.0-ultramodern.0 → 3.4.0-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.
- package/README.md +7 -7
- package/dist/cjs/ultramodern-workspace/add-vertical.cjs +24 -0
- package/dist/cjs/ultramodern-workspace/contracts.cjs +8 -4
- package/dist/cjs/ultramodern-workspace/demo-components.cjs +14 -21
- package/dist/cjs/ultramodern-workspace/descriptors.cjs +2 -2
- package/dist/cjs/ultramodern-workspace/effect-api.cjs +162 -31
- package/dist/cjs/ultramodern-workspace/module-federation.cjs +29 -10
- package/dist/cjs/ultramodern-workspace/package-json.cjs +102 -14
- package/dist/cjs/ultramodern-workspace/versions.cjs +1 -5
- package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +7 -0
- package/dist/cjs/ultramodern-workspace/write-workspace.cjs +6 -8
- package/dist/esm/ultramodern-workspace/add-vertical.js +25 -1
- package/dist/esm/ultramodern-workspace/contracts.js +9 -5
- package/dist/esm/ultramodern-workspace/demo-components.js +14 -21
- package/dist/esm/ultramodern-workspace/descriptors.js +2 -2
- package/dist/esm/ultramodern-workspace/effect-api.js +162 -31
- package/dist/esm/ultramodern-workspace/module-federation.js +27 -11
- package/dist/esm/ultramodern-workspace/package-json.js +87 -11
- package/dist/esm/ultramodern-workspace/versions.js +2 -3
- package/dist/esm/ultramodern-workspace/workspace-scripts.js +5 -1
- package/dist/esm/ultramodern-workspace/write-workspace.js +9 -11
- package/dist/esm-node/ultramodern-workspace/add-vertical.js +25 -1
- package/dist/esm-node/ultramodern-workspace/contracts.js +9 -5
- package/dist/esm-node/ultramodern-workspace/demo-components.js +14 -21
- package/dist/esm-node/ultramodern-workspace/descriptors.js +2 -2
- package/dist/esm-node/ultramodern-workspace/effect-api.js +162 -31
- package/dist/esm-node/ultramodern-workspace/module-federation.js +27 -11
- package/dist/esm-node/ultramodern-workspace/package-json.js +87 -11
- package/dist/esm-node/ultramodern-workspace/versions.js +2 -3
- package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +5 -1
- package/dist/esm-node/ultramodern-workspace/write-workspace.js +9 -11
- package/dist/types/ultramodern-workspace/module-federation.d.ts +1 -0
- package/dist/types/ultramodern-workspace/package-json.d.ts +12 -2
- package/dist/types/ultramodern-workspace/versions.d.ts +1 -2
- package/dist/types/ultramodern-workspace/workspace-scripts.d.ts +1 -0
- package/package.json +4 -4
- package/template-workspace/AGENTS.md.handlebars +2 -1
- package/template-workspace/README.md.handlebars +7 -0
- package/template-workspace/oxfmt.config.ts +3 -1
- package/template-workspace/oxlint.config.ts +3 -1
- package/template-workspace/patches/@tanstack__router-core@1.171.13.patch +51 -0
- package/template-workspace/pnpm-workspace.yaml.handlebars +11 -0
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +42 -10
- package/templates/app/shell-frame.tsx +1 -2
- package/templates/app/ultramodern-route-head.tsx.handlebars +22 -11
- package/templates/packages/shared-contracts-index.ts +206 -272
- package/templates/workspace-scripts/assert-mf-types.mjs.handlebars +9 -0
- package/templates/workspace-scripts/ultramodern-typecheck.mjs +197 -0
- package/templates/workspace-scripts/validate-ultramodern-workspace.mjs.handlebars +234 -2
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import {
|
|
5
|
+
accessSync,
|
|
6
|
+
chmodSync,
|
|
7
|
+
constants,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const workspaceRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const cpuCount = Math.max(
|
|
18
|
+
1,
|
|
19
|
+
typeof os.availableParallelism === 'function'
|
|
20
|
+
? os.availableParallelism()
|
|
21
|
+
: os.cpus().length,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
function fail(message) {
|
|
25
|
+
console.error(message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parsePositiveInt(value, label) {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
32
|
+
fail(`${label} must be a positive integer.`);
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function envPositiveInt(name, fallback) {
|
|
38
|
+
const value = process.env[name]?.trim();
|
|
39
|
+
return value ? parsePositiveInt(value, name) : fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readArgs() {
|
|
43
|
+
let buildTarget;
|
|
44
|
+
let projectTarget;
|
|
45
|
+
let checkers;
|
|
46
|
+
let builders;
|
|
47
|
+
const passthrough = [];
|
|
48
|
+
|
|
49
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
50
|
+
const arg = args[index];
|
|
51
|
+
const next = args[index + 1];
|
|
52
|
+
|
|
53
|
+
if (arg === '--') {
|
|
54
|
+
passthrough.push(...args.slice(index + 1));
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (arg === '--build' || arg === '-b') {
|
|
59
|
+
buildTarget = next && !next.startsWith('-') ? next : 'tsconfig.json';
|
|
60
|
+
if (buildTarget === next) {
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (arg === '--project' || arg === '-p') {
|
|
67
|
+
if (!next || next.startsWith('-')) {
|
|
68
|
+
fail(`${arg} requires a tsconfig path.`);
|
|
69
|
+
}
|
|
70
|
+
projectTarget = next;
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (arg === '--checkers') {
|
|
76
|
+
if (!next || next.startsWith('-')) {
|
|
77
|
+
fail('--checkers requires a positive integer.');
|
|
78
|
+
}
|
|
79
|
+
checkers = parsePositiveInt(next, '--checkers');
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (arg === '--builders') {
|
|
85
|
+
if (!next || next.startsWith('-')) {
|
|
86
|
+
fail('--builders requires a positive integer.');
|
|
87
|
+
}
|
|
88
|
+
builders = parsePositiveInt(next, '--builders');
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
passthrough.push(arg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (buildTarget && projectTarget) {
|
|
97
|
+
fail('Choose either --build or --project, not both.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
mode: buildTarget ? 'build' : 'project',
|
|
102
|
+
target: buildTarget ?? projectTarget ?? 'tsconfig.json',
|
|
103
|
+
checkers,
|
|
104
|
+
builders,
|
|
105
|
+
passthrough,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveTsgoBinary() {
|
|
110
|
+
const explicitBinary = process.env.EFFECT_TSGO_BIN || process.env.TSGO_BIN;
|
|
111
|
+
if (explicitBinary) {
|
|
112
|
+
return explicitBinary;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const cli = process.env.EFFECT_TSGO_CLI || 'effect-tsgo';
|
|
116
|
+
const result = spawnSync(cli, ['get-exe-path'], {
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (result.error) {
|
|
122
|
+
fail(
|
|
123
|
+
`Unable to run ${cli}. Run pnpm install or set EFFECT_TSGO_BIN to the native-preview tsgo binary.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (result.status !== 0) {
|
|
127
|
+
fail(
|
|
128
|
+
result.stderr?.trim() ||
|
|
129
|
+
`${cli} get-exe-path exited with status ${result.status}.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result.stdout.trim() || cli;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parsed = readArgs();
|
|
137
|
+
const defaultBuilders = Math.min(8, Math.max(1, Math.floor(cpuCount / 2)));
|
|
138
|
+
const builders =
|
|
139
|
+
parsed.builders ??
|
|
140
|
+
envPositiveInt('ULTRAMODERN_TSGO_BUILDERS', defaultBuilders);
|
|
141
|
+
const defaultCheckers =
|
|
142
|
+
parsed.mode === 'build'
|
|
143
|
+
? Math.min(4, Math.max(1, Math.floor(cpuCount / builders)))
|
|
144
|
+
: Math.min(8, Math.max(2, cpuCount - 1));
|
|
145
|
+
const checkers =
|
|
146
|
+
parsed.checkers ??
|
|
147
|
+
envPositiveInt('ULTRAMODERN_TSGO_CHECKERS', defaultCheckers);
|
|
148
|
+
const tsgoBin = resolveTsgoBinary();
|
|
149
|
+
|
|
150
|
+
if (!existsSync(tsgoBin)) {
|
|
151
|
+
fail(`TS-Go binary not found at ${tsgoBin}. Run pnpm install.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
accessSync(tsgoBin, constants.X_OK);
|
|
156
|
+
} catch {
|
|
157
|
+
chmodSync(tsgoBin, 0o755);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
mkdirSync(join(workspaceRoot, 'node_modules/.cache/tsgo'), {
|
|
161
|
+
recursive: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const tsgoArgs =
|
|
165
|
+
parsed.mode === 'build'
|
|
166
|
+
? [
|
|
167
|
+
'--build',
|
|
168
|
+
parsed.target,
|
|
169
|
+
'--pretty',
|
|
170
|
+
'false',
|
|
171
|
+
'--checkers',
|
|
172
|
+
String(checkers),
|
|
173
|
+
'--builders',
|
|
174
|
+
String(builders),
|
|
175
|
+
'--stopBuildOnErrors',
|
|
176
|
+
...parsed.passthrough,
|
|
177
|
+
]
|
|
178
|
+
: [
|
|
179
|
+
'--project',
|
|
180
|
+
parsed.target,
|
|
181
|
+
'--noEmit',
|
|
182
|
+
'--pretty',
|
|
183
|
+
'false',
|
|
184
|
+
'--checkers',
|
|
185
|
+
String(checkers),
|
|
186
|
+
...parsed.passthrough,
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const result = spawnSync(tsgoBin, tsgoArgs, {
|
|
190
|
+
stdio: 'inherit',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (result.error) {
|
|
194
|
+
throw result.error;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.exit(result.status ?? 1);
|
|
@@ -91,6 +91,8 @@ const expectedRemoteSubsetsForRefs = refs =>
|
|
|
91
91
|
.map(expectedRemoteContractSubset);
|
|
92
92
|
const requiredMicroVerticalPaths = vertical => [
|
|
93
93
|
`${vertical.path}/package.json`,
|
|
94
|
+
`${vertical.path}/tsconfig.json`,
|
|
95
|
+
`${vertical.path}/tsconfig.mf-types.json`,
|
|
94
96
|
`${vertical.path}/modern.config.ts`,
|
|
95
97
|
`${vertical.path}/module-federation.config.ts`,
|
|
96
98
|
`${vertical.path}/api/effect/index.ts`,
|
|
@@ -222,6 +224,15 @@ const assertMicroVerticalContractGraph = ({
|
|
|
222
224
|
'.modernjs/ultramodern-generated-contract.json shell moduleFederation.remotes',
|
|
223
225
|
'regenerate the generated shell Module Federation contract',
|
|
224
226
|
);
|
|
227
|
+
assertSameJson(
|
|
228
|
+
shellContract.ssr,
|
|
229
|
+
{
|
|
230
|
+
mode: 'string',
|
|
231
|
+
moduleFederationAppSSR: true,
|
|
232
|
+
},
|
|
233
|
+
'.modernjs/ultramodern-generated-contract.json shell SSR contract',
|
|
234
|
+
'restore generated string SSR Module Federation settings',
|
|
235
|
+
);
|
|
225
236
|
|
|
226
237
|
for (const vertical of fullStackVerticals) {
|
|
227
238
|
const expectedRefs = vertical.verticalRefs ?? [];
|
|
@@ -353,6 +364,7 @@ const assertMicroVerticalContractGraph = ({
|
|
|
353
364
|
contract: contractEntry.effect?.contract,
|
|
354
365
|
client: contractEntry.effect?.client,
|
|
355
366
|
},
|
|
367
|
+
ssr: contractEntry.ssr,
|
|
356
368
|
},
|
|
357
369
|
{
|
|
358
370
|
kind: 'vertical',
|
|
@@ -369,12 +381,136 @@ const assertMicroVerticalContractGraph = ({
|
|
|
369
381
|
contract: './shared/effect/api',
|
|
370
382
|
client: './effect/client',
|
|
371
383
|
},
|
|
384
|
+
ssr: {
|
|
385
|
+
mode: 'string',
|
|
386
|
+
moduleFederationAppSSR: true,
|
|
387
|
+
},
|
|
372
388
|
},
|
|
373
389
|
`.modernjs/ultramodern-generated-contract.json apps.${vertical.id}`,
|
|
374
390
|
'regenerate the generated MicroVertical contract entry',
|
|
375
391
|
);
|
|
376
392
|
}
|
|
377
393
|
};
|
|
394
|
+
const toPosixPath = value => value.split(path.sep).join('/');
|
|
395
|
+
const referenceFrom = (fromPath, toPath) => ({
|
|
396
|
+
path: toPosixPath(path.relative(fromPath, toPath)),
|
|
397
|
+
});
|
|
398
|
+
const sharedPackagePaths = [
|
|
399
|
+
'packages/shared-contracts',
|
|
400
|
+
'packages/shared-design-tokens',
|
|
401
|
+
];
|
|
402
|
+
const tsgoCacheKey = packagePath => packagePath.replace(/[^a-zA-Z0-9._-]+/gu, '__');
|
|
403
|
+
const assertProjectReferenceEmitConfig = (tsConfig, packagePath) => {
|
|
404
|
+
const compilerOptions = tsConfig.compilerOptions ?? {};
|
|
405
|
+
const relativeRoot = toPosixPath(path.relative(packagePath, '.')) || '.';
|
|
406
|
+
assert(compilerOptions.composite === true, `${packagePath} must stay a composite TS-Go project`);
|
|
407
|
+
assert(compilerOptions.declaration === true, `${packagePath} must emit declarations for TS-Go build mode`);
|
|
408
|
+
assert(compilerOptions.declarationMap === false, `${packagePath} must not emit declaration maps during checks`);
|
|
409
|
+
assert(compilerOptions.emitDeclarationOnly === true, `${packagePath} must only emit declarations during checks`);
|
|
410
|
+
assert(compilerOptions.noEmit === false, `${packagePath} must override root noEmit for TS-Go build mode`);
|
|
411
|
+
assert(
|
|
412
|
+
compilerOptions.outDir === `${relativeRoot}/node_modules/.cache/tsgo/declarations/${tsgoCacheKey(packagePath)}`,
|
|
413
|
+
`${packagePath} must emit TS-Go declarations into the generated cache`,
|
|
414
|
+
);
|
|
415
|
+
assert(
|
|
416
|
+
compilerOptions.tsBuildInfoFile === `${relativeRoot}/node_modules/.cache/tsgo/${tsgoCacheKey(packagePath)}.tsbuildinfo`,
|
|
417
|
+
`${packagePath} must keep TS-Go build info in the generated cache`,
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
const assertTsConfigReferenceGraph = () => {
|
|
421
|
+
const rootTsConfig = readJson('tsconfig.json');
|
|
422
|
+
const shellTsConfig = readJson('apps/shell-super-app/tsconfig.json');
|
|
423
|
+
const shellMfTypesTsConfig = readJson(
|
|
424
|
+
'apps/shell-super-app/tsconfig.mf-types.json',
|
|
425
|
+
);
|
|
426
|
+
const expectedRootReferences = [
|
|
427
|
+
...sharedPackagePaths,
|
|
428
|
+
'apps/shell-super-app',
|
|
429
|
+
...fullStackVerticals.map(vertical => vertical.path),
|
|
430
|
+
].map(referencePath => ({ path: referencePath }));
|
|
431
|
+
const expectedShellReferences = [
|
|
432
|
+
...sharedPackagePaths,
|
|
433
|
+
...fullStackVerticals.map(vertical => vertical.path),
|
|
434
|
+
].map(referencePath =>
|
|
435
|
+
referenceFrom('apps/shell-super-app', referencePath),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
assertSameJson(
|
|
439
|
+
rootTsConfig.files,
|
|
440
|
+
[],
|
|
441
|
+
'tsconfig.json files',
|
|
442
|
+
'restore the generated root project-reference tsconfig',
|
|
443
|
+
);
|
|
444
|
+
assertSameJson(
|
|
445
|
+
rootTsConfig.references ?? [],
|
|
446
|
+
expectedRootReferences,
|
|
447
|
+
'tsconfig.json references',
|
|
448
|
+
'restore the generated root project-reference graph',
|
|
449
|
+
);
|
|
450
|
+
assertSameJson(
|
|
451
|
+
shellTsConfig.references ?? [],
|
|
452
|
+
expectedShellReferences,
|
|
453
|
+
'apps/shell-super-app/tsconfig.json references',
|
|
454
|
+
'restore the generated shell project-reference graph',
|
|
455
|
+
);
|
|
456
|
+
assertProjectReferenceEmitConfig(
|
|
457
|
+
shellTsConfig,
|
|
458
|
+
'apps/shell-super-app',
|
|
459
|
+
);
|
|
460
|
+
assertSameJson(
|
|
461
|
+
shellMfTypesTsConfig,
|
|
462
|
+
{
|
|
463
|
+
extends: './tsconfig.json',
|
|
464
|
+
include: ['src/modern-app-env.d.ts'],
|
|
465
|
+
},
|
|
466
|
+
'apps/shell-super-app/tsconfig.mf-types.json',
|
|
467
|
+
'restore the generated shell Module Federation DTS boundary',
|
|
468
|
+
);
|
|
469
|
+
for (const sharedPackagePath of sharedPackagePaths) {
|
|
470
|
+
assertProjectReferenceEmitConfig(
|
|
471
|
+
readJson(`${sharedPackagePath}/tsconfig.json`),
|
|
472
|
+
sharedPackagePath,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const vertical of fullStackVerticals) {
|
|
477
|
+
const verticalTsConfig = readJson(`${vertical.path}/tsconfig.json`);
|
|
478
|
+
const verticalMfTypesTsConfig = readJson(
|
|
479
|
+
`${vertical.path}/tsconfig.mf-types.json`,
|
|
480
|
+
);
|
|
481
|
+
const expectedVerticalReferences = [
|
|
482
|
+
...sharedPackagePaths,
|
|
483
|
+
...(vertical.verticalRefs ?? [])
|
|
484
|
+
.map(verticalRef =>
|
|
485
|
+
fullStackVerticals.find(candidate => candidate.id === verticalRef),
|
|
486
|
+
)
|
|
487
|
+
.filter(Boolean)
|
|
488
|
+
.map(referencedVertical => referencedVertical.path),
|
|
489
|
+
].map(referencePath => referenceFrom(vertical.path, referencePath));
|
|
490
|
+
assertSameJson(
|
|
491
|
+
verticalTsConfig.references ?? [],
|
|
492
|
+
expectedVerticalReferences,
|
|
493
|
+
`${vertical.path}/tsconfig.json references`,
|
|
494
|
+
'restore the generated MicroVertical project-reference graph',
|
|
495
|
+
);
|
|
496
|
+
assertProjectReferenceEmitConfig(verticalTsConfig, vertical.path);
|
|
497
|
+
assertSameJson(
|
|
498
|
+
verticalMfTypesTsConfig,
|
|
499
|
+
{
|
|
500
|
+
extends: './tsconfig.json',
|
|
501
|
+
include: [
|
|
502
|
+
'src/federation-entry.tsx',
|
|
503
|
+
...vertical.componentPaths.map(componentPath =>
|
|
504
|
+
componentPath.replace(`${vertical.path}/`, ''),
|
|
505
|
+
),
|
|
506
|
+
'src/modern-app-env.d.ts',
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
`${vertical.path}/tsconfig.mf-types.json`,
|
|
510
|
+
'restore the generated MicroVertical Module Federation DTS boundary',
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
378
514
|
const packageJsonFiles = startDir => {
|
|
379
515
|
const files = [];
|
|
380
516
|
const queue = [startDir];
|
|
@@ -511,10 +647,79 @@ const assertCloudflareQualityGates = (appId, qualityGates) => {
|
|
|
511
647
|
assert(qualityGates?.csp?.finalMode === 'report-only-dogfood', `${appId} CSP final mode decision is missing`);
|
|
512
648
|
};
|
|
513
649
|
const extractAssetPrefixExpression = modernConfig => {
|
|
514
|
-
const match = /const
|
|
650
|
+
const match = /const\s+assetPrefix\s*=\s*(?<expression>[\s\S]*?);/u.exec(modernConfig);
|
|
515
651
|
assert(match?.groups?.expression, 'modern.config.ts must assign assetPrefix');
|
|
516
652
|
return match.groups.expression;
|
|
517
653
|
};
|
|
654
|
+
const stripYamlInlineComment = value => {
|
|
655
|
+
let quote = undefined;
|
|
656
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
657
|
+
const character = value[index];
|
|
658
|
+
if (quote) {
|
|
659
|
+
if (character === quote && value[index - 1] !== '\\') {
|
|
660
|
+
quote = undefined;
|
|
661
|
+
}
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (character === '"' || character === "'") {
|
|
665
|
+
quote = character;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (character === '#' && (index === 0 || /\s/u.test(value[index - 1]))) {
|
|
669
|
+
return value.slice(0, index).trimEnd();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return value.trimEnd();
|
|
673
|
+
};
|
|
674
|
+
const normalizeYamlScalar = value => {
|
|
675
|
+
const trimmed = stripYamlInlineComment(value).trim();
|
|
676
|
+
if (
|
|
677
|
+
((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
678
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) &&
|
|
679
|
+
trimmed.length >= 2
|
|
680
|
+
) {
|
|
681
|
+
return trimmed.slice(1, -1);
|
|
682
|
+
}
|
|
683
|
+
return trimmed;
|
|
684
|
+
};
|
|
685
|
+
const extractWorkflowNodeVersions = workflowText => {
|
|
686
|
+
const versions = [];
|
|
687
|
+
const lines = workflowText.split(/\r?\n/u);
|
|
688
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
689
|
+
const match = /^(?<indent>\s*)node-version\s*:\s*(?<value>.*)$/u.exec(
|
|
690
|
+
lines[lineIndex],
|
|
691
|
+
);
|
|
692
|
+
if (!match?.groups) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let value = normalizeYamlScalar(match.groups.value);
|
|
697
|
+
if (value === '' || value === '|' || value === '>') {
|
|
698
|
+
const currentIndent = match.groups.indent.length;
|
|
699
|
+
for (
|
|
700
|
+
let nextLineIndex = lineIndex + 1;
|
|
701
|
+
nextLineIndex < lines.length;
|
|
702
|
+
nextLineIndex += 1
|
|
703
|
+
) {
|
|
704
|
+
const nextLine = lines[nextLineIndex];
|
|
705
|
+
const nextTrimmed = nextLine.trim();
|
|
706
|
+
if (nextTrimmed === '' || nextTrimmed.startsWith('#')) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const nextIndent = nextLine.match(/^\s*/u)?.[0]?.length ?? 0;
|
|
710
|
+
if (nextIndent <= currentIndent) {
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
value = normalizeYamlScalar(nextTrimmed);
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (value !== '') {
|
|
718
|
+
versions.push(value);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return versions;
|
|
722
|
+
};
|
|
518
723
|
const expectedWorkerName = packageSuffix => `${packageScope}-${packageSuffix}`.slice(0, 63);
|
|
519
724
|
const expectedChunkLoadingGlobal = mfName =>
|
|
520
725
|
`__ULTRAMODERN_${mfName
|
|
@@ -559,6 +764,7 @@ const requiredPaths = [
|
|
|
559
764
|
'.gitignore',
|
|
560
765
|
'package.json',
|
|
561
766
|
'pnpm-workspace.yaml',
|
|
767
|
+
'tsconfig.json',
|
|
562
768
|
'tsconfig.base.json',
|
|
563
769
|
'oxlint.config.ts',
|
|
564
770
|
'oxfmt.config.ts',
|
|
@@ -581,7 +787,10 @@ const requiredPaths = [
|
|
|
581
787
|
'scripts/setup-agent-reference-repos.mjs',
|
|
582
788
|
'scripts/ultramodern-performance-readiness.config.mjs',
|
|
583
789
|
'scripts/ultramodern-performance-readiness.mjs',
|
|
790
|
+
'scripts/ultramodern-typecheck.mjs',
|
|
584
791
|
'apps/shell-super-app/package.json',
|
|
792
|
+
'apps/shell-super-app/tsconfig.json',
|
|
793
|
+
'apps/shell-super-app/tsconfig.mf-types.json',
|
|
585
794
|
'apps/shell-super-app/modern.config.ts',
|
|
586
795
|
'apps/shell-super-app/module-federation.config.ts',
|
|
587
796
|
'apps/shell-super-app/src/modern-app-env.d.ts',
|
|
@@ -597,9 +806,13 @@ const requiredPaths = [
|
|
|
597
806
|
'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts',
|
|
598
807
|
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
599
808
|
...{{shellRouteMetaPathsJson}},
|
|
809
|
+
'packages/shared-contracts/package.json',
|
|
600
810
|
'packages/shared-contracts/src/index.ts',
|
|
811
|
+
'packages/shared-contracts/tsconfig.json',
|
|
812
|
+
'packages/shared-design-tokens/package.json',
|
|
601
813
|
'packages/shared-design-tokens/src/index.ts',
|
|
602
814
|
'packages/shared-design-tokens/src/tokens.css',
|
|
815
|
+
'packages/shared-design-tokens/tsconfig.json',
|
|
603
816
|
];
|
|
604
817
|
|
|
605
818
|
for (const vertical of fullStackVerticals) {
|
|
@@ -643,6 +856,7 @@ assertMicroVerticalContractGraph({
|
|
|
643
856
|
overlay,
|
|
644
857
|
shellPackage,
|
|
645
858
|
});
|
|
859
|
+
assertTsConfigReferenceGraph();
|
|
646
860
|
|
|
647
861
|
assert(rootPackage.private === true, 'Root package must be private');
|
|
648
862
|
assert(typeof rootPackage.packageManager === 'string', 'Root must declare packageManager');
|
|
@@ -660,7 +874,12 @@ assert(generatedContract.node?.engineRange === '>=26', 'Generated contract must
|
|
|
660
874
|
assert(readText('.mise.toml').includes(`node = "${expectedNodeVersion}"`), 'mise must pin the generated Node version');
|
|
661
875
|
assert(readText('.mise.toml').includes(`pnpm = "${packageManagerPnpmVersion}"`), 'mise must pin the generated pnpm version');
|
|
662
876
|
const workflowText = readText('.github/workflows/ultramodern-workspace-gates.yml');
|
|
663
|
-
|
|
877
|
+
const workflowNodeVersions = extractWorkflowNodeVersions(workflowText);
|
|
878
|
+
assert(workflowNodeVersions.length > 0, 'CI workflow must configure setup-node node-version');
|
|
879
|
+
assert(
|
|
880
|
+
workflowNodeVersions.every(nodeVersion => nodeVersion === expectedNodeVersion),
|
|
881
|
+
`CI workflow must pin the generated Node version ${expectedNodeVersion}; found ${workflowNodeVersions.join(', ')}`,
|
|
882
|
+
);
|
|
664
883
|
assert(!workflowText.includes('FORCE_JAVASCRIPT_ACTIONS_TO_NODE24'), 'CI workflow must not carry the legacy Node 24 override');
|
|
665
884
|
assert(rootPackage.modernjs?.preset === 'presetUltramodern', 'Root must declare presetUltramodern');
|
|
666
885
|
assert(rootPackage.modernjs?.packageSource?.config === './.modernjs/ultramodern-package-source.json', 'Root must point at package source metadata');
|
|
@@ -711,6 +930,7 @@ assert(
|
|
|
711
930
|
);
|
|
712
931
|
assert(rootPackage.scripts?.['cloudflare:build'] === expectedCloudflareBuildScript, 'Root cloudflare:build script is incorrect');
|
|
713
932
|
assert(!('ultramodern:check' in (rootPackage.scripts ?? {})), 'Root must not expose ultramodern:check');
|
|
933
|
+
assert(rootPackage.scripts?.typecheck === 'node ./scripts/ultramodern-typecheck.mjs --build tsconfig.json', 'Root typecheck must run TS-Go build mode across project references');
|
|
714
934
|
assert(rootPackage.scripts?.['contract:check'] === 'node ./scripts/validate-ultramodern-workspace.mjs', 'Root must expose contract:check');
|
|
715
935
|
assert(rootPackage.scripts?.['i18n:boundaries'] === 'node ./scripts/check-ultramodern-i18n-boundaries.mjs', 'Root must expose i18n:boundaries');
|
|
716
936
|
assert(rootPackage.scripts?.['performance:readiness'] === 'node ./scripts/ultramodern-performance-readiness.mjs', 'Root must expose default-on performance readiness diagnostics');
|
|
@@ -723,6 +943,9 @@ assert(performanceReadinessConfig.includes("failOn: 'framework-invariant'"), 'Pe
|
|
|
723
943
|
assert(performanceReadinessScript.includes("ULTRAMODERN_PERFORMANCE_READINESS_DIAGNOSTICS"), 'Performance readiness script must support env opt-out');
|
|
724
944
|
assert(performanceReadinessScript.includes('ultramodern-performance-readiness-diagnostics-v1'), 'Performance readiness script must emit the deterministic report profile');
|
|
725
945
|
const i18nBoundaryScript = readText('scripts/check-ultramodern-i18n-boundaries.mjs');
|
|
946
|
+
const ultramodernTypecheckScript = readText('scripts/ultramodern-typecheck.mjs');
|
|
947
|
+
assert(ultramodernTypecheckScript.includes("'--checkers'") && ultramodernTypecheckScript.includes("'--builders'"), 'Root typecheck script must drive TS-Go checker and builder parallelism');
|
|
948
|
+
assert(ultramodernTypecheckScript.includes('effect-tsgo') && ultramodernTypecheckScript.includes('get-exe-path'), 'Root typecheck script must resolve the @effect/tsgo native-preview binary');
|
|
726
949
|
assert(
|
|
727
950
|
i18nBoundaryScript.includes("from '@modern-js/code-tools'") &&
|
|
728
951
|
i18nBoundaryScript.includes('runWorkspaceSourceCheck'),
|
|
@@ -775,6 +998,7 @@ assert(generatedContract.performanceReadiness?.optOut?.env === 'ULTRAMODERN_PERF
|
|
|
775
998
|
assert(JSON.stringify(generatedContract.performanceReadiness?.signals?.map(signal => signal.id)) === JSON.stringify(expectedPerformanceReadinessSignals), 'Performance readiness signal ids are incorrect');
|
|
776
999
|
|
|
777
1000
|
const shellModernConfig = readText('apps/shell-super-app/modern.config.ts');
|
|
1001
|
+
const shellModuleFederationConfig = readText('apps/shell-super-app/module-federation.config.ts');
|
|
778
1002
|
const shellRouteHead = readText('apps/shell-super-app/src/routes/ultramodern-route-head.tsx');
|
|
779
1003
|
const shellRouteMetadata = readText('apps/shell-super-app/src/routes/ultramodern-route-metadata.ts');
|
|
780
1004
|
assert(shellRouteMetadata.includes('@generated by @modern-js/create'), 'Shell route metadata compatibility manifest must be marked generated');
|
|
@@ -819,12 +1043,16 @@ assert(shellModernConfig.includes('assetPrefix,'), 'Shell modern.config.ts must
|
|
|
819
1043
|
assert(shellContract?.config?.dev?.assetPrefix === '/', 'Shell dev asset prefix must stay origin-relative');
|
|
820
1044
|
assert(shellContract?.config?.output?.assetPrefix?.default === '/', 'Shell asset prefix must default to origin-relative paths');
|
|
821
1045
|
assert(JSON.stringify(shellContract?.config?.output?.assetPrefix?.envFallbackOrder) === JSON.stringify(['MODERN_ASSET_PREFIX', 'ULTRAMODERN_ASSET_PREFIX']), 'Shell asset prefix env fallback order is incorrect');
|
|
1046
|
+
assert(shellContract?.config?.output?.disableTsChecker === false, 'Shell must keep the framework TypeScript checker enabled');
|
|
822
1047
|
assert(shellContract?.config?.performance?.readinessDiagnostics?.default === 'enabled', 'Shell performance readiness diagnostics must be default-on');
|
|
823
1048
|
assert(shellContract?.config?.performance?.readinessDiagnostics?.failOn === 'framework-invariant', 'Shell performance readiness diagnostics must only fail framework invariants by default');
|
|
824
1049
|
assert(shellContract?.config?.performance?.readinessDiagnostics?.optOut?.env === 'ULTRAMODERN_PERFORMANCE_READINESS_DIAGNOSTICS=false', 'Shell performance readiness env opt-out is incorrect');
|
|
825
1050
|
assert(JSON.stringify(shellContract?.config?.source?.siteUrl?.envFallbackOrder) === JSON.stringify(['MODERN_PUBLIC_SITE_URL', 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP', 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN', 'SHELL_SUPER_APP_PORT']), 'Shell site URL env fallback order is incorrect');
|
|
826
1051
|
assert(shellContract?.config?.rspack?.output?.uniqueName === 'shellSuperApp', 'Shell Rspack uniqueName is incorrect');
|
|
827
1052
|
assert(shellContract?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal('shellSuperApp'), 'Shell Rspack chunkLoadingGlobal is incorrect');
|
|
1053
|
+
assert(shellContract?.moduleFederation?.dts?.compilerInstance === 'tsgo', 'Shell must keep mandatory DTS compiler');
|
|
1054
|
+
assert(shellContract?.moduleFederation?.dts?.tsConfigPath === './tsconfig.mf-types.json', 'Shell must keep dedicated Module Federation DTS tsconfig');
|
|
1055
|
+
assert(shellModuleFederationConfig.includes("tsConfigPath: './tsconfig.mf-types.json'"), 'Shell Module Federation config must use the dedicated DTS tsconfig');
|
|
828
1056
|
assert(topology.shell?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell topology Cloudflare workerName is incorrect');
|
|
829
1057
|
assert(shellContract?.styling?.federation?.owner?.id === 'shell-super-app', 'Shell CSS federation owner is missing');
|
|
830
1058
|
assert(shellContract?.styling?.federation?.role === 'shell-base-overlay', 'Shell must own base and overlay CSS');
|
|
@@ -859,6 +1087,7 @@ assert(!('effectServices' in topology), 'Default APIs must be vertical-owned, no
|
|
|
859
1087
|
for (const vertical of fullStackVerticals) {
|
|
860
1088
|
const packageJson = readJson(`${vertical.path}/package.json`);
|
|
861
1089
|
const modernConfig = readText(`${vertical.path}/modern.config.ts`);
|
|
1090
|
+
const moduleFederationConfig = readText(`${vertical.path}/module-federation.config.ts`);
|
|
862
1091
|
const routeHead = readText(`${vertical.path}/src/routes/ultramodern-route-head.tsx`);
|
|
863
1092
|
const routeMetadata = readText(`${vertical.path}/src/routes/ultramodern-route-metadata.ts`);
|
|
864
1093
|
assert(routeMetadata.includes('@generated by @modern-js/create'), `${vertical.id} route metadata compatibility manifest must be marked generated`);
|
|
@@ -913,6 +1142,7 @@ for (const vertical of fullStackVerticals) {
|
|
|
913
1142
|
assert(contractEntry?.config?.dev?.assetPrefix === '/', `${vertical.id} dev asset prefix must stay origin-relative`);
|
|
914
1143
|
assert(contractEntry?.config?.output?.assetPrefix?.default === '/', `${vertical.id} asset prefix must default to origin-relative paths`);
|
|
915
1144
|
assert(JSON.stringify(contractEntry?.config?.output?.assetPrefix?.envFallbackOrder) === JSON.stringify(['MODERN_ASSET_PREFIX', 'ULTRAMODERN_ASSET_PREFIX']), `${vertical.id} asset prefix env fallback order is incorrect`);
|
|
1145
|
+
assert(contractEntry?.config?.output?.disableTsChecker === false, `${vertical.id} must keep the framework TypeScript checker enabled`);
|
|
916
1146
|
assert(contractEntry?.config?.performance?.readinessDiagnostics?.default === 'enabled', `${vertical.id} performance readiness diagnostics must be default-on`);
|
|
917
1147
|
assert(contractEntry?.config?.performance?.readinessDiagnostics?.failOn === 'framework-invariant', `${vertical.id} performance readiness diagnostics must only fail framework invariants by default`);
|
|
918
1148
|
assert(contractEntry?.config?.performance?.readinessDiagnostics?.optOut?.config === 'scripts/ultramodern-performance-readiness.config.mjs', `${vertical.id} performance readiness opt-out config is incorrect`);
|
|
@@ -922,6 +1152,8 @@ for (const vertical of fullStackVerticals) {
|
|
|
922
1152
|
assert(contractEntry?.moduleFederation?.name === vertical.mfName, `${vertical.id} MF name is incorrect`);
|
|
923
1153
|
assert(JSON.stringify(contractEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), `${vertical.id} MF exposes are incorrect`);
|
|
924
1154
|
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === 'tsgo', `${vertical.id} must keep mandatory DTS compiler`);
|
|
1155
|
+
assert(contractEntry?.moduleFederation?.dts?.tsConfigPath === './tsconfig.mf-types.json', `${vertical.id} must keep dedicated Module Federation DTS tsconfig`);
|
|
1156
|
+
assert(moduleFederationConfig.includes("tsConfigPath: './tsconfig.mf-types.json'"), `${vertical.id} Module Federation config must use the dedicated DTS tsconfig`);
|
|
925
1157
|
assert(JSON.stringify(contractEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), `${vertical.id} MF verticalRefs are incorrect`);
|
|
926
1158
|
assert(
|
|
927
1159
|
JSON.stringify((contractEntry?.moduleFederation?.remotes ?? []).map(remote => remote.id)) ===
|