typeflake 0.0.1-alpha.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.
@@ -0,0 +1,760 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import * as Data from "effect/Data";
5
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
6
+ import * as Clock from "effect/Clock";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ //#region src/errors.ts
10
+ var FlakeImportError = class extends Data.TaggedError("FlakeImportError") {};
11
+ var NixCheckFailed = class extends Data.TaggedError("NixCheckFailed") {};
12
+ var TypeScriptCheckFailed = class extends Data.TaggedError("TypeScriptCheckFailed") {};
13
+ var DoctorFailed = class extends Data.TaggedError("DoctorFailed") {};
14
+ var OptionMetadataParseError = class extends Data.TaggedError("OptionMetadataParseError") {};
15
+ var UnsupportedOptionsFound = class extends Data.TaggedError("UnsupportedOptionsFound") {};
16
+ //#endregion
17
+ //#region src/process.ts
18
+ const runCommandExitCode = (options) => Effect.gen(function* () {
19
+ const process = yield* ChildProcessSpawner.ChildProcessSpawner;
20
+ const command = ChildProcess.make(options.command, options.args, {
21
+ stderr: options.stderr ?? "ignore",
22
+ stdin: options.stdin ?? "ignore",
23
+ stdout: options.stdout ?? "ignore"
24
+ });
25
+ const exitCode = yield* process.exitCode(command);
26
+ return Number(exitCode);
27
+ });
28
+ const runCommandString = (options) => Effect.gen(function* () {
29
+ const process = yield* ChildProcessSpawner.ChildProcessSpawner;
30
+ const command = ChildProcess.make(options.command, options.args, { stdin: options.stdin ?? "ignore" });
31
+ return yield* process.string(command, { includeStderr: options.stderr === "inherit" });
32
+ });
33
+ //#endregion
34
+ //#region src/nix/expr.ts
35
+ const nixValueSymbol = Symbol("typeflake.nixValue");
36
+ const rawNix = (code) => ({
37
+ [nixValueSymbol]: true,
38
+ tag: "raw",
39
+ kind: "raw",
40
+ code
41
+ });
42
+ const nixExpr = (kind, code) => ({
43
+ [nixValueSymbol]: true,
44
+ tag: "raw",
45
+ kind,
46
+ code
47
+ });
48
+ const nixAttrPath = (kind, parts) => ({
49
+ [nixValueSymbol]: true,
50
+ tag: "raw",
51
+ kind,
52
+ code: parts.map(renderAttrPathSegment).join(".")
53
+ });
54
+ const nixString = (value) => ({
55
+ [nixValueSymbol]: true,
56
+ tag: "string",
57
+ value
58
+ });
59
+ const nixNumber = (value) => ({
60
+ [nixValueSymbol]: true,
61
+ tag: "number",
62
+ value
63
+ });
64
+ const nixBoolean = (value) => ({
65
+ [nixValueSymbol]: true,
66
+ tag: "boolean",
67
+ value
68
+ });
69
+ const nixNull = {
70
+ [nixValueSymbol]: true,
71
+ tag: "null"
72
+ };
73
+ const nixList = (items) => ({
74
+ [nixValueSymbol]: true,
75
+ tag: "list",
76
+ items: items.map(normalizeNixInput)
77
+ });
78
+ const nixAttrSet = (attrs) => ({
79
+ [nixValueSymbol]: true,
80
+ tag: "attrset",
81
+ attrs: Object.fromEntries(Object.entries(attrs).map(([key, value]) => [key, value === void 0 ? void 0 : normalizeNixInput(value)]))
82
+ });
83
+ const normalizeNixInput = (value) => {
84
+ if (isNixValue(value)) return value;
85
+ switch (typeof value) {
86
+ case "string": return nixString(value);
87
+ case "number": return nixNumber(value);
88
+ case "boolean": return nixBoolean(value);
89
+ case "object":
90
+ if (value === null) return nixNull;
91
+ if (isReadonlyArray(value)) return nixList(value);
92
+ return nixAttrSet(value);
93
+ case "undefined":
94
+ case "bigint":
95
+ case "function":
96
+ case "symbol": throw new Error(`Cannot normalize ${typeof value} as Nix`);
97
+ }
98
+ throw new Error(`Cannot normalize unsupported value as Nix: ${String(value)}`);
99
+ };
100
+ const renderAttrName = (name) => /^[A-Za-z_][A-Za-z0-9_'-]*$/.test(name) ? name : JSON.stringify(name);
101
+ const renderAttrPathSegment = renderAttrName;
102
+ const isNixValue = (value) => typeof value === "object" && value !== null && nixValueSymbol in value && value[nixValueSymbol] === true && "tag" in value && (value.tag === "raw" || value.tag === "string" || value.tag === "number" || value.tag === "boolean" || value.tag === "null" || value.tag === "list" || value.tag === "attrset");
103
+ const isReadonlyArray = (value) => Array.isArray(value);
104
+ //#endregion
105
+ //#region src/nix/render.ts
106
+ const renderNixValue = (value, options = {}) => renderValue(normalizeNixInput(value), options.indent ?? 0);
107
+ const renderValue = (value, indent) => {
108
+ switch (value.tag) {
109
+ case "raw": return indentRaw(value.code, indent);
110
+ case "string": return JSON.stringify(value.value);
111
+ case "number":
112
+ if (!Number.isFinite(value.value)) throw new Error(`Cannot render non-finite number as Nix: ${value.value}`);
113
+ return String(value.value);
114
+ case "boolean": return value.value ? "true" : "false";
115
+ case "null": return "null";
116
+ case "list": return renderList(value.items, indent);
117
+ case "attrset": return renderAttrSet(value.attrs, indent);
118
+ }
119
+ return absurd(value);
120
+ };
121
+ const renderList = (items, indent) => {
122
+ const normalized = items.map(normalizeNixInput);
123
+ if (normalized.length === 0) return "[]";
124
+ const childIndent = indent + 1;
125
+ const pad = indentation$1(indent);
126
+ const childPad = indentation$1(childIndent);
127
+ return `[\n${normalized.map((item) => `${childPad}${renderValue(item, childIndent)}`).join("\n")}\n${pad}]`;
128
+ };
129
+ const renderAttrSet = (attrs, indent) => {
130
+ const entries = Object.entries(attrs).filter((entry) => entry[1] !== void 0);
131
+ if (entries.length === 0) return "{}";
132
+ const childIndent = indent + 1;
133
+ const pad = indentation$1(indent);
134
+ const childPad = indentation$1(childIndent);
135
+ return `{\n${entries.toSorted(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${childPad}${renderAttrName(key)} = ${renderValue(normalizeNixInput(value), childIndent)};`).join("\n")}\n${pad}}`;
136
+ };
137
+ const indentation$1 = (level) => " ".repeat(level);
138
+ const indentRaw = (code, indent) => {
139
+ const lines = code.split("\n");
140
+ if (lines.length === 1) return code;
141
+ const pad = indentation$1(indent);
142
+ const [first, ...rest] = lines;
143
+ return [first, ...rest.map((line) => line.length === 0 ? line : `${pad}${line}`)].join("\n");
144
+ };
145
+ const absurd = (value) => {
146
+ throw new Error(`Unexpected Nix expression: ${String(value)}`);
147
+ };
148
+ //#endregion
149
+ //#region src/flake.ts
150
+ const Flake = {
151
+ input(name, url) {
152
+ return {
153
+ ...nixExpr("input", renderAttrName(name)),
154
+ name,
155
+ url
156
+ };
157
+ },
158
+ inputs(inputs) {
159
+ return inputs;
160
+ },
161
+ make(spec) {
162
+ return {
163
+ taint: "pure",
164
+ spec: Effect.succeed(spec)
165
+ };
166
+ },
167
+ effect(spec) {
168
+ return {
169
+ spec,
170
+ taint: "effect"
171
+ };
172
+ },
173
+ impure(spec) {
174
+ return {
175
+ spec,
176
+ taint: "impure"
177
+ };
178
+ }
179
+ };
180
+ const resolveFlakeSpec = (flake) => flake.spec;
181
+ const renderFlake = (spec) => {
182
+ const outputs = spec.outputs(spec.inputs);
183
+ return `${renderNixValue({
184
+ description: spec.description ?? "Generated by Typeflake",
185
+ inputs: renderInputs(spec.inputs),
186
+ outputs: rawNix(renderOutputs(spec.inputs, outputs))
187
+ })}\n`;
188
+ };
189
+ const renderInputs = (inputs) => Object.fromEntries(Object.entries(inputs).map(([name, input]) => [name, { url: input.url }]));
190
+ const renderOutputs = (inputs, outputs) => {
191
+ return `{ ${["self", ...Object.keys(inputs)].join(", ")} }: ${renderNixValue({
192
+ devShells: outputs.devShells === void 0 ? void 0 : renderDevShells(outputs.devShells),
193
+ nixosConfigurations: outputs.nixosConfigurations
194
+ })}`;
195
+ };
196
+ const renderDevShells = (systems) => Object.fromEntries(Object.entries(systems).map(([system, shells]) => [system, Object.fromEntries(Object.entries(shells).map(([name, shell]) => [name, rawNix(`let pkgs = nixpkgs.legacyPackages.${renderAttrName(system)}; in pkgs.mkShell ${renderNixValue({ packages: shell.packages ?? [] })}`)]))]));
197
+ //#endregion
198
+ //#region src/tooling.ts
199
+ const effectTsgoCommand = () => resolvePackageBin("@effect/tsgo/package.json", "dist/effect-tsgo.js");
200
+ const tsgoCommand = () => resolvePackageBin("@typescript/native-preview/package.json", "bin/tsgo");
201
+ const resolvePackageBin = (packageJsonSpecifier, binPath) => {
202
+ return join(dirname(fileURLToPath(import.meta.resolve(packageJsonSpecifier))), binPath);
203
+ };
204
+ //#endregion
205
+ //#region src/sync.ts
206
+ const sync = (options) => Effect.gen(function* () {
207
+ const fs = yield* FileSystem.FileSystem;
208
+ const path = yield* Path.Path;
209
+ const inputPath = path.resolve(options.input);
210
+ const outputPath = path.resolve(options.output);
211
+ const moduleUrl = yield* path.toFileUrl(inputPath);
212
+ yield* runTypeScriptCheck("tsconfig.json");
213
+ const rendered = renderFlake(yield* resolveFlakeSpec((yield* loadFlakeModule(moduleUrl)).default));
214
+ yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true });
215
+ yield* fs.writeFileString(outputPath, rendered);
216
+ });
217
+ const loadFlakeModule = (moduleUrl) => Effect.gen(function* () {
218
+ const cacheBust = yield* Clock.currentTimeMillis;
219
+ const specifier = `${moduleUrl.href}?typeflake=${cacheBust}`;
220
+ return yield* Effect.tryPromise({
221
+ try: () => importFlakeModule(specifier),
222
+ catch: (cause) => new FlakeImportError({
223
+ cause,
224
+ specifier
225
+ })
226
+ });
227
+ });
228
+ const importFlakeModule = (specifier) => import(specifier);
229
+ const runTypeScriptCheck = (project) => Effect.gen(function* () {
230
+ const exitCode = yield* runCommandExitCode({
231
+ args: [
232
+ "--noEmit",
233
+ "--project",
234
+ project
235
+ ],
236
+ command: tsgoCommand(),
237
+ stderr: "inherit",
238
+ stdin: "inherit",
239
+ stdout: "inherit"
240
+ });
241
+ if (exitCode !== 0) return yield* new TypeScriptCheckFailed({
242
+ exitCode,
243
+ project
244
+ });
245
+ });
246
+ //#endregion
247
+ //#region src/check.ts
248
+ const check = (options) => Effect.gen(function* () {
249
+ yield* sync(options);
250
+ const target = yield* prepareFlakeTarget(options.output);
251
+ yield* runNixFlakeCheck(target, options.noBuild ?? true);
252
+ });
253
+ const prepareFlakeTarget = (output) => Effect.gen(function* () {
254
+ const fs = yield* FileSystem.FileSystem;
255
+ const path = yield* Path.Path;
256
+ const outputPath = path.resolve(output);
257
+ if (path.basename(outputPath) === "flake.nix") return path.dirname(outputPath);
258
+ const directory = yield* fs.makeTempDirectory({ prefix: "typeflake-check-" });
259
+ yield* fs.copyFile(outputPath, path.join(directory, "flake.nix"));
260
+ return directory;
261
+ });
262
+ const runNixFlakeCheck = (target, noBuild) => Effect.gen(function* () {
263
+ const exitCode = yield* runCommandExitCode({
264
+ args: noBuild ? [
265
+ "flake",
266
+ "check",
267
+ "--no-build",
268
+ target
269
+ ] : [
270
+ "flake",
271
+ "check",
272
+ target
273
+ ],
274
+ command: "nix",
275
+ stderr: "inherit",
276
+ stdin: "inherit",
277
+ stdout: "inherit"
278
+ });
279
+ if (exitCode !== 0) return yield* new NixCheckFailed({
280
+ exitCode,
281
+ target
282
+ });
283
+ });
284
+ //#endregion
285
+ //#region src/doctor.ts
286
+ const doctor = (options = {}) => Effect.gen(function* () {
287
+ const project = options.project ?? "tsconfig.json";
288
+ const checks = yield* Effect.all([
289
+ checkCommand("Nix", "nix", ["--version"]),
290
+ checkCommand("Node", "node", ["--version"]),
291
+ checkCommand("TypeScript-Go", tsgoCommand(), ["--version"]),
292
+ checkCommand("Effect TSGO", effectTsgoCommand(), ["get-exe-path"]),
293
+ checkCommand("Project TypeScript", tsgoCommand(), [
294
+ "--noEmit",
295
+ "--project",
296
+ project
297
+ ])
298
+ ], { concurrency: "unbounded" });
299
+ return {
300
+ checks,
301
+ ok: checks.every((check) => check.ok)
302
+ };
303
+ });
304
+ const renderDoctorReport = (report) => ["Typeflake doctor", ...report.checks.map((check) => `${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}`)].join("\n");
305
+ const checkCommand = (name, command, args) => runCommandExitCode({
306
+ args,
307
+ command
308
+ }).pipe(Effect.match({
309
+ onFailure: (cause) => ({
310
+ detail: String(cause),
311
+ name,
312
+ ok: false
313
+ }),
314
+ onSuccess: (exitCode) => ({
315
+ detail: exitCode === 0 ? `${command} ${args.join(" ")}` : `exit code ${exitCode}`,
316
+ name,
317
+ ok: exitCode === 0
318
+ })
319
+ }));
320
+ //#endregion
321
+ //#region src/options/generate.ts
322
+ const generateOptionTypes = (options) => {
323
+ const body = renderRootInterface(options.rootTypeName, options.scope, options.options);
324
+ return `${renderGeneratedHeader()}
325
+ import type { NixExpr, NixInput } from ${JSON.stringify(options.importPath)};
326
+
327
+ ${renderSharedTypes()}
328
+
329
+ ${body}
330
+ `;
331
+ };
332
+ const generateOptionTypeFile = (options) => {
333
+ const interfaces = options.roots.map((root) => renderRootInterface(root.rootTypeName, root.scope, options.options)).join("\n\n");
334
+ return `${renderGeneratedHeader(options.fixtureScope)}
335
+ import type { NixExpr, NixInput } from ${JSON.stringify(options.importPath)};
336
+
337
+ ${renderSharedTypes()}
338
+
339
+ ${interfaces}
340
+ `;
341
+ };
342
+ const renderRootInterface = (rootTypeName, scope, options) => {
343
+ const tree = buildOptionTree(options.filter((option) => option.scope === scope).map((option) => option));
344
+ return `export interface ${rootTypeName} ${renderTree(tree, 0)}`;
345
+ };
346
+ const renderGeneratedHeader = (fixtureScope) => {
347
+ const lines = ["// Generated by Typeflake from real Nix option metadata."];
348
+ if (fixtureScope !== void 0) lines.push(`// Fixture scope: ${fixtureScope}`);
349
+ return lines.join("\n");
350
+ };
351
+ const renderSharedTypes = () => `export type NixOptionValue<T> = T | NixExpr;
352
+
353
+ export type UnsupportedNixOption<Description extends string> = NixExpr<"unsupported"> & {
354
+ readonly __unsupportedNixOption: Description;
355
+ };`;
356
+ const buildOptionTree = (options) => {
357
+ const root = createTree();
358
+ for (const option of options) insertOption(root, option.path, option);
359
+ return root;
360
+ };
361
+ const createTree = () => ({ children: /* @__PURE__ */ new Map() });
362
+ const insertOption = (root, path, option) => {
363
+ let current = root;
364
+ for (const segment of path) {
365
+ const existing = current.children.get(segment);
366
+ if (existing !== void 0) current = existing;
367
+ else {
368
+ const next = createTree();
369
+ current.children.set(segment, next);
370
+ current = next;
371
+ }
372
+ }
373
+ current.option = option;
374
+ };
375
+ const renderTree = (tree, depth) => {
376
+ const entries = [...tree.children.entries()].toSorted(([left], [right]) => left.localeCompare(right));
377
+ if (entries.length === 0) return tree.option === void 0 ? "{}" : toOptionValueType(tree.option);
378
+ const pad = indentation(depth);
379
+ const childPad = indentation(depth + 1);
380
+ return `{\n${entries.map(([key, child]) => `${childPad}readonly ${quoteProperty(key)}?: ${renderTree(child, depth + 1)};`).join("\n")}\n${pad}}`;
381
+ };
382
+ const toOptionValueType = (option) => `NixOptionValue<${toTypeScriptType(option.type, option.path)}>`;
383
+ const toTypeScriptType = (nixType, path = []) => {
384
+ const normalized = normalizeTypeName(nixType.name);
385
+ const description = nixType.description?.toLowerCase() ?? "";
386
+ if (normalized === "bool" || normalized === "boolean") return "boolean";
387
+ if (normalized === "nullor") return renderNullableType(nixType, path);
388
+ if (normalized === "listof") return renderListType(nixType, path);
389
+ if (normalized === "attrsof") return renderAttrsType(nixType, path);
390
+ if (normalized === "str" || normalized === "string" || normalized === "enum" || normalized.startsWith("strmatching")) return "string";
391
+ if (isNumberType(normalized, description)) return "number";
392
+ if (normalized === "package") return "NixInput";
393
+ if (normalized === "path") return "string";
394
+ if (normalized === "submodule") return "NixInput";
395
+ if (description.includes("list of package")) return "readonly NixInput[]";
396
+ if (description.includes("list of string")) return "readonly string[]";
397
+ if (description.includes("attribute set")) return "Readonly<Record<string, NixInput>>";
398
+ return unsupportedType(nixType);
399
+ };
400
+ const collectUnsupportedOptions = (options) => options.filter((option) => toTypeScriptType(option.type, option.path).startsWith("Unsupported"));
401
+ const renderNullableType = (nixType, path) => {
402
+ const nested = nixType.nestedTypes.elemType ?? nixType.nestedTypes.valueType;
403
+ if (nested === void 0) return `${unsupportedType(nixType)} | null`;
404
+ return `${toTypeScriptType(nested, path)} | null`;
405
+ };
406
+ const renderListType = (nixType, path) => {
407
+ const elemType = nixType.nestedTypes.elemType;
408
+ if (elemType === void 0) return "readonly NixInput[]";
409
+ return `readonly ${toCollectionElementType(elemType, path)}[]`;
410
+ };
411
+ const renderAttrsType = (nixType, path) => {
412
+ const elemType = nixType.nestedTypes.elemType;
413
+ if (elemType === void 0) return "Readonly<Record<string, NixInput>>";
414
+ return `Readonly<Record<string, ${toCollectionElementType(elemType, path)}>>`;
415
+ };
416
+ const toCollectionElementType = (nixType, path) => {
417
+ if (normalizeTypeName(nixType.name) === "submodule") return "NixInput";
418
+ return toTypeScriptType(nixType, path);
419
+ };
420
+ const isNumberType = (normalized, description) => normalized.includes("int") || normalized === "number" || description.includes("integer") || description.includes("signed integer");
421
+ const unsupportedType = (nixType) => `UnsupportedNixOption<${JSON.stringify(renderUnsupportedDescription(nixType))}>`;
422
+ const renderUnsupportedDescription = (nixType) => nixType.description === null ? nixType.name : `${nixType.name}: ${nixType.description}`;
423
+ const normalizeTypeName = (name) => name.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
424
+ const quoteProperty = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
425
+ const indentation = (depth) => " ".repeat(depth);
426
+ //#endregion
427
+ //#region src/options/probe.ts
428
+ const defaultNixOSOptionPaths = [
429
+ [
430
+ "boot",
431
+ "loader",
432
+ "grub",
433
+ "devices"
434
+ ],
435
+ ["environment", "systemPackages"],
436
+ ["fileSystems"],
437
+ [
438
+ "networking",
439
+ "firewall",
440
+ "allowedTCPPorts"
441
+ ],
442
+ ["networking", "hostName"],
443
+ [
444
+ "services",
445
+ "nginx",
446
+ "enable"
447
+ ],
448
+ [
449
+ "services",
450
+ "openssh",
451
+ "enable"
452
+ ],
453
+ [
454
+ "services",
455
+ "openssh",
456
+ "ports"
457
+ ],
458
+ ["system", "stateVersion"],
459
+ ["users", "users"]
460
+ ];
461
+ const defaultHomeManagerOptionPaths = [
462
+ ["home", "stateVersion"],
463
+ ["home", "packages"],
464
+ [
465
+ "programs",
466
+ "git",
467
+ "enable"
468
+ ]
469
+ ];
470
+ const renderOptionProbeExpression = (options) => {
471
+ const nixosPaths = options.nixosOptions?.optionPaths ?? [];
472
+ const homeManagerPaths = options.homeManagerOptions?.optionPaths ?? [];
473
+ return `let
474
+ nixpkgs = ${options.nixpkgs.code};
475
+ system = ${renderNixValue(options.system)};
476
+ nixpkgsPath = nixpkgs.outPath or nixpkgs;
477
+ pkgs = import nixpkgsPath { inherit system; };
478
+
479
+ renderDoc = value:
480
+ if value == null then null
481
+ else if builtins.isString value then value
482
+ else if builtins.isAttrs value && value ? text then value.text
483
+ else builtins.toJSON value;
484
+
485
+ getPath = options: path:
486
+ builtins.foldl' (value: key: value.\${key}) options path;
487
+
488
+ typeToIR = type: {
489
+ name = type.name or "unknown";
490
+ description = renderDoc (type.description or null);
491
+ nestedTypes = builtins.mapAttrs (_: nestedType: typeToIR nestedType) (type.nestedTypes or {});
492
+ };
493
+
494
+ optionToIR = scope: options: path:
495
+ let option = getPath options path; in {
496
+ inherit path scope;
497
+ declarations = map toString (option.declarations or []);
498
+ defaultText = renderDoc (option.defaultText or null);
499
+ description = renderDoc (option.description or null);
500
+ exampleText = renderDoc (option.example or null);
501
+ internal = option.internal or false;
502
+ readOnly = option.readOnly or false;
503
+ type = typeToIR option.type;
504
+ visible = option.visible or true;
505
+ };
506
+
507
+ nixosEval = import "\${nixpkgsPath}/nixos/lib/eval-config.nix" {
508
+ inherit system;
509
+ modules = [];
510
+ };
511
+ ${renderHomeManagerBinding(options)}
512
+ in {
513
+ nixos = map (optionToIR "nixos" nixosEval.options) ${renderPathList(nixosPaths)};
514
+ homeManager = ${renderHomeManagerOptions(options, homeManagerPaths)};
515
+ }`;
516
+ };
517
+ const flakeInput = (name) => nixExpr("flakeInput", `(builtins.getFlake ("git+file://" + toString ./.))
518
+ .inputs.${name}`);
519
+ const projectFlakeInput = (flakeReference, name) => nixExpr("flakeInput", `(builtins.getFlake ${renderNixValue(flakeReference)})
520
+ .inputs.${name}`);
521
+ const renderPathList = (paths) => renderNixValue(paths);
522
+ const renderHomeManagerBinding = (options) => {
523
+ if (options.homeManager === void 0) return "";
524
+ return ` homeManager = ${options.homeManager.code};
525
+ homeManagerEval = homeManager.lib.homeManagerConfiguration {
526
+ inherit pkgs;
527
+ modules = [{
528
+ home.homeDirectory = "/home/typeflake";
529
+ home.stateVersion = "25.11";
530
+ home.username = "typeflake";
531
+ }];
532
+ };
533
+ `;
534
+ };
535
+ const renderHomeManagerOptions = (options, paths) => {
536
+ if (options.homeManager === void 0) return "[]";
537
+ return `map (optionToIR "home-manager" homeManagerEval.options) ${renderPathList(paths)}`;
538
+ };
539
+ //#endregion
540
+ //#region src/options/document.ts
541
+ const makeOptionMetadataDocument = (source, payload) => ({
542
+ options: sortOptions([...payload.nixos, ...payload.homeManager]),
543
+ source: {
544
+ flake: source.flake,
545
+ homeManagerInput: source.homeManagerInput,
546
+ nixpkgsInput: source.nixpkgsInput,
547
+ scopes: sortScopes(source.scopes),
548
+ system: source.system
549
+ },
550
+ version: 1
551
+ });
552
+ const optionMetadataDocumentToJson = (document) => `${JSON.stringify(document, null, 2)}\n`;
553
+ const parseOptionMetadataDocument = (value) => {
554
+ const record = requireRecord(value, "Option metadata document");
555
+ const version = record.version;
556
+ if (version !== 1) throw new Error(`Unsupported option metadata version: ${String(version)}`);
557
+ return {
558
+ options: requireArray(record.options, "options").map(parseOptionIR),
559
+ source: parseSource(record.source),
560
+ version
561
+ };
562
+ };
563
+ const parseOptionProbePayload = (value) => {
564
+ const record = requireRecord(value, "Option probe payload");
565
+ return {
566
+ homeManager: requireArray(record.homeManager, "homeManager").map(parseOptionIR),
567
+ nixos: requireArray(record.nixos, "nixos").map(parseOptionIR)
568
+ };
569
+ };
570
+ const parseSource = (value) => {
571
+ const record = requireRecord(value, "source");
572
+ return {
573
+ flake: requireString(record.flake, "source.flake"),
574
+ homeManagerInput: record.homeManagerInput === null ? null : requireString(record.homeManagerInput, "source.homeManagerInput"),
575
+ nixpkgsInput: requireString(record.nixpkgsInput, "source.nixpkgsInput"),
576
+ scopes: requireArray(record.scopes, "source.scopes").map(parseScope),
577
+ system: requireString(record.system, "source.system")
578
+ };
579
+ };
580
+ const parseOptionIR = (value) => {
581
+ const record = requireRecord(value, "option");
582
+ return {
583
+ declarations: requireArray(record.declarations, "option.declarations").map((item) => requireString(item, "option.declarations[]")),
584
+ defaultText: requireNullableString(record.defaultText, "option.defaultText"),
585
+ description: requireNullableString(record.description, "option.description"),
586
+ exampleText: requireNullableString(record.exampleText, "option.exampleText"),
587
+ internal: requireBoolean(record.internal, "option.internal"),
588
+ path: parsePath(record.path),
589
+ readOnly: requireBoolean(record.readOnly, "option.readOnly"),
590
+ scope: parseScope(record.scope),
591
+ type: parseType(record.type),
592
+ visible: parseVisible(record.visible)
593
+ };
594
+ };
595
+ const parseType = (value) => {
596
+ const record = requireRecord(value, "option.type");
597
+ const nestedTypes = requireRecord(record.nestedTypes, "option.type.nestedTypes");
598
+ return {
599
+ description: requireNullableString(record.description, "option.type.description"),
600
+ name: requireString(record.name, "option.type.name"),
601
+ nestedTypes: Object.fromEntries(Object.entries(nestedTypes).map(([key, nestedType]) => [key, parseType(nestedType)]))
602
+ };
603
+ };
604
+ const parsePath = (value) => {
605
+ const [first, ...rest] = requireArray(value, "option.path").map((item) => requireString(item, "option.path[]"));
606
+ if (first === void 0) throw new Error("Option paths must contain at least one segment");
607
+ return [first, ...rest];
608
+ };
609
+ const parseScope = (value) => {
610
+ switch (value) {
611
+ case "home-manager":
612
+ case "nixos": return value;
613
+ default: throw new Error(`Unexpected option scope: ${String(value)}`);
614
+ }
615
+ };
616
+ const parseVisible = (value) => {
617
+ if (value === true || value === false || value === "shallow") return value;
618
+ throw new Error(`Unexpected option visibility: ${String(value)}`);
619
+ };
620
+ const sortOptions = (options) => options.toSorted((left, right) => {
621
+ const scopeOrder = left.scope.localeCompare(right.scope);
622
+ if (scopeOrder !== 0) return scopeOrder;
623
+ return left.path.join(".").localeCompare(right.path.join("."));
624
+ });
625
+ const sortScopes = (scopes) => [...new Set(scopes)].toSorted((left, right) => left.localeCompare(right));
626
+ const requireRecord = (value, name) => {
627
+ if (isRecord(value)) return value;
628
+ throw new Error(`${name} must be an object`);
629
+ };
630
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
631
+ const requireArray = (value, name) => {
632
+ if (Array.isArray(value)) return value;
633
+ throw new Error(`${name} must be an array`);
634
+ };
635
+ const requireString = (value, name) => {
636
+ if (typeof value === "string") return value;
637
+ throw new Error(`${name} must be a string`);
638
+ };
639
+ const requireNullableString = (value, name) => {
640
+ if (value === null || typeof value === "string") return value;
641
+ throw new Error(`${name} must be a string or null`);
642
+ };
643
+ const requireBoolean = (value, name) => {
644
+ if (typeof value === "boolean") return value;
645
+ throw new Error(`${name} must be a boolean`);
646
+ };
647
+ //#endregion
648
+ //#region src/options/commands.ts
649
+ const defaultOptionScopes = ["nixos", "home-manager"];
650
+ const probeProjectOptions = (options) => Effect.gen(function* () {
651
+ const fs = yield* FileSystem.FileSystem;
652
+ const path = yield* Path.Path;
653
+ const flakeRoot = path.resolve(options.flake);
654
+ const flakeReference = yield* resolveFlakeReference(flakeRoot);
655
+ const scopes = normalizeScopes(options.scopes);
656
+ const includesNixOS = scopes.includes("nixos");
657
+ const includesHomeManager = scopes.includes("home-manager");
658
+ const output = yield* runCommandString({
659
+ args: [
660
+ "eval",
661
+ "--impure",
662
+ "--json",
663
+ "--expr",
664
+ renderOptionProbeExpression({
665
+ nixpkgs: projectFlakeInput(flakeReference, options.nixpkgsInput),
666
+ system: options.system,
667
+ ...includesHomeManager ? {
668
+ homeManager: projectFlakeInput(flakeReference, options.homeManagerInput),
669
+ homeManagerOptions: { optionPaths: options.homeManagerOptionPaths ?? defaultHomeManagerOptionPaths }
670
+ } : {},
671
+ ...includesNixOS ? { nixosOptions: { optionPaths: options.nixosOptionPaths ?? defaultNixOSOptionPaths } } : {}
672
+ })
673
+ ],
674
+ command: "nix"
675
+ });
676
+ const payload = yield* parseProbeOutput(output);
677
+ const document = makeOptionMetadataDocument({
678
+ flake: flakeReference,
679
+ homeManagerInput: includesHomeManager ? options.homeManagerInput : null,
680
+ nixpkgsInput: options.nixpkgsInput,
681
+ scopes,
682
+ system: options.system
683
+ }, payload);
684
+ const outputPath = path.resolve(options.output);
685
+ yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true });
686
+ yield* fs.writeFileString(outputPath, optionMetadataDocumentToJson(document));
687
+ return document;
688
+ });
689
+ const generateProjectOptionTypes = (options) => Effect.gen(function* () {
690
+ const fs = yield* FileSystem.FileSystem;
691
+ const path = yield* Path.Path;
692
+ const inputPath = path.resolve(options.input);
693
+ const outputPath = path.resolve(options.output);
694
+ const document = yield* readOptionMetadataDocument(inputPath);
695
+ const unsupported = collectUnsupportedOptions(document.options);
696
+ if (options.strict === true && unsupported.length > 0) return yield* new UnsupportedOptionsFound({ options: unsupported.map((option) => `${option.scope}:${option.path.join(".")}`) });
697
+ yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true });
698
+ yield* fs.writeFileString(outputPath, renderGeneratedOptions(document));
699
+ return {
700
+ document,
701
+ unsupported
702
+ };
703
+ });
704
+ const readOptionMetadataDocument = (inputPath) => Effect.gen(function* () {
705
+ const text = yield* (yield* FileSystem.FileSystem).readFileString(inputPath);
706
+ return yield* parseMetadataText(text, inputPath);
707
+ });
708
+ const renderGeneratedOptions = (document) => generateOptionTypeFile({
709
+ fixtureScope: `project-local ${document.source.scopes.join(" + ")} options from ${document.source.flake}.`,
710
+ importPath: "typeflake",
711
+ options: document.options,
712
+ roots: rootsForScopes(document.source.scopes)
713
+ });
714
+ const resolveFlakeReference = (flakeRoot) => Effect.gen(function* () {
715
+ const path = yield* Path.Path;
716
+ const gitRoot = yield* findGitRoot(flakeRoot);
717
+ if (gitRoot === null) return `path:${flakeRoot}`;
718
+ const relative = path.relative(gitRoot, flakeRoot);
719
+ const base = `git+file://${gitRoot}`;
720
+ if (relative === "") return base;
721
+ return `${base}?dir=${encodeFlakeDir(relative, path.sep)}`;
722
+ });
723
+ const rootsForScopes = (scopes) => normalizeScopes(scopes).map((scope) => scope === "nixos" ? {
724
+ rootTypeName: "NixOSGeneratedConfig",
725
+ scope
726
+ } : {
727
+ rootTypeName: "HomeManagerGeneratedConfig",
728
+ scope
729
+ });
730
+ const normalizeScopes = (scopes) => {
731
+ const normalized = [...new Set(scopes)];
732
+ return normalized.length === 0 ? defaultOptionScopes : normalized;
733
+ };
734
+ const findGitRoot = (start) => Effect.gen(function* () {
735
+ const fs = yield* FileSystem.FileSystem;
736
+ const path = yield* Path.Path;
737
+ let current = path.resolve(start);
738
+ while (true) {
739
+ if (yield* fs.exists(path.join(current, ".git"))) return current;
740
+ const parent = path.dirname(current);
741
+ if (parent === current) return null;
742
+ current = parent;
743
+ }
744
+ });
745
+ const encodeFlakeDir = (relative, separator) => relative.split(separator).map(encodeURIComponent).join("/");
746
+ const parseProbeOutput = (text) => Effect.try({
747
+ try: () => parseOptionProbePayload(JSON.parse(text)),
748
+ catch: (cause) => new OptionMetadataParseError({ cause })
749
+ });
750
+ const parseMetadataText = (text, path) => Effect.try({
751
+ try: () => parseOptionMetadataDocument(JSON.parse(text)),
752
+ catch: (cause) => new OptionMetadataParseError({
753
+ cause,
754
+ path
755
+ })
756
+ });
757
+ //#endregion
758
+ export { nixExpr as A, sync as C, renderList as D, resolveFlakeSpec as E, DoctorFailed as M, renderNixValue as O, check as S, renderFlake as T, generateOptionTypeFile as _, renderGeneratedOptions as a, doctor as b, optionMetadataDocumentToJson as c, defaultHomeManagerOptionPaths as d, defaultNixOSOptionPaths as f, collectUnsupportedOptions as g, renderOptionProbeExpression as h, readOptionMetadataDocument as i, rawNix as j, nixAttrPath as k, parseOptionMetadataDocument as l, projectFlakeInput as m, generateProjectOptionTypes as n, resolveFlakeReference as o, flakeInput as p, probeProjectOptions as r, makeOptionMetadataDocument as s, defaultOptionScopes as t, parseOptionProbePayload as u, generateOptionTypes as v, Flake as w, renderDoctorReport as x, toTypeScriptType as y };
759
+
760
+ //# sourceMappingURL=options-CE3YO7EL.mjs.map