@teacss/engine 0.8.5-alpha.4 → 0.9.0-alpha.3

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,680 @@
1
+ // Selects the configurations the Rust engine can serve: the first item it cannot translate comes
2
+ // back as `{ ok: false, reason, path }` (v02.9 §4.1); later cards (F7.07-F7.12) append the rest.
3
+ import { readFile } from "node:fs/promises";
4
+ import { CONFIG_ENTRY_PROVENANCE, CONFIG_SOURCE_DECLARATIONS, ENGINE_COMPAT, ENGINE_PACKAGE_VERSION, PRESET_OPTIONS } from "./constants.js";
5
+ import { TeacssEngineError } from "./errors.js";
6
+ export function fail(reason, path) {
7
+ return { ok: false, reason, path };
8
+ }
9
+ export function ok(value) {
10
+ return { ok: true, value };
11
+ }
12
+ /**
13
+ * Port of core/config.ts:48-59: `defaults` underneath, then the user config's own enumerable keys
14
+ * whose value is not `undefined`. Only string keys are copied (v02.9 §4.4); values are defined
15
+ * rather than assigned so a `__proto__` key stays a plain property.
16
+ */
17
+ export function mergeDefinedConfig(defaults, userConfig) {
18
+ const merged = { ...defaults };
19
+ for (const key of Object.keys(userConfig)) {
20
+ const value = userConfig[key];
21
+ if (value === undefined)
22
+ continue;
23
+ Object.defineProperty(merged, key, { value, configurable: true, enumerable: true, writable: true });
24
+ }
25
+ return merged;
26
+ }
27
+ export const KNOWN_KEYS = new Set([
28
+ "presets",
29
+ "shortcuts",
30
+ "safelist",
31
+ "blocklist",
32
+ "theme",
33
+ "layers",
34
+ "content",
35
+ "envMode",
36
+ "unmatched",
37
+ "shortcutsLayer",
38
+ "virtualModulePrefix",
39
+ "outputToCssLayers",
40
+ "mergeSelectors",
41
+ "warn",
42
+ "details",
43
+ "separators",
44
+ "transformers",
45
+ "extractors",
46
+ "extractorDefault",
47
+ "rules",
48
+ "variants",
49
+ "preflights",
50
+ "preprocess",
51
+ "postprocess",
52
+ "extendTheme",
53
+ "configResolved",
54
+ "autocomplete",
55
+ "sortLayers",
56
+ "cli",
57
+ "engine",
58
+ ]);
59
+ /** Keys whose presence with a non-empty value means fallback; `transformers` is checked by name (F7.09). */
60
+ export const DYNAMIC_KEYS = Object.freeze(["rules", "variants", "extractors", "extractorDefault", "preflights", "preprocess", "postprocess", "extendTheme", "configResolved", "autocomplete", "cli"]);
61
+ /** A function, a non-empty array or object, or any other defined non-null value. */
62
+ export function isNonEmpty(value) {
63
+ if (value === undefined || value === null)
64
+ return false;
65
+ if (typeof value === "function")
66
+ return true;
67
+ if (Array.isArray(value))
68
+ return value.length > 0;
69
+ if (typeof value === "object")
70
+ return Object.keys(value).length > 0;
71
+ return true;
72
+ }
73
+ function isDefaultSeparators(separators) {
74
+ return Array.isArray(separators) && separators.length === 2 && separators[0] === ":" && separators[1] === "-";
75
+ }
76
+ /**
77
+ * v02.9 §4.2 step 2: unknown keys, then the dynamic keys in order, then the static options only
78
+ * the TypeScript engine honours. `engine` is a hook option and its value is not checked here.
79
+ */
80
+ export function checkTopLevelKeys(merged) {
81
+ for (const key of Object.keys(merged)) {
82
+ if (!KNOWN_KEYS.has(key))
83
+ return fail("unknown-key", key);
84
+ }
85
+ for (const key of DYNAMIC_KEYS) {
86
+ if (isNonEmpty(merged[key]))
87
+ return fail(key, key);
88
+ }
89
+ if (merged.separators !== undefined && !isDefaultSeparators(merged.separators))
90
+ return fail("separators", "separators");
91
+ if (merged.mergeSelectors === false)
92
+ return fail("mergeSelectors", "mergeSelectors");
93
+ if (merged.details !== undefined)
94
+ return fail("details", "details");
95
+ if (merged.sortLayers !== undefined)
96
+ return fail("sortLayers", "sortLayers");
97
+ return ok(null);
98
+ }
99
+ const isThenable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
100
+ /**
101
+ * The first own accessor property of `object`. Translation reads every field, so a getter would
102
+ * run before TS reads it again (presets.test.ts "snapshots an own preset name exactly once"); an
103
+ * object with accessors falls back without invoking them.
104
+ */
105
+ export function ownAccessor(object) {
106
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(object))) {
107
+ if ("get" in descriptor || "set" in descriptor)
108
+ return key;
109
+ }
110
+ return undefined;
111
+ }
112
+ /**
113
+ * v02.9 §4.2 step 3: nested presets flattened depth first, deduplicated by name keeping the first
114
+ * (core/config.ts:26-36 — a missing name is a name too), then ordered pre, normal, post
115
+ * (config.ts:216-220; any other `enforce` value drops the preset, as there).
116
+ */
117
+ export function flattenPresets(presetsValue) {
118
+ const list = presetsValue === undefined ? [] : Array.isArray(presetsValue) ? presetsValue : [presetsValue];
119
+ const flat = [];
120
+ const visit = (entry, path, ancestors) => {
121
+ if (Array.isArray(entry)) {
122
+ for (let index = 0; index < entry.length; index++) {
123
+ const failure = visit(entry[index], `${path}[${index}]`, ancestors);
124
+ if (failure)
125
+ return failure;
126
+ }
127
+ return undefined;
128
+ }
129
+ if (typeof entry === "function" || isThenable(entry) || entry === null || typeof entry !== "object")
130
+ return fail("preset-not-instantiated", path);
131
+ if (ancestors.has(entry))
132
+ return fail("preset-cycle", path);
133
+ const accessor = ownAccessor(entry);
134
+ if (accessor !== undefined)
135
+ return fail("accessor", `${path}.${accessor}`);
136
+ flat.push(entry);
137
+ const children = Object.hasOwn(entry, "presets") ? entry.presets : undefined;
138
+ if (children === undefined)
139
+ return undefined;
140
+ const nested = Array.isArray(children) ? children : [children];
141
+ const nextAncestors = new Set([...ancestors, entry]);
142
+ for (let index = 0; index < nested.length; index++) {
143
+ const failure = visit(nested[index], `${path}.presets[${index}]`, nextAncestors);
144
+ if (failure)
145
+ return failure;
146
+ }
147
+ return undefined;
148
+ };
149
+ for (let index = 0; index < list.length; index++) {
150
+ const failure = visit(list[index], `presets[${index}]`, new Set());
151
+ if (failure)
152
+ return failure;
153
+ }
154
+ const names = new Set();
155
+ const unique = flat.filter((preset) => {
156
+ const name = preset.name;
157
+ if (names.has(name))
158
+ return false;
159
+ names.add(name);
160
+ return true;
161
+ });
162
+ const enforce = (preset) => preset.enforce;
163
+ return ok([...unique.filter((preset) => enforce(preset) === "pre"), ...unique.filter((preset) => enforce(preset) == null), ...unique.filter((preset) => enforce(preset) === "post")]);
164
+ }
165
+ /** The oracle's `PresetOptions` base (types.ts:53) has no named fields, so these five are Standard's whole option set. */
166
+ export const STANDARD_OPTION_KEYS = new Set(["modeSelectors", "containers", "preflight", "variablePrefix", "compileClass"]);
167
+ export const COMPILE_CLASS_KEYS = ["classPrefix", "alwaysHash", "keepUnknown", "layer"];
168
+ export const ICONS_OPTION_KEYS = ["scale", "mode", "extraProperties", "warn", "layer", "unit"];
169
+ export const BUNDLED_ICON_COLLECTIONS = new Set(["carbon", "crypto", "flagpack", "logo", "lucide"]);
170
+ export const ROLE_PALETTES_PRESET = "@teacss/config/role-palettes";
171
+ export function pick(obj, keys) {
172
+ const out = {};
173
+ for (const key of keys)
174
+ if (obj[key] !== undefined)
175
+ out[key] = obj[key];
176
+ return out;
177
+ }
178
+ const markedOptions = (preset, marker) => Object.getOwnPropertyDescriptor(preset, marker)?.value ?? {};
179
+ /**
180
+ * v02.9 §4.2 step 4, first four items: the marked built-in presets and the role-palettes marker.
181
+ * `ok(null)` means the preset yields no `PresetSpec`; `undefined` means it is not one of these
182
+ * (unmarked presets go to F7.08's static translation).
183
+ * such as `presets[1]`
184
+ */
185
+ export function translateTaggedPreset(preset, path, ctx) {
186
+ if (Object.getOwnPropertyDescriptor(preset, PRESET_OPTIONS.standard) !== undefined) {
187
+ const options = markedOptions(preset, PRESET_OPTIONS.standard);
188
+ for (const key of Object.keys(options)) {
189
+ if (!STANDARD_OPTION_KEYS.has(key))
190
+ return fail("standard.unknown-option", `${path}.options.${key}`);
191
+ }
192
+ const compileClass = options.compileClass;
193
+ if (compileClass !== null && typeof compileClass === "object") {
194
+ if (Object.hasOwn(compileClass, "hashFn"))
195
+ return fail("standard.compileClass.hashFn", `${path}.options.compileClass.hashFn`);
196
+ if (Object.hasOwn(compileClass, "trigger"))
197
+ return fail("standard.compileClass.trigger", `${path}.options.compileClass.trigger`);
198
+ }
199
+ const spec = { kind: "standard", options: pick(options, STANDARD_OPTION_KEYS) };
200
+ if (compileClass !== null && typeof compileClass === "object")
201
+ spec.options.compileClass = pick(compileClass, COMPILE_CLASS_KEYS);
202
+ return ok(spec);
203
+ }
204
+ if (Object.getOwnPropertyDescriptor(preset, PRESET_OPTIONS.icons) !== undefined) {
205
+ const options = markedOptions(preset, PRESET_OPTIONS.icons);
206
+ const collections = Object.keys(options.collections ?? {});
207
+ for (const key of collections) {
208
+ if (!BUNDLED_ICON_COLLECTIONS.has(key))
209
+ return fail("icons.collections", `${path}.options.collections.${key}`);
210
+ }
211
+ if (isNonEmpty(options.customizations))
212
+ return fail("icons.customizations", `${path}.options.customizations`);
213
+ if (options.processor !== undefined)
214
+ return fail("icons.processor", `${path}.options.processor`);
215
+ const spec = { kind: "icons", options: pick(options, ICONS_OPTION_KEYS) };
216
+ if (options.collections !== undefined)
217
+ spec.options.collections = collections;
218
+ return ok(spec);
219
+ }
220
+ if (Object.getOwnPropertyDescriptor(preset, PRESET_OPTIONS.official) !== undefined)
221
+ return ok({ kind: "official" });
222
+ if (preset.name === ROLE_PALETTES_PRESET)
223
+ return ctx.hasEntry ? ok(null) : fail("role-palettes-without-entry", path);
224
+ return undefined;
225
+ }
226
+ /** Keys of an unmarked preset whose non-empty value means fallback. */
227
+ export const PRESET_DYNAMIC_KEYS = Object.freeze(["rules", "variants", "extractors", "preflights", "preprocess", "postprocess", "extendTheme", "configResolved", "transformers", "autocomplete", "extractorDefault", "content"]);
228
+ const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
229
+ /** Values `undefined` are left out. */
230
+ const defined = (entries) => Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== undefined));
231
+ /**
232
+ * v02.9 §4.5: the static shortcut forms as `[name, expansion]` pairs. A shortcut meta (`[n, e, meta]`)
233
+ * has no place in `StaticPreset.shortcuts`, so any meta key falls back.
234
+ *
235
+ */
236
+ export function translateShortcuts(value, path, reasonOnFail) {
237
+ if (value === undefined)
238
+ return ok(undefined);
239
+ const pairs = [];
240
+ const fromObject = (object, objectPath) => {
241
+ for (const [key, expansion] of Object.entries(object)) {
242
+ if (typeof expansion !== "string")
243
+ return fail(reasonOnFail, `${objectPath}.${key}`);
244
+ pairs.push([key, expansion]);
245
+ }
246
+ return undefined;
247
+ };
248
+ if (isPlainObject(value)) {
249
+ const failure = fromObject(value, path);
250
+ if (failure)
251
+ return failure;
252
+ }
253
+ else if (Array.isArray(value)) {
254
+ for (let index = 0; index < value.length; index++) {
255
+ const item = value[index];
256
+ const itemPath = `${path}[${index}]`;
257
+ if (Array.isArray(item)) {
258
+ if (typeof item[0] !== "string" || typeof item[1] !== "string")
259
+ return fail(reasonOnFail, itemPath);
260
+ if (item.length > 2 && item[2] !== undefined) {
261
+ const meta = item[2];
262
+ if (!isPlainObject(meta))
263
+ return fail(reasonOnFail, `${itemPath}[2]`);
264
+ const key = Object.keys(meta)[0];
265
+ if (key !== undefined)
266
+ return fail(reasonOnFail, `${itemPath}[2].${key}`);
267
+ }
268
+ pairs.push([item[0], item[1]]);
269
+ }
270
+ else if (isPlainObject(item)) {
271
+ const failure = fromObject(item, itemPath);
272
+ if (failure)
273
+ return failure;
274
+ }
275
+ else {
276
+ return fail(reasonOnFail, itemPath);
277
+ }
278
+ }
279
+ }
280
+ else {
281
+ return fail(reasonOnFail, path);
282
+ }
283
+ return ok(pairs.length > 0 ? pairs : undefined);
284
+ }
285
+ /**
286
+ * v02.9 §4.5: a theme as dotted keys to string leaves (numbers stringified); empty objects vanish.
287
+ *
288
+ */
289
+ export function flattenTheme(theme, path, reasonOnFail) {
290
+ if (theme === undefined)
291
+ return ok(undefined);
292
+ if (!isPlainObject(theme))
293
+ return fail(reasonOnFail, path);
294
+ // The removed `rolePalettes` theme key is an error in TS (role-palette-snapshot.test.ts).
295
+ const modes = isPlainObject(theme.modes) ? Object.entries(theme.modes) : [];
296
+ if (Object.hasOwn(theme, "rolePalettes"))
297
+ return fail(reasonOnFail, `${path}.rolePalettes`);
298
+ for (const [mode, values] of modes) {
299
+ if (isPlainObject(values) && Object.hasOwn(values, "rolePalettes"))
300
+ return fail(reasonOnFail, `${path}.modes.${mode}.rolePalettes`);
301
+ }
302
+ const flat = {};
303
+ const visit = (object, prefix) => {
304
+ for (const [key, value] of Object.entries(object)) {
305
+ const dotted = prefix ? `${prefix}.${key}` : key;
306
+ if (typeof value === "string")
307
+ flat[dotted] = value;
308
+ else if (typeof value === "number")
309
+ flat[dotted] = String(value);
310
+ else if (isPlainObject(value)) {
311
+ const failure = visit(value, dotted);
312
+ if (failure)
313
+ return failure;
314
+ }
315
+ else
316
+ return fail(reasonOnFail, `${path}.${dotted}`);
317
+ }
318
+ return undefined;
319
+ };
320
+ const failure = visit(theme, "");
321
+ if (failure)
322
+ return failure;
323
+ return ok(Object.keys(flat).length > 0 ? flat : undefined);
324
+ }
325
+ export function translateStringList(list, path, reasonOnFail) {
326
+ if (list === undefined)
327
+ return ok(undefined);
328
+ if (!Array.isArray(list))
329
+ return fail(reasonOnFail, path);
330
+ for (let index = 0; index < list.length; index++) {
331
+ if (typeof list[index] !== "string")
332
+ return fail(reasonOnFail, `${path}[${index}]`);
333
+ }
334
+ return ok(list.length > 0 ? [...list] : undefined);
335
+ }
336
+ export function translateLayers(layers, path, reasonOnFail) {
337
+ if (layers === undefined)
338
+ return ok(undefined);
339
+ if (!isPlainObject(layers))
340
+ return fail(reasonOnFail, path);
341
+ for (const [key, order] of Object.entries(layers)) {
342
+ if (typeof order !== "number")
343
+ return fail(reasonOnFail, `${path}.${key}`);
344
+ }
345
+ return ok({ ...layers });
346
+ }
347
+ /**
348
+ * v02.9 §4.2 step 4, "other objects": an unmarked preset as a `StaticPreset`.
349
+ */
350
+ export function translateStaticPreset(preset, path) {
351
+ if (typeof preset.name !== "string")
352
+ return fail("preset.name", `${path}.name`);
353
+ for (const key of PRESET_DYNAMIC_KEYS) {
354
+ if (isNonEmpty(preset[key]))
355
+ return fail(`preset.${key}`, `${path}.${key}`);
356
+ }
357
+ const shortcuts = translateShortcuts(preset.shortcuts, `${path}.shortcuts`, "preset.shortcuts");
358
+ if (!shortcuts.ok)
359
+ return shortcuts;
360
+ const safelist = translateStringList(preset.safelist, `${path}.safelist`, "preset.safelist");
361
+ if (!safelist.ok)
362
+ return safelist;
363
+ const blocklist = translateStringList(preset.blocklist, `${path}.blocklist`, "preset.blocklist");
364
+ if (!blocklist.ok)
365
+ return blocklist;
366
+ const theme = flattenTheme(preset.theme, `${path}.theme`, "preset.theme");
367
+ if (!theme.ok)
368
+ return theme;
369
+ const layers = translateLayers(preset.layers, `${path}.layers`, "preset.layers");
370
+ if (!layers.ok)
371
+ return layers;
372
+ if (preset.enforce !== undefined && preset.enforce !== "pre" && preset.enforce !== "post")
373
+ return fail("preset.enforce", `${path}.enforce`);
374
+ if (preset.layer !== undefined && typeof preset.layer !== "string")
375
+ return fail("preset.layer", `${path}.layer`);
376
+ return ok({
377
+ kind: "static",
378
+ preset: defined({ name: preset.name, enforce: preset.enforce, layer: preset.layer, layers: layers.value, shortcuts: shortcuts.value, safelist: safelist.value, blocklist: blocklist.value, theme: theme.value }),
379
+ });
380
+ }
381
+ /**
382
+ * v02.9 §4.2 step 5: the merged config's static fields as a `StaticConfig` (possibly empty).
383
+ */
384
+ export function translateInline(merged) {
385
+ const shortcuts = translateShortcuts(merged.shortcuts, "shortcuts", "shortcuts");
386
+ if (!shortcuts.ok)
387
+ return shortcuts;
388
+ const safelist = translateStringList(merged.safelist, "safelist", "safelist");
389
+ if (!safelist.ok)
390
+ return safelist;
391
+ const blocklist = translateStringList(merged.blocklist, "blocklist", "blocklist");
392
+ if (!blocklist.ok)
393
+ return blocklist;
394
+ const theme = flattenTheme(merged.theme, "theme", "theme");
395
+ if (!theme.ok)
396
+ return theme;
397
+ const layers = translateLayers(merged.layers, "layers", "layers");
398
+ if (!layers.ok)
399
+ return layers;
400
+ let content;
401
+ if (merged.content !== undefined) {
402
+ if (!isPlainObject(merged.content))
403
+ return fail("content", "content");
404
+ if (Object.hasOwn(merged.content, "pipeline") && merged.content.pipeline !== undefined)
405
+ return fail("content.pipeline", "content.pipeline");
406
+ if (isNonEmpty(merged.content.inline))
407
+ return fail("content.inline", "content.inline");
408
+ const filesystem = merged.content.filesystem;
409
+ if (filesystem !== undefined) {
410
+ if (!Array.isArray(filesystem) || filesystem.some((pattern) => typeof pattern !== "string"))
411
+ return fail("content", "content.filesystem");
412
+ content = { filesystem: [...filesystem] };
413
+ }
414
+ }
415
+ return ok(defined({ shortcuts: shortcuts.value, safelist: safelist.value, blocklist: blocklist.value, theme: theme.value, layers: layers.value, content }));
416
+ }
417
+ export const ALLOWED_TRANSFORMER_NAMES = new Set(["@teacss/transformer-compile-class", "@teacss/directives"]);
418
+ /**
419
+ * v02.2 §4: allow-listed transformers stay with the host (kept in the facade's `config.transformers`)
420
+ * and never reach Rust; any other name falls back. Repeated names are kept in order; the config view
421
+ * applies `uniqueNamed` later (F7.20). `@teacss/directives` options live in its closure and are
422
+ * checked per call (v02.9 §6.3), so none are read here.
423
+ */
424
+ export function translateTransformers(list) {
425
+ if (list === undefined || list === null)
426
+ return ok([]);
427
+ if (!Array.isArray(list))
428
+ return fail("transformers", "transformers");
429
+ const kept = [];
430
+ for (let index = 0; index < list.length; index++) {
431
+ const transformer = list[index];
432
+ if (!transformer || typeof transformer !== "object" || typeof transformer.name !== "string" || !ALLOWED_TRANSFORMER_NAMES.has(transformer.name)) {
433
+ return fail("transformers", `transformers[${index}]`);
434
+ }
435
+ kept.push(transformer);
436
+ }
437
+ return ok(kept);
438
+ }
439
+ export const scanners = {
440
+ load: null,
441
+ };
442
+ async function loadScanner() {
443
+ if (scanners.load)
444
+ return scanners.load();
445
+ // biome-ignore lint/suspicious/noTsIgnore: the optional peer may or may not be installed
446
+ // @ts-ignore optional peer: @teacss/config may be absent at compile time
447
+ return import("@teacss/config/internal/source-scan");
448
+ }
449
+ /**
450
+ * Entry source and declaration files (v02.9 §4.4). The symbols are read from `userConfig`, not
451
+ * `merged`: `mergeDefinedConfig` copies no symbols.
452
+ */
453
+ export async function resolveEntrySource(userConfig, merged) {
454
+ const provenance = userConfig[CONFIG_ENTRY_PROVENANCE];
455
+ const state = userConfig[CONFIG_SOURCE_DECLARATIONS];
456
+ if (!provenance || typeof provenance.entryFile !== "string")
457
+ return ok({ root: process.cwd(), entry: null, declarationFiles: [], derivedKeys: [] });
458
+ const root = provenance.base;
459
+ const entry = { path: provenance.entryFile, css: provenance.entryCss ?? null };
460
+ const derivedKeys = Array.isArray(provenance.derivedKeys) ? [...new Set(provenance.derivedKeys)] : [];
461
+ let declarationFiles = [];
462
+ const content = merged.content;
463
+ if (state?.consumed?.status === "read") {
464
+ const members = state.consumed.members;
465
+ // Rust reads declaration files from disk; text resolved from an unsaved override
466
+ // (`resolveEntryConfig(..., fileContents)`) only the TS engine can serve.
467
+ for (const member of members) {
468
+ if (typeof member.contents !== "string")
469
+ continue;
470
+ const onDisk = await readFile(member.path, "utf8").catch(() => null);
471
+ if (onDisk !== member.contents)
472
+ return fail("declarations-unsaved", member.path);
473
+ }
474
+ declarationFiles = members.map((member) => member.path);
475
+ }
476
+ else if (content != null && typeof content === "object" && Array.isArray(content.filesystem)) {
477
+ const filesystem = content.filesystem;
478
+ const includes = filesystem.filter((glob) => !glob.startsWith("!"));
479
+ const excludes = filesystem.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1));
480
+ let files;
481
+ try {
482
+ const { scanSourceFiles } = await loadScanner();
483
+ files = await scanSourceFiles(includes, { cwd: root, ignore: excludes, expandDirectories: false });
484
+ }
485
+ catch {
486
+ return fail("declarations-unavailable", "content.filesystem");
487
+ }
488
+ declarationFiles = files.filter((file) => file.endsWith(".css") && file !== entry.path).sort();
489
+ }
490
+ return ok({ root, entry, declarationFiles, derivedKeys });
491
+ }
492
+ const ENV_MODES = new Set(["dev", "build"]);
493
+ const UNMATCHED_MODES = new Set(["warn", "error", "off"]);
494
+ function translateScalars(merged) {
495
+ const envMode = merged.envMode ?? "build";
496
+ if (!ENV_MODES.has(envMode))
497
+ return fail("envMode", "envMode");
498
+ const unmatched = merged.unmatched ?? "off";
499
+ if (!UNMATCHED_MODES.has(unmatched))
500
+ return fail("unmatched", "unmatched");
501
+ const layers = merged.outputToCssLayers;
502
+ let outputToCssLayers;
503
+ if (layers === undefined)
504
+ outputToCssLayers = null;
505
+ else if (typeof layers === "boolean")
506
+ outputToCssLayers = layers;
507
+ else if (isPlainObject(layers)) {
508
+ if (Object.hasOwn(layers, "cssLayerName") && layers.cssLayerName !== undefined)
509
+ return fail("outputToCssLayers.cssLayerName", "outputToCssLayers.cssLayerName");
510
+ outputToCssLayers = { allLayers: Boolean(layers.allLayers) };
511
+ }
512
+ else
513
+ return fail("outputToCssLayers", "outputToCssLayers");
514
+ return ok({
515
+ envMode,
516
+ unmatched,
517
+ outputToCssLayers,
518
+ shortcutsLayer: typeof merged.shortcutsLayer === "string" ? merged.shortcutsLayer : null,
519
+ virtualModulePrefix: typeof merged.virtualModulePrefix === "string" ? merged.virtualModulePrefix : null,
520
+ });
521
+ }
522
+ async function translate(userConfig, defaults) {
523
+ for (const object of [userConfig, defaults]) {
524
+ const accessor = object !== null && typeof object === "object" ? ownAccessor(object) : undefined;
525
+ if (accessor !== undefined)
526
+ return fail("accessor", accessor);
527
+ }
528
+ const merged = mergeDefinedConfig(defaults, userConfig);
529
+ delete merged.engine;
530
+ const keys = checkTopLevelKeys(merged);
531
+ if (!keys.ok)
532
+ return keys;
533
+ const source = await resolveEntrySource(userConfig, merged);
534
+ if (!source.ok)
535
+ return source;
536
+ const hasEntry = source.value.entry !== null;
537
+ const flat = flattenPresets(merged.presets);
538
+ if (!flat.ok)
539
+ return flat;
540
+ const presetSpecs = [];
541
+ for (let index = 0; index < flat.value.length; index++) {
542
+ const preset = flat.value[index];
543
+ const path = `presets[${index}]`;
544
+ const spec = translateTaggedPreset(preset, path, { hasEntry }) ?? translateStaticPreset(preset, path);
545
+ if (!spec.ok)
546
+ return spec;
547
+ if (spec.value !== null)
548
+ presetSpecs.push(spec.value);
549
+ }
550
+ const presets = flat.value.filter((preset) => preset.name !== ROLE_PALETTES_PRESET);
551
+ const transformers = translateTransformers(merged.transformers);
552
+ if (!transformers.ok)
553
+ return transformers;
554
+ const inline = translateInline(merged);
555
+ if (!inline.ok)
556
+ return inline;
557
+ for (const key of source.value.derivedKeys)
558
+ delete inline.value[key];
559
+ const scalars = translateScalars(merged);
560
+ if (!scalars.ok)
561
+ return scalars;
562
+ const { root, entry, declarationFiles } = source.value;
563
+ const request = {
564
+ root,
565
+ entry,
566
+ presets: presetSpecs,
567
+ declarationFiles,
568
+ inline: Object.keys(inline.value).length === 0 ? null : inline.value,
569
+ ...scalars.value,
570
+ };
571
+ return { ok: true, request, presets, transformers: transformers.value };
572
+ }
573
+ export async function translateConfig(userConfig = {}, defaults = {}) {
574
+ const outcome = await translate(userConfig, defaults);
575
+ debugTranslate(outcome, userConfig);
576
+ // `translate` builds the request field by field; its shape is v02.2 §3 `LoadProjectRequest`.
577
+ return outcome;
578
+ }
579
+ /**
580
+ * v02.9 §9: with `TEACSS_ENGINE_DEBUG=1`, the request or the failure goes to stderr.
581
+ */
582
+ export function debugTranslate(outcome, userConfig) {
583
+ if (process.env.TEACSS_ENGINE_DEBUG !== "1")
584
+ return;
585
+ if (outcome.ok) {
586
+ process.stderr.write(`[teacss] engine: request for ${outcome.request.entry?.path ?? "<inline>"}\n`);
587
+ process.stderr.write(`${JSON.stringify(outcome.request, null, 2)}\n`);
588
+ }
589
+ else {
590
+ process.stderr.write(`[teacss] engine: translate failed ${outcome.reason} at ${outcome.path} (keys: ${Object.keys(userConfig).join(", ")})\n`);
591
+ }
592
+ }
593
+ export const VALID_MODES = new Set(["auto", "rust", "ts"]);
594
+ const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
595
+ function parseSemver(value) {
596
+ const match = SEMVER_RE.exec(value);
597
+ if (!match)
598
+ throw new TypeError(`[@teacss/engine] invalid semver: ${value}`);
599
+ return { core: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease: match[4] === undefined ? undefined : match[4].split(".") };
600
+ }
601
+ /**
602
+ * SemVer 2.0 precedence: the numeric triple, then a release above any of its prereleases, then
603
+ * the prerelease identifiers in order (numeric ones by value and below alphanumeric ones, a shorter
604
+ * list first when one is a prefix of the other).
605
+ */
606
+ export function compareSemver(a, b) {
607
+ const left = parseSemver(a);
608
+ const right = parseSemver(b);
609
+ for (let index = 0; index < 3; index++) {
610
+ if (left.core[index] !== right.core[index])
611
+ return left.core[index] < right.core[index] ? -1 : 1;
612
+ }
613
+ if (!left.prerelease || !right.prerelease)
614
+ return left.prerelease === right.prerelease ? 0 : left.prerelease ? -1 : 1;
615
+ const length = Math.min(left.prerelease.length, right.prerelease.length);
616
+ for (let index = 0; index < length; index++) {
617
+ const x = left.prerelease[index];
618
+ const y = right.prerelease[index];
619
+ if (x === y)
620
+ continue;
621
+ const xNumeric = /^\d+$/.test(x);
622
+ const yNumeric = /^\d+$/.test(y);
623
+ if (xNumeric && yNumeric)
624
+ return Number(x) < Number(y) ? -1 : 1;
625
+ if (xNumeric !== yNumeric)
626
+ return xNumeric ? -1 : 1;
627
+ return x < y ? -1 : 1;
628
+ }
629
+ if (left.prerelease.length === right.prerelease.length)
630
+ return 0;
631
+ return left.prerelease.length < right.prerelease.length ? -1 : 1;
632
+ }
633
+ /** Only the `>=A <B` form `engineCompat.core` uses (v02.11 §7.3). */
634
+ export function satisfiesRange(version, range) {
635
+ const parts = range.trim().split(/\s+/);
636
+ const [lower, upper] = parts;
637
+ if (parts.length !== 2 || !lower?.startsWith(">=") || !upper?.startsWith("<") || upper.startsWith("<="))
638
+ throw new TypeError(`[@teacss/engine] unsupported engineCompat range: ${range}`);
639
+ return compareSemver(version, lower.slice(2)) >= 0 && compareSemver(version, upper.slice(1)) < 0;
640
+ }
641
+ /**
642
+ * v02.9 §8.1: `TEACSS_ENGINE` over the hook option over `"ts"`; an `"auto"` option stays on TS while
643
+ * `defaultMode` is `"opt-in"`; a host core outside `engineCompat.core` keeps TS, or throws when
644
+ * Rust was asked for.
645
+ *
646
+ */
647
+ export function selectEngineRequest({ hostCoreVersion, option, env, compat = ENGINE_COMPAT }) {
648
+ let mode;
649
+ let source;
650
+ let invalidEnv;
651
+ if (env !== undefined && env !== "" && VALID_MODES.has(env)) {
652
+ mode = env;
653
+ source = "env";
654
+ }
655
+ else {
656
+ if (env !== undefined && env !== "")
657
+ invalidEnv = env;
658
+ if (typeof option === "string" && VALID_MODES.has(option)) {
659
+ mode = option;
660
+ source = "option";
661
+ }
662
+ else {
663
+ mode = "ts";
664
+ source = "default";
665
+ }
666
+ }
667
+ const base = invalidEnv === undefined ? { mode, source } : { mode, source, invalidEnv };
668
+ if (mode === "ts")
669
+ return { engine: "ts", ...base };
670
+ if (mode === "auto" && source === "option" && compat.defaultMode === "opt-in")
671
+ return { engine: "ts", ...base, resolvedBy: "opt-in" };
672
+ if (compat.core && typeof hostCoreVersion === "string" && !satisfiesRange(hostCoreVersion, compat.core)) {
673
+ const detail = { hostCoreVersion, engineVersion: ENGINE_PACKAGE_VERSION, range: compat.core };
674
+ if (mode === "rust")
675
+ throw new TeacssEngineError("E_ENGINE_COMPAT", `[@teacss/engine] @teacss/core ${hostCoreVersion} is outside engineCompat.core ${compat.core} (engine ${ENGINE_PACKAGE_VERSION})`, detail);
676
+ return { engine: "ts", ...base, reason: "engine-compat", path: "@teacss/core", detail };
677
+ }
678
+ return { engine: "rust", ...base };
679
+ }
680
+ //# sourceMappingURL=select.js.map