@bleedingdev/modern-js-app-tools-extensions 3.8.3-ultramodern.6 → 3.9.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.
Files changed (133) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +256 -0
  2. package/dist/cjs/backend-federation-build/config.js +189 -0
  3. package/dist/cjs/backend-federation-build/emit.js +150 -0
  4. package/dist/cjs/backend-federation-build/index.js +53 -0
  5. package/dist/cjs/build-config/build-environment.js +268 -0
  6. package/dist/cjs/build-config/precompress/plugin.js +124 -0
  7. package/dist/cjs/build-config/precompress/types.js +18 -0
  8. package/dist/cjs/build-config/public.js +46 -0
  9. package/dist/cjs/cloudflare/constants.js +1 -1
  10. package/dist/cjs/cloudflare/delivery-unit.js +7 -16
  11. package/dist/cjs/cloudflare-output-contract.js +37 -1
  12. package/dist/cjs/cloudflare-output-verifier/identity.js +6 -6
  13. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +4 -1
  14. package/dist/cjs/config.js +29 -1
  15. package/dist/cjs/deploy-output/npmAliases.js +226 -0
  16. package/dist/cjs/deploy-output/plugin.js +76 -0
  17. package/dist/cjs/deploy-output/target.js +48 -0
  18. package/dist/cjs/native-type-checker.js +193 -0
  19. package/dist/cjs/policy-defaults.js +101 -0
  20. package/dist/cjs/release-envelope/framework-output.js +77 -32
  21. package/dist/cjs/release-envelope/plugin.js +5 -3
  22. package/dist/cjs/release-identity.js +3 -6
  23. package/dist/cjs/runtime-package-resolution.js +118 -0
  24. package/dist/esm/backend-federation-build/codegen.mjs +205 -0
  25. package/dist/esm/backend-federation-build/config.mjs +116 -0
  26. package/dist/esm/backend-federation-build/emit.mjs +99 -0
  27. package/dist/esm/backend-federation-build/index.mjs +1 -0
  28. package/dist/esm/build-config/build-environment.mjs +224 -0
  29. package/dist/esm/build-config/precompress/plugin.mjs +76 -0
  30. package/dist/esm/build-config/precompress/types.mjs +0 -0
  31. package/dist/esm/build-config/public.mjs +1 -0
  32. package/dist/esm/cloudflare/constants.mjs +1 -1
  33. package/dist/esm/cloudflare/delivery-unit.mjs +1 -10
  34. package/dist/esm/cloudflare-output-contract.mjs +35 -2
  35. package/dist/esm/cloudflare-output-verifier/identity.mjs +1 -1
  36. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +5 -2
  37. package/dist/esm/config.mjs +1 -0
  38. package/dist/esm/deploy-output/npmAliases.mjs +174 -0
  39. package/dist/esm/deploy-output/plugin.mjs +28 -0
  40. package/dist/esm/deploy-output/target.mjs +10 -0
  41. package/dist/esm/native-type-checker.mjs +142 -0
  42. package/dist/esm/policy-defaults.mjs +45 -0
  43. package/dist/esm/release-envelope/framework-output.mjs +51 -6
  44. package/dist/esm/release-envelope/plugin.mjs +5 -3
  45. package/dist/esm/release-identity.mjs +3 -6
  46. package/dist/esm/runtime-package-resolution.mjs +60 -0
  47. package/dist/esm-node/backend-federation-build/codegen.mjs +206 -0
  48. package/dist/esm-node/backend-federation-build/config.mjs +117 -0
  49. package/dist/esm-node/backend-federation-build/emit.mjs +100 -0
  50. package/dist/esm-node/backend-federation-build/index.mjs +2 -0
  51. package/dist/esm-node/build-config/build-environment.mjs +225 -0
  52. package/dist/esm-node/build-config/precompress/plugin.mjs +77 -0
  53. package/dist/esm-node/build-config/precompress/types.mjs +1 -0
  54. package/dist/esm-node/build-config/public.mjs +2 -0
  55. package/dist/esm-node/cloudflare/constants.mjs +1 -1
  56. package/dist/esm-node/cloudflare/delivery-unit.mjs +1 -10
  57. package/dist/esm-node/cloudflare-output-contract.mjs +35 -2
  58. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +1 -1
  59. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +5 -2
  60. package/dist/esm-node/config.mjs +1 -0
  61. package/dist/esm-node/deploy-output/npmAliases.mjs +175 -0
  62. package/dist/esm-node/deploy-output/plugin.mjs +31 -0
  63. package/dist/esm-node/deploy-output/target.mjs +11 -0
  64. package/dist/esm-node/native-type-checker.mjs +143 -0
  65. package/dist/esm-node/policy-defaults.mjs +46 -0
  66. package/dist/esm-node/release-envelope/framework-output.mjs +51 -6
  67. package/dist/esm-node/release-envelope/plugin.mjs +5 -3
  68. package/dist/esm-node/release-identity.mjs +3 -6
  69. package/dist/esm-node/runtime-package-resolution.mjs +61 -0
  70. package/dist/types/backend-federation-build/codegen.d.ts +86 -0
  71. package/dist/types/backend-federation-build/config.d.ts +102 -0
  72. package/dist/types/backend-federation-build/emit.d.ts +21 -0
  73. package/dist/types/backend-federation-build/index.d.ts +2 -0
  74. package/dist/types/build-config/build-environment.d.ts +22 -0
  75. package/dist/types/build-config/precompress/plugin.d.ts +4 -0
  76. package/dist/types/build-config/precompress/types.d.ts +19 -0
  77. package/dist/types/build-config/public.d.ts +2 -0
  78. package/dist/types/cloudflare/artifacts.d.ts +1 -1
  79. package/dist/types/cloudflare/constants.d.ts +2 -2
  80. package/dist/types/cloudflare/delivery-unit.d.ts +2 -2
  81. package/dist/types/cloudflare/i18n-worker.d.ts +1 -1
  82. package/dist/types/cloudflare/index.d.ts +1 -1
  83. package/dist/types/cloudflare/security-policies.d.ts +1 -1
  84. package/dist/types/cloudflare/types.d.ts +1 -1
  85. package/dist/types/cloudflare/utils.d.ts +1 -1
  86. package/dist/types/cloudflare/worker-manifest.d.ts +4 -4
  87. package/dist/types/cloudflare/wrangler-config.d.ts +2 -2
  88. package/dist/types/cloudflare-output-contract.d.ts +2 -1
  89. package/dist/types/cloudflare-output-plan.d.ts +1 -1
  90. package/dist/types/cloudflare-output-verifier/identity.d.ts +2 -2
  91. package/dist/types/cloudflare-output-verifier/index.d.ts +5 -5
  92. package/dist/types/cloudflare-output-verifier/manifest.d.ts +1 -1
  93. package/dist/types/cloudflare-output-verifier/mutation-policy.d.ts +1 -1
  94. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +1 -1
  95. package/dist/types/config.d.ts +2 -0
  96. package/dist/types/deploy-output/npmAliases.d.ts +14 -0
  97. package/dist/types/deploy-output/plugin.d.ts +15 -0
  98. package/dist/types/deploy-output/target.d.ts +6 -0
  99. package/dist/types/index.d.ts +10 -10
  100. package/dist/types/native-type-checker.d.ts +15 -0
  101. package/dist/types/policy-defaults.d.ts +46 -0
  102. package/dist/types/precompress/LICENSE +20 -0
  103. package/dist/types/precompress/index.d.ts +76 -0
  104. package/dist/types/release-envelope/canonical.d.ts +1 -1
  105. package/dist/types/release-envelope/framework-output.d.ts +3 -2
  106. package/dist/types/release-envelope/index.d.ts +3 -3
  107. package/dist/types/runtime-package-resolution.d.ts +28 -0
  108. package/package.json +72 -14
  109. package/src/backend-federation-build/codegen.ts +245 -0
  110. package/src/backend-federation-build/config.ts +356 -0
  111. package/src/backend-federation-build/emit.ts +202 -0
  112. package/src/backend-federation-build/index.ts +2 -0
  113. package/src/build-config/build-environment.ts +404 -0
  114. package/src/build-config/precompress/plugin.ts +140 -0
  115. package/src/build-config/precompress/types.ts +26 -0
  116. package/src/build-config/public.ts +6 -0
  117. package/src/cloudflare/constants.ts +1 -1
  118. package/src/cloudflare/delivery-unit.ts +3 -20
  119. package/src/cloudflare/worker-manifest.ts +1 -0
  120. package/src/cloudflare-builder.ts +1 -0
  121. package/src/cloudflare-output-contract.ts +37 -0
  122. package/src/cloudflare-output-verifier/identity.ts +1 -1
  123. package/src/cloudflare-output-verifier/worker-bundles.ts +5 -3
  124. package/src/config.ts +7 -0
  125. package/src/deploy-output/npmAliases.ts +333 -0
  126. package/src/deploy-output/plugin.ts +49 -0
  127. package/src/deploy-output/target.ts +23 -0
  128. package/src/native-type-checker.ts +183 -0
  129. package/src/policy-defaults.ts +127 -0
  130. package/src/release-envelope/framework-output.ts +135 -4
  131. package/src/release-envelope/plugin.ts +15 -3
  132. package/src/release-identity.ts +3 -2
  133. package/src/runtime-package-resolution.ts +115 -0
@@ -0,0 +1,49 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { preserveNpmAliases, readPackageIdentity } from './npmAliases';
4
+ import {
5
+ type DeployOutputConfig,
6
+ resolveDeployTarget as defaultResolveDeployTarget,
7
+ } from './target';
8
+
9
+ export interface DeployOutputPluginApi {
10
+ getAppContext(): { appDirectory: string; metaName: string };
11
+ getNormalizedConfig(): DeployOutputConfig;
12
+ onAfterDeploy(handler: () => Promise<void>): void;
13
+ }
14
+
15
+ export const createDeployOutputAliasesPlugin = ({
16
+ resolveDeployTarget = defaultResolveDeployTarget,
17
+ }: {
18
+ resolveDeployTarget?: (config: DeployOutputConfig) => string;
19
+ } = {}) => ({
20
+ name: '@modern-js/deploy-output-aliases',
21
+ setup(api: DeployOutputPluginApi) {
22
+ api.onAfterDeploy(async () => {
23
+ const { appDirectory, metaName } = api.getAppContext();
24
+ const config = api.getNormalizedConfig();
25
+ if (
26
+ (metaName !== 'modern-js' &&
27
+ !config.deploy?.target &&
28
+ !process.env.MODERNJS_DEPLOY) ||
29
+ resolveDeployTarget(config) !== 'node'
30
+ ) {
31
+ return;
32
+ }
33
+
34
+ const entry = createRequire(__filename).resolve('@modern-js/prod-server');
35
+ const identity = await readPackageIdentity(entry);
36
+ await preserveNpmAliases({
37
+ appDirectory,
38
+ outputDirectory: path.join(appDirectory, '.output'),
39
+ implicitAliases: [
40
+ {
41
+ aliasName: '@modern-js/prod-server',
42
+ targetName: identity.name,
43
+ targetVersion: identity.version,
44
+ },
45
+ ],
46
+ });
47
+ });
48
+ },
49
+ });
@@ -0,0 +1,23 @@
1
+ import { provider } from 'std-env';
2
+
3
+ export interface DeployOutputConfig {
4
+ deploy?: { target?: string };
5
+ }
6
+
7
+ const providerDeployTargets: Partial<Record<string, string>> = {
8
+ vercel: 'vercel',
9
+ netlify: 'netlify',
10
+ cloudflare: 'cloudflare',
11
+ cloudflare_pages: 'cloudflare',
12
+ cloudflare_workers: 'cloudflare',
13
+ };
14
+
15
+ export const resolveDeployTarget = (
16
+ config: DeployOutputConfig,
17
+ envDeployTarget = process.env.MODERNJS_DEPLOY,
18
+ detectedProvider = provider,
19
+ ): string =>
20
+ config.deploy?.target ||
21
+ envDeployTarget ||
22
+ (detectedProvider ? providerDeployTargets[detectedProvider] : undefined) ||
23
+ 'node';
@@ -0,0 +1,183 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { refreshTsgoCheckerConfig } from '@modern-js/builder';
6
+ import type { Rspack, RspackChain } from '@rsbuild/core';
7
+
8
+ const execute = promisify(execFile);
9
+ const name = 'UltramodernNativeTypeChecker';
10
+
11
+ // Windows file events expand 8.3 aliases; register the same native path spelling.
12
+ // A path that does not exist yet (a project tsconfig mid-edit) cannot be
13
+ // realpathed; it is registered as spelled so its return still triggers a build.
14
+ const watchDependencyPath = (file: string): string => {
15
+ if (process.platform !== 'win32') return file;
16
+ try {
17
+ return realpathSync.native(file);
18
+ } catch {
19
+ return file;
20
+ }
21
+ };
22
+
23
+ export class UltramodernNativeTypeChecker {
24
+ constructor(
25
+ private readonly options: {
26
+ compiler: () => string;
27
+ configFile: string;
28
+ build: boolean;
29
+ },
30
+ ) {}
31
+
32
+ async check(): Promise<void> {
33
+ const { configFile, build } = this.options;
34
+ if (!build) {
35
+ const config = JSON.parse(
36
+ await this.run(['--showConfig', '--project', configFile]),
37
+ ) as { references?: { path: string }[] };
38
+ // Project mode consumes referenced declarations but never builds them.
39
+ // Prepare only the references using their own emit contracts, then keep
40
+ // the app itself in no-emit mode. This also refreshes stale declarations.
41
+ if (config.references?.length) {
42
+ await this.run([
43
+ '--build',
44
+ ...config.references.map(reference =>
45
+ path.resolve(path.dirname(configFile), reference.path),
46
+ ),
47
+ '--stopBuildOnErrors',
48
+ '--pretty',
49
+ 'false',
50
+ ]);
51
+ }
52
+ }
53
+ const args = build
54
+ ? ['--build', configFile, '--stopBuildOnErrors']
55
+ : ['--project', configFile, '--noEmit'];
56
+ await this.run([...args, '--pretty', 'false']);
57
+ }
58
+
59
+ private async run(args: string[]): Promise<string> {
60
+ try {
61
+ const result = await execute(this.options.compiler(), args, {
62
+ cwd: path.dirname(this.options.configFile),
63
+ maxBuffer: 16 * 1024 * 1024,
64
+ });
65
+ return result.stdout;
66
+ } catch (cause) {
67
+ const output = cause as { stdout?: string; stderr?: string };
68
+ const diagnostics = `${output.stdout ?? ''}${output.stderr ?? ''}`;
69
+ throw new Error(
70
+ `${name} failed:\n${diagnostics || (cause instanceof Error ? cause.message : String(cause))}`,
71
+ { cause },
72
+ );
73
+ }
74
+ }
75
+
76
+ private async watchInputs(): Promise<{
77
+ inputs: Set<string>;
78
+ missing: Set<string>;
79
+ }> {
80
+ const inputs = new Set<string>();
81
+ const missing = new Set<string>();
82
+ const projects = [this.options.configFile];
83
+ const visited = new Set<string>();
84
+ for (const configFile of projects) {
85
+ if (visited.has(configFile)) continue;
86
+ visited.add(configFile);
87
+ // Registered before it is parsed: a referenced config that is absent or
88
+ // unparsable right now must still be watched, or creating or repairing
89
+ // it never re-triggers the compilation. `check()` reports the error.
90
+ inputs.add(configFile);
91
+ if (!existsSync(configFile)) {
92
+ missing.add(configFile);
93
+ continue;
94
+ }
95
+ let config: { files?: string[]; references?: { path: string }[] };
96
+ try {
97
+ config = JSON.parse(
98
+ await this.run(['--showConfig', '--project', configFile]),
99
+ );
100
+ } catch {
101
+ continue;
102
+ }
103
+ const directory = path.dirname(configFile);
104
+ for (const file of config.files ?? [])
105
+ inputs.add(path.resolve(directory, file));
106
+ for (const reference of config.references ?? []) {
107
+ const target = path.resolve(directory, reference.path);
108
+ projects.push(
109
+ path.extname(target) === '.json'
110
+ ? target
111
+ : path.join(target, 'tsconfig.json'),
112
+ );
113
+ }
114
+ }
115
+ return { inputs, missing };
116
+ }
117
+
118
+ apply(compiler: Rspack.Compiler): void {
119
+ compiler.hooks.thisCompilation.tap(name, compilation => {
120
+ compilation.hooks.processAssets.tapPromise(name, async () => {
121
+ try {
122
+ compilation.fileDependencies.add(
123
+ watchDependencyPath(this.options.configFile),
124
+ );
125
+ // A generated checker config restates values TypeScript does not
126
+ // inherit through `extends` (project references above all). Rebuild
127
+ // it from the project's own tsconfig before every check, and watch
128
+ // that tsconfig, so an edit during `modern dev` reaches the next
129
+ // compilation instead of the next restart.
130
+ const projectConfigFile = refreshTsgoCheckerConfig(
131
+ this.options.configFile,
132
+ );
133
+ if (projectConfigFile) {
134
+ const watched = watchDependencyPath(projectConfigFile);
135
+ compilation.fileDependencies.add(watched);
136
+ // Absent mid-edit: a missing dependency makes its (re)appearance
137
+ // a rebuild trigger, so the dev server recovers on its own.
138
+ if (!existsSync(projectConfigFile)) {
139
+ compilation.missingDependencies.add(watched);
140
+ }
141
+ }
142
+ const { inputs, missing } = await this.watchInputs();
143
+ for (const file of inputs)
144
+ compilation.fileDependencies.add(watchDependencyPath(file));
145
+ for (const file of missing)
146
+ compilation.missingDependencies.add(watchDependencyPath(file));
147
+ await this.check();
148
+ } catch (cause) {
149
+ compilation.errors.push(
150
+ new Error(cause instanceof Error ? cause.message : String(cause), {
151
+ cause,
152
+ }),
153
+ );
154
+ }
155
+ });
156
+ });
157
+ }
158
+ }
159
+
160
+ /** Preserve project-reference emit contracts in the native Effect compiler. */
161
+ export function configureUltramodernTypeChecker(
162
+ chain: RspackChain,
163
+ pluginId: string,
164
+ resolveCompiler: (configFile: string) => string,
165
+ ): void {
166
+ if (!chain.plugins.has(pluginId)) return;
167
+ const options = chain.plugin(pluginId).get('args')?.[0] as
168
+ | { typescript?: { configFile?: string; build?: boolean; tsgo?: boolean } }
169
+ | undefined;
170
+ const typescript = options?.typescript;
171
+ if (typescript?.tsgo === false) return;
172
+ if (!typescript?.configFile) {
173
+ throw new Error(`${name} requires the configured TypeScript project.`);
174
+ }
175
+ const configFile = path.resolve(typescript.configFile);
176
+ chain.plugin(pluginId).use(UltramodernNativeTypeChecker, [
177
+ {
178
+ build: typescript.build === true,
179
+ compiler: () => resolveCompiler(configFile),
180
+ configFile,
181
+ },
182
+ ]);
183
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ collectRuntimePackageModuleDirectories,
3
+ createRuntimePackageResolutionPlugin,
4
+ } from './runtime-package-resolution';
5
+
6
+ /**
7
+ * The fork's renderer policy (head rendering, HTML template assembly and asset
8
+ * ordering) and server policy (localised loaders, error responses, telemetry,
9
+ * module-federation CSS and asset cache headers, static serving) are the
10
+ * default behaviour of an UltraModern.js app.
11
+ *
12
+ * They live in their own packages so they can also be composed explicitly, but
13
+ * a plain `appTools()` app gets them without opting in — that is what this fork
14
+ * has always done. Pass `appTools({ rendererExtensions: false })` or
15
+ * `appTools({ serverExtensions: false })` to run without them.
16
+ */
17
+ export const RENDERER_EXTENSIONS_PACKAGE =
18
+ '@modern-js/runtime-renderer-extensions';
19
+
20
+ export const RENDERER_EXTENSIONS_PLUGIN_NAME = 'rendererHead';
21
+
22
+ export const SERVER_EXTENSIONS_PLUGIN_NAME =
23
+ '@modern-js/app-tools/server-plugin';
24
+
25
+ export interface PolicyDefaultsOptions {
26
+ /** Default `true`. Set to `false` to drop the fork's renderer policy. */
27
+ rendererExtensions?: boolean;
28
+ /** Default `true`. Set to `false` to drop the fork's server policy. */
29
+ serverExtensions?: boolean;
30
+ }
31
+
32
+ type RuntimePluginDescriptor = {
33
+ name?: string;
34
+ path: string;
35
+ config?: unknown;
36
+ };
37
+
38
+ type ServerPluginDescriptor = { name: string };
39
+
40
+ /**
41
+ * The subset of the CLI plugin API these defaults use. Typed structurally, and
42
+ * loosely at the hook boundary, so this package stays free of a dependency on
43
+ * the solution that consumes it — which depends on this one.
44
+ */
45
+ export interface PolicyDefaultsPluginApi {
46
+ _internalRuntimePlugins(fn: (input: any) => any): void;
47
+ _internalServerPlugins(fn: (input: any) => any): void;
48
+ modifyResolvedConfig(fn: (config: any) => any): void;
49
+ }
50
+
51
+ type RuntimePluginsInput = {
52
+ entrypoint: unknown;
53
+ plugins: RuntimePluginDescriptor[];
54
+ };
55
+
56
+ type ServerPluginsInput = { plugins: ServerPluginDescriptor[] };
57
+
58
+ export const POLICY_DEFAULTS_PLUGIN_NAME =
59
+ '@modern-js/ultramodern-policy-defaults';
60
+
61
+ /** Registers the fork's default renderer and server policy. */
62
+ export const applyPolicyDefaults = (
63
+ api: PolicyDefaultsPluginApi,
64
+ options: PolicyDefaultsOptions = {},
65
+ ): void => {
66
+ if (options.rendererExtensions !== false) {
67
+ api._internalRuntimePlugins(
68
+ ({ entrypoint, plugins }: RuntimePluginsInput) => {
69
+ // An explicit composition (`ultramodernAppTools()`) may already have
70
+ // registered it; the descriptor list must stay free of duplicates.
71
+ if (
72
+ !plugins.some(plugin => plugin.path === RENDERER_EXTENSIONS_PACKAGE)
73
+ ) {
74
+ plugins.push({
75
+ name: RENDERER_EXTENSIONS_PLUGIN_NAME,
76
+ path: RENDERER_EXTENSIONS_PACKAGE,
77
+ config: {},
78
+ });
79
+ }
80
+ return { entrypoint, plugins };
81
+ },
82
+ );
83
+
84
+ const moduleDirectories = collectRuntimePackageModuleDirectories(
85
+ [RENDERER_EXTENSIONS_PACKAGE],
86
+ import.meta.url,
87
+ );
88
+ if (moduleDirectories.length > 0) {
89
+ api.modifyResolvedConfig(config => ({
90
+ ...config,
91
+ builderPlugins: [
92
+ ...(config.builderPlugins ?? []),
93
+ createRuntimePackageResolutionPlugin(moduleDirectories),
94
+ ],
95
+ }));
96
+ }
97
+ }
98
+
99
+ if (options.serverExtensions !== false) {
100
+ api._internalServerPlugins(({ plugins }: ServerPluginsInput) => {
101
+ if (
102
+ !plugins.some(plugin => plugin.name === SERVER_EXTENSIONS_PLUGIN_NAME)
103
+ ) {
104
+ plugins.push({ name: SERVER_EXTENSIONS_PLUGIN_NAME });
105
+ }
106
+ return { plugins };
107
+ });
108
+ }
109
+ };
110
+
111
+ /**
112
+ * The fork defaults as a CLI plugin.
113
+ *
114
+ * The descriptor this registers may land before the ones it wraps — CLI
115
+ * ordering cannot be constrained here without a cycle, because the plugins
116
+ * that emit those descriptors are themselves ordered after `appTools()`.
117
+ * Ordering is settled on the runtime side instead: `rendererHeadPlugin`
118
+ * declares the runtime plugins that run before it.
119
+ */
120
+ export const createPolicyDefaultsPlugin = (
121
+ options: PolicyDefaultsOptions = {},
122
+ ) => ({
123
+ name: POLICY_DEFAULTS_PLUGIN_NAME,
124
+ setup(api: PolicyDefaultsPluginApi) {
125
+ applyPolicyDefaults(api, options);
126
+ },
127
+ });
@@ -7,7 +7,7 @@ import {
7
7
  isUltramodernBuildArtifact,
8
8
  ULTRAMODERN_BUILD_ARTIFACT_FILE,
9
9
  type UltramodernBuildArtifact,
10
- } from '@modern-js/utils/universal';
10
+ } from '@modern-js/backend-federation-contracts';
11
11
  import {
12
12
  canonicalSerializeMicroVerticalReleaseEnvelope,
13
13
  createMicroVerticalReleaseEnvelope,
@@ -57,6 +57,111 @@ const collectNestedStrings = (value: unknown, key: string): string[] => {
57
57
  ]);
58
58
  };
59
59
 
60
+ // Empty native MF producers emit no remote entry or exposed execution assets.
61
+ // Only their routeAssets declarations may supply the missing browser evidence.
62
+ const emptyProducerRouteModules = async (
63
+ distDirectory: string,
64
+ files: string[],
65
+ manifest: unknown,
66
+ manifestLogicalPath: string,
67
+ ): Promise<string[]> => {
68
+ if (
69
+ !isRecord(manifest) ||
70
+ !Array.isArray(manifest.exposes) ||
71
+ manifest.exposes.length !== 0 ||
72
+ !Array.isArray(manifest.remotes) ||
73
+ manifest.remotes.length !== 0 ||
74
+ !isRecord(manifest.metaData) ||
75
+ !isRecord(manifest.metaData.remoteEntry) ||
76
+ manifest.metaData.remoteEntry.name !== '' ||
77
+ manifest.metaData.remoteEntry.path !== '' ||
78
+ manifest.metaData.remoteEntry.type !== 'global'
79
+ ) {
80
+ return [];
81
+ }
82
+ const publicPath = manifest.metaData.publicPath;
83
+ // Reject encoded/ambiguous paths before URL parsing can normalize traversal.
84
+ const safeReference = (value: unknown): value is string =>
85
+ typeof value === 'string' &&
86
+ value.length > 0 &&
87
+ !/[\\\\%?#\s]/u.test(value) &&
88
+ !value.split('/').some(segment => segment === '.' || segment === '..');
89
+ if (!safeReference(publicPath)) {
90
+ return [];
91
+ }
92
+ const absoluteBase = /^https?:\/\//u.test(publicPath);
93
+ if (
94
+ publicPath !== 'auto' &&
95
+ (!publicPath.endsWith('/') ||
96
+ (!absoluteBase &&
97
+ (!publicPath.startsWith('/') || publicPath.startsWith('//'))))
98
+ ) {
99
+ return [];
100
+ }
101
+ if (absoluteBase) {
102
+ const base = new URL(publicPath);
103
+ if (base.username || base.password || base.href !== publicPath) {
104
+ return [];
105
+ }
106
+ }
107
+ const directory = path.posix.dirname(manifestLogicalPath);
108
+ const routesManifest = await readJson(
109
+ path.join(distDirectory, directory, 'routes-manifest.json'),
110
+ );
111
+ if (!isRecord(routesManifest) || !isRecord(routesManifest.routeAssets)) {
112
+ return [];
113
+ }
114
+ const modules = new Set<string>();
115
+ for (const route of Object.values(routesManifest.routeAssets)) {
116
+ if (!isRecord(route) || !Array.isArray(route.assets)) {
117
+ return [];
118
+ }
119
+ for (const reference of route.assets) {
120
+ if (!safeReference(reference) || reference.startsWith('//')) {
121
+ return [];
122
+ }
123
+ let relative = reference;
124
+ if (/^https?:\/\//u.test(reference)) {
125
+ if (!absoluteBase || !reference.startsWith(publicPath)) {
126
+ return [];
127
+ }
128
+ relative = reference.slice(publicPath.length);
129
+ } else if (reference.startsWith('/')) {
130
+ const prefix =
131
+ publicPath === 'auto'
132
+ ? '/'
133
+ : absoluteBase
134
+ ? new URL(publicPath).pathname
135
+ : publicPath;
136
+ if (!reference.startsWith(prefix)) {
137
+ return [];
138
+ }
139
+ relative = reference.slice(prefix.length);
140
+ }
141
+ // Never allow API, SSR, backend containers, or unrelated on-disk modules.
142
+ if (
143
+ !/^(?:static|html|public)\/[^:]+$/u.test(relative) ||
144
+ relative.includes('//')
145
+ ) {
146
+ return [];
147
+ }
148
+ const logicalPath = path.posix.join(directory, relative);
149
+ if (!files.includes(logicalPath)) {
150
+ return [];
151
+ }
152
+ // A browser-named symlink to server/private bytes is not browser evidence.
153
+ const assetStat = await fs.lstat(path.join(distDirectory, logicalPath));
154
+ if (!assetStat.isFile()) {
155
+ return [];
156
+ }
157
+ if (COMPILED_MODULE_PATTERN.test(logicalPath)) {
158
+ modules.add(logicalPath);
159
+ }
160
+ }
161
+ }
162
+ return [...modules];
163
+ };
164
+
60
165
  const manifestReferencedClientModules = async (
61
166
  distDirectory: string,
62
167
  files: string[],
@@ -77,7 +182,7 @@ const manifestReferencedClientModules = async (
77
182
  .map(value => value.replace(/^https?:\/\/[^/]+/u, ''))
78
183
  .map(value => value.replace(/^\/+/u, '')),
79
184
  );
80
- return files.filter(
185
+ const modules = files.filter(
81
186
  logicalPath =>
82
187
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
83
188
  [...normalizedReferences].some(
@@ -87,6 +192,14 @@ const manifestReferencedClientModules = async (
87
192
  logicalPath.endsWith(`/${reference}`),
88
193
  ),
89
194
  );
195
+ return modules.length > 0
196
+ ? modules
197
+ : emptyProducerRouteModules(
198
+ distDirectory,
199
+ files,
200
+ manifest,
201
+ manifestLogicalPath,
202
+ );
90
203
  };
91
204
 
92
205
  const routeReferencedSsrModules = async (
@@ -209,7 +322,8 @@ const writeReleaseEnvelope = async (
209
322
 
210
323
  const readBuildArtifact = async (
211
324
  distDirectory: string,
212
- ): Promise<UltramodernBuildArtifact> => {
325
+ requirePromotable = true,
326
+ ): Promise<UltramodernBuildArtifact | undefined> => {
213
327
  const artifactPath = path.join(
214
328
  distDirectory,
215
329
  ULTRAMODERN_BUILD_ARTIFACT_FILE,
@@ -221,6 +335,15 @@ const readBuildArtifact = async (
221
335
  );
222
336
  }
223
337
  if (artifact.deliveryUnit.sourceRevision === 'workspace') {
338
+ // `resolveUltramodernSourceRevision` deliberately labels a dirty or
339
+ // non-Git checkout `workspace` so development stays usable; only a
340
+ // promotion may not carry that identity. A plain `modern build` is the
341
+ // development path, so it skips the envelope instead of failing the
342
+ // build. The deploy path still asks for a promotable artifact, which is
343
+ // where the release gate belongs.
344
+ if (!requirePromotable) {
345
+ return undefined;
346
+ }
224
347
  throw new Error(
225
348
  '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.',
226
349
  );
@@ -635,10 +758,12 @@ const pathExists = async (filePath: string) => {
635
758
  export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
636
759
  apiOnly,
637
760
  distDirectory,
761
+ requirePromotable = true,
638
762
  target,
639
763
  }: {
640
764
  apiOnly: boolean;
641
765
  distDirectory: string;
766
+ requirePromotable?: boolean;
642
767
  target: MicroVerticalReleaseTarget;
643
768
  }): Promise<MicroVerticalReleaseEnvelope | undefined> => {
644
769
  if (apiOnly) {
@@ -665,7 +790,13 @@ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
665
790
  );
666
791
  }
667
792
 
668
- const buildArtifact = await readBuildArtifact(distDirectory);
793
+ const buildArtifact = await readBuildArtifact(
794
+ distDirectory,
795
+ requirePromotable,
796
+ );
797
+ if (!buildArtifact) {
798
+ return undefined;
799
+ }
669
800
  const identity = {
670
801
  unitId: buildArtifact.deliveryUnit.unitId,
671
802
  buildMarker: buildArtifact.deliveryUnit.buildMarker,
@@ -67,14 +67,22 @@ export const createUltramodernReleaseEnvelopePlugin = <
67
67
  }): ReleaseEnvelopePlugin<Config> => {
68
68
  return {
69
69
  name: '@modern-js/ultramodern-release-envelope',
70
- pre: ['@modern-js/backend-federation-build', '@modern-js/plugin-bff'],
70
+ pre: [
71
+ '@modern-js/backend-federation-build',
72
+ '@modern-js/plugin-bff',
73
+ '@modern-js/deploy-output-aliases',
74
+ ],
71
75
  post: ['@modern-js/plugin-deploy'],
72
76
  setup(api) {
73
- const emitBuildEnvelope = async (target: 'node' | 'cloudflare') => {
77
+ const emitBuildEnvelope = async (
78
+ target: 'node' | 'cloudflare',
79
+ requirePromotable = true,
80
+ ) => {
74
81
  const { apiOnly, distDirectory } = api.getAppContext();
75
82
  await emitFrameworkMicroVerticalReleaseEnvelope({
76
83
  apiOnly,
77
84
  distDirectory,
85
+ requirePromotable,
78
86
  target,
79
87
  });
80
88
  };
@@ -84,7 +92,11 @@ export const createUltramodernReleaseEnvelopePlugin = <
84
92
  if (configuredTarget !== 'node' && configuredTarget !== 'cloudflare') {
85
93
  return;
86
94
  }
87
- await emitBuildEnvelope(configuredTarget);
95
+ // A plain `modern build` is the development path: a dirty or non-Git
96
+ // checkout resolves to the `workspace` source revision, which is not
97
+ // promotable but must still build. Deploy re-emits the envelope and
98
+ // verifies it, so the release gate keeps its teeth there.
99
+ await emitBuildEnvelope(configuredTarget, false);
88
100
  });
89
101
 
90
102
  api.onBeforeDeploy(async () => {
@@ -15,7 +15,8 @@ const gitOutput = (
15
15
  args: string[],
16
16
  ): string | undefined => {
17
17
  try {
18
- return execFileSync('git', ['-C', workspaceRoot, ...args], {
18
+ return execFileSync('git', args, {
19
+ cwd: workspaceRoot,
19
20
  encoding: 'utf8',
20
21
  stdio: ['ignore', 'pipe', 'ignore'],
21
22
  }).trim();
@@ -84,7 +85,7 @@ export const resolveUltramodernSourceRevision = (
84
85
  const revision = gitOutput(gitRoot, ['rev-parse', 'HEAD']);
85
86
  const status = gitOutput(gitRoot, [
86
87
  'status',
87
- '--porcelain=v1',
88
+ '--porcelain',
88
89
  '--untracked-files=all',
89
90
  ]);
90
91
  if (!revision || status === undefined || status.length > 0) {