@lorion-org/react 1.0.0-beta.1 → 1.0.0-beta.3
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 +349 -20
- package/dist/index.cjs +72 -10
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +67 -9
- package/dist/vite.cjs +336 -45
- package/dist/vite.d.cts +69 -7
- package/dist/vite.d.ts +69 -7
- package/dist/vite.js +335 -49
- package/package.json +27 -10
- package/src/index.ts +225 -0
- package/src/relations.ts +8 -0
- package/src/runtime-config.ts +63 -0
- package/src/vite.ts +891 -0
package/src/vite.ts
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { loadEnv } from 'vite';
|
|
5
|
+
import type {
|
|
6
|
+
CompositionPolicy,
|
|
7
|
+
Descriptor,
|
|
8
|
+
DescriptorId,
|
|
9
|
+
DescriptorSelectionSeedInput,
|
|
10
|
+
RelationDescriptor,
|
|
11
|
+
} from '@lorion-org/composition-graph';
|
|
12
|
+
import {
|
|
13
|
+
descriptorSchema,
|
|
14
|
+
discoverDescriptors,
|
|
15
|
+
requirePackageName,
|
|
16
|
+
type DiscoveredDescriptor,
|
|
17
|
+
} from '@lorion-org/descriptor-discovery';
|
|
18
|
+
import { resolveDescriptorSelection, selectDescriptors } from '@lorion-org/descriptor-selection';
|
|
19
|
+
import {
|
|
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';
|
|
30
|
+
import {
|
|
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';
|
|
38
|
+
|
|
39
|
+
const virtualModuleId = 'virtual:capabilities';
|
|
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;
|
|
61
|
+
|
|
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 };
|
|
72
|
+
capabilitiesDir?: string;
|
|
73
|
+
baseDescriptors?: readonly DescriptorId[];
|
|
74
|
+
defaultSelection?: readonly DescriptorId[];
|
|
75
|
+
policy?: Partial<CompositionPolicy>;
|
|
76
|
+
relationDescriptors?: readonly RelationDescriptor[];
|
|
77
|
+
runtimeConfig?: false | ReactRuntimeConfigOptions;
|
|
78
|
+
selected?: readonly DescriptorId[];
|
|
79
|
+
selectionSeed?: false | CapabilitySelectionSeedOptions;
|
|
80
|
+
workspaceRoot?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type CapabilitySelectionSeedOptions = Omit<DescriptorSelectionSeedInput, 'defaultValue'>;
|
|
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
|
+
|
|
119
|
+
export type CapabilityRouteConfigOptions = CapabilityLoaderOptions & {
|
|
120
|
+
indexRouteFile?: false | string;
|
|
121
|
+
routesDirectory: string;
|
|
122
|
+
workspaceRoot: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type LorionReactViteOptions = CapabilityRouteConfigOptions;
|
|
126
|
+
|
|
127
|
+
export type LorionReactViteSetup = {
|
|
128
|
+
capabilityLoader: VitePlugin;
|
|
129
|
+
routeConfig: VirtualRootRoute;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type DiscoveredCapability = {
|
|
133
|
+
capabilityDir: string;
|
|
134
|
+
disabled: boolean;
|
|
135
|
+
entryFile?: string;
|
|
136
|
+
// Absent for graph-only capabilities that resolve but do not activate.
|
|
137
|
+
exportName?: string;
|
|
138
|
+
id: string;
|
|
139
|
+
importSpecifier?: string;
|
|
140
|
+
manifest: Descriptor;
|
|
141
|
+
packageName: string;
|
|
142
|
+
routesDirectory?: string;
|
|
143
|
+
variableName: string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type VirtualIndexRoute = {
|
|
147
|
+
file: string;
|
|
148
|
+
type: 'index';
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type VirtualPhysicalRouteSubtree = {
|
|
152
|
+
directory: string;
|
|
153
|
+
pathPrefix: string;
|
|
154
|
+
type: 'physical';
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type VirtualRootRoute = {
|
|
158
|
+
children: Array<VirtualIndexRoute | VirtualPhysicalRouteSubtree>;
|
|
159
|
+
file: string;
|
|
160
|
+
type: 'root';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type ViteResolvedConfig = {
|
|
164
|
+
env?: Record<string, string | undefined>;
|
|
165
|
+
envDir?: false | string;
|
|
166
|
+
mode?: string;
|
|
167
|
+
root: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export type VitePlugin = {
|
|
171
|
+
configResolved: (resolvedConfig: ViteResolvedConfig) => void;
|
|
172
|
+
enforce: 'pre';
|
|
173
|
+
load: (id: string, options?: { ssr?: boolean | undefined }) => string | null;
|
|
174
|
+
name: string;
|
|
175
|
+
resolveId: (id: string) => string | undefined;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export function capabilityLoader(options: CapabilityLoaderOptions = {}): VitePlugin {
|
|
179
|
+
let config: ViteResolvedConfig;
|
|
180
|
+
let capabilities: DiscoveredCapability[] = [];
|
|
181
|
+
let runtimeConfig: ReactRuntimeConfig = { private: {}, public: {} };
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
name: 'lorion-react-capability-loader',
|
|
185
|
+
enforce: 'pre',
|
|
186
|
+
configResolved(resolvedConfig) {
|
|
187
|
+
config = resolvedConfig;
|
|
188
|
+
capabilities = discoverSelectedCapabilities(
|
|
189
|
+
resolveWorkspaceRoot(config.root, options),
|
|
190
|
+
options,
|
|
191
|
+
);
|
|
192
|
+
runtimeConfig = createReactRuntimeConfig(
|
|
193
|
+
capabilities,
|
|
194
|
+
resolveWorkspaceRoot(config.root, options),
|
|
195
|
+
options.runtimeConfig,
|
|
196
|
+
config,
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
resolveId(id) {
|
|
200
|
+
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
|
201
|
+
if (id === runtimeConfigModuleId) return resolvedRuntimeConfigModuleId;
|
|
202
|
+
if (id === serverRuntimeConfigModuleId) return resolvedServerRuntimeConfigModuleId;
|
|
203
|
+
|
|
204
|
+
return capabilities.find((capability) => capability.importSpecifier === id)?.entryFile;
|
|
205
|
+
},
|
|
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
|
+
}
|
|
217
|
+
if (id !== resolvedVirtualModuleId) return null;
|
|
218
|
+
|
|
219
|
+
return renderCapabilityModule(capabilities, resolveSelectionSeed(options));
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
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
|
+
|
|
267
|
+
export function lorionReact(options: LorionReactViteOptions): LorionReactViteSetup {
|
|
268
|
+
return {
|
|
269
|
+
capabilityLoader: capabilityLoader(options),
|
|
270
|
+
routeConfig: createCapabilityRouteConfig(options),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
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
|
+
|
|
293
|
+
export function discoverCapabilities(
|
|
294
|
+
workspaceRoot: string,
|
|
295
|
+
options: Pick<CapabilityLoaderOptions, 'activation' | 'capabilitiesDir' | 'surface'> = {},
|
|
296
|
+
): DiscoveredCapability[] {
|
|
297
|
+
const capabilitiesRoot = resolve(workspaceRoot, options.capabilitiesDir ?? 'capabilities');
|
|
298
|
+
|
|
299
|
+
if (!existsSync(capabilitiesRoot)) {
|
|
300
|
+
throw new Error(`Capabilities directory not found: ${capabilitiesRoot}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const resolveActivation = toResolveActivation(options);
|
|
304
|
+
return discoverCapabilityDescriptors(workspaceRoot, options)
|
|
305
|
+
.map((entry) => discoverCapability(entry, resolveActivation))
|
|
306
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function discoverSelectedCapabilities(
|
|
310
|
+
workspaceRoot: string,
|
|
311
|
+
options: CapabilityLoaderOptions = {},
|
|
312
|
+
): DiscoveredCapability[] {
|
|
313
|
+
return selectCapabilities(discoverCapabilities(workspaceRoot, options), options);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function selectCapabilities(
|
|
317
|
+
capabilities: readonly DiscoveredCapability[],
|
|
318
|
+
options: Pick<
|
|
319
|
+
CapabilityLoaderOptions,
|
|
320
|
+
| 'baseDescriptors'
|
|
321
|
+
| 'defaultSelection'
|
|
322
|
+
| 'policy'
|
|
323
|
+
| 'relationDescriptors'
|
|
324
|
+
| 'selected'
|
|
325
|
+
| 'selectionSeed'
|
|
326
|
+
> = {},
|
|
327
|
+
): DiscoveredCapability[] {
|
|
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 } : {}),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function resolveSelectionSeed(
|
|
339
|
+
options: Pick<CapabilityLoaderOptions, 'defaultSelection' | 'selected' | 'selectionSeed'>,
|
|
340
|
+
): DescriptorId[] {
|
|
341
|
+
return resolveDescriptorSelection(options);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function renderCapabilityModule(
|
|
345
|
+
capabilities: readonly DiscoveredCapability[],
|
|
346
|
+
selected: readonly DescriptorId[] = [],
|
|
347
|
+
): string {
|
|
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
|
|
355
|
+
.map(
|
|
356
|
+
(capability) =>
|
|
357
|
+
`import { ${capability.exportName} as ${capability.variableName} } from '${capability.importSpecifier}'`,
|
|
358
|
+
)
|
|
359
|
+
.join('\n');
|
|
360
|
+
const variables = activated.map((capability) => ` ${capability.variableName},`).join('\n');
|
|
361
|
+
const capabilityIds = capabilities.map((capability) => capability.id);
|
|
362
|
+
|
|
363
|
+
return `${imports}
|
|
364
|
+
|
|
365
|
+
export const selectedCapabilityIds = ${JSON.stringify([...selected])}
|
|
366
|
+
|
|
367
|
+
export const resolvedCapabilityIds = ${JSON.stringify(capabilityIds)}
|
|
368
|
+
|
|
369
|
+
export const capabilityModules = [
|
|
370
|
+
${variables}
|
|
371
|
+
]
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
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
|
+
|
|
387
|
+
export function createCapabilityRouteConfig(
|
|
388
|
+
options: CapabilityRouteConfigOptions,
|
|
389
|
+
): VirtualRootRoute {
|
|
390
|
+
if (!options?.workspaceRoot) {
|
|
391
|
+
throw new Error('createCapabilityRouteConfig requires a workspaceRoot option.');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!options?.routesDirectory) {
|
|
395
|
+
throw new Error('createCapabilityRouteConfig requires a routesDirectory option.');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const routesDirectory = resolve(options.routesDirectory);
|
|
399
|
+
const capabilities = discoverSelectedCapabilities(options.workspaceRoot, options);
|
|
400
|
+
const capabilityRouteSubtrees = capabilities
|
|
401
|
+
.filter(hasRouteDirectory)
|
|
402
|
+
.filter((capability) => capability.disabled !== true)
|
|
403
|
+
.map((capability) => ({
|
|
404
|
+
type: 'physical' as const,
|
|
405
|
+
pathPrefix: '',
|
|
406
|
+
directory: toPosixPath(relative(routesDirectory, capability.routesDirectory)),
|
|
407
|
+
}));
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
type: 'root',
|
|
411
|
+
file: '__root.tsx',
|
|
412
|
+
children: [
|
|
413
|
+
...(options.indexRouteFile === false
|
|
414
|
+
? []
|
|
415
|
+
: [
|
|
416
|
+
{
|
|
417
|
+
type: 'index' as const,
|
|
418
|
+
file: options.indexRouteFile ?? 'index.tsx',
|
|
419
|
+
},
|
|
420
|
+
]),
|
|
421
|
+
...capabilityRouteSubtrees,
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function discoverCapabilityDescriptors(
|
|
427
|
+
workspaceRoot: string,
|
|
428
|
+
options: Pick<CapabilityLoaderOptions, 'capabilitiesDir'>,
|
|
429
|
+
): DiscoveredDescriptor[] {
|
|
430
|
+
const capabilitiesDir = options.capabilitiesDir ?? 'capabilities';
|
|
431
|
+
|
|
432
|
+
return discoverDescriptors({
|
|
433
|
+
cwd: workspaceRoot,
|
|
434
|
+
descriptorPaths: [`${capabilitiesDir}/*/capability.json`],
|
|
435
|
+
validation: {
|
|
436
|
+
schema: descriptorSchema,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function discoverCapability(
|
|
442
|
+
entry: DiscoveredDescriptor,
|
|
443
|
+
resolveActivation?: ResolveCapabilityActivation,
|
|
444
|
+
): DiscoveredCapability {
|
|
445
|
+
const capabilityDir = entry.cwd;
|
|
446
|
+
const packagePath = resolve(capabilityDir, 'package.json');
|
|
447
|
+
|
|
448
|
+
if (!existsSync(packagePath)) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Capability must define both capability.json and package.json: ${capabilityDir}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const packageJson = readJson(packagePath);
|
|
455
|
+
const packageName = requirePackageName(packageJson, packagePath);
|
|
456
|
+
|
|
457
|
+
const activationEntry = resolveActivationEntry(
|
|
458
|
+
capabilityDir,
|
|
459
|
+
packageJson,
|
|
460
|
+
packageName,
|
|
461
|
+
entry.descriptor,
|
|
462
|
+
resolveActivation,
|
|
463
|
+
);
|
|
464
|
+
const routesDirectory = resolveRouteDirectory(capabilityDir);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
capabilityDir,
|
|
468
|
+
id: entry.descriptor.id,
|
|
469
|
+
disabled: entry.descriptor.disabled === true,
|
|
470
|
+
manifest: entry.descriptor,
|
|
471
|
+
packageName,
|
|
472
|
+
...(activationEntry
|
|
473
|
+
? {
|
|
474
|
+
exportName: activationEntry.exportName,
|
|
475
|
+
importSpecifier: activationEntry.importSpecifier,
|
|
476
|
+
...(activationEntry.entryFile ? { entryFile: activationEntry.entryFile } : {}),
|
|
477
|
+
}
|
|
478
|
+
: {}),
|
|
479
|
+
...(routesDirectory ? { routesDirectory } : {}),
|
|
480
|
+
variableName: toVariableName(entry.descriptor.id),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
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
|
+
|
|
799
|
+
function resolveRouteDirectory(capabilityDir: string): string | undefined {
|
|
800
|
+
const routesDirectory = resolve(capabilityDir, 'src', 'routes');
|
|
801
|
+
|
|
802
|
+
return existsSync(routesDirectory) ? routesDirectory : undefined;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function resolveActivationEntry(
|
|
806
|
+
capabilityDir: string,
|
|
807
|
+
packageJson: Record<string, unknown>,
|
|
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
|
+
}
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
entryFile: resolve(capabilityDir, packageExports['./capability']),
|
|
824
|
+
exportName: 'capability',
|
|
825
|
+
importSpecifier: capabilitySpecifier(packageName, './capability'),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
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;
|
|
834
|
+
|
|
835
|
+
const exportName = activation.exportName ?? 'capability';
|
|
836
|
+
const exportSubpath = activation.exportSubpath ?? './capability';
|
|
837
|
+
return { exportName, importSpecifier: capabilitySpecifier(packageName, exportSubpath) };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function hasRouteDirectory(
|
|
841
|
+
capability: DiscoveredCapability,
|
|
842
|
+
): capability is DiscoveredCapability & { routesDirectory: string } {
|
|
843
|
+
return typeof capability.routesDirectory === 'string';
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
847
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function resolveWorkspaceRoot(configRoot: string, options: CapabilityLoaderOptions): string {
|
|
851
|
+
if (options.workspaceRoot) return resolve(options.workspaceRoot);
|
|
852
|
+
|
|
853
|
+
return findWorkspaceRoot(configRoot);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function findWorkspaceRoot(startDir: string): string {
|
|
857
|
+
let current = resolve(startDir);
|
|
858
|
+
|
|
859
|
+
while (true) {
|
|
860
|
+
if (
|
|
861
|
+
existsSync(join(current, 'pnpm-workspace.yaml')) &&
|
|
862
|
+
existsSync(join(current, 'capabilities'))
|
|
863
|
+
) {
|
|
864
|
+
return current;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const parent = dirname(current);
|
|
868
|
+
|
|
869
|
+
if (parent === current) {
|
|
870
|
+
throw new Error(`Could not find React workspace root from: ${startDir}`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
current = parent;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function readJson(path: string): Record<string, unknown> {
|
|
878
|
+
return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function toVariableName(name: string): string {
|
|
882
|
+
return `${name
|
|
883
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
884
|
+
.trim()
|
|
885
|
+
.replace(/(?:^|\s)([a-zA-Z0-9])/g, (_, char: string) => char.toUpperCase())
|
|
886
|
+
.replace(/^([A-Z])/, (char) => char.toLowerCase())}Capability`;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function toPosixPath(path: string): string {
|
|
890
|
+
return path.split(sep).join('/');
|
|
891
|
+
}
|