@bleedingdev/modern-js-create 3.4.0-ultramodern.9 → 3.5.0-ultramodern.1
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 +49 -23
- package/dist/cjs/index.cjs +37 -4
- package/dist/cjs/locale/en.cjs +12 -2
- package/dist/cjs/locale/zh.cjs +12 -2
- package/dist/cjs/ultramodern-tooling/commands.cjs +431 -0
- package/dist/cjs/ultramodern-tooling/config.cjs +172 -0
- package/dist/cjs/ultramodern-workspace/add-vertical.cjs +87 -75
- package/dist/cjs/ultramodern-workspace/{effect-api.cjs → api.cjs} +207 -208
- package/dist/cjs/ultramodern-workspace/bridge-config.cjs +327 -0
- package/dist/cjs/ultramodern-workspace/codesmith.cjs +1 -0
- package/dist/cjs/ultramodern-workspace/contracts.cjs +128 -657
- package/dist/cjs/ultramodern-workspace/demo-components.cjs +12 -12
- package/dist/cjs/ultramodern-workspace/descriptors.cjs +25 -25
- package/dist/cjs/ultramodern-workspace/fs-io.cjs +117 -0
- package/dist/cjs/ultramodern-workspace/generation-result.cjs +5 -5
- package/dist/cjs/ultramodern-workspace/index.cjs +4 -0
- package/dist/cjs/ultramodern-workspace/mf-validation.cjs +571 -0
- package/dist/cjs/ultramodern-workspace/module-federation.cjs +39 -12
- package/dist/cjs/ultramodern-workspace/overlays.cjs +2 -2
- package/dist/cjs/ultramodern-workspace/package-json.cjs +58 -41
- package/dist/cjs/ultramodern-workspace/policy.cjs +4 -4
- package/dist/cjs/ultramodern-workspace/public-api.cjs +4 -0
- package/dist/cjs/ultramodern-workspace/public-surface.cjs +2 -2
- package/dist/cjs/ultramodern-workspace/versions.cjs +15 -0
- package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +120 -50
- package/dist/cjs/ultramodern-workspace/write-workspace.cjs +64 -21
- package/dist/esm/index.js +37 -4
- package/dist/esm/locale/en.js +12 -2
- package/dist/esm/locale/zh.js +12 -2
- package/dist/esm/ultramodern-tooling/commands.js +378 -0
- package/dist/esm/ultramodern-tooling/config.js +120 -0
- package/dist/esm/ultramodern-workspace/add-vertical.js +88 -79
- package/dist/esm/ultramodern-workspace/{effect-api.js → api.js} +146 -147
- package/dist/esm/ultramodern-workspace/bridge-config.js +270 -0
- package/dist/esm/ultramodern-workspace/codesmith.js +1 -0
- package/dist/esm/ultramodern-workspace/contracts.js +130 -611
- package/dist/esm/ultramodern-workspace/demo-components.js +13 -13
- package/dist/esm/ultramodern-workspace/descriptors.js +11 -11
- package/dist/esm/ultramodern-workspace/fs-io.js +116 -2
- package/dist/esm/ultramodern-workspace/generation-result.js +6 -6
- package/dist/esm/ultramodern-workspace/index.js +1 -0
- package/dist/esm/ultramodern-workspace/mf-validation.js +516 -0
- package/dist/esm/ultramodern-workspace/module-federation.js +40 -13
- package/dist/esm/ultramodern-workspace/overlays.js +2 -2
- package/dist/esm/ultramodern-workspace/package-json.js +57 -40
- package/dist/esm/ultramodern-workspace/policy.js +5 -5
- package/dist/esm/ultramodern-workspace/public-api.js +1 -0
- package/dist/esm/ultramodern-workspace/public-surface.js +2 -2
- package/dist/esm/ultramodern-workspace/versions.js +7 -1
- package/dist/esm/ultramodern-workspace/workspace-scripts.js +108 -34
- package/dist/esm/ultramodern-workspace/write-workspace.js +66 -26
- package/dist/esm-node/index.js +37 -4
- package/dist/esm-node/locale/en.js +12 -2
- package/dist/esm-node/locale/zh.js +12 -2
- package/dist/esm-node/ultramodern-tooling/commands.js +379 -0
- package/dist/esm-node/ultramodern-tooling/config.js +121 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical.js +88 -79
- package/dist/esm-node/ultramodern-workspace/{effect-api.js → api.js} +146 -147
- package/dist/esm-node/ultramodern-workspace/bridge-config.js +271 -0
- package/dist/esm-node/ultramodern-workspace/codesmith.js +1 -0
- package/dist/esm-node/ultramodern-workspace/contracts.js +130 -611
- package/dist/esm-node/ultramodern-workspace/demo-components.js +13 -13
- package/dist/esm-node/ultramodern-workspace/descriptors.js +11 -11
- package/dist/esm-node/ultramodern-workspace/fs-io.js +116 -2
- package/dist/esm-node/ultramodern-workspace/generation-result.js +6 -6
- package/dist/esm-node/ultramodern-workspace/index.js +1 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation.js +517 -0
- package/dist/esm-node/ultramodern-workspace/module-federation.js +40 -13
- package/dist/esm-node/ultramodern-workspace/overlays.js +2 -2
- package/dist/esm-node/ultramodern-workspace/package-json.js +57 -40
- package/dist/esm-node/ultramodern-workspace/policy.js +5 -5
- package/dist/esm-node/ultramodern-workspace/public-api.js +1 -0
- package/dist/esm-node/ultramodern-workspace/public-surface.js +2 -2
- package/dist/esm-node/ultramodern-workspace/versions.js +7 -1
- package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +108 -34
- package/dist/esm-node/ultramodern-workspace/write-workspace.js +66 -26
- package/dist/types/locale/en.d.ts +10 -0
- package/dist/types/locale/index.d.ts +20 -0
- package/dist/types/locale/zh.d.ts +10 -0
- package/dist/types/ultramodern-tooling/commands.d.ts +1 -0
- package/dist/types/ultramodern-tooling/config.d.ts +43 -0
- package/dist/types/ultramodern-workspace/add-vertical.d.ts +5 -2
- package/dist/types/ultramodern-workspace/api.d.ts +73 -0
- package/dist/types/ultramodern-workspace/bridge-config.d.ts +64 -0
- package/dist/types/ultramodern-workspace/codesmith.d.ts +1 -0
- package/dist/types/ultramodern-workspace/contracts.d.ts +2 -17
- package/dist/types/ultramodern-workspace/descriptors.d.ts +10 -10
- package/dist/types/ultramodern-workspace/fs-io.d.ts +1 -0
- package/dist/types/ultramodern-workspace/index.d.ts +2 -0
- package/dist/types/ultramodern-workspace/mf-validation.d.ts +27 -0
- package/dist/types/ultramodern-workspace/package-json.d.ts +5 -4
- package/dist/types/ultramodern-workspace/public-api.d.ts +2 -0
- package/dist/types/ultramodern-workspace/types.d.ts +9 -7
- package/dist/types/ultramodern-workspace/versions.d.ts +6 -0
- package/dist/types/ultramodern-workspace/workspace-scripts.d.ts +2 -7
- package/dist/types/ultramodern-workspace/write-workspace.d.ts +4 -2
- package/package.json +5 -3
- package/template-workspace/{.agents → .codex}/skills-lock.json +12 -12
- package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +2 -0
- package/template-workspace/.gitignore.handlebars +3 -0
- package/template-workspace/AGENTS.md.handlebars +11 -5
- package/template-workspace/README.md.handlebars +55 -13
- package/template-workspace/oxfmt.config.ts +1 -0
- package/template-workspace/oxlint.config.ts +1 -0
- package/template-workspace/patches/@tanstack__router-core@1.171.13.patch +51 -0
- package/template-workspace/pnpm-workspace.yaml.handlebars +7 -0
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +77 -67
- package/templates/workspace-scripts/assert-mf-types.mjs.handlebars +51 -8
- package/templates/workspace-scripts/check-ultramodern-api-boundaries.mts +366 -0
- package/templates/workspace-scripts/{check-ultramodern-i18n-boundaries.mjs → check-ultramodern-i18n-boundaries.mts} +1 -1
- package/templates/workspace-scripts/generate-public-surface-assets.mjs +246 -9
- package/templates/workspace-scripts/proof-cloudflare-version.mjs +277 -9
- package/templates/workspace-scripts/ultramodern-cloudflare-proof.mjs +35 -10
- package/templates/workspace-scripts/ultramodern-performance-readiness.mjs +165 -2
- package/templates/workspace-scripts/ultramodern-typecheck.mjs +4 -2
- package/templates/workspace-scripts/validate-ultramodern-workspace.mjs.handlebars +842 -84
- package/dist/types/ultramodern-workspace/effect-api.d.ts +0 -73
- /package/template-workspace/{.agents → .codex}/rstackjs-agent-skills-LICENSE +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsbuild-best-practices/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/command-map.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/common-analysis-patterns.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-common.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/rsdoctor-data-types.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rslib-best-practices/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rslib-modern-package/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-best-practices/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/references/bottlenecks.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/references/tracing-guide.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/scripts/analyze_trace.js +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rstest-best-practices/SKILL.md +0 -0
|
@@ -4,18 +4,255 @@ import path from 'node:path';
|
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
|
|
6
6
|
const workspaceRoot = path.resolve(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
);
|
|
10
|
-
const contractPath = path.join(
|
|
11
|
-
workspaceRoot,
|
|
12
|
-
'.modernjs/ultramodern-generated-contract.json',
|
|
7
|
+
process.env.ULTRAMODERN_WORKSPACE_ROOT ??
|
|
8
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), '..'),
|
|
13
9
|
);
|
|
10
|
+
const compactConfigPath = path.join(workspaceRoot, '.modernjs/ultramodern.json');
|
|
14
11
|
|
|
15
12
|
function readJson(filePath) {
|
|
16
13
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
17
14
|
}
|
|
18
15
|
|
|
16
|
+
function toKebabCase(value) {
|
|
17
|
+
return String(value)
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
|
|
20
|
+
.replace(/[^a-zA-Z0-9._-]+/gu, '-')
|
|
21
|
+
.replace(/[._]+/gu, '-')
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/-+/gu, '-')
|
|
24
|
+
.replace(/^-+|-+$/gu, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toEnvSegment(value) {
|
|
28
|
+
return toKebabCase(value).replace(/-/gu, '_').toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeRelativePath(value) {
|
|
32
|
+
return String(value ?? '').replace(/\\/gu, '/').replace(/^\.\/+/u, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appNamespace(app) {
|
|
36
|
+
return app.kind === 'shell' ? 'shell' : (app.domain ?? app.id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultPortEnv(app) {
|
|
40
|
+
return app.kind === 'shell'
|
|
41
|
+
? 'SHELL_SUPER_APP_PORT'
|
|
42
|
+
: `VERTICAL_${toEnvSegment(app.domain ?? app.id)}_PORT`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultPort(app) {
|
|
46
|
+
return app.kind === 'shell' ? 3020 : 3030;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeCompactApp(rawApp) {
|
|
50
|
+
const id = String(rawApp.id);
|
|
51
|
+
const kind = rawApp.kind === 'vertical' ? 'vertical' : 'shell';
|
|
52
|
+
const appPath =
|
|
53
|
+
typeof rawApp.path === 'string'
|
|
54
|
+
? normalizeRelativePath(rawApp.path)
|
|
55
|
+
: kind === 'shell'
|
|
56
|
+
? 'apps/shell-super-app'
|
|
57
|
+
: `verticals/${toKebabCase(id)}`;
|
|
58
|
+
const packageSuffix =
|
|
59
|
+
typeof rawApp.packageSuffix === 'string'
|
|
60
|
+
? rawApp.packageSuffix
|
|
61
|
+
: appPath.split('/').at(-1) ?? id;
|
|
62
|
+
const domain =
|
|
63
|
+
typeof rawApp.domain === 'string'
|
|
64
|
+
? rawApp.domain
|
|
65
|
+
: kind === 'vertical'
|
|
66
|
+
? packageSuffix
|
|
67
|
+
: undefined;
|
|
68
|
+
const moduleFederation =
|
|
69
|
+
rawApp.moduleFederation && typeof rawApp.moduleFederation === 'object'
|
|
70
|
+
? rawApp.moduleFederation
|
|
71
|
+
: {};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
kind,
|
|
76
|
+
path: appPath,
|
|
77
|
+
packageSuffix,
|
|
78
|
+
domain,
|
|
79
|
+
port:
|
|
80
|
+
typeof rawApp.port === 'number'
|
|
81
|
+
? rawApp.port
|
|
82
|
+
: defaultPort({ id, kind, domain }),
|
|
83
|
+
portEnv:
|
|
84
|
+
typeof rawApp.portEnv === 'string'
|
|
85
|
+
? rawApp.portEnv
|
|
86
|
+
: defaultPortEnv({ id, kind, domain }),
|
|
87
|
+
mfName:
|
|
88
|
+
typeof moduleFederation.name === 'string'
|
|
89
|
+
? moduleFederation.name
|
|
90
|
+
: kind === 'shell'
|
|
91
|
+
? 'shellSuperApp'
|
|
92
|
+
: `vertical${toKebabCase(domain ?? id)
|
|
93
|
+
.split('-')
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
96
|
+
.join('')}`,
|
|
97
|
+
exposes: Array.isArray(moduleFederation.exposes)
|
|
98
|
+
? moduleFederation.exposes.filter(expose => typeof expose === 'string')
|
|
99
|
+
: [],
|
|
100
|
+
verticalRefs: Array.isArray(moduleFederation.verticalRefs)
|
|
101
|
+
? moduleFederation.verticalRefs.filter(ref => typeof ref === 'string')
|
|
102
|
+
: [],
|
|
103
|
+
marker: {
|
|
104
|
+
appId: id,
|
|
105
|
+
},
|
|
106
|
+
routes:
|
|
107
|
+
rawApp.routes && typeof rawApp.routes === 'object'
|
|
108
|
+
? rawApp.routes
|
|
109
|
+
: undefined,
|
|
110
|
+
api:
|
|
111
|
+
rawApp.api && typeof rawApp.api === 'object'
|
|
112
|
+
? {
|
|
113
|
+
stem:
|
|
114
|
+
typeof rawApp.api.stem === 'string'
|
|
115
|
+
? rawApp.api.stem
|
|
116
|
+
: domain ?? id,
|
|
117
|
+
prefix:
|
|
118
|
+
typeof rawApp.api.prefix === 'string'
|
|
119
|
+
? rawApp.api.prefix
|
|
120
|
+
: `/${domain ?? id}-api`,
|
|
121
|
+
}
|
|
122
|
+
: undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createRouteOwnedEntries(app) {
|
|
127
|
+
const namespace = appNamespace(app);
|
|
128
|
+
const base = {
|
|
129
|
+
descriptionKey: `${namespace}.seo.description`,
|
|
130
|
+
mfBoundaryId: app.mfName,
|
|
131
|
+
namespace,
|
|
132
|
+
ownerAppId: app.id,
|
|
133
|
+
public: false,
|
|
134
|
+
indexable: false,
|
|
135
|
+
publicSurface: 'private-app-screen',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
{
|
|
140
|
+
...base,
|
|
141
|
+
canonicalPath: '/',
|
|
142
|
+
id: app.kind === 'shell' ? 'shell-home' : `${app.id}-home`,
|
|
143
|
+
localisedPaths: {
|
|
144
|
+
cs: '/',
|
|
145
|
+
en: '/',
|
|
146
|
+
},
|
|
147
|
+
titleKey: app.kind === 'shell' ? 'shell.title' : `${namespace}.title`,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createPublicRoutes(app) {
|
|
153
|
+
return createRouteOwnedEntries(app)
|
|
154
|
+
.filter(route => route.public && route.indexable)
|
|
155
|
+
.map(route => ({
|
|
156
|
+
canonicalPath: route.canonicalPath,
|
|
157
|
+
id: route.id,
|
|
158
|
+
localisedPaths: route.localisedPaths,
|
|
159
|
+
namespace: route.namespace,
|
|
160
|
+
ownerAppId: route.ownerAppId,
|
|
161
|
+
descriptionKey: route.descriptionKey,
|
|
162
|
+
titleKey: route.titleKey,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createPublicSurface(app) {
|
|
167
|
+
const publicRoutes = createPublicRoutes(app);
|
|
168
|
+
const basePublicSurface = {
|
|
169
|
+
authoring: 'colocated-route-meta',
|
|
170
|
+
artifactLifecycle: 'build-and-deploy-output',
|
|
171
|
+
generatedManifest: './src/routes/ultramodern-route-metadata',
|
|
172
|
+
source: 'route-owned-public-routes',
|
|
173
|
+
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
174
|
+
generator: 'scripts/generate-public-surface-assets.mts',
|
|
175
|
+
outputRoot: 'dist/public',
|
|
176
|
+
cloudflareOutputRoot: '.output/public',
|
|
177
|
+
privateRoutePolicy: 'omit-from-generated-public-surface',
|
|
178
|
+
files:
|
|
179
|
+
publicRoutes.length > 0
|
|
180
|
+
? ['robots.txt', 'sitemap.xml', 'site.webmanifest']
|
|
181
|
+
: ['robots.txt'],
|
|
182
|
+
omittedByDefault: ['api-catalog.json', 'llms.txt', 'security.txt'],
|
|
183
|
+
languages: ['en', 'cs'],
|
|
184
|
+
contentExpansion: {
|
|
185
|
+
authoring: 'route-owned-esm-provider',
|
|
186
|
+
defaultProviderFile: 'route.sitemap.mjs',
|
|
187
|
+
entryExport: 'default-or-entries',
|
|
188
|
+
paramsSource: 'params-or-localeParams',
|
|
189
|
+
draftPolicy: 'omit-draft-by-default',
|
|
190
|
+
indexablePolicy: 'omit-indexable-false',
|
|
191
|
+
lifecycle: 'executed-during-public-surface-generation',
|
|
192
|
+
},
|
|
193
|
+
contentSources: [],
|
|
194
|
+
publicRoutes,
|
|
195
|
+
routeEntries: [],
|
|
196
|
+
concreteUrlPaths: [],
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return app.routes?.publicSurface &&
|
|
200
|
+
typeof app.routes.publicSurface === 'object'
|
|
201
|
+
? {
|
|
202
|
+
...basePublicSurface,
|
|
203
|
+
...app.routes.publicSurface,
|
|
204
|
+
}
|
|
205
|
+
: basePublicSurface;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function createCloudflareDeploy(config, app) {
|
|
209
|
+
const packageScope =
|
|
210
|
+
typeof config.workspace?.packageScope === 'string'
|
|
211
|
+
? config.workspace.packageScope
|
|
212
|
+
: path.basename(workspaceRoot);
|
|
213
|
+
const compatibilityDate =
|
|
214
|
+
typeof config.deploy?.worker?.compatibilityDate === 'string'
|
|
215
|
+
? config.deploy.worker.compatibilityDate
|
|
216
|
+
: '2026-06-02';
|
|
217
|
+
return {
|
|
218
|
+
workerName: `${toKebabCase(packageScope)}-${app.packageSuffix}`.slice(0, 63),
|
|
219
|
+
publicUrlEnv: `ULTRAMODERN_PUBLIC_URL_${toEnvSegment(app.id)}`,
|
|
220
|
+
compatibilityDate,
|
|
221
|
+
compatibilityFlags: ['nodejs_compat', 'global_fetch_strictly_public'],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function synthesizeContractFromCompactConfig(config) {
|
|
226
|
+
const apps = Array.isArray(config.topology?.apps)
|
|
227
|
+
? config.topology.apps.map(normalizeCompactApp)
|
|
228
|
+
: [];
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
sourcePath: compactConfigPath,
|
|
232
|
+
apps: apps.map(app => ({
|
|
233
|
+
...app,
|
|
234
|
+
deploy: {
|
|
235
|
+
cloudflare: createCloudflareDeploy(config, app),
|
|
236
|
+
},
|
|
237
|
+
routes: {
|
|
238
|
+
publicSurface: createPublicSurface(app),
|
|
239
|
+
},
|
|
240
|
+
})),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readGeneratedContractView() {
|
|
245
|
+
if (fs.existsSync(compactConfigPath)) {
|
|
246
|
+
return synthesizeContractFromCompactConfig(readJson(compactConfigPath));
|
|
247
|
+
}
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Missing UltraModern config. Expected ${path.relative(
|
|
250
|
+
workspaceRoot,
|
|
251
|
+
compactConfigPath,
|
|
252
|
+
)}.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
19
256
|
function parseArgs(argv) {
|
|
20
257
|
const parsed = {
|
|
21
258
|
appId: undefined,
|
|
@@ -52,7 +289,7 @@ function parseArgs(argv) {
|
|
|
52
289
|
|
|
53
290
|
function printHelp() {
|
|
54
291
|
process.stdout.write(`Usage:
|
|
55
|
-
node scripts/generate-public-surface-assets.
|
|
292
|
+
node scripts/generate-public-surface-assets.mts --app shell-super-app [--target dist|cloudflare] [--require-public-origin]
|
|
56
293
|
|
|
57
294
|
Set each app's production URL using the contract env key, for example:
|
|
58
295
|
ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP=https://example.com
|
|
@@ -517,10 +754,10 @@ try {
|
|
|
517
754
|
printHelp();
|
|
518
755
|
process.exit(0);
|
|
519
756
|
}
|
|
520
|
-
const contract =
|
|
757
|
+
const contract = readGeneratedContractView();
|
|
521
758
|
const app = contract.apps?.find(candidate => candidate.id === args.appId);
|
|
522
759
|
if (!app) {
|
|
523
|
-
throw new Error(`Unknown app in
|
|
760
|
+
throw new Error(`Unknown app in UltraModern config: ${args.appId}`);
|
|
524
761
|
}
|
|
525
762
|
await generatePublicSurfaceAssets(app, args.target, args.requirePublicOrigin);
|
|
526
763
|
} catch (error) {
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { validateApp } from './ultramodern-cloudflare-proof.mjs';
|
|
6
7
|
|
|
7
8
|
const workspaceRoot = path.resolve(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
);
|
|
11
|
-
const contractPath = path.join(
|
|
12
|
-
workspaceRoot,
|
|
13
|
-
'.modernjs/ultramodern-generated-contract.json',
|
|
9
|
+
process.env.ULTRAMODERN_WORKSPACE_ROOT ??
|
|
10
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), '..'),
|
|
14
11
|
);
|
|
12
|
+
const compactConfigPath = path.join(workspaceRoot, '.modernjs/ultramodern.json');
|
|
15
13
|
const defaultOut = path.join(
|
|
16
14
|
workspaceRoot,
|
|
17
15
|
'.codex/reports/cloudflare-version-proof/public-url-proof.json',
|
|
@@ -21,6 +19,276 @@ function readJson(filePath) {
|
|
|
21
19
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
22
20
|
}
|
|
23
21
|
|
|
22
|
+
function toKebabCase(value) {
|
|
23
|
+
return String(value)
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
|
|
26
|
+
.replace(/[^a-zA-Z0-9._-]+/gu, '-')
|
|
27
|
+
.replace(/[._]+/gu, '-')
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/-+/gu, '-')
|
|
30
|
+
.replace(/^-+|-+$/gu, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toPascalCase(value) {
|
|
34
|
+
return toKebabCase(value)
|
|
35
|
+
.split('-')
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
38
|
+
.join('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toEnvSegment(value) {
|
|
42
|
+
return toKebabCase(value).replace(/-/gu, '_').toUpperCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeRelativePath(value) {
|
|
46
|
+
return String(value ?? '').replace(/\\/gu, '/').replace(/^\.\/+/u, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appNamespace(app) {
|
|
50
|
+
return app.kind === 'shell' ? 'shell' : (app.domain ?? app.id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeCompactApp(rawApp) {
|
|
54
|
+
const id = String(rawApp.id);
|
|
55
|
+
const kind = rawApp.kind === 'vertical' ? 'vertical' : 'shell';
|
|
56
|
+
const appPath =
|
|
57
|
+
typeof rawApp.path === 'string'
|
|
58
|
+
? normalizeRelativePath(rawApp.path)
|
|
59
|
+
: kind === 'shell'
|
|
60
|
+
? 'apps/shell-super-app'
|
|
61
|
+
: `verticals/${toKebabCase(id)}`;
|
|
62
|
+
const packageSuffix =
|
|
63
|
+
typeof rawApp.packageSuffix === 'string'
|
|
64
|
+
? rawApp.packageSuffix
|
|
65
|
+
: appPath.split('/').at(-1) ?? id;
|
|
66
|
+
const domain =
|
|
67
|
+
typeof rawApp.domain === 'string'
|
|
68
|
+
? rawApp.domain
|
|
69
|
+
: kind === 'vertical'
|
|
70
|
+
? packageSuffix
|
|
71
|
+
: undefined;
|
|
72
|
+
const moduleFederation =
|
|
73
|
+
rawApp.moduleFederation && typeof rawApp.moduleFederation === 'object'
|
|
74
|
+
? rawApp.moduleFederation
|
|
75
|
+
: {};
|
|
76
|
+
const api =
|
|
77
|
+
rawApp.api && typeof rawApp.api === 'object'
|
|
78
|
+
? {
|
|
79
|
+
stem:
|
|
80
|
+
typeof rawApp.api.stem === 'string'
|
|
81
|
+
? rawApp.api.stem
|
|
82
|
+
: domain ?? id,
|
|
83
|
+
prefix:
|
|
84
|
+
typeof rawApp.api.prefix === 'string'
|
|
85
|
+
? rawApp.api.prefix
|
|
86
|
+
: `/${domain ?? id}-api`,
|
|
87
|
+
}
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
kind,
|
|
93
|
+
path: appPath,
|
|
94
|
+
packageSuffix,
|
|
95
|
+
domain,
|
|
96
|
+
mfName:
|
|
97
|
+
typeof moduleFederation.name === 'string'
|
|
98
|
+
? moduleFederation.name
|
|
99
|
+
: kind === 'shell'
|
|
100
|
+
? 'shellSuperApp'
|
|
101
|
+
: `vertical${toPascalCase(domain ?? id)}`,
|
|
102
|
+
api,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createBuildMarker(scope, app) {
|
|
107
|
+
return crypto
|
|
108
|
+
.createHash('sha256')
|
|
109
|
+
.update(`${scope}:${app.packageSuffix}:${app.id}:0.1.0`)
|
|
110
|
+
.digest('hex')
|
|
111
|
+
.slice(0, 16);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createCloudflareSecurity() {
|
|
115
|
+
return {
|
|
116
|
+
enabled: true,
|
|
117
|
+
headers: {
|
|
118
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
119
|
+
contentTypeOptions: 'nosniff',
|
|
120
|
+
permissionsPolicy:
|
|
121
|
+
'camera=(), geolocation=(), microphone=(), payment=(), usb=()',
|
|
122
|
+
},
|
|
123
|
+
contentSecurityPolicy: {
|
|
124
|
+
mode: 'report-only',
|
|
125
|
+
directives: {
|
|
126
|
+
'base-uri': [`'self'`],
|
|
127
|
+
'connect-src': [`'self'`, 'https:', 'http:', 'wss:', 'ws:'],
|
|
128
|
+
'default-src': [`'self'`],
|
|
129
|
+
'font-src': [`'self'`, 'data:', 'https:', 'http:'],
|
|
130
|
+
'form-action': [`'self'`],
|
|
131
|
+
'frame-ancestors': [`'self'`],
|
|
132
|
+
'img-src': [`'self'`, 'data:', 'blob:', 'https:', 'http:'],
|
|
133
|
+
'manifest-src': [`'self'`, 'https:', 'http:'],
|
|
134
|
+
'object-src': [`'none'`],
|
|
135
|
+
'script-src': [
|
|
136
|
+
`'self'`,
|
|
137
|
+
`'unsafe-inline'`,
|
|
138
|
+
`'unsafe-eval'`,
|
|
139
|
+
'https:',
|
|
140
|
+
'http:',
|
|
141
|
+
'blob:',
|
|
142
|
+
],
|
|
143
|
+
'style-src': [`'self'`, `'unsafe-inline'`, 'https:', 'http:'],
|
|
144
|
+
'worker-src': [`'self'`, 'blob:'],
|
|
145
|
+
},
|
|
146
|
+
reason:
|
|
147
|
+
'Report-only by default so Cloudflare Module Federation SSR can prove remote script, style, and connect compatibility before enforcement.',
|
|
148
|
+
},
|
|
149
|
+
noindex: {
|
|
150
|
+
workersDev: true,
|
|
151
|
+
localhost: true,
|
|
152
|
+
previewHostnames: [],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createQualityGates() {
|
|
158
|
+
return {
|
|
159
|
+
publicRoutes: {
|
|
160
|
+
requireSitemapWhenPresent: true,
|
|
161
|
+
requireRobotsSitemapConsistency: true,
|
|
162
|
+
requireWebManifestWhenPresent: true,
|
|
163
|
+
},
|
|
164
|
+
statusCodes: {
|
|
165
|
+
notFoundRoute: '/__ultramodern-smoke-missing/nope',
|
|
166
|
+
unknownRouteStatus: 404,
|
|
167
|
+
},
|
|
168
|
+
indexing: {
|
|
169
|
+
previewNoindex: true,
|
|
170
|
+
productionPublicRoutesIndexable: true,
|
|
171
|
+
},
|
|
172
|
+
assets: {
|
|
173
|
+
cssPreloadRequired: true,
|
|
174
|
+
cssResponseRequired: true,
|
|
175
|
+
cacheControlRequiredForCss: true,
|
|
176
|
+
sourcemapsPubliclyReferenced: false,
|
|
177
|
+
},
|
|
178
|
+
budgets: {
|
|
179
|
+
ssrHtmlMaxBytes: 250_000,
|
|
180
|
+
mfManifestMaxBytes: 500_000,
|
|
181
|
+
localeJsonMaxBytes: 100_000,
|
|
182
|
+
sitemapXmlMaxBytes: 500_000,
|
|
183
|
+
cssAssetMaxBytes: 750_000,
|
|
184
|
+
},
|
|
185
|
+
csp: {
|
|
186
|
+
finalMode: 'report-only-dogfood',
|
|
187
|
+
decision:
|
|
188
|
+
'Report-only remains the generated final mode until public smoke proof records MF SSR script/style/connect compatibility for the deployed surface.',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createPublicHead() {
|
|
194
|
+
return {
|
|
195
|
+
alternates: {
|
|
196
|
+
hreflang: ['en', 'cs'],
|
|
197
|
+
xDefault: 'en',
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function createPublicSurface() {
|
|
203
|
+
return {
|
|
204
|
+
publicRoutes: [],
|
|
205
|
+
routeEntries: [],
|
|
206
|
+
contentSources: [],
|
|
207
|
+
concreteUrlPaths: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createCloudflareRoutes(app) {
|
|
212
|
+
return {
|
|
213
|
+
ssr: '/en',
|
|
214
|
+
mfManifest: '/mf-manifest.json',
|
|
215
|
+
locale: `/locales/en/${appNamespace(app)}.json`,
|
|
216
|
+
...(app.api
|
|
217
|
+
? {
|
|
218
|
+
apiReadiness: `${app.api.prefix}/${app.api.stem}/readiness`,
|
|
219
|
+
}
|
|
220
|
+
: {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function createContractApp(config, app) {
|
|
225
|
+
const packageScope =
|
|
226
|
+
typeof config.workspace?.packageScope === 'string'
|
|
227
|
+
? config.workspace.packageScope
|
|
228
|
+
: path.basename(workspaceRoot);
|
|
229
|
+
const compatibilityDate =
|
|
230
|
+
typeof config.deploy?.worker?.compatibilityDate === 'string'
|
|
231
|
+
? config.deploy.worker.compatibilityDate
|
|
232
|
+
: '2026-06-02';
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
id: app.id,
|
|
236
|
+
deploy: {
|
|
237
|
+
cloudflare: {
|
|
238
|
+
workerName: `${toKebabCase(packageScope)}-${app.packageSuffix}`.slice(
|
|
239
|
+
0,
|
|
240
|
+
63,
|
|
241
|
+
),
|
|
242
|
+
publicUrlEnv: `ULTRAMODERN_PUBLIC_URL_${toEnvSegment(app.id)}`,
|
|
243
|
+
compatibilityDate,
|
|
244
|
+
compatibilityFlags: ['nodejs_compat', 'global_fetch_strictly_public'],
|
|
245
|
+
routes: createCloudflareRoutes(app),
|
|
246
|
+
security: createCloudflareSecurity(),
|
|
247
|
+
qualityGates: createQualityGates(),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
i18n: {
|
|
251
|
+
namespace: appNamespace(app),
|
|
252
|
+
},
|
|
253
|
+
marker: {
|
|
254
|
+
appId: app.id,
|
|
255
|
+
build: createBuildMarker(packageScope, app),
|
|
256
|
+
},
|
|
257
|
+
routes: {
|
|
258
|
+
publicHead: createPublicHead(),
|
|
259
|
+
publicSurface: createPublicSurface(),
|
|
260
|
+
},
|
|
261
|
+
styling: {
|
|
262
|
+
federation: {
|
|
263
|
+
rootSelector: `[data-app-id="${app.id}"]`,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function synthesizeContractFromCompactConfig(config) {
|
|
270
|
+
const apps = Array.isArray(config.topology?.apps)
|
|
271
|
+
? config.topology.apps.map(normalizeCompactApp)
|
|
272
|
+
: [];
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
sourcePath: compactConfigPath,
|
|
276
|
+
apps: apps.map(app => createContractApp(config, app)),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function readGeneratedContractView() {
|
|
281
|
+
if (fs.existsSync(compactConfigPath)) {
|
|
282
|
+
return synthesizeContractFromCompactConfig(readJson(compactConfigPath));
|
|
283
|
+
}
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Missing UltraModern config. Expected ${path.relative(
|
|
286
|
+
workspaceRoot,
|
|
287
|
+
compactConfigPath,
|
|
288
|
+
)}.`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
24
292
|
function parseArgs(argv) {
|
|
25
293
|
const parsed = {
|
|
26
294
|
appId: undefined,
|
|
@@ -50,7 +318,7 @@ function parseArgs(argv) {
|
|
|
50
318
|
|
|
51
319
|
function printHelp() {
|
|
52
320
|
process.stdout.write(`Usage:
|
|
53
|
-
node scripts/proof-cloudflare-version.
|
|
321
|
+
node scripts/proof-cloudflare-version.mts [--app workspace] [--out evidence.json] [--require-public-urls]
|
|
54
322
|
|
|
55
323
|
Set each app's public URL using the contract env key, for example:
|
|
56
324
|
ULTRAMODERN_PUBLIC_URL_WORKSPACE=https://workspace.example.workers.dev
|
|
@@ -70,7 +338,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
70
338
|
return 0;
|
|
71
339
|
}
|
|
72
340
|
|
|
73
|
-
const contract =
|
|
341
|
+
const contract = readGeneratedContractView();
|
|
74
342
|
const apps = args.appId
|
|
75
343
|
? contract.apps.filter(app => app.id === args.appId)
|
|
76
344
|
: contract.apps;
|
|
@@ -101,7 +369,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
101
369
|
schemaVersion: 1,
|
|
102
370
|
generatedAt: new Date().toISOString(),
|
|
103
371
|
status: results.length > 0 ? 'pass' : 'skipped',
|
|
104
|
-
contractPath,
|
|
372
|
+
contractPath: contract.sourcePath ?? compactConfigPath,
|
|
105
373
|
results,
|
|
106
374
|
skipped,
|
|
107
375
|
};
|
|
@@ -6,6 +6,20 @@ function normalizeUrlWithTrailingSlash(url) {
|
|
|
6
6
|
return url.endsWith('/') ? url : `${url}/`;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function resolveModuleFederationPublicPath(publicPath, manifestUrl) {
|
|
10
|
+
if (typeof publicPath !== 'string' || publicPath.trim().length === 0) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
return normalizeUrlWithTrailingSlash(
|
|
16
|
+
new URL(publicPath.trim(), manifestUrl).toString(),
|
|
17
|
+
);
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
async function fetchText(url) {
|
|
10
24
|
const response = await fetch(url);
|
|
11
25
|
return {
|
|
@@ -464,7 +478,8 @@ async function validateSsrHead(evidence, app, publicUrl, ssrRoute, ssr) {
|
|
|
464
478
|
async function validateNotFound(evidence, app, publicUrl) {
|
|
465
479
|
const qualityGates = app.deploy?.cloudflare?.qualityGates ?? {};
|
|
466
480
|
const notFoundRoute =
|
|
467
|
-
qualityGates.statusCodes?.notFoundRoute ??
|
|
481
|
+
qualityGates.statusCodes?.notFoundRoute ??
|
|
482
|
+
'/__ultramodern-smoke-missing/nope';
|
|
468
483
|
const expectedStatus = qualityGates.statusCodes?.unknownRouteStatus ?? 404;
|
|
469
484
|
const response = await fetchText(joinUrl(publicUrl, notFoundRoute));
|
|
470
485
|
evidence.assertions.push({
|
|
@@ -721,7 +736,8 @@ async function validateModuleFederationManifestEvidence(evidence, app, publicUrl
|
|
|
721
736
|
const budgets = qualityGates.budgets ?? {};
|
|
722
737
|
|
|
723
738
|
const manifestRoute = routes.mfManifest ?? '/mf-manifest.json';
|
|
724
|
-
const
|
|
739
|
+
const manifestUrl = joinUrl(publicUrl, manifestRoute);
|
|
740
|
+
const manifest = await fetchText(manifestUrl);
|
|
725
741
|
const manifestJson = parseMaybeJson(manifest.body);
|
|
726
742
|
evidence.assertions.push({
|
|
727
743
|
type: 'mf-manifest',
|
|
@@ -754,17 +770,26 @@ async function validateModuleFederationManifestEvidence(evidence, app, publicUrl
|
|
|
754
770
|
manifest.accessControlAllowOrigin === '*',
|
|
755
771
|
`${app.id} MF manifest is missing Cloudflare CORS headers`,
|
|
756
772
|
);
|
|
757
|
-
const expectedPublicPath = normalizeUrlWithTrailingSlash(
|
|
773
|
+
const expectedPublicPath = normalizeUrlWithTrailingSlash(
|
|
774
|
+
new URL('.', manifestUrl).toString(),
|
|
775
|
+
);
|
|
758
776
|
const manifestPublicPath = manifestJson?.metaData?.publicPath;
|
|
777
|
+
const resolvedPublicPath = resolveModuleFederationPublicPath(
|
|
778
|
+
manifestPublicPath,
|
|
779
|
+
manifestUrl,
|
|
780
|
+
);
|
|
759
781
|
evidence.assertions.push({
|
|
760
782
|
type: 'mf-manifest-public-path',
|
|
761
783
|
expected: expectedPublicPath,
|
|
762
784
|
actual: manifestPublicPath,
|
|
763
|
-
|
|
785
|
+
resolved: resolvedPublicPath,
|
|
786
|
+
status: resolvedPublicPath === expectedPublicPath ? 'pass' : 'fail',
|
|
764
787
|
});
|
|
765
788
|
assert(
|
|
766
|
-
|
|
767
|
-
`${app.id} MF manifest publicPath must resolve remote assets from ${expectedPublicPath}
|
|
789
|
+
resolvedPublicPath === expectedPublicPath,
|
|
790
|
+
`${app.id} MF manifest publicPath must resolve remote assets from ${expectedPublicPath}; got ${String(
|
|
791
|
+
manifestPublicPath,
|
|
792
|
+
)}`,
|
|
768
793
|
);
|
|
769
794
|
}
|
|
770
795
|
|
|
@@ -815,13 +840,13 @@ async function validateI18nEvidence(evidence, app, publicUrl, routes) {
|
|
|
815
840
|
}
|
|
816
841
|
|
|
817
842
|
async function validateReadinessEvidence(evidence, app, publicUrl, routes) {
|
|
818
|
-
if (routes.
|
|
819
|
-
const readiness = await fetchText(joinUrl(publicUrl, routes.
|
|
843
|
+
if (routes.apiReadiness) {
|
|
844
|
+
const readiness = await fetchText(joinUrl(publicUrl, routes.apiReadiness));
|
|
820
845
|
const readinessJson = parseMaybeJson(readiness.body);
|
|
821
846
|
const apiMarker = markerFromJson(readinessJson);
|
|
822
847
|
evidence.assertions.push({
|
|
823
848
|
type: 'api-marker',
|
|
824
|
-
route: routes.
|
|
849
|
+
route: routes.apiReadiness,
|
|
825
850
|
expected: app.marker?.build,
|
|
826
851
|
actual: apiMarker,
|
|
827
852
|
status: readiness.ok && apiMarker === app.marker?.build ? 'pass' : 'fail',
|
|
@@ -848,4 +873,4 @@ async function validateApp(app, publicUrl) {
|
|
|
848
873
|
return evidence;
|
|
849
874
|
}
|
|
850
875
|
|
|
851
|
-
export { validateApp };
|
|
876
|
+
export { resolveModuleFederationPublicPath, validateApp };
|