@teacss/engine 0.9.4 → 0.10.2

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/dist/select.js DELETED
@@ -1,741 +0,0 @@
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; `content` is checked by `hasContentBeyondPipeline`. */
227
- export const PRESET_DYNAMIC_KEYS = Object.freeze(["rules", "variants", "extractors", "preflights", "preprocess", "postprocess", "extendTheme", "configResolved", "transformers", "autocomplete", "extractorDefault"]);
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
- * `content.pipeline` only filters the modules an integration feeds in; generation never reads it
233
- * (v02.2 §4), so it stays in the config view and never reaches Rust. Any other content field of a
234
- * preset is not translated.
235
- */
236
- function hasContentBeyondPipeline(content) {
237
- if (!isNonEmpty(content))
238
- return false;
239
- if (!isPlainObject(content))
240
- return true;
241
- return Object.keys(content).some((key) => key !== "pipeline" && isNonEmpty(content[key]));
242
- }
243
- /**
244
- * v02.9 §4.5: the static shortcut forms as `[name, expansion]` pairs. A shortcut meta (`[n, e, meta]`)
245
- * has no place in `StaticPreset.shortcuts`, so any meta key falls back.
246
- *
247
- */
248
- export function translateShortcuts(value, path, reasonOnFail) {
249
- if (value === undefined)
250
- return ok(undefined);
251
- const pairs = [];
252
- const fromObject = (object, objectPath) => {
253
- for (const [key, expansion] of Object.entries(object)) {
254
- if (typeof expansion !== "string")
255
- return fail(reasonOnFail, `${objectPath}.${key}`);
256
- pairs.push([key, expansion]);
257
- }
258
- return undefined;
259
- };
260
- if (isPlainObject(value)) {
261
- const failure = fromObject(value, path);
262
- if (failure)
263
- return failure;
264
- }
265
- else if (Array.isArray(value)) {
266
- for (let index = 0; index < value.length; index++) {
267
- const item = value[index];
268
- const itemPath = `${path}[${index}]`;
269
- if (Array.isArray(item)) {
270
- if (typeof item[0] !== "string" || typeof item[1] !== "string")
271
- return fail(reasonOnFail, itemPath);
272
- if (item.length > 2 && item[2] !== undefined) {
273
- const meta = item[2];
274
- if (!isPlainObject(meta))
275
- return fail(reasonOnFail, `${itemPath}[2]`);
276
- const key = Object.keys(meta)[0];
277
- if (key !== undefined)
278
- return fail(reasonOnFail, `${itemPath}[2].${key}`);
279
- }
280
- pairs.push([item[0], item[1]]);
281
- }
282
- else if (isPlainObject(item)) {
283
- const failure = fromObject(item, itemPath);
284
- if (failure)
285
- return failure;
286
- }
287
- else {
288
- return fail(reasonOnFail, itemPath);
289
- }
290
- }
291
- }
292
- else {
293
- return fail(reasonOnFail, path);
294
- }
295
- return ok(pairs.length > 0 ? pairs : undefined);
296
- }
297
- /** context.ts `isContextName` behind its falsy check: no NUL and no lone surrogate. */
298
- function isContextName(value) {
299
- if (!value)
300
- return false;
301
- if (typeof value !== "string")
302
- return true;
303
- for (let index = 0; index < value.length; index++) {
304
- const code = value.charCodeAt(index);
305
- if (code === 0)
306
- return false;
307
- if (code >= 0xd800 && code <= 0xdbff) {
308
- const next = value.charCodeAt(++index);
309
- if (!(next >= 0xdc00 && next <= 0xdfff))
310
- return false;
311
- }
312
- else if (code >= 0xdc00 && code <= 0xdfff)
313
- return false;
314
- }
315
- return true;
316
- }
317
- /**
318
- * v02.9 §4.5: a theme as dotted keys to string leaves (numbers stringified); empty objects vanish.
319
- *
320
- */
321
- export function flattenTheme(theme, path, reasonOnFail) {
322
- if (theme === undefined)
323
- return ok(undefined);
324
- if (!isPlainObject(theme))
325
- return fail(reasonOnFail, path);
326
- // The removed `rolePalettes` theme key is an error in TS (role-palette-snapshot.test.ts).
327
- const modes = isPlainObject(theme.modes) ? Object.entries(theme.modes) : [];
328
- if (Object.hasOwn(theme, "rolePalettes"))
329
- return fail(reasonOnFail, `${path}.rolePalettes`);
330
- for (const [mode, values] of modes) {
331
- if (isPlainObject(values) && Object.hasOwn(values, "rolePalettes"))
332
- return fail(reasonOnFail, `${path}.modes.${mode}.rolePalettes`);
333
- }
334
- // Standard's `validateContextNames` (context.ts) throws on these. Flattening drops an empty
335
- // object and the binding replaces lone surrogates, so Rust cannot see them.
336
- if (Object.hasOwn(theme, "themes"))
337
- return fail(reasonOnFail, `${path}.themes`);
338
- if (Object.hasOwn(theme, "defaultMode") && !isContextName(theme.defaultMode))
339
- return fail(reasonOnFail, `${path}.defaultMode`);
340
- for (const [mode] of modes) {
341
- if (!isContextName(mode))
342
- return fail(reasonOnFail, `${path}.modes.${mode}`);
343
- }
344
- const flat = {};
345
- const visit = (object, prefix) => {
346
- for (const [key, value] of Object.entries(object)) {
347
- const dotted = prefix ? `${prefix}.${key}` : key;
348
- if (typeof value === "string")
349
- flat[dotted] = value;
350
- else if (typeof value === "number")
351
- flat[dotted] = String(value);
352
- else if (isPlainObject(value)) {
353
- const failure = visit(value, dotted);
354
- if (failure)
355
- return failure;
356
- }
357
- else
358
- return fail(reasonOnFail, `${path}.${dotted}`);
359
- }
360
- return undefined;
361
- };
362
- const failure = visit(theme, "");
363
- if (failure)
364
- return failure;
365
- return ok(Object.keys(flat).length > 0 ? flat : undefined);
366
- }
367
- export function translateStringList(list, path, reasonOnFail) {
368
- if (list === undefined)
369
- return ok(undefined);
370
- if (!Array.isArray(list))
371
- return fail(reasonOnFail, path);
372
- for (let index = 0; index < list.length; index++) {
373
- if (typeof list[index] !== "string")
374
- return fail(reasonOnFail, `${path}[${index}]`);
375
- }
376
- return ok(list.length > 0 ? [...list] : undefined);
377
- }
378
- export function translateLayers(layers, path, reasonOnFail) {
379
- if (layers === undefined)
380
- return ok(undefined);
381
- if (!isPlainObject(layers))
382
- return fail(reasonOnFail, path);
383
- for (const [key, order] of Object.entries(layers)) {
384
- if (typeof order !== "number")
385
- return fail(reasonOnFail, `${path}.${key}`);
386
- }
387
- return ok({ ...layers });
388
- }
389
- /**
390
- * v02.9 §4.2 step 4, "other objects": an unmarked preset as a `StaticPreset`.
391
- */
392
- export function translateStaticPreset(preset, path) {
393
- if (typeof preset.name !== "string")
394
- return fail("preset.name", `${path}.name`);
395
- for (const key of PRESET_DYNAMIC_KEYS) {
396
- if (isNonEmpty(preset[key]))
397
- return fail(`preset.${key}`, `${path}.${key}`);
398
- }
399
- if (hasContentBeyondPipeline(preset.content))
400
- return fail("preset.content", `${path}.content`);
401
- const shortcuts = translateShortcuts(preset.shortcuts, `${path}.shortcuts`, "preset.shortcuts");
402
- if (!shortcuts.ok)
403
- return shortcuts;
404
- const safelist = translateStringList(preset.safelist, `${path}.safelist`, "preset.safelist");
405
- if (!safelist.ok)
406
- return safelist;
407
- const blocklist = translateStringList(preset.blocklist, `${path}.blocklist`, "preset.blocklist");
408
- if (!blocklist.ok)
409
- return blocklist;
410
- const theme = flattenTheme(preset.theme, `${path}.theme`, "preset.theme");
411
- if (!theme.ok)
412
- return theme;
413
- const layers = translateLayers(preset.layers, `${path}.layers`, "preset.layers");
414
- if (!layers.ok)
415
- return layers;
416
- if (preset.enforce !== undefined && preset.enforce !== "pre" && preset.enforce !== "post")
417
- return fail("preset.enforce", `${path}.enforce`);
418
- if (preset.layer !== undefined && typeof preset.layer !== "string")
419
- return fail("preset.layer", `${path}.layer`);
420
- return ok({
421
- kind: "static",
422
- 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 }),
423
- });
424
- }
425
- /**
426
- * v02.9 §4.2 step 5: the merged config's static fields as a `StaticConfig` (possibly empty).
427
- */
428
- export function translateInline(merged) {
429
- const shortcuts = translateShortcuts(merged.shortcuts, "shortcuts", "shortcuts");
430
- if (!shortcuts.ok)
431
- return shortcuts;
432
- const safelist = translateStringList(merged.safelist, "safelist", "safelist");
433
- if (!safelist.ok)
434
- return safelist;
435
- const blocklist = translateStringList(merged.blocklist, "blocklist", "blocklist");
436
- if (!blocklist.ok)
437
- return blocklist;
438
- const theme = flattenTheme(merged.theme, "theme", "theme");
439
- if (!theme.ok)
440
- return theme;
441
- const layers = translateLayers(merged.layers, "layers", "layers");
442
- if (!layers.ok)
443
- return layers;
444
- let content;
445
- if (merged.content !== undefined) {
446
- if (!isPlainObject(merged.content))
447
- return fail("content", "content");
448
- if (isNonEmpty(merged.content.inline))
449
- return fail("content.inline", "content.inline");
450
- const filesystem = merged.content.filesystem;
451
- if (filesystem !== undefined) {
452
- if (!Array.isArray(filesystem) || filesystem.some((pattern) => typeof pattern !== "string"))
453
- return fail("content", "content.filesystem");
454
- content = { filesystem: [...filesystem] };
455
- }
456
- }
457
- return ok(defined({ shortcuts: shortcuts.value, safelist: safelist.value, blocklist: blocklist.value, theme: theme.value, layers: layers.value, content }));
458
- }
459
- export const ALLOWED_TRANSFORMER_NAMES = new Set(["@teacss/transformer-compile-class", "@teacss/directives"]);
460
- /**
461
- * v02.2 §4: allow-listed transformers stay with the host (kept in the facade's `config.transformers`)
462
- * and never reach Rust; any other name falls back. Repeated names are kept in order; the config view
463
- * applies `uniqueNamed` later (F7.20). `@teacss/directives` options live in its closure and are
464
- * checked per call (v02.9 §6.3), so none are read here.
465
- */
466
- export function translateTransformers(list) {
467
- if (list === undefined || list === null)
468
- return ok([]);
469
- if (!Array.isArray(list))
470
- return fail("transformers", "transformers");
471
- const kept = [];
472
- for (let index = 0; index < list.length; index++) {
473
- const transformer = list[index];
474
- if (!transformer || typeof transformer !== "object" || typeof transformer.name !== "string" || !ALLOWED_TRANSFORMER_NAMES.has(transformer.name)) {
475
- return fail("transformers", `transformers[${index}]`);
476
- }
477
- kept.push(transformer);
478
- }
479
- return ok(kept);
480
- }
481
- /**
482
- * A config's own `transformerCompileClass(options)` keeps its options in a closure the facade
483
- * cannot read, and it registers classes by pushing onto `config.shortcuts`, which never reaches
484
- * the Engine. It is harmless only when the last Standard preset enables compileClass: the facade's
485
- * Rust transformer then comes first and `uniqueNamed` drops it, as the oracle drops it after the
486
- * preset's own. Otherwise it would run and its classes would generate no CSS, so it falls back.
487
- */
488
- export function hostCompileClass(transformers, presetSpecs) {
489
- const index = transformers.findIndex((transformer) => transformer.name === "@teacss/transformer-compile-class");
490
- if (index < 0)
491
- return undefined;
492
- const standard = presetSpecs.findLast((spec) => spec.kind === "standard");
493
- if (standard && standard.options.compileClass !== false)
494
- return undefined;
495
- return fail("transformers.compile-class", `transformers[${index}]`);
496
- }
497
- export const scanners = {
498
- load: null,
499
- };
500
- async function loadScanner() {
501
- if (scanners.load)
502
- return scanners.load();
503
- // biome-ignore lint/suspicious/noTsIgnore: the optional peer may or may not be installed
504
- // @ts-ignore optional peer: @teacss/config may be absent at compile time
505
- return import("@teacss/config/internal/source-scan");
506
- }
507
- /**
508
- * Entry source and declaration files (v02.9 §4.4). The symbols are read from `userConfig`, not
509
- * `merged`: `mergeDefinedConfig` copies no symbols.
510
- */
511
- export async function resolveEntrySource(userConfig, merged) {
512
- const provenance = userConfig[CONFIG_ENTRY_PROVENANCE];
513
- const state = userConfig[CONFIG_SOURCE_DECLARATIONS];
514
- if (!provenance || typeof provenance.entryFile !== "string")
515
- return ok({ root: process.cwd(), entry: null, declarationFiles: [], derivedKeys: [] });
516
- const root = provenance.base;
517
- const entry = { path: provenance.entryFile, css: provenance.entryCss ?? null };
518
- const derivedKeys = Array.isArray(provenance.derivedKeys) ? [...new Set(provenance.derivedKeys)] : [];
519
- let declarationFiles = [];
520
- const content = merged.content;
521
- if (state?.consumed?.status === "read") {
522
- const members = state.consumed.members;
523
- // Rust reads declaration files from disk; text resolved from an unsaved override
524
- // (`resolveEntryConfig(..., fileContents)`) only the TS engine can serve.
525
- for (const member of members) {
526
- if (typeof member.contents !== "string")
527
- continue;
528
- const onDisk = await readFile(member.path, "utf8").catch(() => null);
529
- if (onDisk !== member.contents)
530
- return fail("declarations-unsaved", member.path);
531
- }
532
- declarationFiles = members.map((member) => member.path);
533
- }
534
- else if (content != null && typeof content === "object" && Array.isArray(content.filesystem)) {
535
- const filesystem = content.filesystem;
536
- const includes = filesystem.filter((glob) => !glob.startsWith("!"));
537
- const excludes = filesystem.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1));
538
- let files;
539
- try {
540
- const { scanSourceFiles } = await loadScanner();
541
- files = await scanSourceFiles(includes, { cwd: root, ignore: excludes, expandDirectories: false });
542
- }
543
- catch {
544
- return fail("declarations-unavailable", "content.filesystem");
545
- }
546
- declarationFiles = files.filter((file) => file.endsWith(".css") && file !== entry.path).sort();
547
- }
548
- return ok({ root, entry, declarationFiles, derivedKeys });
549
- }
550
- const ENV_MODES = new Set(["dev", "build"]);
551
- const UNMATCHED_MODES = new Set(["warn", "error", "off"]);
552
- function translateScalars(merged) {
553
- const envMode = merged.envMode ?? "build";
554
- if (!ENV_MODES.has(envMode))
555
- return fail("envMode", "envMode");
556
- const unmatched = merged.unmatched ?? "off";
557
- if (!UNMATCHED_MODES.has(unmatched))
558
- return fail("unmatched", "unmatched");
559
- const layers = merged.outputToCssLayers;
560
- let outputToCssLayers;
561
- if (layers === undefined)
562
- outputToCssLayers = null;
563
- else if (typeof layers === "boolean")
564
- outputToCssLayers = layers;
565
- else if (isPlainObject(layers)) {
566
- if (Object.hasOwn(layers, "cssLayerName") && layers.cssLayerName !== undefined)
567
- return fail("outputToCssLayers.cssLayerName", "outputToCssLayers.cssLayerName");
568
- outputToCssLayers = { allLayers: Boolean(layers.allLayers) };
569
- }
570
- else
571
- return fail("outputToCssLayers", "outputToCssLayers");
572
- return ok({
573
- envMode,
574
- unmatched,
575
- outputToCssLayers,
576
- shortcutsLayer: typeof merged.shortcutsLayer === "string" ? merged.shortcutsLayer : null,
577
- virtualModulePrefix: typeof merged.virtualModulePrefix === "string" ? merged.virtualModulePrefix : null,
578
- });
579
- }
580
- async function translate(userConfig, defaults) {
581
- for (const object of [userConfig, defaults]) {
582
- const accessor = object !== null && typeof object === "object" ? ownAccessor(object) : undefined;
583
- if (accessor !== undefined)
584
- return fail("accessor", accessor);
585
- }
586
- const merged = mergeDefinedConfig(defaults, userConfig);
587
- delete merged.engine;
588
- const keys = checkTopLevelKeys(merged);
589
- if (!keys.ok)
590
- return keys;
591
- const source = await resolveEntrySource(userConfig, merged);
592
- if (!source.ok)
593
- return source;
594
- const hasEntry = source.value.entry !== null;
595
- const flat = flattenPresets(merged.presets);
596
- if (!flat.ok)
597
- return flat;
598
- const presetSpecs = [];
599
- for (let index = 0; index < flat.value.length; index++) {
600
- const preset = flat.value[index];
601
- const path = `presets[${index}]`;
602
- const spec = translateTaggedPreset(preset, path, { hasEntry }) ?? translateStaticPreset(preset, path);
603
- if (!spec.ok)
604
- return spec;
605
- if (spec.value !== null)
606
- presetSpecs.push(spec.value);
607
- }
608
- const presets = flat.value.filter((preset) => preset.name !== ROLE_PALETTES_PRESET);
609
- const transformers = translateTransformers(merged.transformers);
610
- if (!transformers.ok)
611
- return transformers;
612
- const compileClass = hostCompileClass(transformers.value, presetSpecs);
613
- if (compileClass)
614
- return compileClass;
615
- const inline = translateInline(merged);
616
- if (!inline.ok)
617
- return inline;
618
- for (const key of source.value.derivedKeys)
619
- delete inline.value[key];
620
- const scalars = translateScalars(merged);
621
- if (!scalars.ok)
622
- return scalars;
623
- const { root, entry, declarationFiles } = source.value;
624
- const request = {
625
- root,
626
- entry,
627
- presets: presetSpecs,
628
- declarationFiles,
629
- inline: Object.keys(inline.value).length === 0 ? null : inline.value,
630
- ...scalars.value,
631
- };
632
- return { ok: true, request, presets, transformers: transformers.value };
633
- }
634
- export async function translateConfig(userConfig = {}, defaults = {}) {
635
- const outcome = await translate(userConfig, defaults);
636
- debugTranslate(outcome, userConfig);
637
- // `translate` builds the request field by field; its shape is v02.2 §3 `LoadProjectRequest`.
638
- return outcome;
639
- }
640
- /**
641
- * v02.9 §9: with `TEACSS_ENGINE_DEBUG=1`, the request or the failure goes to stderr.
642
- */
643
- export function debugTranslate(outcome, userConfig) {
644
- if (process.env.TEACSS_ENGINE_DEBUG !== "1")
645
- return;
646
- if (outcome.ok) {
647
- process.stderr.write(`[teacss] engine: request for ${outcome.request.entry?.path ?? "<inline>"}\n`);
648
- process.stderr.write(`${JSON.stringify(outcome.request, null, 2)}\n`);
649
- }
650
- else {
651
- process.stderr.write(`[teacss] engine: translate failed ${outcome.reason} at ${outcome.path} (keys: ${Object.keys(userConfig).join(", ")})\n`);
652
- }
653
- }
654
- export const VALID_MODES = new Set(["auto", "rust", "ts"]);
655
- const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
656
- function parseSemver(value) {
657
- const match = SEMVER_RE.exec(value);
658
- if (!match)
659
- throw new TypeError(`[@teacss/engine] invalid semver: ${value}`);
660
- return { core: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease: match[4] === undefined ? undefined : match[4].split(".") };
661
- }
662
- /**
663
- * SemVer 2.0 precedence: the numeric triple, then a release above any of its prereleases, then
664
- * the prerelease identifiers in order (numeric ones by value and below alphanumeric ones, a shorter
665
- * list first when one is a prefix of the other).
666
- */
667
- export function compareSemver(a, b) {
668
- const left = parseSemver(a);
669
- const right = parseSemver(b);
670
- for (let index = 0; index < 3; index++) {
671
- if (left.core[index] !== right.core[index])
672
- return left.core[index] < right.core[index] ? -1 : 1;
673
- }
674
- if (!left.prerelease || !right.prerelease)
675
- return left.prerelease === right.prerelease ? 0 : left.prerelease ? -1 : 1;
676
- const length = Math.min(left.prerelease.length, right.prerelease.length);
677
- for (let index = 0; index < length; index++) {
678
- const x = left.prerelease[index];
679
- const y = right.prerelease[index];
680
- if (x === y)
681
- continue;
682
- const xNumeric = /^\d+$/.test(x);
683
- const yNumeric = /^\d+$/.test(y);
684
- if (xNumeric && yNumeric)
685
- return Number(x) < Number(y) ? -1 : 1;
686
- if (xNumeric !== yNumeric)
687
- return xNumeric ? -1 : 1;
688
- return x < y ? -1 : 1;
689
- }
690
- if (left.prerelease.length === right.prerelease.length)
691
- return 0;
692
- return left.prerelease.length < right.prerelease.length ? -1 : 1;
693
- }
694
- /** Only the `>=A <B` form `engineCompat.core` uses (v02.11 §7.3). */
695
- export function satisfiesRange(version, range) {
696
- const parts = range.trim().split(/\s+/);
697
- const [lower, upper] = parts;
698
- if (parts.length !== 2 || !lower?.startsWith(">=") || !upper?.startsWith("<") || upper.startsWith("<="))
699
- throw new TypeError(`[@teacss/engine] unsupported engineCompat range: ${range}`);
700
- return compareSemver(version, lower.slice(2)) >= 0 && compareSemver(version, upper.slice(1)) < 0;
701
- }
702
- /**
703
- * v02.9 §8.1: `TEACSS_ENGINE` over the hook option over `"ts"`; an `"auto"` option stays on TS while
704
- * `defaultMode` is `"opt-in"`; a host core outside `engineCompat.core` keeps TS, or throws when
705
- * Rust was asked for.
706
- *
707
- */
708
- export function selectEngineRequest({ hostCoreVersion, option, env, compat = ENGINE_COMPAT }) {
709
- let mode;
710
- let source;
711
- let invalidEnv;
712
- if (env !== undefined && env !== "" && VALID_MODES.has(env)) {
713
- mode = env;
714
- source = "env";
715
- }
716
- else {
717
- if (env !== undefined && env !== "")
718
- invalidEnv = env;
719
- if (typeof option === "string" && VALID_MODES.has(option)) {
720
- mode = option;
721
- source = "option";
722
- }
723
- else {
724
- mode = "ts";
725
- source = "default";
726
- }
727
- }
728
- const base = invalidEnv === undefined ? { mode, source } : { mode, source, invalidEnv };
729
- if (mode === "ts")
730
- return { engine: "ts", ...base };
731
- if (mode === "auto" && source === "option" && compat.defaultMode === "opt-in")
732
- return { engine: "ts", ...base, resolvedBy: "opt-in" };
733
- if (compat.core && typeof hostCoreVersion === "string" && !satisfiesRange(hostCoreVersion, compat.core)) {
734
- const detail = { hostCoreVersion, engineVersion: ENGINE_PACKAGE_VERSION, range: compat.core };
735
- if (mode === "rust")
736
- throw new TeacssEngineError("E_ENGINE_COMPAT", `[@teacss/engine] @teacss/core ${hostCoreVersion} is outside engineCompat.core ${compat.core} (engine ${ENGINE_PACKAGE_VERSION})`, detail);
737
- return { engine: "ts", ...base, reason: "engine-compat", path: "@teacss/core", detail };
738
- }
739
- return { engine: "rust", ...base };
740
- }
741
- //# sourceMappingURL=select.js.map