@lorion-org/react 1.0.0-beta.2 → 1.0.0-beta.4
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 +304 -15
- package/dist/index.cjs +49 -38
- package/dist/index.d.cts +21 -5
- package/dist/index.d.ts +21 -5
- package/dist/index.js +53 -15
- package/dist/vite.cjs +317 -130
- package/dist/vite.d.cts +64 -6
- package/dist/vite.d.ts +64 -6
- package/dist/vite.js +325 -107
- package/package.json +15 -8
- package/src/index.ts +3 -0
- package/src/relations.ts +8 -37
- package/src/runtime-config.ts +63 -0
- package/src/vite.ts +575 -129
- package/dist/chunk-5FUI4JPB.js +0 -38
package/src/vite.ts
CHANGED
|
@@ -1,40 +1,80 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type DescriptorSelectionSeedInput,
|
|
12
|
-
type RelationDescriptor,
|
|
4
|
+
import { loadEnv } from 'vite';
|
|
5
|
+
import type {
|
|
6
|
+
CompositionPolicy,
|
|
7
|
+
Descriptor,
|
|
8
|
+
DescriptorId,
|
|
9
|
+
DescriptorSelectionSeedInput,
|
|
10
|
+
RelationDescriptor,
|
|
13
11
|
} from '@lorion-org/composition-graph';
|
|
14
12
|
import {
|
|
15
13
|
descriptorSchema,
|
|
16
14
|
discoverDescriptors,
|
|
15
|
+
requirePackageName,
|
|
17
16
|
type DiscoveredDescriptor,
|
|
18
17
|
} from '@lorion-org/descriptor-discovery';
|
|
18
|
+
import { resolveDescriptorSelection, selectDescriptors } from '@lorion-org/descriptor-selection';
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
createRuntimeConfigValidatorRegistry,
|
|
21
|
+
projectRuntimeConfigNamespaces,
|
|
22
|
+
resolveRuntimeConfigValidationMode,
|
|
23
|
+
toRuntimeConfigFragment,
|
|
24
|
+
toSnakeUpperCase,
|
|
25
|
+
type RuntimeConfigFragment,
|
|
26
|
+
type RuntimeConfigFragmentMap,
|
|
27
|
+
type RuntimeConfigSection,
|
|
28
|
+
type RuntimeConfigValidationPolicyInput,
|
|
29
|
+
} from '@lorion-org/runtime-config';
|
|
24
30
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
loadRuntimeConfigSourceTree,
|
|
32
|
+
readJsonFile,
|
|
33
|
+
resolveRuntimeConfigSource as resolveRuntimeConfigVarDirSource,
|
|
34
|
+
type RuntimeConfigPathPatternSource,
|
|
35
|
+
type RuntimeConfigSchemaValidationErrorFormatter,
|
|
36
|
+
} from '@lorion-org/runtime-config-node';
|
|
37
|
+
import { capabilitySpecifier, type ActivationResolver } from '@lorion-org/surface-activation';
|
|
28
38
|
|
|
29
39
|
const virtualModuleId = 'virtual:capabilities';
|
|
30
40
|
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
|
41
|
+
const runtimeConfigModuleId = 'virtual:capability-runtime-config';
|
|
42
|
+
const resolvedRuntimeConfigModuleId = `\0${runtimeConfigModuleId}`;
|
|
43
|
+
const serverRuntimeConfigModuleId = 'virtual:capability-runtime-config/server';
|
|
44
|
+
const resolvedServerRuntimeConfigModuleId = `\0${serverRuntimeConfigModuleId}`;
|
|
45
|
+
const defaultRuntimeConfigFileName = 'capability.runtime.json';
|
|
46
|
+
const defaultRuntimeConfigSchemaFileName = 'capability.schema.json';
|
|
47
|
+
|
|
48
|
+
export type CapabilityActivationEntry = {
|
|
49
|
+
exportName?: string;
|
|
50
|
+
exportSubpath?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Returning a nullish activation marks the capability as graph-only: it takes
|
|
54
|
+
// part in dependency resolution but activates nothing (no import is emitted).
|
|
55
|
+
export type ResolveCapabilityActivation = (input: {
|
|
56
|
+
capabilityDir: string;
|
|
57
|
+
descriptor: Descriptor;
|
|
58
|
+
packageJson: Record<string, unknown>;
|
|
59
|
+
packageName: string;
|
|
60
|
+
}) => CapabilityActivationEntry | null | undefined;
|
|
31
61
|
|
|
32
62
|
export type CapabilityLoaderOptions = {
|
|
63
|
+
// How an active capability's activation is resolved (which module and export to
|
|
64
|
+
// import). Pass EITHER `surface` to reuse a lorion `conventionActivation` resolver
|
|
65
|
+
// for a named surface directly (no per-host adapter), OR the richer `activation`
|
|
66
|
+
// resolver that also sees the descriptor and package.json — not both. A nullish
|
|
67
|
+
// result marks the capability graph-only.
|
|
68
|
+
activation?: ResolveCapabilityActivation;
|
|
69
|
+
// Reuse a `conventionActivation` resolver (from `@lorion-org/surface-activation`)
|
|
70
|
+
// for a named surface, e.g. `{ name: 'web', resolver: conventionActivation({...}) }`.
|
|
71
|
+
surface?: { name: string; resolver: ActivationResolver };
|
|
33
72
|
capabilitiesDir?: string;
|
|
34
73
|
baseDescriptors?: readonly DescriptorId[];
|
|
35
74
|
defaultSelection?: readonly DescriptorId[];
|
|
36
75
|
policy?: Partial<CompositionPolicy>;
|
|
37
76
|
relationDescriptors?: readonly RelationDescriptor[];
|
|
77
|
+
runtimeConfig?: false | ReactRuntimeConfigOptions;
|
|
38
78
|
selected?: readonly DescriptorId[];
|
|
39
79
|
selectionSeed?: false | CapabilitySelectionSeedOptions;
|
|
40
80
|
workspaceRoot?: string;
|
|
@@ -42,6 +82,40 @@ export type CapabilityLoaderOptions = {
|
|
|
42
82
|
|
|
43
83
|
export type CapabilitySelectionSeedOptions = Omit<DescriptorSelectionSeedInput, 'defaultValue'>;
|
|
44
84
|
|
|
85
|
+
export type ReactRuntimeConfigEnvOptions = {
|
|
86
|
+
env?: Record<string, string | undefined>;
|
|
87
|
+
privatePrefix?: string;
|
|
88
|
+
publicPrefix?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type ReactRuntimeConfigValidationOptions = {
|
|
92
|
+
formatError?: RuntimeConfigSchemaValidationErrorFormatter;
|
|
93
|
+
policy?: RuntimeConfigValidationPolicyInput;
|
|
94
|
+
schemaFileName?: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type ReactRuntimeConfigVarDirOptions = {
|
|
98
|
+
defaultValue?: string;
|
|
99
|
+
env?: Record<string, string | undefined>;
|
|
100
|
+
envKey?: string;
|
|
101
|
+
value?: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type ReactRuntimeConfigOptions = {
|
|
105
|
+
configFileName?: string;
|
|
106
|
+
enabled?: boolean;
|
|
107
|
+
env?: false | ReactRuntimeConfigEnvOptions;
|
|
108
|
+
schemaFileName?: string;
|
|
109
|
+
source?: false | RuntimeConfigPathPatternSource;
|
|
110
|
+
validation?: false | ReactRuntimeConfigValidationOptions;
|
|
111
|
+
varDir?: string | ReactRuntimeConfigVarDirOptions;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type ReactRuntimeConfig = {
|
|
115
|
+
private: Record<string, RuntimeConfigSection>;
|
|
116
|
+
public: Record<string, RuntimeConfigSection>;
|
|
117
|
+
};
|
|
118
|
+
|
|
45
119
|
export type CapabilityRouteConfigOptions = CapabilityLoaderOptions & {
|
|
46
120
|
indexRouteFile?: false | string;
|
|
47
121
|
routesDirectory: string;
|
|
@@ -56,10 +130,13 @@ export type LorionReactViteSetup = {
|
|
|
56
130
|
};
|
|
57
131
|
|
|
58
132
|
export type DiscoveredCapability = {
|
|
133
|
+
capabilityDir: string;
|
|
59
134
|
disabled: boolean;
|
|
60
|
-
entryFile
|
|
135
|
+
entryFile?: string;
|
|
136
|
+
// Absent for graph-only capabilities that resolve but do not activate.
|
|
137
|
+
exportName?: string;
|
|
61
138
|
id: string;
|
|
62
|
-
importSpecifier
|
|
139
|
+
importSpecifier?: string;
|
|
63
140
|
manifest: Descriptor;
|
|
64
141
|
packageName: string;
|
|
65
142
|
routesDirectory?: string;
|
|
@@ -84,13 +161,16 @@ export type VirtualRootRoute = {
|
|
|
84
161
|
};
|
|
85
162
|
|
|
86
163
|
export type ViteResolvedConfig = {
|
|
164
|
+
env?: Record<string, string | undefined>;
|
|
165
|
+
envDir?: false | string;
|
|
166
|
+
mode?: string;
|
|
87
167
|
root: string;
|
|
88
168
|
};
|
|
89
169
|
|
|
90
170
|
export type VitePlugin = {
|
|
91
171
|
configResolved: (resolvedConfig: ViteResolvedConfig) => void;
|
|
92
172
|
enforce: 'pre';
|
|
93
|
-
load: (id: string) => string | null;
|
|
173
|
+
load: (id: string, options?: { ssr?: boolean | undefined }) => string | null;
|
|
94
174
|
name: string;
|
|
95
175
|
resolveId: (id: string) => string | undefined;
|
|
96
176
|
};
|
|
@@ -98,6 +178,7 @@ export type VitePlugin = {
|
|
|
98
178
|
export function capabilityLoader(options: CapabilityLoaderOptions = {}): VitePlugin {
|
|
99
179
|
let config: ViteResolvedConfig;
|
|
100
180
|
let capabilities: DiscoveredCapability[] = [];
|
|
181
|
+
let runtimeConfig: ReactRuntimeConfig = { private: {}, public: {} };
|
|
101
182
|
|
|
102
183
|
return {
|
|
103
184
|
name: 'lorion-react-capability-loader',
|
|
@@ -108,13 +189,31 @@ export function capabilityLoader(options: CapabilityLoaderOptions = {}): VitePlu
|
|
|
108
189
|
resolveWorkspaceRoot(config.root, options),
|
|
109
190
|
options,
|
|
110
191
|
);
|
|
192
|
+
runtimeConfig = createReactRuntimeConfig(
|
|
193
|
+
capabilities,
|
|
194
|
+
resolveWorkspaceRoot(config.root, options),
|
|
195
|
+
options.runtimeConfig,
|
|
196
|
+
config,
|
|
197
|
+
);
|
|
111
198
|
},
|
|
112
199
|
resolveId(id) {
|
|
113
200
|
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
|
201
|
+
if (id === runtimeConfigModuleId) return resolvedRuntimeConfigModuleId;
|
|
202
|
+
if (id === serverRuntimeConfigModuleId) return resolvedServerRuntimeConfigModuleId;
|
|
114
203
|
|
|
115
204
|
return capabilities.find((capability) => capability.importSpecifier === id)?.entryFile;
|
|
116
205
|
},
|
|
117
|
-
load(id) {
|
|
206
|
+
load(id, loadOptions) {
|
|
207
|
+
if (id === resolvedRuntimeConfigModuleId) return renderRuntimeConfigModule(runtimeConfig);
|
|
208
|
+
if (id === resolvedServerRuntimeConfigModuleId) {
|
|
209
|
+
if (!loadOptions?.ssr) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'virtual:capability-runtime-config/server may only be imported from SSR/server code.',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return renderServerRuntimeConfigModule(runtimeConfig);
|
|
216
|
+
}
|
|
118
217
|
if (id !== resolvedVirtualModuleId) return null;
|
|
119
218
|
|
|
120
219
|
return renderCapabilityModule(capabilities, resolveSelectionSeed(options));
|
|
@@ -122,6 +221,49 @@ export function capabilityLoader(options: CapabilityLoaderOptions = {}): VitePlu
|
|
|
122
221
|
};
|
|
123
222
|
}
|
|
124
223
|
|
|
224
|
+
export function createReactRuntimeConfig(
|
|
225
|
+
capabilities: readonly DiscoveredCapability[],
|
|
226
|
+
workspaceRoot: string,
|
|
227
|
+
options: false | ReactRuntimeConfigOptions = {},
|
|
228
|
+
viteConfig: Partial<ViteResolvedConfig> = {},
|
|
229
|
+
): ReactRuntimeConfig {
|
|
230
|
+
const normalizedOptions = normalizeRuntimeConfigOptions(options);
|
|
231
|
+
if (!normalizedOptions.enabled) return { private: {}, public: {} };
|
|
232
|
+
|
|
233
|
+
const runtimeEnv = resolveRuntimeConfigEnv(viteConfig, normalizedOptions.env ?? {});
|
|
234
|
+
const sourceFragments = normalizedOptions.source
|
|
235
|
+
? loadRuntimeConfigSourceTree(
|
|
236
|
+
resolveRuntimeConfigPatternSource(workspaceRoot, normalizedOptions, runtimeEnv),
|
|
237
|
+
)
|
|
238
|
+
: new Map<string, RuntimeConfigFragment>();
|
|
239
|
+
const schemaEntries = readRuntimeConfigSchemas(capabilities, normalizedOptions.schemaFileName);
|
|
240
|
+
const envFragments =
|
|
241
|
+
normalizedOptions.env === false
|
|
242
|
+
? new Map<string, RuntimeConfigFragment>()
|
|
243
|
+
: createRuntimeConfigEnvFragments(
|
|
244
|
+
createRuntimeConfigEnvFragmentOptions(
|
|
245
|
+
capabilities,
|
|
246
|
+
schemaEntries.schemas,
|
|
247
|
+
runtimeEnv,
|
|
248
|
+
normalizedOptions.env,
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
const fragments = mergeRuntimeConfigFragmentMaps(sourceFragments, envFragments);
|
|
252
|
+
|
|
253
|
+
validateReactRuntimeConfig(capabilities, fragments, schemaEntries, normalizedOptions.validation);
|
|
254
|
+
|
|
255
|
+
const projected = projectRuntimeConfigNamespaces(fragments, {
|
|
256
|
+
namespaceStrategy: 'nested',
|
|
257
|
+
scopeIds: capabilities.map((capability) => capability.id),
|
|
258
|
+
});
|
|
259
|
+
const { public: publicConfig, ...privateConfig } = projected;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
public: publicConfig as Record<string, RuntimeConfigSection>,
|
|
263
|
+
private: privateConfig as Record<string, RuntimeConfigSection>,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
125
267
|
export function lorionReact(options: LorionReactViteOptions): LorionReactViteSetup {
|
|
126
268
|
return {
|
|
127
269
|
capabilityLoader: capabilityLoader(options),
|
|
@@ -129,9 +271,28 @@ export function lorionReact(options: LorionReactViteOptions): LorionReactViteSet
|
|
|
129
271
|
};
|
|
130
272
|
}
|
|
131
273
|
|
|
274
|
+
// Normalizes the activation option into a ResolveCapabilityActivation. With
|
|
275
|
+
// `surface`, `resolver` is a lorion ActivationResolver (from conventionActivation)
|
|
276
|
+
// resolved for that surface, so a host reuses the shared surface convention
|
|
277
|
+
// directly with no adapter. Without `surface`, it is the richer `activation`
|
|
278
|
+
// resolver. The two are mutually exclusive.
|
|
279
|
+
function toResolveActivation(
|
|
280
|
+
options: Pick<CapabilityLoaderOptions, 'activation' | 'surface'>,
|
|
281
|
+
): ResolveCapabilityActivation | undefined {
|
|
282
|
+
if (options.surface) {
|
|
283
|
+
if (options.activation) {
|
|
284
|
+
throw new Error('capabilityLoader: pass either `surface` or `activation`, not both.');
|
|
285
|
+
}
|
|
286
|
+
const { name, resolver } = options.surface;
|
|
287
|
+
return ({ capabilityDir, descriptor }) =>
|
|
288
|
+
resolver(name, { directory: capabilityDir, id: descriptor.id });
|
|
289
|
+
}
|
|
290
|
+
return options.activation;
|
|
291
|
+
}
|
|
292
|
+
|
|
132
293
|
export function discoverCapabilities(
|
|
133
294
|
workspaceRoot: string,
|
|
134
|
-
options: Pick<CapabilityLoaderOptions, 'capabilitiesDir'> = {},
|
|
295
|
+
options: Pick<CapabilityLoaderOptions, 'activation' | 'capabilitiesDir' | 'surface'> = {},
|
|
135
296
|
): DiscoveredCapability[] {
|
|
136
297
|
const capabilitiesRoot = resolve(workspaceRoot, options.capabilitiesDir ?? 'capabilities');
|
|
137
298
|
|
|
@@ -139,8 +300,9 @@ export function discoverCapabilities(
|
|
|
139
300
|
throw new Error(`Capabilities directory not found: ${capabilitiesRoot}`);
|
|
140
301
|
}
|
|
141
302
|
|
|
303
|
+
const resolveActivation = toResolveActivation(options);
|
|
142
304
|
return discoverCapabilityDescriptors(workspaceRoot, options)
|
|
143
|
-
.map(discoverCapability)
|
|
305
|
+
.map((entry) => discoverCapability(entry, resolveActivation))
|
|
144
306
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
145
307
|
}
|
|
146
308
|
|
|
@@ -163,105 +325,39 @@ function selectCapabilities(
|
|
|
163
325
|
| 'selectionSeed'
|
|
164
326
|
> = {},
|
|
165
327
|
): DiscoveredCapability[] {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const selectedProviders = collectSelectedProviderPreferences({
|
|
174
|
-
items: enabledCapabilities,
|
|
175
|
-
getCapabilityId: (capability) => capability.manifest.providesFor,
|
|
176
|
-
getProviderId: (capability) => capability.id,
|
|
177
|
-
selectedProviderIds: selected,
|
|
178
|
-
});
|
|
179
|
-
const selectionCapabilities = createProviderSelectionAwareCapabilities(
|
|
180
|
-
enabledCapabilities,
|
|
181
|
-
selectedProviders,
|
|
182
|
-
);
|
|
183
|
-
const catalog = createDescriptorCatalog({
|
|
184
|
-
descriptors: selectionCapabilities.map((capability) => capability.manifest),
|
|
185
|
-
relationDescriptors: [
|
|
186
|
-
...defaultCapabilityRelationDescriptors,
|
|
187
|
-
...(options.relationDescriptors ?? []),
|
|
188
|
-
],
|
|
189
|
-
});
|
|
190
|
-
const selection = createCompositionSelection({
|
|
191
|
-
catalog,
|
|
192
|
-
selected: [...selected],
|
|
193
|
-
baseDescriptors: [...(options.baseDescriptors ?? [])],
|
|
194
|
-
policy: createCapabilityCompositionPolicy(options.policy),
|
|
195
|
-
});
|
|
196
|
-
const selectedIds = new Set(selection.getResolved());
|
|
197
|
-
|
|
198
|
-
return selectionCapabilities.filter((capability) => selectedIds.has(capability.id));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function createProviderSelectionAwareCapabilities(
|
|
202
|
-
capabilities: readonly DiscoveredCapability[],
|
|
203
|
-
selectedProviders: ProviderPreferenceMap,
|
|
204
|
-
): DiscoveredCapability[] {
|
|
205
|
-
if (!Object.keys(selectedProviders).length) return [...capabilities];
|
|
206
|
-
|
|
207
|
-
return capabilities.map((capability) => {
|
|
208
|
-
const manifest = { ...capability.manifest };
|
|
209
|
-
const preferences = resolveSelectedProviderRelationPreferences({
|
|
210
|
-
providerId: capability.id,
|
|
211
|
-
defaultFor: manifest.defaultFor,
|
|
212
|
-
providerPreferences: manifest.providerPreferences as ProviderPreferenceMap | undefined,
|
|
213
|
-
selectedProviders,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
delete manifest.defaultFor;
|
|
217
|
-
delete manifest.providerPreferences;
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
...capability,
|
|
221
|
-
manifest: {
|
|
222
|
-
...manifest,
|
|
223
|
-
...preferences,
|
|
224
|
-
},
|
|
225
|
-
};
|
|
328
|
+
return selectDescriptors({
|
|
329
|
+
items: capabilities,
|
|
330
|
+
getDescriptor: (capability) => capability.manifest,
|
|
331
|
+
withDescriptor: (capability, manifest) => ({ ...capability, manifest }),
|
|
332
|
+
seed: options,
|
|
333
|
+
...(options.relationDescriptors ? { relationDescriptors: options.relationDescriptors } : {}),
|
|
334
|
+
...(options.policy ? { policy: options.policy } : {}),
|
|
226
335
|
});
|
|
227
336
|
}
|
|
228
337
|
|
|
229
338
|
function resolveSelectionSeed(
|
|
230
339
|
options: Pick<CapabilityLoaderOptions, 'defaultSelection' | 'selected' | 'selectionSeed'>,
|
|
231
340
|
): DescriptorId[] {
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function resolveCapabilitySelectionSeed(
|
|
236
|
-
options: Pick<CapabilityLoaderOptions, 'defaultSelection' | 'selected' | 'selectionSeed'>,
|
|
237
|
-
): DescriptorId[] {
|
|
238
|
-
if (options.selected?.length) return [...options.selected];
|
|
239
|
-
|
|
240
|
-
if (options.selectionSeed === false) return [...(options.defaultSelection ?? [])];
|
|
241
|
-
|
|
242
|
-
const seedOptions = options.selectionSeed ?? {};
|
|
243
|
-
const selected = resolveDescriptorSelectionSeed({
|
|
244
|
-
argv: seedOptions.argv ?? process.argv,
|
|
245
|
-
env: seedOptions.env ?? process.env,
|
|
246
|
-
key: seedOptions.key ?? 'capability',
|
|
247
|
-
...(seedOptions.cliKeys ? { cliKeys: seedOptions.cliKeys } : {}),
|
|
248
|
-
...(seedOptions.envKeys ? { envKeys: seedOptions.envKeys } : {}),
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
return selected.length ? selected : [...(options.defaultSelection ?? [])];
|
|
341
|
+
return resolveDescriptorSelection(options);
|
|
252
342
|
}
|
|
253
343
|
|
|
254
344
|
export function renderCapabilityModule(
|
|
255
345
|
capabilities: readonly DiscoveredCapability[],
|
|
256
346
|
selected: readonly DescriptorId[] = [],
|
|
257
347
|
): string {
|
|
258
|
-
|
|
348
|
+
// Only capabilities with a resolved activation entry are imported and
|
|
349
|
+
// registered. Graph-only capabilities take part in dependency resolution and
|
|
350
|
+
// still appear in resolvedCapabilityIds, but emit no import.
|
|
351
|
+
const activated = capabilities.filter(
|
|
352
|
+
(capability) => capability.exportName && capability.importSpecifier,
|
|
353
|
+
);
|
|
354
|
+
const imports = activated
|
|
259
355
|
.map(
|
|
260
356
|
(capability) =>
|
|
261
|
-
`import { capability as ${capability.variableName} } from '${capability.importSpecifier}'`,
|
|
357
|
+
`import { ${capability.exportName} as ${capability.variableName} } from '${capability.importSpecifier}'`,
|
|
262
358
|
)
|
|
263
359
|
.join('\n');
|
|
264
|
-
const variables =
|
|
360
|
+
const variables = activated.map((capability) => ` ${capability.variableName},`).join('\n');
|
|
265
361
|
const capabilityIds = capabilities.map((capability) => capability.id);
|
|
266
362
|
|
|
267
363
|
return `${imports}
|
|
@@ -276,6 +372,18 @@ ${variables}
|
|
|
276
372
|
`;
|
|
277
373
|
}
|
|
278
374
|
|
|
375
|
+
export function renderRuntimeConfigModule(runtimeConfig: ReactRuntimeConfig): string {
|
|
376
|
+
return `export const capabilityRuntimeConfig = ${JSON.stringify({ public: runtimeConfig.public })}
|
|
377
|
+
|
|
378
|
+
export const publicCapabilityRuntimeConfig = capabilityRuntimeConfig.public
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function renderServerRuntimeConfigModule(runtimeConfig: ReactRuntimeConfig): string {
|
|
383
|
+
return `export const capabilityServerRuntimeConfig = ${JSON.stringify(runtimeConfig)}
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
|
|
279
387
|
export function createCapabilityRouteConfig(
|
|
280
388
|
options: CapabilityRouteConfigOptions,
|
|
281
389
|
): VirtualRootRoute {
|
|
@@ -330,7 +438,10 @@ function discoverCapabilityDescriptors(
|
|
|
330
438
|
});
|
|
331
439
|
}
|
|
332
440
|
|
|
333
|
-
function discoverCapability(
|
|
441
|
+
function discoverCapability(
|
|
442
|
+
entry: DiscoveredDescriptor,
|
|
443
|
+
resolveActivation?: ResolveCapabilityActivation,
|
|
444
|
+
): DiscoveredCapability {
|
|
334
445
|
const capabilityDir = entry.cwd;
|
|
335
446
|
const packagePath = resolve(capabilityDir, 'package.json');
|
|
336
447
|
|
|
@@ -341,27 +452,350 @@ function discoverCapability(entry: DiscoveredDescriptor): DiscoveredCapability {
|
|
|
341
452
|
}
|
|
342
453
|
|
|
343
454
|
const packageJson = readJson(packagePath);
|
|
344
|
-
const packageName = packageJson
|
|
455
|
+
const packageName = requirePackageName(packageJson, packagePath);
|
|
345
456
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
457
|
+
const activationEntry = resolveActivationEntry(
|
|
458
|
+
capabilityDir,
|
|
459
|
+
packageJson,
|
|
460
|
+
packageName,
|
|
461
|
+
entry.descriptor,
|
|
462
|
+
resolveActivation,
|
|
463
|
+
);
|
|
351
464
|
const routesDirectory = resolveRouteDirectory(capabilityDir);
|
|
352
465
|
|
|
353
466
|
return {
|
|
467
|
+
capabilityDir,
|
|
354
468
|
id: entry.descriptor.id,
|
|
355
469
|
disabled: entry.descriptor.disabled === true,
|
|
356
|
-
entryFile: activationEntry.entryFile,
|
|
357
|
-
importSpecifier: activationEntry.importSpecifier,
|
|
358
470
|
manifest: entry.descriptor,
|
|
359
471
|
packageName,
|
|
472
|
+
...(activationEntry
|
|
473
|
+
? {
|
|
474
|
+
exportName: activationEntry.exportName,
|
|
475
|
+
importSpecifier: activationEntry.importSpecifier,
|
|
476
|
+
...(activationEntry.entryFile ? { entryFile: activationEntry.entryFile } : {}),
|
|
477
|
+
}
|
|
478
|
+
: {}),
|
|
360
479
|
...(routesDirectory ? { routesDirectory } : {}),
|
|
361
480
|
variableName: toVariableName(entry.descriptor.id),
|
|
362
481
|
};
|
|
363
482
|
}
|
|
364
483
|
|
|
484
|
+
type NormalizedReactRuntimeConfigOptions = {
|
|
485
|
+
configFileName: string;
|
|
486
|
+
enabled: boolean;
|
|
487
|
+
env: false | ReactRuntimeConfigEnvOptions;
|
|
488
|
+
schemaFileName: string;
|
|
489
|
+
source?: RuntimeConfigPathPatternSource;
|
|
490
|
+
validation: false | ReactRuntimeConfigValidationOptions;
|
|
491
|
+
varDir?: string | ReactRuntimeConfigVarDirOptions;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
type RuntimeConfigSchemaEntries = {
|
|
495
|
+
paths: Map<string, string>;
|
|
496
|
+
schemas: Record<string, object>;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
function normalizeRuntimeConfigOptions(
|
|
500
|
+
options: false | ReactRuntimeConfigOptions,
|
|
501
|
+
): NormalizedReactRuntimeConfigOptions {
|
|
502
|
+
if (options === false) {
|
|
503
|
+
return {
|
|
504
|
+
configFileName: defaultRuntimeConfigFileName,
|
|
505
|
+
enabled: false,
|
|
506
|
+
env: false,
|
|
507
|
+
schemaFileName: defaultRuntimeConfigSchemaFileName,
|
|
508
|
+
validation: false,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const configFileName = options.configFileName ?? defaultRuntimeConfigFileName;
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
configFileName,
|
|
516
|
+
enabled: options.enabled !== false,
|
|
517
|
+
env: options.env ?? {},
|
|
518
|
+
schemaFileName: options.schemaFileName ?? defaultRuntimeConfigSchemaFileName,
|
|
519
|
+
...(options.source === false
|
|
520
|
+
? {}
|
|
521
|
+
: {
|
|
522
|
+
source: options.source ?? { paths: [] },
|
|
523
|
+
}),
|
|
524
|
+
validation: options.validation === false ? false : (options.validation ?? {}),
|
|
525
|
+
...(options.varDir ? { varDir: options.varDir } : {}),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function resolveRuntimeConfigPatternSource(
|
|
530
|
+
workspaceRoot: string,
|
|
531
|
+
options: Pick<NormalizedReactRuntimeConfigOptions, 'configFileName' | 'source' | 'varDir'>,
|
|
532
|
+
env: Record<string, string | undefined>,
|
|
533
|
+
): RuntimeConfigPathPatternSource {
|
|
534
|
+
if (!options.source?.paths.length) {
|
|
535
|
+
const varDir = resolveRuntimeConfigVarDir(workspaceRoot, options.varDir, env);
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
paths: [join(varDir, 'runtime-config', '*', options.configFileName)],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
paths: options.source.paths.map((entry: string) =>
|
|
544
|
+
isAbsolute(entry) ? entry : resolve(workspaceRoot, entry),
|
|
545
|
+
),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function resolveRuntimeConfigVarDir(
|
|
550
|
+
workspaceRoot: string,
|
|
551
|
+
options: string | ReactRuntimeConfigVarDirOptions | undefined,
|
|
552
|
+
env: Record<string, string | undefined>,
|
|
553
|
+
): string {
|
|
554
|
+
const rawVarDir =
|
|
555
|
+
typeof options === 'string'
|
|
556
|
+
? options
|
|
557
|
+
: resolveRuntimeConfigVarDirSource({
|
|
558
|
+
defaultVarDir: resolve(workspaceRoot, '.data'),
|
|
559
|
+
env: options?.env ?? env,
|
|
560
|
+
envKey: options?.envKey ?? 'RUNTIME_CONFIG_VAR_DIR',
|
|
561
|
+
...(options?.value ? { varDir: options.value } : {}),
|
|
562
|
+
...(options?.defaultValue ? { defaultVarDir: options.defaultValue } : {}),
|
|
563
|
+
}).varDir;
|
|
564
|
+
|
|
565
|
+
return isAbsolute(rawVarDir) ? rawVarDir : resolve(workspaceRoot, rawVarDir);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function resolveRuntimeConfigEnv(
|
|
569
|
+
viteConfig: Partial<ViteResolvedConfig>,
|
|
570
|
+
options: ReactRuntimeConfigEnvOptions | false,
|
|
571
|
+
): Record<string, string | undefined> {
|
|
572
|
+
if (options === false) return {};
|
|
573
|
+
if (options.env) return options.env;
|
|
574
|
+
|
|
575
|
+
const mode = viteConfig.mode ?? process.env.NODE_ENV ?? 'development';
|
|
576
|
+
const envDir =
|
|
577
|
+
typeof viteConfig.envDir === 'string' ? viteConfig.envDir : (viteConfig.root ?? process.cwd());
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
...loadEnv(mode, envDir, ''),
|
|
581
|
+
...process.env,
|
|
582
|
+
...(viteConfig.env ?? {}),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function readRuntimeConfigSchemas(
|
|
587
|
+
capabilities: readonly DiscoveredCapability[],
|
|
588
|
+
schemaFileName: string,
|
|
589
|
+
): RuntimeConfigSchemaEntries {
|
|
590
|
+
const paths = new Map<string, string>();
|
|
591
|
+
const schemas: Record<string, object> = {};
|
|
592
|
+
|
|
593
|
+
for (const capability of capabilities) {
|
|
594
|
+
const schemaPath = resolve(capability.capabilityDir, schemaFileName);
|
|
595
|
+
const schema = readJsonFile<object>(schemaPath, {
|
|
596
|
+
onParseError: (error, filePath) => {
|
|
597
|
+
throw new Error(`RuntimeConfig schema JSON parse error in "${filePath}": ${String(error)}`);
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
if (!schema) continue;
|
|
602
|
+
|
|
603
|
+
paths.set(capability.id, schemaPath);
|
|
604
|
+
schemas[capability.id] = schema;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return { paths, schemas };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function isJsonSchemaObject(value: unknown): value is { properties?: Record<string, unknown> } {
|
|
611
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function getRuntimeConfigSchemaSectionKeys(
|
|
615
|
+
schema: object | undefined,
|
|
616
|
+
visibility: string,
|
|
617
|
+
): string[] {
|
|
618
|
+
if (!isJsonSchemaObject(schema)) return [];
|
|
619
|
+
|
|
620
|
+
const section = schema.properties?.[visibility];
|
|
621
|
+
if (!isJsonSchemaObject(section) || !isJsonSchemaObject(section.properties)) return [];
|
|
622
|
+
|
|
623
|
+
return Object.keys(section.properties).sort();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function createRuntimeConfigEnvKey(input: {
|
|
627
|
+
key: string;
|
|
628
|
+
prefix?: string;
|
|
629
|
+
scopeId: string;
|
|
630
|
+
}): string {
|
|
631
|
+
return toSnakeUpperCase([input.prefix, input.scopeId, input.key].filter(Boolean).join('_'));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function createRuntimeConfigEnvFragments(input: {
|
|
635
|
+
capabilities: readonly DiscoveredCapability[];
|
|
636
|
+
env: Record<string, string | undefined>;
|
|
637
|
+
privatePrefix?: string;
|
|
638
|
+
publicPrefix?: string;
|
|
639
|
+
schemas: Record<string, object>;
|
|
640
|
+
}): RuntimeConfigFragmentMap {
|
|
641
|
+
const fragments: RuntimeConfigFragmentMap = new Map();
|
|
642
|
+
|
|
643
|
+
for (const capability of input.capabilities) {
|
|
644
|
+
const schema = input.schemas[capability.id];
|
|
645
|
+
const publicConfig: RuntimeConfigSection = {};
|
|
646
|
+
const privateConfig: RuntimeConfigSection = {};
|
|
647
|
+
|
|
648
|
+
for (const key of getRuntimeConfigSchemaSectionKeys(schema, 'public')) {
|
|
649
|
+
const envKey = createRuntimeConfigEnvKey({
|
|
650
|
+
key,
|
|
651
|
+
prefix: input.publicPrefix ?? 'VITE',
|
|
652
|
+
scopeId: capability.id,
|
|
653
|
+
});
|
|
654
|
+
const value = input.env[envKey];
|
|
655
|
+
|
|
656
|
+
if (value !== undefined) publicConfig[key] = value;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
for (const key of getRuntimeConfigSchemaSectionKeys(schema, 'private')) {
|
|
660
|
+
const envKey = createRuntimeConfigEnvKey({
|
|
661
|
+
key,
|
|
662
|
+
scopeId: capability.id,
|
|
663
|
+
...(input.privatePrefix !== undefined ? { prefix: input.privatePrefix } : {}),
|
|
664
|
+
});
|
|
665
|
+
const value = input.env[envKey];
|
|
666
|
+
|
|
667
|
+
if (value !== undefined) privateConfig[key] = value;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (Object.keys(publicConfig).length || Object.keys(privateConfig).length) {
|
|
671
|
+
fragments.set(capability.id, {
|
|
672
|
+
...(Object.keys(publicConfig).length ? { public: publicConfig } : {}),
|
|
673
|
+
...(Object.keys(privateConfig).length ? { private: privateConfig } : {}),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return fragments;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function createRuntimeConfigEnvFragmentOptions(
|
|
682
|
+
capabilities: readonly DiscoveredCapability[],
|
|
683
|
+
schemas: Record<string, object>,
|
|
684
|
+
env: Record<string, string | undefined>,
|
|
685
|
+
options: ReactRuntimeConfigEnvOptions,
|
|
686
|
+
): Parameters<typeof createRuntimeConfigEnvFragments>[0] {
|
|
687
|
+
return {
|
|
688
|
+
capabilities,
|
|
689
|
+
env,
|
|
690
|
+
...(options.privatePrefix !== undefined ? { privatePrefix: options.privatePrefix } : {}),
|
|
691
|
+
...(options.publicPrefix !== undefined ? { publicPrefix: options.publicPrefix } : {}),
|
|
692
|
+
schemas,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function mergeRuntimeConfigSections(
|
|
697
|
+
left: RuntimeConfigSection | undefined,
|
|
698
|
+
right: RuntimeConfigSection | undefined,
|
|
699
|
+
): RuntimeConfigSection | undefined {
|
|
700
|
+
const section = {
|
|
701
|
+
...(left ?? {}),
|
|
702
|
+
...(right ?? {}),
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
return Object.keys(section).length ? section : undefined;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function mergeRuntimeConfigFragment(
|
|
709
|
+
left: RuntimeConfigFragment | undefined,
|
|
710
|
+
right: RuntimeConfigFragment | undefined,
|
|
711
|
+
): RuntimeConfigFragment {
|
|
712
|
+
const publicConfig = mergeRuntimeConfigSections(left?.public, right?.public);
|
|
713
|
+
const privateConfig = mergeRuntimeConfigSections(left?.private, right?.private);
|
|
714
|
+
|
|
715
|
+
return toRuntimeConfigFragment({
|
|
716
|
+
...(publicConfig ? { public: publicConfig } : {}),
|
|
717
|
+
...(privateConfig ? { private: privateConfig } : {}),
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function mergeRuntimeConfigFragmentMaps(
|
|
722
|
+
left: RuntimeConfigFragmentMap,
|
|
723
|
+
right: RuntimeConfigFragmentMap,
|
|
724
|
+
): RuntimeConfigFragmentMap {
|
|
725
|
+
const fragments: RuntimeConfigFragmentMap = new Map(left);
|
|
726
|
+
|
|
727
|
+
for (const [scopeId, config] of right.entries()) {
|
|
728
|
+
fragments.set(scopeId, mergeRuntimeConfigFragment(fragments.get(scopeId), config));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return fragments;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function getCapabilityRuntimeConfigPolicy(
|
|
735
|
+
capability: DiscoveredCapability,
|
|
736
|
+
validation: ReactRuntimeConfigValidationOptions,
|
|
737
|
+
): RuntimeConfigValidationPolicyInput {
|
|
738
|
+
return (
|
|
739
|
+
(capability.manifest.runtimeConfig as RuntimeConfigValidationPolicyInput | undefined) ??
|
|
740
|
+
validation.policy ??
|
|
741
|
+
'optional'
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function shouldValidateRuntimeConfigFragment(input: {
|
|
746
|
+
capability: DiscoveredCapability;
|
|
747
|
+
fragment: RuntimeConfigFragment | undefined;
|
|
748
|
+
schema: object | undefined;
|
|
749
|
+
validation: ReactRuntimeConfigValidationOptions;
|
|
750
|
+
}): boolean {
|
|
751
|
+
if (!input.schema) return false;
|
|
752
|
+
|
|
753
|
+
const mode = resolveRuntimeConfigValidationMode(
|
|
754
|
+
getCapabilityRuntimeConfigPolicy(input.capability, input.validation),
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
if (mode === 'none' || mode === 'onUse') return false;
|
|
758
|
+
if (mode === 'startup') return true;
|
|
759
|
+
|
|
760
|
+
return Boolean(input.fragment);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function validateReactRuntimeConfig(
|
|
764
|
+
capabilities: readonly DiscoveredCapability[],
|
|
765
|
+
fragments: RuntimeConfigFragmentMap,
|
|
766
|
+
schemas: RuntimeConfigSchemaEntries,
|
|
767
|
+
validation: false | ReactRuntimeConfigValidationOptions,
|
|
768
|
+
): void {
|
|
769
|
+
if (validation === false) return;
|
|
770
|
+
|
|
771
|
+
const registry = createRuntimeConfigValidatorRegistry(schemas.schemas, {
|
|
772
|
+
...(validation.formatError
|
|
773
|
+
? {
|
|
774
|
+
formatError: (target, validationError) =>
|
|
775
|
+
validation.formatError!(
|
|
776
|
+
{
|
|
777
|
+
configPath: `${target.scopeId} runtime config`,
|
|
778
|
+
schemaPath: schemas.paths.get(target.scopeId) ?? '',
|
|
779
|
+
scopeId: target.scopeId,
|
|
780
|
+
},
|
|
781
|
+
validationError,
|
|
782
|
+
),
|
|
783
|
+
}
|
|
784
|
+
: {}),
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
for (const capability of capabilities) {
|
|
788
|
+
const fragment = fragments.get(capability.id);
|
|
789
|
+
const schema = schemas.schemas[capability.id];
|
|
790
|
+
|
|
791
|
+
if (!shouldValidateRuntimeConfigFragment({ capability, fragment, schema, validation })) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
registry.assert(capability.id, fragment ?? {});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
365
799
|
function resolveRouteDirectory(capabilityDir: string): string | undefined {
|
|
366
800
|
const routesDirectory = resolve(capabilityDir, 'src', 'routes');
|
|
367
801
|
|
|
@@ -371,24 +805,36 @@ function resolveRouteDirectory(capabilityDir: string): string | undefined {
|
|
|
371
805
|
function resolveActivationEntry(
|
|
372
806
|
capabilityDir: string,
|
|
373
807
|
packageJson: Record<string, unknown>,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
808
|
+
packageName: string,
|
|
809
|
+
descriptor: Descriptor,
|
|
810
|
+
resolveActivation?: ResolveCapabilityActivation,
|
|
811
|
+
): { entryFile?: string; exportName: string; importSpecifier: string } | null {
|
|
812
|
+
// Without a custom activation resolver the default convention stays strict:
|
|
813
|
+
// the package must declare a string "./capability" export that LORION
|
|
814
|
+
// self-resolves to a local file.
|
|
815
|
+
if (!resolveActivation) {
|
|
816
|
+
const packageExports = packageJson.exports;
|
|
817
|
+
|
|
818
|
+
if (!isRecord(packageExports) || typeof packageExports['./capability'] !== 'string') {
|
|
819
|
+
throw new Error(`Capability package is missing a "./capability" export: ${capabilityDir}`);
|
|
820
|
+
}
|
|
377
821
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
822
|
+
return {
|
|
823
|
+
entryFile: resolve(capabilityDir, packageExports['./capability']),
|
|
824
|
+
exportName: 'capability',
|
|
825
|
+
importSpecifier: capabilitySpecifier(packageName, './capability'),
|
|
826
|
+
};
|
|
382
827
|
}
|
|
383
828
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
829
|
+
// A custom resolver may target any package export and leaves specifier
|
|
830
|
+
// resolution to the host bundler. A nullish result marks a graph-only
|
|
831
|
+
// capability: resolved for the dependency graph, but not activated.
|
|
832
|
+
const activation = resolveActivation({ capabilityDir, descriptor, packageJson, packageName });
|
|
833
|
+
if (!activation) return null;
|
|
387
834
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
};
|
|
835
|
+
const exportName = activation.exportName ?? 'capability';
|
|
836
|
+
const exportSubpath = activation.exportSubpath ?? './capability';
|
|
837
|
+
return { exportName, importSpecifier: capabilitySpecifier(packageName, exportSubpath) };
|
|
392
838
|
}
|
|
393
839
|
|
|
394
840
|
function hasRouteDirectory(
|