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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/bin/typeflake.js +3 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +123 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +341 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +43 -0
- package/dist/index.mjs.map +1 -0
- package/dist/options-CE3YO7EL.mjs +760 -0
- package/dist/options-CE3YO7EL.mjs.map +1 -0
- package/package.json +79 -0
|
@@ -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
|