@lorion-org/nuxt 1.0.0-beta.0 → 1.0.0-beta.2

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.
@@ -0,0 +1,496 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import process from 'node:process';
4
+ import {
5
+ createDescriptorCatalog,
6
+ parseDescriptorIds,
7
+ resolveDescriptorSelectionSeed,
8
+ type DescriptorCatalog,
9
+ type Descriptor,
10
+ type RelationDescriptor,
11
+ } from '@lorion-org/composition-graph';
12
+ import { discoverDescriptors } from '@lorion-org/descriptor-discovery';
13
+ import {
14
+ collectProviderDefaults,
15
+ collectProviderPreferences,
16
+ collectSelectedProviderPreferences,
17
+ resolveItemProviderSelection,
18
+ resolveSelectedProviderRelationPreferences,
19
+ type ProviderPreferenceMap,
20
+ } from '@lorion-org/provider-selection';
21
+ import type { RuntimeConfigValidationPolicy } from '@lorion-org/runtime-config';
22
+ import type {
23
+ NuxtBaseExtensionSelectionInput,
24
+ NuxtExtensionSelectionRuntimeConfig,
25
+ NuxtExtensionModuleOptions,
26
+ NuxtExtensionSelectionSeedOptions,
27
+ NuxtProviderSelectionModuleOptions,
28
+ NuxtProviderSelectionRuntimeConfig,
29
+ NuxtRuntimeConfig,
30
+ } from './types';
31
+ import { nuxtExtensionDescriptorSchema } from './descriptor-schema';
32
+
33
+ export type {
34
+ LorionNuxtModuleOptions,
35
+ NuxtExtensionModuleOptions,
36
+ NuxtExtensionSelectionSeedOptions,
37
+ RuntimeConfigNuxtModuleOptions,
38
+ } from './types';
39
+
40
+ export type NuxtExtensionDescriptor = Descriptor & {
41
+ defaultFor?: string | string[];
42
+ providerPreferences?: ProviderPreferenceMap;
43
+ publicRuntimeConfig?: NuxtRuntimeConfig['public'];
44
+ runtimeConfig?: RuntimeConfigValidationPolicy;
45
+ };
46
+
47
+ export type NuxtExtensionEntry = {
48
+ appDir?: string;
49
+ configFile?: string;
50
+ cwd: string;
51
+ descriptor: NuxtExtensionDescriptor;
52
+ modulesDir?: string;
53
+ publicDir?: string;
54
+ serverDir?: string;
55
+ sharedDir?: string;
56
+ };
57
+
58
+ export type NuxtExtensionBootstrap = {
59
+ activeExtensions: NuxtExtensionEntry[];
60
+ baseExtensionIds: string[];
61
+ catalog: DescriptorCatalog;
62
+ discoveredExtensions: NuxtExtensionEntry[];
63
+ publicRuntimeConfig: NuxtRuntimeConfig;
64
+ resolvedExtensionIds: string[];
65
+ resolvedExtensions: NuxtExtensionEntry[];
66
+ selectedExtensions: string[];
67
+ };
68
+
69
+ type NuxtProviderSelectionOptions = Omit<NuxtProviderSelectionModuleOptions, 'enabled'>;
70
+
71
+ type ResolvedNuxtExtensionOptions = {
72
+ descriptorSchema: false | object;
73
+ descriptorPaths: string[];
74
+ };
75
+
76
+ const defaultExtensionOptions = {
77
+ defaultSelection: 'default',
78
+ publicRuntimeConfigKey: 'extensionSelection',
79
+ descriptorPaths: ['extensions/*/extension.json'],
80
+ } as const;
81
+ const defaultExtensionSelectionSeedKey = 'capability';
82
+ const defaultExtensionResolutionRelations = [
83
+ 'dependencies',
84
+ 'defaultProviders',
85
+ 'providerPreferences',
86
+ ];
87
+ const defaultNuxtRelationDescriptors: RelationDescriptor[] = [
88
+ {
89
+ direction: 'incoming',
90
+ field: 'defaultFor',
91
+ id: 'defaultProviders',
92
+ },
93
+ {
94
+ id: 'providerPreferences',
95
+ field: 'providerPreferences',
96
+ targetMode: 'values',
97
+ },
98
+ ];
99
+
100
+ function isRecord(value: unknown): value is Record<string, unknown> {
101
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
102
+ }
103
+
104
+ function normalizeSelection(value: string | string[] | undefined): string[] {
105
+ return parseDescriptorIds(value);
106
+ }
107
+
108
+ function resolveNuxtExtensionSelectionSeed(
109
+ seedOptions: false | NuxtExtensionSelectionSeedOptions | undefined,
110
+ ): string[] {
111
+ if (seedOptions === false) return [];
112
+
113
+ return resolveDescriptorSelectionSeed({
114
+ argv: seedOptions?.argv ?? process.argv,
115
+ env: seedOptions?.env ?? process.env,
116
+ key: seedOptions?.key ?? defaultExtensionSelectionSeedKey,
117
+ ...(seedOptions?.cliKeys ? { cliKeys: seedOptions.cliKeys } : {}),
118
+ ...(seedOptions?.envKeys ? { envKeys: seedOptions.envKeys } : {}),
119
+ });
120
+ }
121
+
122
+ function resolveExtensionOptions(
123
+ options: NuxtExtensionModuleOptions,
124
+ ): ResolvedNuxtExtensionOptions {
125
+ return {
126
+ descriptorSchema: options.descriptorSchema ?? nuxtExtensionDescriptorSchema,
127
+ descriptorPaths: options.descriptorPaths ?? [...defaultExtensionOptions.descriptorPaths],
128
+ };
129
+ }
130
+
131
+ export function resolveExtensionSelection(
132
+ input: {
133
+ defaultSelection?: string | string[];
134
+ selected?: string | string[];
135
+ } = {},
136
+ ): string[] {
137
+ const candidates = [
138
+ normalizeSelection(input.selected),
139
+ normalizeSelection(input.defaultSelection ?? defaultExtensionOptions.defaultSelection),
140
+ ];
141
+
142
+ return candidates.find((candidate) => candidate.length > 0) ?? [];
143
+ }
144
+
145
+ function resolveBaseExtensionSelection(
146
+ input: NuxtBaseExtensionSelectionInput & {
147
+ options: NuxtExtensionModuleOptions;
148
+ },
149
+ ): string[] {
150
+ const baseExtensions = input.options.baseExtensions;
151
+
152
+ return typeof baseExtensions === 'function'
153
+ ? normalizeSelection(baseExtensions(input))
154
+ : normalizeSelection(baseExtensions);
155
+ }
156
+
157
+ function optionalDir(path: string): string | undefined {
158
+ return existsSync(path) ? path : undefined;
159
+ }
160
+
161
+ function optionalFile(path: string): string | undefined {
162
+ return existsSync(path) ? path : undefined;
163
+ }
164
+
165
+ function findNuxtConfigFile(cwd: string): string | undefined {
166
+ return ['nuxt.config.ts', 'nuxt.config.mts', 'nuxt.config.js', 'nuxt.config.mjs']
167
+ .map((fileName) => optionalFile(join(cwd, fileName)))
168
+ .find(Boolean);
169
+ }
170
+
171
+ function createExtensionEntry(input: {
172
+ cwd: string;
173
+ descriptor: NuxtExtensionDescriptor;
174
+ }): NuxtExtensionEntry {
175
+ const descriptor: NuxtExtensionDescriptor = {
176
+ ...input.descriptor,
177
+ location: input.descriptor.location ?? input.cwd,
178
+ };
179
+ const appDir = optionalDir(join(input.cwd, 'app'));
180
+ const modulesDir = optionalDir(join(input.cwd, 'modules'));
181
+ const publicDir = optionalDir(join(input.cwd, 'public'));
182
+ const serverDir = optionalDir(join(input.cwd, 'server'));
183
+ const sharedDir = optionalDir(join(input.cwd, 'shared'));
184
+ const configFile = findNuxtConfigFile(input.cwd);
185
+ const entry: NuxtExtensionEntry = {
186
+ cwd: input.cwd,
187
+ descriptor,
188
+ };
189
+
190
+ if (appDir) entry.appDir = appDir;
191
+ if (configFile) entry.configFile = configFile;
192
+ if (modulesDir) entry.modulesDir = modulesDir;
193
+ if (publicDir) entry.publicDir = publicDir;
194
+ if (serverDir) entry.serverDir = serverDir;
195
+ if (sharedDir) entry.sharedDir = sharedDir;
196
+
197
+ return entry;
198
+ }
199
+
200
+ function canRegisterExtensionLayer(entry: NuxtExtensionEntry): boolean {
201
+ return Boolean(
202
+ entry.appDir ||
203
+ entry.configFile ||
204
+ entry.modulesDir ||
205
+ entry.publicDir ||
206
+ entry.serverDir ||
207
+ entry.sharedDir,
208
+ );
209
+ }
210
+
211
+ function canExtendExtensionLayer(entry: NuxtExtensionEntry): boolean {
212
+ return Boolean(entry.configFile);
213
+ }
214
+
215
+ export function discoverNuxtExtensionEntries(input: {
216
+ projectRootDir: string;
217
+ options: NuxtExtensionModuleOptions;
218
+ }): NuxtExtensionEntry[] {
219
+ const resolvedOptions = resolveExtensionOptions(input.options);
220
+
221
+ return discoverDescriptors({
222
+ cwd: input.projectRootDir,
223
+ descriptorPaths: resolvedOptions.descriptorPaths,
224
+ nestedField: 'bundles',
225
+ ...(resolvedOptions.descriptorSchema === false
226
+ ? {}
227
+ : {
228
+ validation: {
229
+ schema: resolvedOptions.descriptorSchema,
230
+ },
231
+ }),
232
+ }).map((entry) =>
233
+ createExtensionEntry({
234
+ cwd: entry.cwd,
235
+ descriptor: entry.descriptor,
236
+ }),
237
+ );
238
+ }
239
+
240
+ export function createNuxtExtensionCatalog(input: {
241
+ entries: NuxtExtensionEntry[];
242
+ relationDescriptors?: RelationDescriptor[];
243
+ }): DescriptorCatalog {
244
+ return createDescriptorCatalog({
245
+ descriptors: input.entries.map((entry) => entry.descriptor),
246
+ relationDescriptors: [...defaultNuxtRelationDescriptors, ...(input.relationDescriptors ?? [])],
247
+ });
248
+ }
249
+
250
+ export function createNuxtExtensionEntryMap(
251
+ entries: NuxtExtensionEntry[],
252
+ ): Map<string, NuxtExtensionEntry> {
253
+ return new Map(entries.map((entry) => [entry.descriptor.id, entry]));
254
+ }
255
+
256
+ function pickEntriesById(
257
+ ids: string[],
258
+ entryById: Map<string, NuxtExtensionEntry>,
259
+ ): NuxtExtensionEntry[] {
260
+ return ids
261
+ .map((id) => entryById.get(id))
262
+ .filter((entry): entry is NuxtExtensionEntry => Boolean(entry));
263
+ }
264
+
265
+ export function createNuxtSelectedProviderPreferences(input: {
266
+ entries: NuxtExtensionEntry[];
267
+ selectedExtensions: string[];
268
+ }): ProviderPreferenceMap {
269
+ return collectSelectedProviderPreferences({
270
+ items: input.entries,
271
+ getCapabilityId: (entry) => entry.descriptor.providesFor,
272
+ getProviderId: (entry) => entry.descriptor.id,
273
+ selectedProviderIds: input.selectedExtensions,
274
+ });
275
+ }
276
+
277
+ function createProviderSelectionAwareEntries(
278
+ entries: NuxtExtensionEntry[],
279
+ selectedProviders: ProviderPreferenceMap,
280
+ ): NuxtExtensionEntry[] {
281
+ if (!Object.keys(selectedProviders).length) return entries;
282
+
283
+ return entries.map((entry) => {
284
+ const descriptor = { ...entry.descriptor };
285
+ const preferences = resolveSelectedProviderRelationPreferences({
286
+ providerId: entry.descriptor.id,
287
+ defaultFor: entry.descriptor.defaultFor,
288
+ providerPreferences: entry.descriptor.providerPreferences,
289
+ selectedProviders,
290
+ });
291
+
292
+ delete descriptor.defaultFor;
293
+ delete descriptor.providerPreferences;
294
+
295
+ return {
296
+ ...entry,
297
+ descriptor: {
298
+ ...descriptor,
299
+ ...preferences,
300
+ },
301
+ };
302
+ });
303
+ }
304
+
305
+ function mergeRuntimeConfigSection(
306
+ target: NuxtRuntimeConfig['public'] = {},
307
+ source: NuxtRuntimeConfig['public'] = {},
308
+ ): NuxtRuntimeConfig['public'] {
309
+ const merged: NuxtRuntimeConfig['public'] = { ...target };
310
+
311
+ for (const [key, value] of Object.entries(source)) {
312
+ const current = merged[key];
313
+
314
+ merged[key] =
315
+ isRecord(current) && isRecord(value) ? mergeRuntimeConfigSection(current, value) : value;
316
+ }
317
+
318
+ return merged;
319
+ }
320
+
321
+ function createExtensionSelectionRuntimeConfig(input: {
322
+ activeExtensions: NuxtExtensionEntry[];
323
+ baseExtensionIds: string[];
324
+ discoveredExtensions: NuxtExtensionEntry[];
325
+ publicRuntimeConfigKey: false | string;
326
+ resolvedExtensionIds: string[];
327
+ selectedExtensions: string[];
328
+ }): NuxtRuntimeConfig {
329
+ const runtimeConfig = input.activeExtensions.reduce<NuxtRuntimeConfig>(
330
+ (current, extension) => ({
331
+ ...current,
332
+ public: mergeRuntimeConfigSection(
333
+ current.public,
334
+ extension.descriptor.publicRuntimeConfig ?? {},
335
+ ),
336
+ }),
337
+ { public: {} },
338
+ );
339
+
340
+ if (input.publicRuntimeConfigKey === false) return runtimeConfig;
341
+
342
+ return {
343
+ ...runtimeConfig,
344
+ public: {
345
+ ...runtimeConfig.public,
346
+ [defaultExtensionOptions.publicRuntimeConfigKey]: {
347
+ discoveredExtensionIds: input.discoveredExtensions
348
+ .map((extension) => extension.descriptor.id)
349
+ .sort((left, right) => left.localeCompare(right)),
350
+ resolvedExtensionIds: input.resolvedExtensionIds,
351
+ selectedExtensionIds: input.selectedExtensions,
352
+ } satisfies NuxtExtensionSelectionRuntimeConfig,
353
+ },
354
+ };
355
+ }
356
+
357
+ export function createNuxtExtensionBootstrap(input: {
358
+ options?: NuxtExtensionModuleOptions;
359
+ rootDir: string;
360
+ }): NuxtExtensionBootstrap {
361
+ const options = input.options ?? {};
362
+ const selectedExtensions = resolveExtensionSelection({
363
+ ...(options.defaultSelection ? { defaultSelection: options.defaultSelection } : {}),
364
+ selected: options.selected ?? resolveNuxtExtensionSelectionSeed(options.selectionSeed),
365
+ });
366
+ const createCatalog = (entries: NuxtExtensionEntry[]): DescriptorCatalog =>
367
+ createNuxtExtensionCatalog({
368
+ entries,
369
+ ...(options.relationDescriptors ? { relationDescriptors: options.relationDescriptors } : {}),
370
+ });
371
+
372
+ if (options.enabled === false) {
373
+ return {
374
+ activeExtensions: [],
375
+ baseExtensionIds: [],
376
+ catalog: createCatalog([]),
377
+ discoveredExtensions: [],
378
+ publicRuntimeConfig: { public: {} },
379
+ resolvedExtensionIds: [],
380
+ resolvedExtensions: [],
381
+ selectedExtensions,
382
+ };
383
+ }
384
+
385
+ const entries = discoverNuxtExtensionEntries({
386
+ projectRootDir: input.rootDir,
387
+ options,
388
+ });
389
+ const baseExtensionIds = resolveBaseExtensionSelection({
390
+ descriptors: entries.map((entry) => entry.descriptor),
391
+ options,
392
+ selectedExtensions,
393
+ });
394
+
395
+ if (!entries.length) {
396
+ return {
397
+ activeExtensions: [],
398
+ baseExtensionIds,
399
+ catalog: createCatalog(entries),
400
+ discoveredExtensions: entries,
401
+ publicRuntimeConfig: { public: {} },
402
+ resolvedExtensionIds: [],
403
+ resolvedExtensions: [],
404
+ selectedExtensions,
405
+ };
406
+ }
407
+
408
+ const selectedProviders = createNuxtSelectedProviderPreferences({
409
+ entries,
410
+ selectedExtensions,
411
+ });
412
+ const resolutionEntries = createProviderSelectionAwareEntries(entries, selectedProviders);
413
+ const catalog = createCatalog(resolutionEntries);
414
+ const selection = catalog.resolveSelection({
415
+ baseDescriptors: baseExtensionIds,
416
+ policy: {
417
+ inspectionRelationIds: defaultExtensionResolutionRelations,
418
+ provenanceRelationIds: defaultExtensionResolutionRelations,
419
+ resolutionRelationIds: defaultExtensionResolutionRelations,
420
+ },
421
+ selected: selectedExtensions,
422
+ });
423
+ const resolvedExtensionIds = selection.getResolved();
424
+ const resolvedExtensions = pickEntriesById(
425
+ resolvedExtensionIds,
426
+ createNuxtExtensionEntryMap(resolutionEntries),
427
+ );
428
+ const activeExtensions = resolvedExtensions.filter(canRegisterExtensionLayer);
429
+
430
+ return {
431
+ activeExtensions,
432
+ baseExtensionIds: selection.getBaseDescriptors(),
433
+ catalog,
434
+ discoveredExtensions: entries,
435
+ publicRuntimeConfig: createExtensionSelectionRuntimeConfig({
436
+ activeExtensions,
437
+ baseExtensionIds: selection.getBaseDescriptors(),
438
+ discoveredExtensions: entries,
439
+ publicRuntimeConfigKey: defaultExtensionOptions.publicRuntimeConfigKey,
440
+ resolvedExtensionIds,
441
+ selectedExtensions,
442
+ }),
443
+ resolvedExtensionIds,
444
+ resolvedExtensions,
445
+ selectedExtensions: selection.getSelected(),
446
+ };
447
+ }
448
+
449
+ export function createNuxtExtensionLayerPaths(bootstrap: NuxtExtensionBootstrap): string[] {
450
+ return bootstrap.activeExtensions
451
+ .filter(canExtendExtensionLayer)
452
+ .map((extension) => extension.cwd);
453
+ }
454
+
455
+ export function createNuxtProviderSelectionRuntimeConfig(
456
+ extensions: NuxtExtensionEntry[],
457
+ options: NuxtProviderSelectionOptions = {},
458
+ ): NuxtRuntimeConfig {
459
+ const publicRuntimeConfigKey = 'providerSelection';
460
+ const descriptorPreferences = collectProviderPreferences({
461
+ items: extensions,
462
+ getProviderPreferences: (extension) => extension.descriptor.providerPreferences,
463
+ });
464
+ const providerDefaults = collectProviderDefaults({
465
+ items: extensions,
466
+ getDefaultFor: (extension) => extension.descriptor.defaultFor,
467
+ getProviderId: (extension) => extension.descriptor.id,
468
+ });
469
+ const configuredProviders = options.configuredProviders ?? {};
470
+ const selectedProviders = options.selectedProviders ?? {};
471
+ const fallbackProviders = {
472
+ ...providerDefaults,
473
+ ...descriptorPreferences,
474
+ ...(options.fallbackProviders ?? {}),
475
+ };
476
+ const resolution = resolveItemProviderSelection({
477
+ items: extensions,
478
+ getCapabilityId: (extension) => extension.descriptor.providesFor,
479
+ getProviderId: (extension) => extension.descriptor.id,
480
+ configuredProviders,
481
+ fallbackProviders,
482
+ selectedProviders,
483
+ });
484
+
485
+ return {
486
+ public: {
487
+ [publicRuntimeConfigKey]: {
488
+ configuredProviders,
489
+ excludedProviderIds: resolution.excludedProviderIds,
490
+ fallbackProviders,
491
+ mismatches: resolution.mismatches,
492
+ selections: Object.fromEntries(resolution.selections),
493
+ } satisfies NuxtProviderSelectionRuntimeConfig,
494
+ },
495
+ };
496
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ export {
2
+ createNuxtExtensionBootstrapLogEvent,
3
+ default,
4
+ formatNuxtExtensionBootstrapLog,
5
+ reportNuxtExtensionBootstrap,
6
+ } from './module';
7
+ export {
8
+ createNuxtExtensionBootstrap,
9
+ createNuxtExtensionCatalog,
10
+ createNuxtExtensionEntryMap,
11
+ createNuxtExtensionLayerPaths,
12
+ createNuxtProviderSelectionRuntimeConfig,
13
+ discoverNuxtExtensionEntries,
14
+ resolveExtensionSelection,
15
+ type NuxtExtensionBootstrap,
16
+ type NuxtExtensionDescriptor,
17
+ type NuxtExtensionEntry,
18
+ type NuxtExtensionModuleOptions,
19
+ type NuxtExtensionSelectionSeedOptions,
20
+ } from './extensions';
21
+ export { nuxtExtensionDescriptorSchema, type JsonSchemaObject } from './descriptor-schema';
22
+ export type {
23
+ LorionNuxtModuleOptions,
24
+ NuxtBaseExtensionSelection,
25
+ NuxtBaseExtensionSelectionInput,
26
+ NuxtExtensionBootstrapLogEvent,
27
+ NuxtExtensionBootstrapReporter,
28
+ NuxtProviderSelectionModuleOptions,
29
+ NuxtProviderSelectionRuntimeConfig,
30
+ RuntimeConfigNuxtModuleOptions,
31
+ } from './types';