create-next-hydra 0.2.0 → 0.3.0
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 +34 -3
- package/dist/composition/add.d.ts.map +1 -1
- package/dist/composition/add.js +64 -33
- package/dist/composition/catalog.d.ts +4 -1
- package/dist/composition/catalog.d.ts.map +1 -1
- package/dist/composition/catalog.js +22 -6
- package/dist/composition/format.d.ts.map +1 -1
- package/dist/composition/format.js +3 -1
- package/dist/composition/packages.d.ts +2 -2
- package/dist/composition/packages.d.ts.map +1 -1
- package/dist/composition/packages.js +13 -12
- package/dist/composition/planner.d.ts.map +1 -1
- package/dist/composition/planner.js +88 -42
- package/dist/composition/schema.d.ts +59 -0
- package/dist/composition/schema.d.ts.map +1 -1
- package/dist/composition/schema.js +52 -1
- package/dist/composition/types.d.ts +26 -2
- package/dist/composition/types.d.ts.map +1 -1
- package/dist/composition/typescript-paths.d.ts +6 -0
- package/dist/composition/typescript-paths.d.ts.map +1 -0
- package/dist/composition/typescript-paths.js +157 -0
- package/dist/composition/use.d.ts.map +1 -1
- package/dist/composition/use.js +5 -0
- package/dist/composition/workspace.d.ts.map +1 -1
- package/dist/composition/workspace.js +5 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +8 -1
- package/dist/sanitize.d.ts.map +1 -1
- package/dist/sanitize.js +89 -2
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +5 -0
- package/package.json +11 -4
- package/schema/selection-definition.json +65 -3
package/README.md
CHANGED
|
@@ -57,10 +57,12 @@ Keep source in its normal package or application directory and place a `registry
|
|
|
57
57
|
A Selection Definition declares:
|
|
58
58
|
|
|
59
59
|
- its stable ID and whether it is a Provider, Add-on, or Preset;
|
|
60
|
-
- a Provider Slot when it is a Provider;
|
|
60
|
+
- a Provider Slot and one package `binding` when it is a Provider;
|
|
61
|
+
- an optional `binding.sourcePath` for resolving a maintained Provider directly to workspace source;
|
|
62
|
+
- `providerDependencies` for packages contributed by the selection that consume a Provider Slot;
|
|
61
63
|
- standard ShadCN `registryDependencies` for any other registry items it needs;
|
|
62
64
|
- required and conflicting selection IDs;
|
|
63
|
-
- exact package entries
|
|
65
|
+
- ordinary exact package entries;
|
|
64
66
|
- exact pnpm patch entries and their patch-file assets; and
|
|
65
67
|
- setup instructions through the registry item's standard `docs` field.
|
|
66
68
|
|
|
@@ -77,7 +79,36 @@ pnpm registry:check
|
|
|
77
79
|
|
|
78
80
|
`registry:sync` regenerates only each registry item's `files` list and its final workspace-root targets. Provider-owned metadata stays in the colocated registry file. Next Hydra first walks the intact `registryDependencies` graph to retain metadata and detect target conflicts, then prepares those exact artifacts and asks ShadCN to install them once from the workspace root.
|
|
79
81
|
|
|
80
|
-
Standard ShadCN `dependencies` and `devDependencies` apply to the workspace root. Use `meta.nextHydra.packages` only when
|
|
82
|
+
Standard ShadCN `dependencies` and `devDependencies` apply to the workspace root. Use `meta.nextHydra.packages` only when an ordinary dependency must be added to a specific workspace package. Stable Provider aliases are derived from the slot and cannot be declared in `packages`.
|
|
83
|
+
|
|
84
|
+
A Provider declares its installable package once:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"kind": "provider",
|
|
89
|
+
"slot": "cms",
|
|
90
|
+
"binding": {
|
|
91
|
+
"specifier": "npm:@vendor/cms-provider@^1.0.0"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
An Add-on that contributes a package which imports the selected CMS declares the consumer, not a concrete CMS package:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"kind": "add-on",
|
|
101
|
+
"providerDependencies": [
|
|
102
|
+
{
|
|
103
|
+
"cwd": "packages/vendor-search",
|
|
104
|
+
"section": "dependencies",
|
|
105
|
+
"slot": "cms"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Scaffold and maintainer `use` resolve that dependency to `@repo/cms` using the selected Provider's `binding.specifier`. If the Provider also declares `binding.sourcePath`, they write the exact alias and its `/*` wildcard into every selected consumer's `tsconfig.json` for direct-source development. Without `sourcePath`, pnpm's installed alias is the only resolution path and Next Hydra writes no TypeScript path override. Catalog-governed overrides are removed when they are no longer selected, and `use --check` reports incorrect or stale paths. Customer `add`, which has no retained selection receipt, copies the exact Provider alias already present in `apps/web/package.json` into each contributed consumer.
|
|
81
112
|
|
|
82
113
|
ShadCN reads registry source files as text. For a binary file that must survive byte-for-byte, or a root patch file referenced by `pnpmPatches`, an official or locally included source-registry selection may declare a typed `assets` source and target instead. Separately fetched external selections cannot contribute assets in v1.
|
|
83
114
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/composition/add.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAY,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/composition/add.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAY,MAAM,gBAAgB,CAAC;AA6BnD,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF,KAAK,aAAa,GAAG,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,KACnC,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAEhC,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAihBF,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,UAAe,EACxB,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,IAAI,CAAC,CAiMf"}
|
package/dist/composition/add.js
CHANGED
|
@@ -13,9 +13,12 @@ import { resolveRegistryTarget } from "./paths.js";
|
|
|
13
13
|
import { formatZodError, NEXT_HYDRA_SELECTION_SCHEMA_URL, selectionDefinitionSchema, } from "./schema.js";
|
|
14
14
|
import { PROVIDER_ALIASES } from "./types.js";
|
|
15
15
|
import { applyPackageEntries } from "./workspace.js";
|
|
16
|
-
const PACKAGE_ALIAS_PROTOCOL = /^(?:workspace|npm):/;
|
|
17
16
|
const EXACT_COPY_FILE_TYPES = new Set(["registry:file", "registry:item"]);
|
|
18
17
|
const KNOWN_PROVIDERS = new Map([
|
|
18
|
+
[
|
|
19
|
+
"next-hydra/auth/clerk",
|
|
20
|
+
{ dependency: "@repo/auth", packageName: "@repo/auth-clerk" },
|
|
21
|
+
],
|
|
19
22
|
[
|
|
20
23
|
"next-hydra/auth/workos",
|
|
21
24
|
{ dependency: "@repo/auth", packageName: "@repo/auth-workos" },
|
|
@@ -171,36 +174,25 @@ function usesProviderAlias(actual, packageName) {
|
|
|
171
174
|
actual.startsWith(`workspace:${packageName}@`) ||
|
|
172
175
|
actual.startsWith(`npm:${packageName}@`));
|
|
173
176
|
}
|
|
174
|
-
function aliasedPackageName(specifier) {
|
|
175
|
-
const request = specifier.replace(PACKAGE_ALIAS_PROTOCOL, "");
|
|
176
|
-
const packageEnd = request.startsWith("@")
|
|
177
|
-
? request.indexOf("@", request.indexOf("/") + 1)
|
|
178
|
-
: request.indexOf("@");
|
|
179
|
-
return packageEnd < 1 ? request || undefined : request.slice(0, packageEnd);
|
|
180
|
-
}
|
|
181
177
|
function providerExpectation(selectionId, selectionsById) {
|
|
182
|
-
const known = KNOWN_PROVIDERS.get(selectionId);
|
|
183
|
-
if (known) {
|
|
184
|
-
return known;
|
|
185
|
-
}
|
|
186
178
|
const selection = selectionsById.get(selectionId);
|
|
187
|
-
if (selection?.kind
|
|
188
|
-
return
|
|
179
|
+
if (selection?.kind === "provider" && selection.slot && selection.binding) {
|
|
180
|
+
return {
|
|
181
|
+
dependency: PROVIDER_ALIASES[selection.slot],
|
|
182
|
+
specifier: selection.binding.specifier,
|
|
183
|
+
};
|
|
189
184
|
}
|
|
190
|
-
|
|
191
|
-
const requirement = selection.packages.find((candidate) => candidate.cwd === "apps/web" &&
|
|
192
|
-
candidate.section === "dependencies" &&
|
|
193
|
-
candidate.name === dependency);
|
|
194
|
-
const packageName = requirement
|
|
195
|
-
? aliasedPackageName(requirement.specifier)
|
|
196
|
-
: undefined;
|
|
197
|
-
return packageName ? { dependency, packageName } : undefined;
|
|
185
|
+
return KNOWN_PROVIDERS.get(selectionId);
|
|
198
186
|
}
|
|
199
|
-
|
|
187
|
+
function matchesProviderExpectation(actual, expectation) {
|
|
188
|
+
return "specifier" in expectation
|
|
189
|
+
? actual === expectation.specifier
|
|
190
|
+
: usesProviderAlias(actual, expectation.packageName);
|
|
191
|
+
}
|
|
192
|
+
function validateKnownProviderCompatibility(manifest, selections) {
|
|
200
193
|
if (selections.length === 0) {
|
|
201
194
|
return [];
|
|
202
195
|
}
|
|
203
|
-
const manifest = await readPackageJson(path.join(workspaceRoot, "apps/web/package.json"), "apps/web/package.json");
|
|
204
196
|
const issues = [];
|
|
205
197
|
const assumptions = [];
|
|
206
198
|
const selectionsById = new Map(selections.map((item) => [item.id, item]));
|
|
@@ -210,11 +202,11 @@ async function validateKnownProviderCompatibility(workspaceRoot, selections) {
|
|
|
210
202
|
}
|
|
211
203
|
const provider = providerExpectation(selection.id, selectionsById);
|
|
212
204
|
if (!provider) {
|
|
213
|
-
issues.push(`${selection.id} is a Provider in the requested graph but does not declare
|
|
205
|
+
issues.push(`${selection.id} is a Provider in the requested graph but does not declare a usable Provider binding`);
|
|
214
206
|
continue;
|
|
215
207
|
}
|
|
216
208
|
const actual = manifest.dependencies?.[provider.dependency];
|
|
217
|
-
if (!
|
|
209
|
+
if (!matchesProviderExpectation(actual, provider)) {
|
|
218
210
|
issues.push(`${selection.id} is a Provider in the requested graph, but the current provider alias ${provider.dependency} is ${actual ?? "missing"}`);
|
|
219
211
|
}
|
|
220
212
|
}
|
|
@@ -223,7 +215,7 @@ async function validateKnownProviderCompatibility(workspaceRoot, selections) {
|
|
|
223
215
|
const provider = providerExpectation(required, selectionsById);
|
|
224
216
|
const requiredSelection = selectionsById.get(required);
|
|
225
217
|
if (provider &&
|
|
226
|
-
!
|
|
218
|
+
!matchesProviderExpectation(manifest.dependencies?.[provider.dependency], provider)) {
|
|
227
219
|
issues.push(`${selection.id} requires ${required}`);
|
|
228
220
|
}
|
|
229
221
|
else if (!(provider || requiredSelection?.kind === "add-on")) {
|
|
@@ -234,7 +226,7 @@ async function validateKnownProviderCompatibility(workspaceRoot, selections) {
|
|
|
234
226
|
const provider = providerExpectation(conflict, selectionsById);
|
|
235
227
|
const conflictingSelection = selectionsById.get(conflict);
|
|
236
228
|
if ((provider &&
|
|
237
|
-
|
|
229
|
+
matchesProviderExpectation(manifest.dependencies?.[provider.dependency], provider)) ||
|
|
238
230
|
conflictingSelection?.kind === "add-on") {
|
|
239
231
|
issues.push(`${selection.id} conflicts with ${conflict}`);
|
|
240
232
|
}
|
|
@@ -248,6 +240,30 @@ async function validateKnownProviderCompatibility(workspaceRoot, selections) {
|
|
|
248
240
|
}
|
|
249
241
|
return [...new Set(assumptions)].sort((left, right) => left.localeCompare(right));
|
|
250
242
|
}
|
|
243
|
+
function resolveCustomerProviderRequirements(manifest, selections) {
|
|
244
|
+
const requirements = [];
|
|
245
|
+
const issues = [];
|
|
246
|
+
for (const selection of selections) {
|
|
247
|
+
for (const dependency of selection.providerDependencies) {
|
|
248
|
+
const name = PROVIDER_ALIASES[dependency.slot];
|
|
249
|
+
const specifier = manifest.dependencies?.[name];
|
|
250
|
+
if (!specifier) {
|
|
251
|
+
issues.push(`${selection.id} needs the ${dependency.slot} Provider at ${dependency.cwd}, but apps/web/package.json does not declare ${name}`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
requirements.push({
|
|
255
|
+
cwd: dependency.cwd,
|
|
256
|
+
name,
|
|
257
|
+
section: dependency.section,
|
|
258
|
+
specifier,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (issues.length > 0) {
|
|
263
|
+
throw new CompositionValidationError("The Add-on's Provider dependencies could not be resolved from the customer workspace.", issues);
|
|
264
|
+
}
|
|
265
|
+
return requirements;
|
|
266
|
+
}
|
|
251
267
|
function validateRegistryTargetClaims(items) {
|
|
252
268
|
const claims = new Map();
|
|
253
269
|
const issues = [];
|
|
@@ -360,9 +376,7 @@ export async function addRegistryItem(reference, options = {}, dependencies = {}
|
|
|
360
376
|
references: [resolvedReference],
|
|
361
377
|
});
|
|
362
378
|
const primaryName = graph.itemByReference.get(resolvedReference);
|
|
363
|
-
const primary = primaryName
|
|
364
|
-
? graph.items.get(primaryName)
|
|
365
|
-
: undefined;
|
|
379
|
+
const primary = primaryName ? graph.items.get(primaryName) : undefined;
|
|
366
380
|
if (!primary) {
|
|
367
381
|
throw new Error(`The registry returned no item for ${reference}.`);
|
|
368
382
|
}
|
|
@@ -390,8 +404,25 @@ export async function addRegistryItem(reference, options = {}, dependencies = {}
|
|
|
390
404
|
if (patchSelections.length > 0) {
|
|
391
405
|
throw new CompositionValidationError("Customer Add-ons cannot change pnpm patches in v1.", patchSelections.map((candidate) => `${candidate.id} must avoid pnpmPatches or be selected during a new scaffold`));
|
|
392
406
|
}
|
|
393
|
-
const compatibilityAssumptions = await validateKnownProviderCompatibility(cwd, graphSelections);
|
|
394
407
|
validateRegistryTargetClaims(graph.items.values());
|
|
408
|
+
const providerSelectionIds = new Set(graphSelections
|
|
409
|
+
.filter((candidate) => candidate.kind === "provider")
|
|
410
|
+
.map((candidate) => candidate.id));
|
|
411
|
+
const needsProviderManifest = graphSelections.some((candidate) => candidate.kind === "provider" ||
|
|
412
|
+
candidate.providerDependencies.length > 0 ||
|
|
413
|
+
[
|
|
414
|
+
...candidate.compatibility.requires,
|
|
415
|
+
...candidate.compatibility.conflicts,
|
|
416
|
+
].some((selectionId) => KNOWN_PROVIDERS.has(selectionId) ||
|
|
417
|
+
providerSelectionIds.has(selectionId)));
|
|
418
|
+
const providerManifest = needsProviderManifest
|
|
419
|
+
? await readPackageJson(path.join(cwd, "apps/web/package.json"), "apps/web/package.json")
|
|
420
|
+
: {};
|
|
421
|
+
const compatibilityAssumptions = validateKnownProviderCompatibility(providerManifest, graphSelections);
|
|
422
|
+
const packageRequirements = mergePackageRequirements([
|
|
423
|
+
...graphSelections.flatMap((candidate) => candidate.packages),
|
|
424
|
+
...resolveCustomerProviderRequirements(providerManifest, graphSelections),
|
|
425
|
+
]);
|
|
395
426
|
await withPreparedRegistryArtifacts({
|
|
396
427
|
artifacts: [...graph.items.values()],
|
|
397
428
|
entryItems: [primary.name],
|
|
@@ -402,7 +433,7 @@ export async function addRegistryItem(reference, options = {}, dependencies = {}
|
|
|
402
433
|
throw new Error(`The registry dependency graph for ${reference} is empty.`);
|
|
403
434
|
}
|
|
404
435
|
const fileChanges = await inspectFiles(cwd, tree);
|
|
405
|
-
const packageChanges = await inspectPackages(cwd,
|
|
436
|
+
const packageChanges = await inspectPackages(cwd, packageRequirements, tree);
|
|
406
437
|
const rootDependencyChanges = await inspectRootDependencies(cwd, tree);
|
|
407
438
|
const disclosedPackageChanges = [
|
|
408
439
|
...packageChanges,
|
|
@@ -17,7 +17,10 @@ export declare function fetchRegistryItemGraph(options: {
|
|
|
17
17
|
repository?: string;
|
|
18
18
|
}): Promise<RegistryItemGraph>;
|
|
19
19
|
export declare function loadSourceRegistryCatalog(cwd: string, registryFile?: string): Promise<SourceRegistryCatalog>;
|
|
20
|
-
export declare function loadGitHubSourceRegistryCatalog(repository: string, ref?: string
|
|
20
|
+
export declare function loadGitHubSourceRegistryCatalog(repository: string, ref?: string, dependencies?: {
|
|
21
|
+
fetchItems?: (addresses: string[]) => RegistryItem[] | Promise<RegistryItem[]>;
|
|
22
|
+
loadRegistryConfig?: (cwd: string) => RegistriesConfig | Promise<RegistriesConfig>;
|
|
23
|
+
}): Promise<SourceRegistryCatalog>;
|
|
21
24
|
export declare function addCatalogReferences(catalog: SourceRegistryCatalog, references: Iterable<string>, cwd?: string): Promise<SourceRegistryCatalog>;
|
|
22
25
|
export declare function resolveCatalogSelection(catalog: SourceRegistryCatalog, reference: string): CatalogSelection;
|
|
23
26
|
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/composition/catalog.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,gBAAgB,EAEjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAclD,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/composition/catalog.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,gBAAgB,EAEjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAclD,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AA8KpB,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,MAAM,EAAE,GACnB,MAAM,EAAE,CA+BV;AAaD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAClC,CAAC;AAEF,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,MAAM,EAAE,gBAAgB,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACrC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC/B,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0F7B;AAED,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,MAAM,EACX,YAAY,SAAkB,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CAqChC;AAED,wBAAsB,+BAA+B,CACnD,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,MAAM,EACZ,YAAY,GAAE;IACZ,UAAU,CAAC,EAAE,CACX,SAAS,EAAE,MAAM,EAAE,KAChB,YAAY,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC9C,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,MAAM,KACR,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC9C,GACL,OAAO,CAAC,qBAAqB,CAAC,CA4BhC;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC5B,GAAG,SAA+B,GACjC,OAAO,CAAC,qBAAqB,CAAC,CAiDhC;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,MAAM,GAChB,gBAAgB,CASlB"}
|
|
@@ -7,7 +7,9 @@ import { CompositionValidationError } from "./errors.js";
|
|
|
7
7
|
import { isManagedApplicationSource, resolveRegistryTarget, resolveWorkspacePath, } from "./paths.js";
|
|
8
8
|
import { formatZodError, NEXT_HYDRA_SELECTION_SCHEMA_URL, selectionDefinitionSchema, } from "./schema.js";
|
|
9
9
|
const GITHUB_HOMEPAGE_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?\/?$/;
|
|
10
|
+
const SHADCN_REGISTRY_ITEM_SCHEMA_URL = "https://ui.shadcn.com/schema/registry-item.json";
|
|
10
11
|
const OFFICIAL_REFERENCES = {
|
|
12
|
+
clerk: "next-hydra/auth/clerk",
|
|
11
13
|
commercetools: "next-hydra/commerce/commercetools",
|
|
12
14
|
contentstack: "next-hydra/cms/contentstack",
|
|
13
15
|
drupal: "next-hydra/cms/drupal",
|
|
@@ -15,6 +17,8 @@ const OFFICIAL_REFERENCES = {
|
|
|
15
17
|
workos: "next-hydra/auth/workos",
|
|
16
18
|
};
|
|
17
19
|
const OFFICIAL_ITEM_NAMES = [
|
|
20
|
+
"auth-clerk",
|
|
21
|
+
"auth-contract",
|
|
18
22
|
"auth-workos",
|
|
19
23
|
"cms-contentstack",
|
|
20
24
|
"cms-drupal",
|
|
@@ -22,6 +26,15 @@ const OFFICIAL_ITEM_NAMES = [
|
|
|
22
26
|
"drupal",
|
|
23
27
|
"next-hydra-standard",
|
|
24
28
|
];
|
|
29
|
+
function restoreFetchedSelectionSchema(item) {
|
|
30
|
+
// ShadCN assigns its registry-item schema to every resolved artifact,
|
|
31
|
+
// including artifacts authored with the Next Hydra Selection schema.
|
|
32
|
+
if (item.meta?.nextHydra !== undefined &&
|
|
33
|
+
item.$schema === SHADCN_REGISTRY_ITEM_SCHEMA_URL) {
|
|
34
|
+
return { ...item, $schema: NEXT_HYDRA_SELECTION_SCHEMA_URL };
|
|
35
|
+
}
|
|
36
|
+
return item;
|
|
37
|
+
}
|
|
25
38
|
function createCatalog(options) {
|
|
26
39
|
const items = new Map(options.registryItems.map((item) => [item.name, item]));
|
|
27
40
|
const itemByReference = new Map(options.itemByReference);
|
|
@@ -162,12 +175,13 @@ export async function fetchRegistryItemGraph(options) {
|
|
|
162
175
|
if (!itemName) {
|
|
163
176
|
// oxlint-disable-next-line no-await-in-loop -- Breadth-first discovery reveals each next reference in order.
|
|
164
177
|
const resolvedReference = await resolveRegistryReference(reference, options.cwd);
|
|
165
|
-
const [
|
|
178
|
+
const [fetchedArtifact] = await fetchItems([resolvedReference], {
|
|
166
179
|
config: options.config,
|
|
167
180
|
});
|
|
168
|
-
if (!
|
|
181
|
+
if (!fetchedArtifact) {
|
|
169
182
|
throw new CompositionValidationError("The registry dependency graph is incomplete.", [`${reference} returned no registry item`]);
|
|
170
183
|
}
|
|
184
|
+
const artifact = restoreFetchedSelectionSchema(fetchedArtifact);
|
|
171
185
|
const fetched = fetchedItems.get(artifact.name);
|
|
172
186
|
const unpinnedReference = reference.split("#", 1)[0] ?? reference;
|
|
173
187
|
const replacesPinnedRepositoryItem = reference.includes("#") &&
|
|
@@ -233,11 +247,12 @@ export async function loadSourceRegistryCatalog(cwd, registryFile = "registry.js
|
|
|
233
247
|
repository,
|
|
234
248
|
});
|
|
235
249
|
}
|
|
236
|
-
export async function loadGitHubSourceRegistryCatalog(repository, ref) {
|
|
250
|
+
export async function loadGitHubSourceRegistryCatalog(repository, ref, dependencies = {}) {
|
|
237
251
|
const suffix = ref ? `#${ref}` : "";
|
|
238
252
|
const addresses = OFFICIAL_ITEM_NAMES.map((item) => `${repository}/${item}${suffix}`);
|
|
239
|
-
const
|
|
240
|
-
const
|
|
253
|
+
const fetchedItems = await (dependencies.fetchItems ?? getRegistryItems)(addresses);
|
|
254
|
+
const registryItems = fetchedItems.map(restoreFetchedSelectionSchema);
|
|
255
|
+
const registryConfig = await (dependencies.loadRegistryConfig ?? getRegistriesConfig)(process.cwd());
|
|
241
256
|
const itemByReference = new Map(registryItems.flatMap((item, index) => {
|
|
242
257
|
const address = addresses[index];
|
|
243
258
|
return address ? [[address, item.name]] : [];
|
|
@@ -282,7 +297,8 @@ export async function addCatalogReferences(catalog, references, cwd = catalog.cw
|
|
|
282
297
|
}
|
|
283
298
|
for (const itemName of graph.fetchedItemNames) {
|
|
284
299
|
const externalSelection = current.byReference.get(itemName);
|
|
285
|
-
if (externalSelection !== undefined &&
|
|
300
|
+
if (externalSelection !== undefined &&
|
|
301
|
+
externalSelection.assets.length > 0) {
|
|
286
302
|
throw new CompositionValidationError("External selections cannot declare separate binary assets in v1.", [
|
|
287
303
|
`${externalSelection.id} must be included by the starter source registry; separately fetched binary assets are not supported`,
|
|
288
304
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/composition/format.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAwCtE,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAET;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/composition/format.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAwCtE,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAET;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,CA6BR;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,CAKR;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CA2BnE"}
|
|
@@ -35,7 +35,7 @@ export function formatCompositionPreview(current, proposed) {
|
|
|
35
35
|
`Add-ons: ${currentAddOns === proposedAddOns ? `${currentAddOns} (unchanged)` : `${currentAddOns} -> ${proposedAddOns}`}`,
|
|
36
36
|
];
|
|
37
37
|
if (hasCompositionChanges(current, proposed)) {
|
|
38
|
-
lines.push("", "Planned actions:", " replace managed application files", " install selected source", " update package aliases", " update pnpm patches", " run pnpm install");
|
|
38
|
+
lines.push("", "Planned actions:", " replace managed application files", " install selected source", " update package aliases", " update TypeScript paths", " update pnpm patches", " run pnpm install");
|
|
39
39
|
}
|
|
40
40
|
return lines.join("\n");
|
|
41
41
|
}
|
|
@@ -54,6 +54,8 @@ export function formatCompositionPlan(plan) {
|
|
|
54
54
|
...plan.registryItems.map((item) => ` ${item}`),
|
|
55
55
|
"Package entries:",
|
|
56
56
|
...plan.packageRequirements.map((requirement) => ` ${requirement.cwd}: ${requirement.name} = ${requirement.specifier}`),
|
|
57
|
+
"TypeScript paths:",
|
|
58
|
+
...plan.typeScriptPathAliases.map((entry) => ` ${entry.cwd}: ${entry.alias} -> ${entry.sourcePath}`),
|
|
57
59
|
"pnpm patches:",
|
|
58
60
|
...plan.pnpmPatches.map((patch) => ` ${patch.dependency} -> ${patch.path}`),
|
|
59
61
|
"Managed application files:",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { DependencySection, PackageRequirement
|
|
1
|
+
import type { DependencySection, PackageRequirement } from "./types.js";
|
|
2
2
|
export type PackageJson = Partial<Record<DependencySection, Record<string, string>>> & Record<string, unknown>;
|
|
3
3
|
export declare function parsePackageJson(source: string, label: string): PackageJson;
|
|
4
4
|
export declare function readPackageJson(filePath: string, label?: string): Promise<PackageJson>;
|
|
5
|
-
export declare function mergePackageRequirements(
|
|
5
|
+
export declare function mergePackageRequirements(entries: Iterable<PackageRequirement>): PackageRequirement[];
|
|
6
6
|
//# sourceMappingURL=packages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"packages.d.ts","sourceRoot":"","sources":["../../src/composition/packages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"packages.d.ts","sourceRoot":"","sources":["../../src/composition/packages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWxE,MAAM,MAAM,WAAW,GAAG,OAAO,CAC/B,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAClD,GACC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAe1B,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAY3E;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAW,GACf,OAAO,CAAC,WAAW,CAAC,CAEtB;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GACpC,kBAAkB,EAAE,CA8BtB"}
|
|
@@ -32,23 +32,24 @@ export function parsePackageJson(source, label) {
|
|
|
32
32
|
export async function readPackageJson(filePath, label = filePath) {
|
|
33
33
|
return parsePackageJson(await readFile(filePath, "utf-8"), label);
|
|
34
34
|
}
|
|
35
|
-
export function mergePackageRequirements(
|
|
35
|
+
export function mergePackageRequirements(entries) {
|
|
36
36
|
const requirements = new Map();
|
|
37
37
|
const issues = [];
|
|
38
|
-
for (const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
requirements.set(key, requirement);
|
|
47
|
-
}
|
|
38
|
+
for (const requirement of entries) {
|
|
39
|
+
const key = `${requirement.cwd}\0${requirement.section}\0${requirement.name}`;
|
|
40
|
+
const existing = requirements.get(key);
|
|
41
|
+
if (existing && existing.specifier !== requirement.specifier) {
|
|
42
|
+
issues.push(`${requirement.cwd}/${requirement.section}.${requirement.name} is requested as both ${existing.specifier} and ${requirement.specifier}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
requirements.set(key, requirement);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
48
|
if (issues.length > 0) {
|
|
51
|
-
throw new CompositionValidationError("Package requirements conflict.",
|
|
49
|
+
throw new CompositionValidationError("Package requirements conflict.",
|
|
50
|
+
// eslint-disable-next-line unicorn/no-array-sort -- The newly-created array is safe to sort in place.
|
|
51
|
+
[...new Set(issues)].sort((left, right) => left.localeCompare(right)));
|
|
52
52
|
}
|
|
53
|
+
// eslint-disable-next-line unicorn/no-array-sort -- The newly-created array is safe to sort in place.
|
|
53
54
|
return [...requirements.values()].sort((left, right) => `${left.cwd}/${left.section}/${left.name}`.localeCompare(`${right.cwd}/${right.section}/${right.name}`));
|
|
54
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/composition/planner.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAEV,eAAe,
|
|
1
|
+
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/composition/planner.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAEV,eAAe,EAMf,qBAAqB,EAGrB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AA2YpB,wBAAgB,eAAe,CAC7B,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,kBAAkB,GAC5B,eAAe,CAmHjB;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,MAAM,GAChB,kBAAkB,CAuBpB"}
|
|
@@ -109,54 +109,92 @@ function validateCompatibility(selections) {
|
|
|
109
109
|
throw new CompositionValidationError("The requested selections are incompatible.", uniqueSorted(issues));
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
112
|
+
const BASELINE_PROVIDER_DEPENDENCIES = [
|
|
113
|
+
{ cwd: "apps/admin", section: "dependencies", slot: "auth" },
|
|
114
|
+
{ cwd: "apps/api", section: "dependencies", slot: "auth" },
|
|
115
|
+
{ cwd: "apps/cli", section: "dependencies", slot: "auth" },
|
|
116
|
+
{ cwd: "apps/web", section: "dependencies", slot: "auth" },
|
|
117
|
+
{ cwd: "packages/feature-flags", section: "dependencies", slot: "auth" },
|
|
118
|
+
{ cwd: "tests/e2e", section: "devDependencies", slot: "auth" },
|
|
119
|
+
{ cwd: "apps/cli", section: "dependencies", slot: "cms" },
|
|
120
|
+
{ cwd: "apps/web", section: "dependencies", slot: "cms" },
|
|
121
|
+
{ cwd: "apps/api", section: "dependencies", slot: "commerce" },
|
|
122
|
+
{ cwd: "apps/cli", section: "dependencies", slot: "commerce" },
|
|
123
|
+
{ cwd: "apps/web", section: "dependencies", slot: "commerce" },
|
|
124
|
+
{ cwd: "tests/e2e", section: "devDependencies", slot: "commerce" },
|
|
125
|
+
];
|
|
126
|
+
function selectedProviderDependencies(selections) {
|
|
127
|
+
return [
|
|
128
|
+
...BASELINE_PROVIDER_DEPENDENCIES,
|
|
129
|
+
...selections.flatMap((selection) => selection.providerDependencies),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
function catalogProviderDependencies(catalog) {
|
|
133
|
+
return [
|
|
134
|
+
...BASELINE_PROVIDER_DEPENDENCIES,
|
|
135
|
+
...catalog.selections.flatMap((selection) => selection.providerDependencies),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
function resolveProviderRequirements(providers, dependencies) {
|
|
139
|
+
const packageRequirements = [];
|
|
140
|
+
const typeScriptPathAliases = new Map();
|
|
141
|
+
for (const dependency of dependencies) {
|
|
142
|
+
const provider = providers.get(dependency.slot);
|
|
143
|
+
if (!provider?.binding) {
|
|
144
|
+
throw new CompositionValidationError("Provider binding is missing.", [
|
|
145
|
+
`${dependency.slot} does not supply a Provider binding`,
|
|
146
|
+
]);
|
|
133
147
|
}
|
|
134
|
-
const alias = PROVIDER_ALIASES[
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
const alias = PROVIDER_ALIASES[dependency.slot];
|
|
149
|
+
packageRequirements.push({
|
|
150
|
+
cwd: dependency.cwd,
|
|
151
|
+
name: alias,
|
|
152
|
+
section: dependency.section,
|
|
153
|
+
specifier: provider.binding.specifier,
|
|
154
|
+
});
|
|
155
|
+
if (provider.binding.sourcePath) {
|
|
156
|
+
typeScriptPathAliases.set(`${dependency.cwd}\0${alias}`, {
|
|
157
|
+
alias,
|
|
158
|
+
cwd: dependency.cwd,
|
|
159
|
+
sourcePath: resolveWorkspacePath(provider.binding.sourcePath, `${provider.id} Provider binding source path`),
|
|
160
|
+
});
|
|
142
161
|
}
|
|
143
162
|
}
|
|
144
|
-
|
|
145
|
-
|
|
163
|
+
// eslint-disable-next-line unicorn/no-array-sort -- The newly-created array is safe to sort in place.
|
|
164
|
+
const aliases = [...typeScriptPathAliases.values()].sort((left, right) => `${left.cwd}/${left.alias}`.localeCompare(`${right.cwd}/${right.alias}`));
|
|
165
|
+
return { packageRequirements, typeScriptPathAliases: aliases };
|
|
166
|
+
}
|
|
167
|
+
function catalogTypeScriptPathAliases(dependencies) {
|
|
168
|
+
const aliases = new Map();
|
|
169
|
+
for (const dependency of dependencies) {
|
|
170
|
+
const alias = PROVIDER_ALIASES[dependency.slot];
|
|
171
|
+
aliases.set(`${dependency.cwd}\0${alias}`, {
|
|
172
|
+
alias,
|
|
173
|
+
cwd: dependency.cwd,
|
|
174
|
+
});
|
|
146
175
|
}
|
|
176
|
+
// eslint-disable-next-line unicorn/no-array-sort -- The newly-created array is safe to sort in place.
|
|
177
|
+
return [...aliases.values()].sort((left, right) => `${left.cwd}/${left.alias}`.localeCompare(`${right.cwd}/${right.alias}`));
|
|
147
178
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
179
|
+
function catalogPackageRequirementTargets(catalog, providerDependencies) {
|
|
180
|
+
const targets = new Map();
|
|
181
|
+
const add = (target) => {
|
|
182
|
+
targets.set(`${target.cwd}\0${target.section}\0${target.name}`, target);
|
|
183
|
+
};
|
|
150
184
|
for (const selection of catalog.selections) {
|
|
151
|
-
for (const
|
|
152
|
-
|
|
153
|
-
if (!requirements.has(key)) {
|
|
154
|
-
requirements.set(key, requirement);
|
|
155
|
-
}
|
|
185
|
+
for (const { cwd, name, section } of selection.packages) {
|
|
186
|
+
add({ cwd, name, section });
|
|
156
187
|
}
|
|
157
188
|
}
|
|
189
|
+
for (const dependency of providerDependencies) {
|
|
190
|
+
add({
|
|
191
|
+
cwd: dependency.cwd,
|
|
192
|
+
name: PROVIDER_ALIASES[dependency.slot],
|
|
193
|
+
section: dependency.section,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
158
196
|
// eslint-disable-next-line unicorn/no-array-sort -- The newly-created array is safe to sort in place.
|
|
159
|
-
return [...
|
|
197
|
+
return [...targets.values()].sort((left, right) => `${left.cwd}/${left.section}/${left.name}`.localeCompare(`${right.cwd}/${right.section}/${right.name}`));
|
|
160
198
|
}
|
|
161
199
|
function resolvePnpmPatches(selections) {
|
|
162
200
|
const patches = new Map();
|
|
@@ -240,8 +278,14 @@ export function planComposition(catalog, selection) {
|
|
|
240
278
|
});
|
|
241
279
|
const addOns = resolveAddOns(catalog, selection.addOns, providerSelections);
|
|
242
280
|
const selections = [...providerSelections, ...addOns];
|
|
243
|
-
validateProviderAliasRequirements(providerSelections);
|
|
244
281
|
validateCompatibility(selections);
|
|
282
|
+
const providerDependencies = selectedProviderDependencies(selections);
|
|
283
|
+
const catalogDependencies = catalogProviderDependencies(catalog);
|
|
284
|
+
const providerRequirements = resolveProviderRequirements(providers, providerDependencies);
|
|
285
|
+
const packageRequirements = mergePackageRequirements([
|
|
286
|
+
...selections.flatMap((selected) => selected.packages),
|
|
287
|
+
...providerRequirements.packageRequirements,
|
|
288
|
+
]);
|
|
245
289
|
const entryItems = uniqueSorted(selections.map((selected) => selected.itemName));
|
|
246
290
|
const registryItems = resolveRegistryItemGraph(catalog, entryItems);
|
|
247
291
|
const assets = selections
|
|
@@ -272,8 +316,9 @@ export function planComposition(catalog, selection) {
|
|
|
272
316
|
return {
|
|
273
317
|
assets,
|
|
274
318
|
catalogManagedTargets: catalog.managedTargets,
|
|
275
|
-
|
|
319
|
+
catalogPackageRequirementTargets: catalogPackageRequirementTargets(catalog, catalogDependencies),
|
|
276
320
|
catalogPnpmPatches: catalogPnpmPatches(catalog),
|
|
321
|
+
catalogTypeScriptPathAliases: catalogTypeScriptPathAliases(catalogDependencies),
|
|
277
322
|
entryItems,
|
|
278
323
|
instructions: [
|
|
279
324
|
...new Set(selections
|
|
@@ -281,7 +326,7 @@ export function planComposition(catalog, selection) {
|
|
|
281
326
|
.filter((value) => Boolean(value))),
|
|
282
327
|
],
|
|
283
328
|
managedTargets,
|
|
284
|
-
packageRequirements
|
|
329
|
+
packageRequirements,
|
|
285
330
|
pnpmPatches,
|
|
286
331
|
registryItems,
|
|
287
332
|
selection: {
|
|
@@ -290,6 +335,7 @@ export function planComposition(catalog, selection) {
|
|
|
290
335
|
providers: { ...selection.providers },
|
|
291
336
|
},
|
|
292
337
|
selections,
|
|
338
|
+
typeScriptPathAliases: providerRequirements.typeScriptPathAliases,
|
|
293
339
|
variableTargets: uniqueSorted([
|
|
294
340
|
...catalogVariableTargets(catalog),
|
|
295
341
|
...catalog.selections.flatMap((selected) => selected.assets.map((asset) => resolveWorkspacePath(asset.target, `${selected.id} asset target`))),
|