create-next-hydra 0.1.0 → 0.2.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/cli.js +0 -0
  4. package/dist/composition/add.d.ts +14 -0
  5. package/dist/composition/add.d.ts.map +1 -0
  6. package/dist/composition/add.js +453 -0
  7. package/dist/composition/catalog.d.ts +23 -0
  8. package/dist/composition/catalog.d.ts.map +1 -0
  9. package/dist/composition/catalog.js +301 -0
  10. package/dist/composition/errors.d.ts +5 -0
  11. package/dist/composition/errors.d.ts.map +1 -0
  12. package/dist/composition/errors.js +8 -0
  13. package/dist/composition/format.d.ts +6 -0
  14. package/dist/composition/format.d.ts.map +1 -0
  15. package/dist/composition/format.js +63 -0
  16. package/dist/composition/install.d.ts +16 -0
  17. package/dist/composition/install.d.ts.map +1 -0
  18. package/dist/composition/install.js +175 -0
  19. package/dist/composition/packages.d.ts +6 -0
  20. package/dist/composition/packages.d.ts.map +1 -0
  21. package/dist/composition/packages.js +54 -0
  22. package/dist/composition/paths.d.ts +4 -0
  23. package/dist/composition/paths.d.ts.map +1 -0
  24. package/dist/composition/paths.js +47 -0
  25. package/dist/composition/planner.d.ts +4 -0
  26. package/dist/composition/planner.d.ts.map +1 -0
  27. package/dist/composition/planner.js +320 -0
  28. package/dist/composition/schema.d.ts +235 -0
  29. package/dist/composition/schema.d.ts.map +1 -0
  30. package/dist/composition/schema.js +115 -0
  31. package/dist/composition/types.d.ts +87 -0
  32. package/dist/composition/types.d.ts.map +1 -0
  33. package/dist/composition/types.js +6 -0
  34. package/dist/composition/use.d.ts +23 -0
  35. package/dist/composition/use.d.ts.map +1 -0
  36. package/dist/composition/use.js +182 -0
  37. package/dist/composition/workspace.d.ts +10 -0
  38. package/dist/composition/workspace.d.ts.map +1 -0
  39. package/dist/composition/workspace.js +208 -0
  40. package/dist/constants.d.ts +4 -1
  41. package/dist/constants.d.ts.map +1 -1
  42. package/dist/constants.js +26 -1
  43. package/dist/fs-utils.js +3 -3
  44. package/dist/git.d.ts.map +1 -1
  45. package/dist/git.js +12 -12
  46. package/dist/index.d.ts +5 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +55 -6
  49. package/dist/instruction-renderer.d.ts +17 -0
  50. package/dist/instruction-renderer.d.ts.map +1 -0
  51. package/dist/instruction-renderer.js +132 -0
  52. package/dist/logger.d.ts +2 -1
  53. package/dist/logger.d.ts.map +1 -1
  54. package/dist/logger.js +7 -3
  55. package/dist/prompts.d.ts +2 -0
  56. package/dist/prompts.d.ts.map +1 -1
  57. package/dist/prompts.js +25 -2
  58. package/dist/sanitize.d.ts +2 -1
  59. package/dist/sanitize.d.ts.map +1 -1
  60. package/dist/sanitize.js +13 -4
  61. package/dist/scaffold.d.ts +5 -1
  62. package/dist/scaffold.d.ts.map +1 -1
  63. package/dist/scaffold.js +247 -71
  64. package/dist/types.d.ts +5 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/version.d.ts +2 -0
  67. package/dist/version.d.ts.map +1 -0
  68. package/dist/version.js +2 -0
  69. package/package.json +35 -23
  70. package/schema/selection-definition.json +237 -0
  71. package/schema/source-registry.json +37 -0
@@ -0,0 +1,115 @@
1
+ import path from "node:path";
2
+ import { z } from "zod";
3
+ import { PROVIDER_SLOTS } from "./types.js";
4
+ export const NEXT_HYDRA_SELECTION_SCHEMA_URL = "https://raw.githubusercontent.com/jakala-na/next-hydra/main/packages/create-next-hydra/schema/selection-definition.json";
5
+ const workspaceRelativePathSchema = z
6
+ .string()
7
+ .min(1)
8
+ .refine((value) => !value.startsWith("/"), "must be workspace-relative")
9
+ .refine((value) => !value.includes("\\"), "must use forward slashes")
10
+ .refine((value) => !value.split("/").includes(".."), "must not escape the workspace");
11
+ const workspaceFilePathSchema = workspaceRelativePathSchema.refine((value) => ![".", "./"].includes(path.posix.normalize(value)), "must name a file or directory below the workspace root");
12
+ const packageRequirementSchema = z
13
+ .object({
14
+ cwd: workspaceRelativePathSchema,
15
+ name: z.string().min(1),
16
+ section: z.enum([
17
+ "dependencies",
18
+ "devDependencies",
19
+ "optionalDependencies",
20
+ ]),
21
+ specifier: z.string().min(1),
22
+ })
23
+ .strict();
24
+ const pnpmPatchSchema = z
25
+ .object({
26
+ dependency: z.string().min(1),
27
+ path: workspaceFilePathSchema,
28
+ })
29
+ .strict();
30
+ const providerSelectionsSchema = z
31
+ .object({
32
+ auth: z.string().min(1).optional(),
33
+ cms: z.string().min(1).optional(),
34
+ commerce: z.string().min(1).optional(),
35
+ })
36
+ .strict();
37
+ const presetSelectionsSchema = z
38
+ .object({
39
+ addOns: z.array(z.string().min(1)).default([]),
40
+ providers: providerSelectionsSchema.optional(),
41
+ })
42
+ .strict();
43
+ export const selectionDefinitionSchema = z
44
+ .object({
45
+ assets: z
46
+ .array(z
47
+ .object({
48
+ source: workspaceFilePathSchema,
49
+ target: workspaceFilePathSchema,
50
+ })
51
+ .strict())
52
+ .default([]),
53
+ compatibility: z
54
+ .object({
55
+ conflicts: z.array(z.string().min(1)).default([]),
56
+ requires: z.array(z.string().min(1)).default([]),
57
+ })
58
+ .strict()
59
+ .default({ conflicts: [], requires: [] }),
60
+ id: z.string().min(1),
61
+ kind: z.enum(["provider", "add-on", "preset"]),
62
+ packages: z.array(packageRequirementSchema).default([]),
63
+ pnpmPatches: z.array(pnpmPatchSchema).default([]),
64
+ selections: presetSelectionsSchema.optional(),
65
+ slot: z.enum(PROVIDER_SLOTS).optional(),
66
+ })
67
+ .strict()
68
+ .superRefine((definition, context) => {
69
+ if (definition.kind === "provider" && !definition.slot) {
70
+ context.addIssue({
71
+ code: z.ZodIssueCode.custom,
72
+ message: "a provider must declare its slot",
73
+ path: ["slot"],
74
+ });
75
+ }
76
+ if (definition.kind !== "provider" && definition.slot) {
77
+ context.addIssue({
78
+ code: z.ZodIssueCode.custom,
79
+ message: `${definition.kind} must not declare a provider slot`,
80
+ path: ["slot"],
81
+ });
82
+ }
83
+ if (definition.kind === "preset" && !definition.selections) {
84
+ context.addIssue({
85
+ code: z.ZodIssueCode.custom,
86
+ message: "a preset must declare selections",
87
+ path: ["selections"],
88
+ });
89
+ }
90
+ if (definition.kind !== "preset" && definition.selections) {
91
+ context.addIssue({
92
+ code: z.ZodIssueCode.custom,
93
+ message: `${definition.kind} must not declare preset selections`,
94
+ path: ["selections"],
95
+ });
96
+ }
97
+ });
98
+ export const workspaceSelectionSchema = z
99
+ .object({
100
+ addOns: z.array(z.string().min(1)).default([]),
101
+ providers: z
102
+ .object({
103
+ auth: z.string().min(1),
104
+ cms: z.string().min(1),
105
+ commerce: z.string().min(1),
106
+ })
107
+ .strict(),
108
+ })
109
+ .strict();
110
+ export function formatZodError(error) {
111
+ return error.issues.map((issue) => {
112
+ const location = issue.path.length > 0 ? issue.path.join(".") : "value";
113
+ return `${location}: ${issue.message}`;
114
+ });
115
+ }
@@ -0,0 +1,87 @@
1
+ import type { getRegistriesConfig } from "shadcn/registry";
2
+ import type { RegistryItem } from "shadcn/schema";
3
+ export type RegistriesConfig = Awaited<ReturnType<typeof getRegistriesConfig>>;
4
+ export declare const PROVIDER_SLOTS: readonly ["auth", "cms", "commerce"];
5
+ export type ProviderSlot = (typeof PROVIDER_SLOTS)[number];
6
+ export declare const PROVIDER_ALIASES: Record<ProviderSlot, string>;
7
+ export type SelectionKind = "provider" | "add-on" | "preset";
8
+ export type DependencySection = "dependencies" | "devDependencies" | "optionalDependencies";
9
+ export type PackageRequirement = {
10
+ cwd: string;
11
+ section: DependencySection;
12
+ name: string;
13
+ specifier: string;
14
+ };
15
+ export type AssetContribution = {
16
+ source: string;
17
+ target: string;
18
+ };
19
+ export type PnpmPatch = {
20
+ dependency: string;
21
+ path: string;
22
+ };
23
+ export type WorkspaceSelection = {
24
+ providers: Record<ProviderSlot, string>;
25
+ addOns: string[];
26
+ };
27
+ export type SelectionDefinition = {
28
+ id: string;
29
+ kind: SelectionKind;
30
+ slot?: ProviderSlot;
31
+ compatibility: {
32
+ requires: string[];
33
+ conflicts: string[];
34
+ };
35
+ packages: PackageRequirement[];
36
+ pnpmPatches: PnpmPatch[];
37
+ assets: AssetContribution[];
38
+ selections?: {
39
+ providers?: Partial<Record<ProviderSlot, string>>;
40
+ addOns: string[];
41
+ };
42
+ };
43
+ export type CatalogSelection = SelectionDefinition & {
44
+ itemName: string;
45
+ };
46
+ export type SourceRegistryCatalog = {
47
+ cwd: string;
48
+ registryFile: string;
49
+ repository?: string;
50
+ authoringPaths: string[];
51
+ items: Map<string, RegistryItem>;
52
+ itemByReference: Map<string, string>;
53
+ registryConfig: RegistriesConfig;
54
+ selections: CatalogSelection[];
55
+ byId: Map<string, CatalogSelection>;
56
+ byReference: Map<string, CatalogSelection>;
57
+ managedTargets: string[];
58
+ };
59
+ export type PreparedComposition = {
60
+ artifacts: RegistryItem[];
61
+ itemByReference: Map<string, string>;
62
+ entryItems: string[];
63
+ registryConfig: RegistriesConfig;
64
+ assets: (AssetContribution & {
65
+ content: Uint8Array;
66
+ })[];
67
+ managedFiles: {
68
+ content: string;
69
+ target: string;
70
+ }[];
71
+ };
72
+ export type CompositionPlan = {
73
+ selection: WorkspaceSelection;
74
+ selections: CatalogSelection[];
75
+ entryItems: string[];
76
+ registryItems: string[];
77
+ packageRequirements: PackageRequirement[];
78
+ catalogPackageRequirements: PackageRequirement[];
79
+ pnpmPatches: PnpmPatch[];
80
+ catalogPnpmPatches: PnpmPatch[];
81
+ assets: AssetContribution[];
82
+ managedTargets: string[];
83
+ catalogManagedTargets: string[];
84
+ variableTargets: string[];
85
+ instructions: string[];
86
+ };
87
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/composition/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export const PROVIDER_SLOTS = ["auth", "cms", "commerce"];
2
+ export const PROVIDER_ALIASES = {
3
+ auth: "@repo/auth",
4
+ cms: "@repo/cms",
5
+ commerce: "@repo/commerce-provider",
6
+ };
@@ -0,0 +1,23 @@
1
+ import { confirm } from "@clack/prompts";
2
+ import { runCommand } from "../git.js";
3
+ export type UseCompositionOptions = {
4
+ cwd?: string;
5
+ auth?: string;
6
+ cms?: string;
7
+ commerce?: string;
8
+ addOns?: string[];
9
+ preset?: string;
10
+ check?: boolean;
11
+ dryRun?: boolean;
12
+ yes?: boolean;
13
+ verbose?: boolean;
14
+ };
15
+ type ConfirmPrompt = (options: Parameters<typeof confirm>[0]) => ReturnType<typeof confirm>;
16
+ type UseDependencies = {
17
+ confirm?: ConfirmPrompt;
18
+ install?: (cwd: string, verbose: boolean) => Promise<void>;
19
+ };
20
+ export declare function installCompositionDependencies(cwd: string, verbose: boolean, execute?: typeof runCommand): Promise<void>;
21
+ export declare function useComposition(options: UseCompositionOptions, dependencies?: UseDependencies): Promise<void>;
22
+ export {};
23
+ //# sourceMappingURL=use.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use.d.ts","sourceRoot":"","sources":["../../src/composition/use.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAY,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAyB,UAAU,EAAE,MAAM,WAAW,CAAC;AA0B9D,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,KAAK,aAAa,GAAG,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,KACnC,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAEhC,KAAK,eAAe,GAAG;IACrB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D,CAAC;AA+DF,wBAAsB,8BAA8B,CAClD,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,OAAO,UAAuB,GACtC,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,EAC9B,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,IAAI,CAAC,CAyJf"}
@@ -0,0 +1,182 @@
1
+ import path from "node:path";
2
+ import { confirm, isCancel } from "@clack/prompts";
3
+ import { CommandExecutionError, runCommand } from "../git.js";
4
+ import { info, printInstructions, success } from "../logger.js";
5
+ import { addCatalogReferences, loadSourceRegistryCatalog } from "./catalog.js";
6
+ import { CompositionValidationError } from "./errors.js";
7
+ import { formatCompositionPlan, formatCompositionPreview, formatCompositionResult, hasCompositionChanges, } from "./format.js";
8
+ import { installPreparedComposition, prepareComposition, validatePackageRequirementTargets, } from "./install.js";
9
+ import { planComposition, selectionFromPreset } from "./planner.js";
10
+ import { applyPackageRequirements, applyPnpmPatches, checkWorkspaceComposition, readWorkspaceSelection, removeWorkspaceTargets, writeWorkspaceSelection, } from "./workspace.js";
11
+ function requestedSelection(current, options, presetSelection) {
12
+ const providers = presetSelection
13
+ ? { ...presetSelection.providers }
14
+ : { ...current.providers };
15
+ const providerOverrides = {
16
+ auth: options.auth,
17
+ cms: options.cms,
18
+ commerce: options.commerce,
19
+ };
20
+ for (const [slot, reference] of Object.entries(providerOverrides)) {
21
+ if (reference) {
22
+ providers[slot] = reference;
23
+ }
24
+ }
25
+ const presetAddOns = presetSelection ? presetSelection.addOns : [];
26
+ let addOns = presetSelection ? presetAddOns : current.addOns;
27
+ if (options.addOns) {
28
+ addOns = [...presetAddOns, ...options.addOns];
29
+ }
30
+ return {
31
+ ...current,
32
+ addOns: [...new Set(addOns)],
33
+ providers,
34
+ };
35
+ }
36
+ function operationFailure(error, failed, completed, pending) {
37
+ let cause = error instanceof Error ? error.message : String(error);
38
+ if (error instanceof CommandExecutionError) {
39
+ const output = error.stderr.trim() || error.stdout.trim();
40
+ if (output) {
41
+ cause = `${cause}\n${output}`;
42
+ }
43
+ }
44
+ return new Error([
45
+ `Composition stopped while ${failed}.`,
46
+ `Completed: ${completed.length > 0 ? completed.join(", ") : "none"}.`,
47
+ `Not attempted: ${pending.length > 0 ? pending.join(", ") : "none"}.`,
48
+ "The workspace changes have been left in place for inspection or repair.",
49
+ "",
50
+ cause,
51
+ ].join("\n"));
52
+ }
53
+ export async function installCompositionDependencies(cwd, verbose, execute = runCommand) {
54
+ await execute("pnpm", ["install", "--no-frozen-lockfile"], {
55
+ cwd,
56
+ verbose,
57
+ });
58
+ }
59
+ export async function useComposition(options, dependencies = {}) {
60
+ const cwd = path.resolve(options.cwd ?? process.cwd());
61
+ const current = await readWorkspaceSelection(cwd);
62
+ if (options.check &&
63
+ (options.auth ||
64
+ options.cms ||
65
+ options.commerce ||
66
+ options.preset ||
67
+ options.addOns)) {
68
+ throw new Error("`use --check` reads next-hydra.json and accepts no selections.");
69
+ }
70
+ if (options.check && options.dryRun) {
71
+ throw new Error("`use --check` cannot be combined with `--dry-run`.");
72
+ }
73
+ if (options.preset && (options.auth || options.cms || options.commerce)) {
74
+ throw new Error("`use --preset` cannot be combined with provider flags.");
75
+ }
76
+ let catalog = await loadSourceRegistryCatalog(cwd);
77
+ catalog = await addCatalogReferences(catalog, [
78
+ ...Object.values(current.providers),
79
+ ...current.addOns,
80
+ ...[options.auth, options.cms, options.commerce, options.preset].filter((value) => Boolean(value)),
81
+ ...(options.addOns ?? []),
82
+ ]);
83
+ const presetSelection = options.preset
84
+ ? selectionFromPreset(catalog, options.preset)
85
+ : undefined;
86
+ const selection = options.check
87
+ ? current
88
+ : requestedSelection(current, options, presetSelection);
89
+ catalog = await addCatalogReferences(catalog, [
90
+ ...Object.values(selection.providers),
91
+ ...selection.addOns,
92
+ ]);
93
+ const plan = planComposition(catalog, selection);
94
+ const prepared = await prepareComposition(catalog, plan);
95
+ await validatePackageRequirementTargets(cwd, plan, prepared, plan.catalogManagedTargets);
96
+ if (options.check) {
97
+ info("Checking the maintainer workspace against next-hydra.json.");
98
+ if (options.verbose) {
99
+ info(`Composition plan:\n${formatCompositionPlan(plan)}`);
100
+ }
101
+ const drift = await checkWorkspaceComposition(cwd, plan, prepared.managedFiles);
102
+ if (drift.length > 0) {
103
+ throw new CompositionValidationError("The maintainer workspace differs from next-hydra.json.", drift);
104
+ }
105
+ success("The maintainer workspace matches next-hydra.json.");
106
+ return;
107
+ }
108
+ info(formatCompositionPreview(current, plan.selection));
109
+ if (options.verbose) {
110
+ info(`Composition plan:\n${formatCompositionPlan(plan)}`);
111
+ }
112
+ if (!hasCompositionChanges(current, plan.selection)) {
113
+ success(formatCompositionResult(current, plan.selection));
114
+ return;
115
+ }
116
+ if (options.dryRun) {
117
+ success("Dry run complete. No changes were made.");
118
+ return;
119
+ }
120
+ if (!options.yes) {
121
+ const approved = await (dependencies.confirm ?? confirm)({
122
+ initialValue: false,
123
+ message: "Apply these composition changes?",
124
+ });
125
+ if (isCancel(approved) || !approved) {
126
+ throw new Error("Composition cancelled. No changes were made.");
127
+ }
128
+ }
129
+ const completed = [];
130
+ const pending = [
131
+ "write next-hydra.json",
132
+ "remove managed application files",
133
+ "install selected source",
134
+ "update package aliases",
135
+ "update pnpm patches",
136
+ "install dependencies",
137
+ ];
138
+ const runStep = async (label, operation) => {
139
+ pending.shift();
140
+ try {
141
+ await operation();
142
+ completed.push(label);
143
+ }
144
+ catch (error) {
145
+ throw operationFailure(error, label, completed, pending);
146
+ }
147
+ };
148
+ await runStep("write next-hydra.json", async () => {
149
+ await writeWorkspaceSelection(cwd, plan.selection);
150
+ });
151
+ await runStep("remove managed application files", async () => {
152
+ await removeWorkspaceTargets(cwd, plan.catalogManagedTargets);
153
+ });
154
+ await runStep("install selected source", async () => {
155
+ await installPreparedComposition(cwd, prepared);
156
+ });
157
+ await runStep("update package aliases", async () => {
158
+ await applyPackageRequirements(cwd, plan);
159
+ });
160
+ await runStep("update pnpm patches", async () => {
161
+ await applyPnpmPatches(cwd, plan);
162
+ });
163
+ await runStep("install dependencies", async () => {
164
+ if (dependencies.install) {
165
+ await dependencies.install(cwd, options.verbose ?? false);
166
+ return;
167
+ }
168
+ await installCompositionDependencies(cwd, options.verbose ?? false);
169
+ });
170
+ if (plan.instructions.length > 0) {
171
+ printInstructions([
172
+ {
173
+ entries: plan.instructions.map((text) => ({
174
+ kind: "text",
175
+ text,
176
+ })),
177
+ title: "Provider setup",
178
+ },
179
+ ]);
180
+ }
181
+ success(formatCompositionResult(current, plan.selection));
182
+ }
@@ -0,0 +1,10 @@
1
+ import type { CompositionPlan, PreparedComposition, WorkspaceSelection } from "./types.js";
2
+ export declare const WORKSPACE_SELECTION_FILE = "next-hydra.json";
3
+ export declare function readWorkspaceSelection(workspaceRoot: string): Promise<WorkspaceSelection>;
4
+ export declare function writeWorkspaceSelection(workspaceRoot: string, selection: WorkspaceSelection): Promise<void>;
5
+ export declare function applyPackageRequirements(workspaceRoot: string, plan: CompositionPlan): Promise<void>;
6
+ export declare function applyPackageEntries(workspaceRoot: string, requirements: CompositionPlan["packageRequirements"]): Promise<void>;
7
+ export declare function applyPnpmPatches(workspaceRoot: string, plan: CompositionPlan): Promise<void>;
8
+ export declare function removeWorkspaceTargets(workspaceRoot: string, targets: string[]): Promise<void>;
9
+ export declare function checkWorkspaceComposition(workspaceRoot: string, plan: CompositionPlan, managedFiles: PreparedComposition["managedFiles"]): Promise<string[]>;
10
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/composition/workspace.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,wBAAwB,oBAAoB,CAAC;AAE1D,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAyB7B;AAED,wBAAsB,uBAAuB,CAC3C,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,kBAAkB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,wBAAwB,CAC5C,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CA6Cf;AAED,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,eAAe,CAAC,qBAAqB,CAAC,GACnD,OAAO,CAAC,IAAI,CAAC,CAgCf;AAuBD,wBAAsB,gBAAgB,CACpC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAyBD,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,yBAAyB,CAC7C,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,mBAAmB,CAAC,cAAc,CAAC,GAChD,OAAO,CAAC,MAAM,EAAE,CAAC,CAkGnB"}
@@ -0,0 +1,208 @@
1
+ import { readdir, readFile, rmdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parseDocument } from "yaml";
4
+ import { pathExists, readJsonFile, removePath, writeJsonFile, } from "../fs-utils.js";
5
+ import { CompositionValidationError } from "./errors.js";
6
+ import { readPackageJson } from "./packages.js";
7
+ import { formatZodError, workspaceSelectionSchema } from "./schema.js";
8
+ export const WORKSPACE_SELECTION_FILE = "next-hydra.json";
9
+ export async function readWorkspaceSelection(workspaceRoot) {
10
+ const filePath = path.join(workspaceRoot, WORKSPACE_SELECTION_FILE);
11
+ if (!(await pathExists(filePath))) {
12
+ throw new Error(`${WORKSPACE_SELECTION_FILE} was not found. The \`use\` command only operates on a Next Hydra maintainer workspace.`);
13
+ }
14
+ const value = await readJsonFile(filePath);
15
+ const result = workspaceSelectionSchema.safeParse(value);
16
+ if (!result.success) {
17
+ throw new CompositionValidationError(`${WORKSPACE_SELECTION_FILE} is invalid.`, formatZodError(result.error));
18
+ }
19
+ // Preserve existing top-level and nested key positions, appending only
20
+ // schema-defaulted keys that were absent from the file.
21
+ const selection = value;
22
+ for (const [key, defaultedValue] of Object.entries(result.data)) {
23
+ if (!Object.hasOwn(selection, key)) {
24
+ selection[key] = defaultedValue;
25
+ }
26
+ }
27
+ return value;
28
+ }
29
+ export async function writeWorkspaceSelection(workspaceRoot, selection) {
30
+ await writeJsonFile(path.join(workspaceRoot, WORKSPACE_SELECTION_FILE), selection);
31
+ }
32
+ export async function applyPackageRequirements(workspaceRoot, plan) {
33
+ const selected = new Set(plan.packageRequirements.map((requirement) => `${requirement.cwd}\0${requirement.section}\0${requirement.name}`));
34
+ const removals = plan.catalogPackageRequirements.filter((requirement) => !selected.has(`${requirement.cwd}\0${requirement.section}\0${requirement.name}`));
35
+ const byManifest = new Map();
36
+ for (const requirement of removals) {
37
+ const manifest = path.posix.join(requirement.cwd, "package.json");
38
+ const entries = byManifest.get(manifest) ?? [];
39
+ entries.push(requirement);
40
+ byManifest.set(manifest, entries);
41
+ }
42
+ await Promise.all([...byManifest].map(async ([manifest, entries]) => {
43
+ const manifestPath = path.join(workspaceRoot, manifest);
44
+ if (!(await pathExists(manifestPath))) {
45
+ return;
46
+ }
47
+ const packageJson = await readPackageJson(manifestPath, manifest);
48
+ let changed = false;
49
+ for (const requirement of entries) {
50
+ const section = packageJson[requirement.section];
51
+ if (!(section && Object.hasOwn(section, requirement.name))) {
52
+ continue;
53
+ }
54
+ delete section[requirement.name];
55
+ changed = true;
56
+ }
57
+ if (changed) {
58
+ await writeJsonFile(manifestPath, packageJson);
59
+ }
60
+ }));
61
+ await applyPackageEntries(workspaceRoot, plan.packageRequirements);
62
+ }
63
+ export async function applyPackageEntries(workspaceRoot, requirements) {
64
+ const byManifest = new Map();
65
+ for (const requirement of requirements) {
66
+ const manifest = path.posix.join(requirement.cwd, "package.json");
67
+ const existing = byManifest.get(manifest) ?? [];
68
+ existing.push(requirement);
69
+ byManifest.set(manifest, existing);
70
+ }
71
+ await Promise.all([...byManifest].map(async ([manifest, manifestRequirements]) => {
72
+ const manifestPath = path.join(workspaceRoot, manifest);
73
+ const packageJson = await readPackageJson(manifestPath, manifest);
74
+ let changed = false;
75
+ for (const requirement of manifestRequirements) {
76
+ const section = packageJson[requirement.section] ?? {};
77
+ if (Object.hasOwn(section, requirement.name) &&
78
+ section[requirement.name] === requirement.specifier) {
79
+ continue;
80
+ }
81
+ section[requirement.name] = requirement.specifier;
82
+ packageJson[requirement.section] = section;
83
+ changed = true;
84
+ }
85
+ if (changed) {
86
+ await writeJsonFile(manifestPath, packageJson);
87
+ }
88
+ }));
89
+ }
90
+ function readPnpmWorkspaceConfig(source) {
91
+ const document = parseDocument(source);
92
+ if (document.errors.length > 0) {
93
+ throw new CompositionValidationError("pnpm-workspace.yaml is invalid.", document.errors.map((error) => error.message));
94
+ }
95
+ return {
96
+ config: document.toJS(),
97
+ document,
98
+ };
99
+ }
100
+ export async function applyPnpmPatches(workspaceRoot, plan) {
101
+ if (plan.catalogPnpmPatches.length === 0) {
102
+ return;
103
+ }
104
+ const workspaceFile = path.join(workspaceRoot, "pnpm-workspace.yaml");
105
+ const { config, document } = readPnpmWorkspaceConfig(await readFile(workspaceFile, "utf-8"));
106
+ const governedDependencies = new Set(plan.catalogPnpmPatches.map((patch) => patch.dependency));
107
+ const patches = Object.fromEntries(Object.entries(config.patchedDependencies ?? {}).filter(([dependency]) => !governedDependencies.has(dependency)));
108
+ for (const patch of plan.pnpmPatches) {
109
+ patches[patch.dependency] = patch.path;
110
+ }
111
+ const sortedPatches = Object.fromEntries(Object.entries(patches).sort(([left], [right]) => left.localeCompare(right)));
112
+ if (Object.keys(sortedPatches).length === 0) {
113
+ document.delete("patchedDependencies");
114
+ }
115
+ else {
116
+ document.set("patchedDependencies", sortedPatches);
117
+ }
118
+ await writeFile(workspaceFile, document.toString(), "utf-8");
119
+ }
120
+ async function pruneEmptyParents(workspaceRoot, relativeTarget) {
121
+ let current = path.dirname(path.join(workspaceRoot, relativeTarget));
122
+ const root = path.resolve(workspaceRoot);
123
+ while (current.startsWith(`${root}${path.sep}`)) {
124
+ try {
125
+ // Parent directories must be checked from the leaf toward the root.
126
+ // oxlint-disable-next-line no-await-in-loop -- Each iteration depends on the previous parent.
127
+ const entries = await readdir(current);
128
+ if (entries.length > 0) {
129
+ return;
130
+ }
131
+ await rmdir(current);
132
+ current = path.dirname(current);
133
+ }
134
+ catch {
135
+ return;
136
+ }
137
+ }
138
+ }
139
+ export async function removeWorkspaceTargets(workspaceRoot, targets) {
140
+ const deepestFirst = [...targets].sort((left, right) => right.split("/").length - left.split("/").length);
141
+ for (const target of deepestFirst) {
142
+ // Targets are deepest-first so pruning one target cannot race another.
143
+ // oxlint-disable-next-line no-await-in-loop -- Ordered removal keeps parent pruning deterministic.
144
+ await removePath(path.join(workspaceRoot, target));
145
+ await pruneEmptyParents(workspaceRoot, target);
146
+ }
147
+ }
148
+ export async function checkWorkspaceComposition(workspaceRoot, plan, managedFiles) {
149
+ const drift = [];
150
+ const selected = new Set(plan.packageRequirements.map((requirement) => `${requirement.cwd}\0${requirement.section}\0${requirement.name}`));
151
+ const unselectedPackageDrift = await Promise.all(plan.catalogPackageRequirements.map(async (requirement) => {
152
+ const key = `${requirement.cwd}\0${requirement.section}\0${requirement.name}`;
153
+ if (selected.has(key)) {
154
+ return;
155
+ }
156
+ const manifest = path.posix.join(requirement.cwd, "package.json");
157
+ const manifestPath = path.join(workspaceRoot, manifest);
158
+ if (!(await pathExists(manifestPath))) {
159
+ return;
160
+ }
161
+ const packageJson = await readPackageJson(manifestPath, manifest);
162
+ const actual = packageJson[requirement.section]?.[requirement.name];
163
+ return actual === undefined
164
+ ? undefined
165
+ : `${manifest}: expected unselected ${requirement.section}.${requirement.name} to be absent, found ${actual}`;
166
+ }));
167
+ drift.push(...unselectedPackageDrift.filter((issue) => issue !== undefined));
168
+ const selectedPackageDrift = await Promise.all(plan.packageRequirements.map(async (requirement) => {
169
+ const manifest = path.posix.join(requirement.cwd, "package.json");
170
+ const packageJson = await readPackageJson(path.join(workspaceRoot, manifest), manifest);
171
+ const actual = packageJson[requirement.section]?.[requirement.name];
172
+ return actual === requirement.specifier
173
+ ? undefined
174
+ : `${manifest}: expected ${requirement.section}.${requirement.name} to be ${requirement.specifier}, found ${actual ?? "missing"}`;
175
+ }));
176
+ drift.push(...selectedPackageDrift.filter((issue) => issue !== undefined));
177
+ if (plan.catalogPnpmPatches.length > 0) {
178
+ const workspaceFile = path.join(workspaceRoot, "pnpm-workspace.yaml");
179
+ const { config } = readPnpmWorkspaceConfig(await readFile(workspaceFile, "utf-8"));
180
+ const selectedPatches = new Map(plan.pnpmPatches.map((patch) => [patch.dependency, patch.path]));
181
+ for (const patch of plan.catalogPnpmPatches) {
182
+ const expected = selectedPatches.get(patch.dependency);
183
+ const actual = config.patchedDependencies?.[patch.dependency];
184
+ if (actual !== expected) {
185
+ drift.push(`pnpm-workspace.yaml: expected patchedDependencies.${patch.dependency} to be ${expected ?? "absent"}, found ${actual ?? "missing"}`);
186
+ }
187
+ }
188
+ }
189
+ const expectedManagedFiles = new Map(managedFiles.map((file) => [file.target, file.content]));
190
+ const managedFileDrift = await Promise.all(plan.catalogManagedTargets.map(async (target) => {
191
+ const filePath = path.join(workspaceRoot, target);
192
+ const expected = expectedManagedFiles.get(target);
193
+ if (!expected) {
194
+ return (await pathExists(filePath))
195
+ ? `${target}: managed file should not exist for this composition`
196
+ : undefined;
197
+ }
198
+ if (!(await pathExists(filePath))) {
199
+ return `${target}: managed file is missing`;
200
+ }
201
+ const actual = await readFile(filePath, "utf-8");
202
+ return actual === expected
203
+ ? undefined
204
+ : `${target}: managed file differs from the composition plan`;
205
+ }));
206
+ drift.push(...managedFileDrift.filter((issue) => issue !== undefined));
207
+ return drift.sort((left, right) => left.localeCompare(right));
208
+ }
@@ -4,5 +4,8 @@ export declare const DEFAULT_REPO_URL = "https://github.com/jakala-na/next-hydra
4
4
  export declare const DEFAULT_COMMIT_MESSAGE = "Initial commit";
5
5
  export declare const DEFAULT_PACKAGE_MANAGER = "pnpm";
6
6
  export declare const DEFAULT_STARTER: StarterDefinition;
7
- export declare const SANITIZE_REMOVE_PATHS: readonly [".git", "apps/docs", "pnpm-lock.yaml"];
7
+ export declare const SANITIZE_REMOVE_PATHS: readonly [".changeset", ".git", ".github/workflows/registry-integrity.yml", ".github/workflows/release-create-next-hydra.yml", ".scratch", "RELEASING.md", "apps/docs", "next-hydra.json", "packages/create-next-hydra", "scripts/release-create-next-hydra.mjs", "scripts/sync-registry-files.ts"];
8
+ export declare const SANITIZE_REMOVE_SCRIPTS: readonly ["changeset", "changeset:status", "publish:cli", "registry:check", "registry:sync", "release:create-next-hydra", "release:create-next-hydra:dry-run", "version:cli"];
9
+ export declare const SANITIZE_REMOVE_ROOT_DEPENDENCIES: readonly ["@clack/prompts", "commander"];
10
+ export declare const SANITIZE_REMOVE_ROOT_DEV_DEPENDENCIES: readonly ["@changesets/cli"];
8
11
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,QAAQ,sBAAsB,CAAC;AAC5C,eAAO,MAAM,gBAAgB,gDAAgD,CAAC;AAC9E,eAAO,MAAM,sBAAsB,mBAAmB,CAAC;AACvD,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAE9C,eAAO,MAAM,eAAe,EAAE,iBAG7B,CAAC;AAEF,eAAO,MAAM,qBAAqB,kDAIxB,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,QAAQ,sBAAsB,CAAC;AAC5C,eAAO,MAAM,gBAAgB,gDAAgD,CAAC;AAC9E,eAAO,MAAM,sBAAsB,mBAAmB,CAAC;AACvD,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAE9C,eAAO,MAAM,eAAe,EAAE,iBAG7B,CAAC;AAEF,eAAO,MAAM,qBAAqB,YAChC,YAAY,EACZ,MAAM,EACN,0CAA0C,EAC1C,iDAAiD,EACjD,UAAU,EACV,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,4BAA4B,EAC5B,uCAAuC,EACvC,gCAAgC,CACxB,CAAC;AAEX,eAAO,MAAM,uBAAuB,YAClC,WAAW,EACX,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,2BAA2B,EAC3B,mCAAmC,EACnC,aAAa,CACL,CAAC;AAEX,eAAO,MAAM,iCAAiC,YAC5C,gBAAgB,EAChB,WAAW,CACH,CAAC;AAEX,eAAO,MAAM,qCAAqC,YAChD,iBAAiB,CACT,CAAC"}