@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.12 → 3.9.0-ultramodern.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +7 -2
  2. package/dist/cjs/backend-federation-build/config.js +86 -36
  3. package/dist/cjs/backend-federation-build/emit.js +17 -13
  4. package/dist/cjs/cloudflare/delivery-unit.js +16 -26
  5. package/dist/cjs/cloudflare/index.js +7 -1
  6. package/dist/cjs/cloudflare-builder.js +13 -3
  7. package/dist/cjs/cloudflare-output-contract.js +1 -0
  8. package/dist/cjs/cloudflare-output-verifier/identity.js +26 -24
  9. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  10. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  11. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  12. package/dist/cjs/deploy-output/npmAliases.js +28 -3
  13. package/dist/cjs/release-envelope/framework-output.js +62 -49
  14. package/dist/cjs/release-envelope/index.js +10 -4
  15. package/dist/esm/backend-federation-build/codegen.mjs +7 -2
  16. package/dist/esm/backend-federation-build/config.mjs +78 -31
  17. package/dist/esm/backend-federation-build/emit.mjs +18 -14
  18. package/dist/esm/cloudflare/delivery-unit.mjs +16 -26
  19. package/dist/esm/cloudflare/index.mjs +7 -1
  20. package/dist/esm/cloudflare-builder.mjs +13 -3
  21. package/dist/esm/cloudflare-output-contract.mjs +1 -0
  22. package/dist/esm/cloudflare-output-verifier/identity.mjs +26 -24
  23. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  24. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  25. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  26. package/dist/esm/deploy-output/npmAliases.mjs +28 -3
  27. package/dist/esm/release-envelope/framework-output.mjs +62 -49
  28. package/dist/esm/release-envelope/index.mjs +10 -4
  29. package/dist/esm-node/backend-federation-build/codegen.mjs +7 -2
  30. package/dist/esm-node/backend-federation-build/config.mjs +78 -31
  31. package/dist/esm-node/backend-federation-build/emit.mjs +18 -14
  32. package/dist/esm-node/cloudflare/delivery-unit.mjs +16 -26
  33. package/dist/esm-node/cloudflare/index.mjs +7 -1
  34. package/dist/esm-node/cloudflare-builder.mjs +13 -3
  35. package/dist/esm-node/cloudflare-output-contract.mjs +1 -0
  36. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +26 -24
  37. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  38. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  39. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  40. package/dist/esm-node/deploy-output/npmAliases.mjs +28 -3
  41. package/dist/esm-node/release-envelope/framework-output.mjs +62 -49
  42. package/dist/esm-node/release-envelope/index.mjs +10 -4
  43. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  44. package/dist/types/backend-federation-build/config.d.ts +32 -13
  45. package/dist/types/cloudflare/delivery-unit.d.ts +1 -1
  46. package/dist/types/cloudflare-builder.d.ts +1 -0
  47. package/dist/types/cloudflare-output-contract.d.ts +1 -1
  48. package/package.json +20 -20
  49. package/src/backend-federation-build/codegen.ts +6 -2
  50. package/src/backend-federation-build/config.ts +207 -71
  51. package/src/backend-federation-build/emit.ts +33 -22
  52. package/src/cloudflare/delivery-unit.ts +39 -33
  53. package/src/cloudflare/index.ts +20 -6
  54. package/src/cloudflare-builder.ts +18 -4
  55. package/src/cloudflare-output-contract.ts +1 -0
  56. package/src/cloudflare-output-verifier/identity.ts +4 -0
  57. package/src/cloudflare-output-verifier/index.ts +20 -0
  58. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  59. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  60. package/src/deploy-output/npmAliases.ts +52 -3
  61. package/src/release-envelope/framework-output.ts +163 -90
  62. package/src/release-envelope/index.ts +26 -5
@@ -5,7 +5,7 @@ import { resolveUltramodernReleaseIdentity } from "@modern-js/app-tools-extensio
5
5
  import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, ULTRAMODERN_BUILD_ARTIFACT_FILE, stampUltramodernBuildArtifactIdentity } from "@modern-js/backend-federation-contracts";
6
6
  import { createBackendFederationEntryIntegrity } from "@modern-js/server-runtime-extensions/backend-federation-security/node";
7
7
  import { createBackendManifest, createBackendRemoteEntrySource } from "./codegen.mjs";
8
- import { COMPACT_CONFIG_PATH, buildArtifactPathFor, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, readBuildIdentity, resolveWorkspaceSourceRevision } from "./config.mjs";
8
+ import { REFERENCE_TOPOLOGY_PATH, buildArtifactPathFor, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, readBuildIdentity, resolveWorkspaceSourceRevision } from "./config.mjs";
9
9
  const emitBackendFederationArtifacts = async (appDirectory, distDirectory)=>{
10
10
  const workspaceRoot = findWorkspaceRoot(appDirectory);
11
11
  if (!workspaceRoot) return;
@@ -15,35 +15,39 @@ const emitBackendFederationArtifacts = async (appDirectory, distDirectory)=>{
15
15
  const app = await findBackendFederationApp(workspaceRoot, appDirectory);
16
16
  if (!app) return;
17
17
  const buildIdentity = await readBuildIdentity(appDirectory);
18
- const compactDeliveryUnit = app.compactDeliveryUnit;
19
- const hasCompactDeliveryUnit = void 0 !== compactDeliveryUnit && (void 0 !== compactDeliveryUnit.unitId || void 0 !== compactDeliveryUnit.buildMarker || void 0 !== compactDeliveryUnit.packageName || void 0 !== compactDeliveryUnit.version);
18
+ const topologyDeliveryUnit = app.topologyDeliveryUnit;
19
+ const hasTopologyDeliveryUnit = void 0 !== topologyDeliveryUnit && (void 0 !== topologyDeliveryUnit.unitId || void 0 !== topologyDeliveryUnit.buildMarker || void 0 !== topologyDeliveryUnit.packageName || void 0 !== topologyDeliveryUnit.version);
20
20
  const hasBuildIdentity = void 0 !== buildIdentity.unitId || void 0 !== buildIdentity.buildVersion || void 0 !== buildIdentity.packageName || void 0 !== buildIdentity.version;
21
21
  if (hasBuildIdentity) {
22
- const compactConfigPath = node_path.join(workspaceRoot, COMPACT_CONFIG_PATH);
22
+ const topologyPath = node_path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
23
23
  const buildIdentityPath = buildArtifactPathFor(appDirectory);
24
24
  const mismatches = [];
25
25
  const compare = (label, a, b, leftLabel = 'deliveryUnit')=>{
26
26
  if (void 0 !== a && void 0 !== b && a !== b) mismatches.push(`${label}: ${leftLabel}=${a} vs ultramodern-build=${b}`);
27
27
  };
28
28
  compare('appId', app.id, buildIdentity.appId, 'topology');
29
- if (hasCompactDeliveryUnit) {
30
- compare('unitId', compactDeliveryUnit?.unitId, buildIdentity.unitId);
31
- compare('buildMarker/build', compactDeliveryUnit?.buildMarker, buildIdentity.buildVersion);
32
- compare('packageName', compactDeliveryUnit?.packageName, buildIdentity.packageName);
33
- compare('version', compactDeliveryUnit?.version, buildIdentity.version);
29
+ if (hasTopologyDeliveryUnit) {
30
+ compare('unitId', topologyDeliveryUnit?.unitId, buildIdentity.unitId);
31
+ compare('buildMarker/build', topologyDeliveryUnit?.buildMarker, buildIdentity.buildVersion);
32
+ compare('packageName', topologyDeliveryUnit?.packageName, buildIdentity.packageName);
33
+ compare('version', topologyDeliveryUnit?.version, buildIdentity.version);
34
34
  }
35
- if (mismatches.length > 0) throw new Error(`[backend-federation-build] Delivery-unit identity drift between ${compactConfigPath} (topology) and ${buildIdentityPath}: ${mismatches.join('; ')}`);
35
+ compare('packageName', app.packageName, buildIdentity.packageName, 'package.json');
36
+ compare('packageName', topologyDeliveryUnit?.packageName, app.packageName, 'topology');
37
+ compare('version', app.version, buildIdentity.version, 'package.json');
38
+ compare('version', topologyDeliveryUnit?.version, app.version, 'topology');
39
+ if (mismatches.length > 0) throw new Error(`[backend-federation-build] Delivery-unit identity drift between ${topologyPath}, package.json and ${buildIdentityPath}: ${mismatches.join('; ')}`);
36
40
  }
37
- const unitId = compactDeliveryUnit?.unitId ?? buildIdentity.unitId;
38
- const generationBuildMarker = compactDeliveryUnit?.buildMarker ?? buildIdentity.buildVersion;
41
+ const unitId = topologyDeliveryUnit?.unitId ?? buildIdentity.unitId;
42
+ const generationBuildMarker = topologyDeliveryUnit?.buildMarker ?? buildIdentity.buildVersion;
39
43
  const sourceRevision = await resolveWorkspaceSourceRevision(workspaceRoot);
40
44
  const buildVersion = generationBuildMarker && unitId ? resolveUltramodernReleaseIdentity({
41
45
  generationBuildMarker,
42
46
  unitId,
43
47
  workspaceRoot
44
48
  }).buildMarker : void 0;
45
- const packageName = compactDeliveryUnit?.packageName ?? buildIdentity.packageName ?? app.packageName;
46
- const version = compactDeliveryUnit?.version ?? buildIdentity.version;
49
+ const packageName = app.packageName;
50
+ const version = app.version;
47
51
  const deliveryUnit = createStampedDeliveryUnit({
48
52
  appId: app.id,
49
53
  unitId,
@@ -3,11 +3,11 @@ import { ULTRAMODERN_BUILD_ARTIFACT_PATH, isUltramodernBuildArtifact, nonEmptySt
3
3
  import { fs } from "@modern-js/utils";
4
4
  import { resolveUltramodernReleaseIdentity } from "../release-identity.mjs";
5
5
  import { isRecord } from "./utils.mjs";
6
- const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
6
+ const TOPOLOGY_PATH = 'topology/reference-topology.json';
7
7
  const findWorkspaceRoot = async (appDirectory)=>{
8
8
  let current = node_path.resolve(appDirectory);
9
9
  for(;;){
10
- if (await fs.pathExists(node_path.join(current, COMPACT_CONFIG_PATH))) return current;
10
+ if (await fs.pathExists(node_path.join(current, TOPOLOGY_PATH))) return current;
11
11
  const parent = node_path.dirname(current);
12
12
  if (parent === current) return;
13
13
  current = parent;
@@ -24,36 +24,25 @@ const stampReleaseIdentity = (identity, workspaceRoot)=>{
24
24
  })
25
25
  };
26
26
  };
27
- const resolveCompactApp = async (appDirectory)=>{
27
+ const resolveTopologyApp = async (appDirectory)=>{
28
28
  const workspaceRoot = await findWorkspaceRoot(appDirectory);
29
29
  if (!workspaceRoot) return;
30
- let compactConfig;
31
- try {
32
- compactConfig = await fs.readJSON(node_path.join(workspaceRoot, COMPACT_CONFIG_PATH));
33
- } catch {
34
- return {
35
- workspaceRoot
36
- };
37
- }
38
- if (!isRecord(compactConfig)) return {
39
- workspaceRoot
40
- };
41
- const topology = isRecord(compactConfig.topology) ? compactConfig.topology : void 0;
42
- const apps = Array.isArray(topology?.apps) ? topology.apps : void 0;
43
- if (!apps) return {
44
- app: compactConfig,
45
- workspaceRoot
46
- };
30
+ const topology = await fs.readJSON(node_path.join(workspaceRoot, TOPOLOGY_PATH));
31
+ if (!isRecord(topology) || !isRecord(topology.shell) || !Array.isArray(topology.verticals) || void 0 !== topology.shells && !Array.isArray(topology.shells)) throw new Error('[cloudflare-delivery-unit] Invalid declared reference topology.');
32
+ const apps = [
33
+ topology.shell,
34
+ ...topology.verticals,
35
+ ...Array.isArray(topology.shells) ? topology.shells : []
36
+ ];
47
37
  const resolvedAppDirectory = node_path.resolve(appDirectory);
48
38
  const app = apps.find((candidate)=>{
49
39
  if (!isRecord(candidate)) return false;
50
40
  const appPath = nonEmptyString(candidate.path);
51
41
  return void 0 !== appPath && node_path.resolve(workspaceRoot, appPath.replace(/^\.\/+/u, '')) === resolvedAppDirectory;
52
42
  });
43
+ if (!isRecord(app)) throw new Error(`[cloudflare-delivery-unit] ${appDirectory} is missing from the declared reference topology.`);
53
44
  return {
54
- ...isRecord(app) ? {
55
- app
56
- } : {},
45
+ app,
57
46
  workspaceRoot
58
47
  };
59
48
  };
@@ -80,10 +69,11 @@ const createDeliveryUnitStamp = (identity, app)=>{
80
69
  };
81
70
  };
82
71
  const resolveTopologyDeliveryUnit = async (appDirectory)=>{
83
- const resolved = await resolveCompactApp(appDirectory);
72
+ const resolved = await resolveTopologyApp(appDirectory);
84
73
  if (!resolved?.app) return;
85
74
  const identity = toDeliveryUnitIdentity(resolved.app.deliveryUnit);
86
- return identity ? createDeliveryUnitStamp(stampReleaseIdentity(identity, resolved.workspaceRoot), resolved.app) : void 0;
75
+ if (!identity) throw new Error('[cloudflare-delivery-unit] Declared app is missing a valid delivery-unit identity.');
76
+ return createDeliveryUnitStamp(stampReleaseIdentity(identity, resolved.workspaceRoot), resolved.app);
87
77
  };
88
78
  const resolveWorkerDeliveryUnitStamp = async (appDirectory)=>{
89
79
  const buildArtifactPath = node_path.join(appDirectory, ULTRAMODERN_BUILD_ARTIFACT_PATH);
@@ -94,7 +84,7 @@ const resolveWorkerDeliveryUnitStamp = async (appDirectory)=>{
94
84
  identity = toDeliveryUnitIdentity(artifact.deliveryUnit);
95
85
  }
96
86
  if (!identity) return;
97
- const resolved = await resolveCompactApp(appDirectory);
87
+ const resolved = await resolveTopologyApp(appDirectory);
98
88
  if (resolved) identity = stampReleaseIdentity(identity, resolved.workspaceRoot);
99
89
  return createDeliveryUnitStamp(identity, resolved?.app);
100
90
  };
@@ -1,4 +1,5 @@
1
1
  import node_path from "node:path";
2
+ import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, ULTRAMODERN_BUILD_ARTIFACT_FILE } from "@modern-js/backend-federation-contracts";
2
3
  import { fs } from "@modern-js/utils";
3
4
  import { createCloudflareOutputPlan } from "../cloudflare-output-plan.mjs";
4
5
  import { assertCloudflareOutput } from "../cloudflare-output-verifier/index.mjs";
@@ -36,7 +37,7 @@ const createCloudflarePreset = ({ api, appContext, modernConfig })=>{
36
37
  await fs.remove(outputDirectory);
37
38
  },
38
39
  async writeOutput () {
39
- if (releaseEnvelopeEnabled && await fs.pathExists(node_path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH))) await emitFrameworkMicroVerticalReleaseEnvelope({
40
+ if (releaseEnvelopeEnabled) await emitFrameworkMicroVerticalReleaseEnvelope({
40
41
  apiOnly,
41
42
  distDirectory,
42
43
  target: 'cloudflare'
@@ -45,6 +46,11 @@ const createCloudflarePreset = ({ api, appContext, modernConfig })=>{
45
46
  await fs.copy(distDirectory, publicDirectory, {
46
47
  filter: (src)=>{
47
48
  const relativePath = node_path.relative(distDirectory, src).replace(/\\/gu, '/');
49
+ if (apiOnly && '' !== relativePath && ![
50
+ BACKEND_FEDERATION_MANIFEST_FILE,
51
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
52
+ ULTRAMODERN_BUILD_ARTIFACT_FILE
53
+ ].includes(relativePath)) return false;
48
54
  return 'release' !== relativePath && !relativePath.startsWith('release/') && 'public' !== relativePath && !relativePath.startsWith('public/') && shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes);
49
55
  }
50
56
  });
@@ -205,9 +205,9 @@ const prependBundlerChain = (environment, handler)=>{
205
205
  };
206
206
  const getWorkerEntries = (environment, normalizedConfig, appContext)=>{
207
207
  const configuredEntries = environment.source?.entry;
208
- if (!configuredEntries) return;
209
- const entries = rewriteWorkerEntries(configuredEntries);
210
208
  const effectApiEntry = getEffectBffEntry(normalizedConfig, appContext);
209
+ if (!configuredEntries && !effectApiEntry) return;
210
+ const entries = configuredEntries ? rewriteWorkerEntries(configuredEntries) : {};
211
211
  return effectApiEntry ? {
212
212
  ...entries,
213
213
  [BFF_EFFECT_WORKER_ENTRY_NAME]: [
@@ -287,7 +287,14 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
287
287
  };
288
288
  function getCloudflareBuilderEnvironments({ appContext, environments, normalizedConfig, resolveDeployProvider = resolveStdEnvDeployProvider }) {
289
289
  if (!isCloudflareWorkerDeploy(normalizedConfig, resolveDeployProvider)) return environments;
290
- const workerEnvironment = environments[SERVICE_WORKER_ENVIRONMENT_NAME];
290
+ const workerEnvironment = environments[SERVICE_WORKER_ENVIRONMENT_NAME] ?? (appContext.apiOnly ? {
291
+ output: {
292
+ target: 'web-worker'
293
+ },
294
+ source: {
295
+ entry: {}
296
+ }
297
+ } : void 0);
291
298
  if (!workerEnvironment) return environments;
292
299
  const workerEntries = getWorkerEntries(workerEnvironment, normalizedConfig, appContext);
293
300
  if (!workerEntries) return environments;
@@ -303,6 +310,9 @@ function getCloudflareBuilderEnvironments({ appContext, environments, normalized
303
310
  entry: workerEntries
304
311
  }
305
312
  }, createCloudflareBundlerChain(appContext, Object.keys(workerEntries)));
313
+ if (appContext.apiOnly) return {
314
+ [SERVICE_WORKER_ENVIRONMENT_NAME]: cloudflareWorkerEnvironment
315
+ };
306
316
  return {
307
317
  ...environments,
308
318
  [SERVICE_WORKER_ENVIRONMENT_NAME]: cloudflareWorkerEnvironment
@@ -25,6 +25,7 @@ const CLOUDFLARE_WORKER_NODE_BUILTINS = [
25
25
  'fs/promises',
26
26
  'http',
27
27
  'http2',
28
+ 'https',
28
29
  'inspector',
29
30
  'module',
30
31
  'net',
@@ -24,30 +24,32 @@ const verifyDeliveryUnitIdentity = (issues, manifest, manifestPath, declared)=>{
24
24
  ]){
25
25
  const marker = stamped.surfaces[surface];
26
26
  const expected = declared?.surfaces?.[surface];
27
- if (declared?.surfaces && !expected) {
28
- if (marker) addIssue(issues, {
29
- code: 'delivery-unit-drift',
30
- message: `Cloudflare worker manifest declares an unexpected ${surface} delivery-unit surface for this topology profile.`,
31
- path: manifestPath
32
- });
33
- continue;
34
- }
35
- if (!marker || 'object' != typeof marker) {
36
- addIssue(issues, {
37
- code: 'missing-delivery-unit',
38
- message: `Cloudflare worker manifest is missing the ${surface} delivery-unit surface marker.`,
39
- path: manifestPath
40
- });
41
- continue;
42
- }
43
- for (const field of DELIVERY_UNIT_IDENTITY_FIELDS){
44
- const markerValue = deliveryUnitIdentityFieldValue(marker, field);
45
- const stampedValue = deliveryUnitIdentityFieldValue(stamped, field);
46
- assertEqual(issues, markerValue, stampedValue, {
47
- code: 'delivery-unit-drift',
48
- message: `Cloudflare worker manifest ${surface} surface deliveryUnit.${field} must derive from one delivery-unit record (expected ${stampedValue ?? 'undefined'}, received ${markerValue ?? 'undefined'}).`,
49
- path: manifestPath
50
- });
27
+ if (expected || marker) {
28
+ if (declared?.surfaces && !expected) {
29
+ if (marker) addIssue(issues, {
30
+ code: 'delivery-unit-drift',
31
+ message: `Cloudflare worker manifest declares an unexpected ${surface} delivery-unit surface for this topology profile.`,
32
+ path: manifestPath
33
+ });
34
+ continue;
35
+ }
36
+ if (!marker || 'object' != typeof marker) {
37
+ addIssue(issues, {
38
+ code: 'missing-delivery-unit',
39
+ message: `Cloudflare worker manifest is missing the ${surface} delivery-unit surface marker.`,
40
+ path: manifestPath
41
+ });
42
+ continue;
43
+ }
44
+ for (const field of DELIVERY_UNIT_IDENTITY_FIELDS){
45
+ const markerValue = deliveryUnitIdentityFieldValue(marker, field);
46
+ const stampedValue = deliveryUnitIdentityFieldValue(stamped, field);
47
+ assertEqual(issues, markerValue, stampedValue, {
48
+ code: 'delivery-unit-drift',
49
+ message: `Cloudflare worker manifest ${surface} surface deliveryUnit.${field} must derive from one delivery-unit record (expected ${stampedValue ?? 'undefined'}, received ${markerValue ?? 'undefined'}).`,
50
+ path: manifestPath
51
+ });
52
+ }
51
53
  }
52
54
  }
53
55
  };
@@ -72,6 +72,16 @@ const verifyCloudflareOutput = async (options)=>{
72
72
  }
73
73
  }
74
74
  if (wrangler) {
75
+ if ('string' != typeof wrangler.name || 0 === wrangler.name.trim().length) addIssue(issues, {
76
+ code: 'invalid-wrangler',
77
+ message: 'wrangler.json name must be a non-empty worker name.',
78
+ path: wranglerPath
79
+ });
80
+ if ('string' != typeof wrangler.compatibility_date || !/^\d{4}-\d{2}-\d{2}$/u.test(wrangler.compatibility_date)) addIssue(issues, {
81
+ code: 'invalid-wrangler',
82
+ message: 'wrangler.json compatibility_date must use YYYY-MM-DD.',
83
+ path: wranglerPath
84
+ });
75
85
  assertEqual(issues, wrangler.main, outputPlan.wrangler.main, {
76
86
  code: 'invalid-wrangler',
77
87
  message: `wrangler.json main must be ${outputPlan.wrangler.main}.`,
@@ -42,6 +42,52 @@ const verifyManifestShape = (issues, manifest, manifestPath)=>{
42
42
  message: 'Cloudflare output manifest routeSpec.routes must be array.',
43
43
  path: manifestPath
44
44
  });
45
+ const security = manifest.security;
46
+ if (security && 'object' == typeof security && 'boolean' == typeof security.enabled) {
47
+ const cors = security.cors;
48
+ if (!cors || 'object' != typeof cors || 'boolean' != typeof cors.assets || !Array.isArray(cors.allowedOrigins) || !Array.isArray(cors.allowedMethods) || !Array.isArray(cors.allowedHeaders)) addIssue(issues, {
49
+ code: 'invalid-manifest',
50
+ message: 'Cloudflare output manifest security.cors is incomplete.',
51
+ path: manifestPath
52
+ });
53
+ if (security.enabled) {
54
+ const headers = security.headers;
55
+ const csp = security.contentSecurityPolicy;
56
+ const noindex = security.noindex;
57
+ if (!headers || 'object' != typeof headers || ![
58
+ 'string',
59
+ 'boolean'
60
+ ].includes(typeof headers.referrerPolicy) || ![
61
+ 'string',
62
+ 'boolean'
63
+ ].includes(typeof headers.contentTypeOptions) || ![
64
+ 'string',
65
+ 'boolean'
66
+ ].includes(typeof headers.permissionsPolicy)) addIssue(issues, {
67
+ code: 'invalid-manifest',
68
+ message: 'Cloudflare output manifest security.headers is incomplete.',
69
+ path: manifestPath
70
+ });
71
+ if (!csp || ![
72
+ 'off',
73
+ 'report-only',
74
+ 'enforce'
75
+ ].includes(csp.mode) || !csp.directives || 'object' != typeof csp.directives || Array.isArray(csp.directives) || Object.values(csp.directives).some((value)=>!Array.isArray(value) || value.some((entry)=>'string' != typeof entry))) addIssue(issues, {
76
+ code: 'invalid-manifest',
77
+ message: 'Cloudflare output manifest security.contentSecurityPolicy is invalid.',
78
+ path: manifestPath
79
+ });
80
+ if (!noindex || 'boolean' != typeof noindex.workersDev || 'boolean' != typeof noindex.localhost || !Array.isArray(noindex.previewHostnames)) addIssue(issues, {
81
+ code: 'invalid-manifest',
82
+ message: 'Cloudflare output manifest security.noindex is incomplete.',
83
+ path: manifestPath
84
+ });
85
+ }
86
+ } else addIssue(issues, {
87
+ code: 'invalid-manifest',
88
+ message: 'Cloudflare output manifest security.enabled must be a boolean.',
89
+ path: manifestPath
90
+ });
45
91
  if (manifest.bff?.runtimeFramework === 'effect') assertEqual(issues, manifest.bff.dispatcherExport, BFF_EFFECT_WORKER_DISPATCHER_EXPORT, {
46
92
  code: 'invalid-manifest',
47
93
  message: `Cloudflare Effect BFF manifest dispatcherExport must be ${BFF_EFFECT_WORKER_DISPATCHER_EXPORT}.`,
@@ -84,7 +84,13 @@ const verifyCloudflareOutputMutationPolicy = async (options)=>{
84
84
  const files = (await Promise.all(options.scanRoots.map((scanRoot)=>walkFiles(node_path.resolve(scanRoot))))).flat();
85
85
  for (const file of files){
86
86
  if (excludedFiles.has(file)) continue;
87
- const source = await promises.readFile(file, 'utf-8');
87
+ let source;
88
+ try {
89
+ source = await promises.readFile(file, 'utf-8');
90
+ } catch (error) {
91
+ if ('ENOENT' === error.code) continue;
92
+ throw error;
93
+ }
88
94
  for (const forbidden of FORBIDDEN_MUTATION_PATTERNS)if (forbidden.pattern.test(source)) addIssue(issues, {
89
95
  code: forbidden.code,
90
96
  message: forbidden.message,
@@ -4,6 +4,10 @@ const dependencyKeys = [
4
4
  'dependencies',
5
5
  'optionalDependencies'
6
6
  ];
7
+ const rootDependencyKeys = [
8
+ ...dependencyKeys,
9
+ 'devDependencies'
10
+ ];
7
11
  const exactVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
8
12
  async function readPackageJson(filePath) {
9
13
  return JSON.parse(await promises.readFile(filePath, 'utf8'));
@@ -19,14 +23,34 @@ function parseNpmAlias(aliasName, specifier) {
19
23
  targetVersion: target.slice(versionSeparator + 1)
20
24
  };
21
25
  }
22
- function aliasesFromPackageJson(packageJson) {
26
+ function aliasesFromPackageJson(packageJson, keys = dependencyKeys) {
23
27
  const aliases = [];
24
- for (const dependencyKey of dependencyKeys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
28
+ for (const dependencyKey of keys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
25
29
  const alias = parseNpmAlias(aliasName, specifier);
26
30
  if (alias) aliases.push(alias);
27
31
  }
28
32
  return aliases;
29
33
  }
34
+ async function catalogAliasesFromInstalledPackages(appDirectory, packageJson) {
35
+ const aliases = [];
36
+ for (const dependencyKey of rootDependencyKeys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
37
+ if (!specifier.startsWith('catalog:')) continue;
38
+ let installedPackage;
39
+ try {
40
+ installedPackage = await readPackageJson(node_path.join(appDirectory, 'node_modules', aliasName, 'package.json'));
41
+ } catch (error) {
42
+ if ('ENOENT' === error.code) continue;
43
+ throw error;
44
+ }
45
+ if (!installedPackage.name || !installedPackage.version) throw new Error(`Installed catalog package ${aliasName} has no identity`);
46
+ if (installedPackage.name !== aliasName) aliases.push({
47
+ aliasName,
48
+ targetName: installedPackage.name,
49
+ targetVersion: installedPackage.version
50
+ });
51
+ }
52
+ return aliases;
53
+ }
30
54
  async function collectPackageRecords(nodeModulesDirectory) {
31
55
  const records = [];
32
56
  const visitedNodeModules = new Set();
@@ -148,7 +172,8 @@ async function preserveNpmAliases({ appDirectory, outputDirectory, implicitAlias
148
172
  const records = await collectPackageRecords(node_path.join(outputDirectory, 'node_modules'));
149
173
  const appPackageJson = await readPackageJson(node_path.join(appDirectory, 'package.json'));
150
174
  const rootAliases = [
151
- ...aliasesFromPackageJson(appPackageJson),
175
+ ...aliasesFromPackageJson(appPackageJson, rootDependencyKeys),
176
+ ...await catalogAliasesFromInstalledPackages(appDirectory, appPackageJson),
152
177
  ...implicitAliases
153
178
  ].filter((alias)=>alias.aliasName !== alias.targetName);
154
179
  const emittedRootAliases = [];