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