@teacss/engine 0.9.3 → 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/entry.js ADDED
@@ -0,0 +1,279 @@
1
+ // Statement-form entry directives the engine consumed from the entry file; the Rust side
2
+ // (`teacss-transform/src/entry_strip.rs`) leaves an entry that carries `@teacss;` to its shell.
3
+ const ENTRY_STATEMENTS = new Set(["presets", "source", "shortcut", "safelist", "primary", "neutral", "success", "warning", "failure", "general"]);
4
+ function skipComment(css, index) {
5
+ const close = css.indexOf("*/", index + 2);
6
+ return close < 0 ? css.length : close + 2;
7
+ }
8
+ function skipString(css, index) {
9
+ const quote = css[index];
10
+ for (let cursor = index + 1; cursor < css.length;) {
11
+ if (css[cursor] === "\\")
12
+ cursor += 2;
13
+ else if (css[cursor] === quote)
14
+ return cursor + 1;
15
+ else
16
+ cursor++;
17
+ }
18
+ return css.length;
19
+ }
20
+ /** The index after the comment, string or backslash escape that starts at `index`, or `index` when none does. */
21
+ function skipOpaque(css, index) {
22
+ if (css.startsWith("/*", index))
23
+ return skipComment(css, index);
24
+ const char = css[index];
25
+ if (char === '"' || char === "'")
26
+ return skipString(css, index);
27
+ if (char === "\\")
28
+ return Math.min(index + 2, css.length);
29
+ return index;
30
+ }
31
+ function findClosingBrace(css, open) {
32
+ let depth = 1;
33
+ for (let cursor = open + 1; cursor < css.length;) {
34
+ const skipped = skipOpaque(css, cursor);
35
+ if (skipped !== cursor) {
36
+ cursor = skipped;
37
+ continue;
38
+ }
39
+ if (css[cursor] === "{")
40
+ depth++;
41
+ else if (css[cursor] === "}" && --depth === 0)
42
+ return cursor;
43
+ cursor++;
44
+ }
45
+ return null;
46
+ }
47
+ /** The prelude text with each comment read as one space and strings kept as written, trimmed. */
48
+ function cleanPrelude(css, start, end) {
49
+ let out = "";
50
+ let segment = start;
51
+ for (let cursor = start; cursor < end;) {
52
+ if (css.startsWith("/*", cursor) && cursor + 1 < end) {
53
+ out += `${css.slice(segment, cursor)} `;
54
+ cursor = Math.min(skipComment(css, cursor), end);
55
+ segment = cursor;
56
+ }
57
+ else if (css[cursor] === '"' || css[cursor] === "'") {
58
+ cursor = Math.min(skipString(css, cursor), end);
59
+ }
60
+ else
61
+ cursor++;
62
+ }
63
+ return (out + css.slice(segment, end)).trim();
64
+ }
65
+ function atRuleAt(css, start, name) {
66
+ const nameEnd = start + 1 + name.length;
67
+ const closers = [];
68
+ for (let cursor = nameEnd; cursor < css.length;) {
69
+ const skipped = skipOpaque(css, cursor);
70
+ if (skipped !== cursor) {
71
+ cursor = skipped;
72
+ continue;
73
+ }
74
+ const char = css[cursor];
75
+ if (closers.length === 0 && char === "{") {
76
+ const close = findClosingBrace(css, cursor);
77
+ if (close !== null)
78
+ return { name, prelude: cleanPrelude(css, nameEnd, cursor), start, end: close + 1, block: true };
79
+ }
80
+ if (char === "(")
81
+ closers.push(")");
82
+ else if (char === "[")
83
+ closers.push("]");
84
+ else if (char === "{")
85
+ closers.push("}");
86
+ else if (closers.at(-1) === char)
87
+ closers.pop();
88
+ if (closers.length === 0 && char === ";")
89
+ return { name, prelude: cleanPrelude(css, nameEnd, cursor), start, end: cursor + 1, block: false };
90
+ cursor++;
91
+ }
92
+ return { name, prelude: cleanPrelude(css, nameEnd, css.length), start, end: css.length, block: false };
93
+ }
94
+ /**
95
+ * Top-level at-rules exactly as `teacss-config`'s `scan_top_level_at_rules` finds them: comments, strings and
96
+ * backslash escapes skipped, nested rules ignored, comments in a prelude read as a space.
97
+ */
98
+ function topLevelAtRules(css) {
99
+ const rules = [];
100
+ let depth = 0;
101
+ for (let index = 0; index < css.length;) {
102
+ const skipped = skipOpaque(css, index);
103
+ if (skipped !== index) {
104
+ index = skipped;
105
+ continue;
106
+ }
107
+ const char = css[index];
108
+ if (char === "{")
109
+ depth++;
110
+ else if (char === "}")
111
+ depth = Math.max(0, depth - 1);
112
+ else if (char === "@" && depth === 0) {
113
+ const name = /^[\w-]+/.exec(css.slice(index + 1))?.[0];
114
+ if (name) {
115
+ const rule = atRuleAt(css, index, name);
116
+ rules.push(rule);
117
+ index = rule.end;
118
+ continue;
119
+ }
120
+ }
121
+ index++;
122
+ }
123
+ return rules;
124
+ }
125
+ function entryPieces(css, generate) {
126
+ const rules = topLevelAtRules(css);
127
+ const entry = rules.find((rule) => rule.name === "teacss" && !rule.block && rule.prelude === "");
128
+ if (!entry)
129
+ return null;
130
+ const pieces = [];
131
+ let cursor = 0;
132
+ for (const rule of rules) {
133
+ const remove = rule.name === "teacss" || rule.name === "custom" || (ENTRY_STATEMENTS.has(rule.name) && !rule.block);
134
+ if (!remove)
135
+ continue;
136
+ pieces.push({ from: cursor, to: rule.start });
137
+ if (rule === entry)
138
+ pieces.push({ generated: generate(), at: rule.start });
139
+ cursor = rule.end;
140
+ }
141
+ pieces.push({ from: cursor, to: css.length });
142
+ return pieces;
143
+ }
144
+ /** `css` with its first top-level `@teacss;` replaced by `generate()` and the entry directives removed; null when it has no `@teacss;`. */
145
+ export function replaceEntry(css, generate) {
146
+ const pieces = entryPieces(css, generate);
147
+ return pieces && pieces.map((piece) => ("generated" in piece ? piece.generated : css.slice(piece.from, piece.to))).join("");
148
+ }
149
+ /**
150
+ * `replaceEntry` with a source map back to `source.original`: hand-written CSS keeps its original lines and
151
+ * columns, and every line of the generated CSS maps to the `@teacss;` it replaced.
152
+ */
153
+ export function replaceEntryWithMap(css, generate, source) {
154
+ const pieces = entryPieces(css, generate);
155
+ if (!pieces)
156
+ return null;
157
+ const points = source.map ? decodePoints(css, source.map.mappings) : identityPoints(css);
158
+ const out = [];
159
+ let code = "";
160
+ for (const piece of pieces) {
161
+ if ("generated" in piece) {
162
+ const original = originalAt(css, points, piece.at);
163
+ for (const start of lineStarts(piece.generated))
164
+ out.push({ offset: code.length + start, original });
165
+ code += piece.generated;
166
+ continue;
167
+ }
168
+ if (piece.from === piece.to)
169
+ continue;
170
+ out.push({ offset: code.length, original: originalAt(css, points, piece.from) });
171
+ for (const point of points) {
172
+ if (point.offset > piece.from && point.offset < piece.to)
173
+ out.push({ offset: point.offset - piece.from + code.length, original: point.original });
174
+ }
175
+ code += css.slice(piece.from, piece.to);
176
+ }
177
+ const map = { version: 3, file: source.map?.file ?? null, sources: [source.id], sourcesContent: [source.original], names: [], mappings: encodePoints(code, out) };
178
+ return { code, map };
179
+ }
180
+ function lineStarts(text) {
181
+ const starts = [0];
182
+ for (let index = text.indexOf("\n"); index >= 0; index = text.indexOf("\n", index + 1))
183
+ starts.push(index + 1);
184
+ return starts;
185
+ }
186
+ function identityPoints(css) {
187
+ return lineStarts(css).map((offset, line) => ({ offset, original: [line, 0] }));
188
+ }
189
+ /** Where `offset` came from: the last point at or before it on the same line, moved by the columns between them. */
190
+ function originalAt(css, points, offset) {
191
+ let last;
192
+ for (const point of points) {
193
+ if (point.offset > offset)
194
+ break;
195
+ last = point;
196
+ }
197
+ if (!last?.original || css.slice(last.offset, offset).includes("\n"))
198
+ return null;
199
+ return [last.original[0], last.original[1] + offset - last.offset];
200
+ }
201
+ const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
202
+ function decodeVlq(segment) {
203
+ const values = [];
204
+ let value = 0;
205
+ let shift = 0;
206
+ for (const char of segment) {
207
+ const digit = BASE64.indexOf(char);
208
+ value += (digit & 31) << shift;
209
+ if (digit & 32)
210
+ shift += 5;
211
+ else {
212
+ values.push(value & 1 ? -(value >>> 1) : value >>> 1);
213
+ value = 0;
214
+ shift = 0;
215
+ }
216
+ }
217
+ return values;
218
+ }
219
+ function encodeVlq(value) {
220
+ let rest = value < 0 ? (-value << 1) | 1 : value << 1;
221
+ let out = "";
222
+ do {
223
+ const digit = rest & 31;
224
+ rest >>>= 5;
225
+ out += BASE64[rest > 0 ? digit | 32 : digit];
226
+ } while (rest > 0);
227
+ return out;
228
+ }
229
+ /** The points of a single-source map's `mappings` over `generated`, in order. */
230
+ function decodePoints(generated, mappings) {
231
+ const starts = lineStarts(generated);
232
+ const points = [];
233
+ let line = 0;
234
+ let column = 0;
235
+ for (const [index, group] of mappings.split(";").entries()) {
236
+ let generatedColumn = 0;
237
+ for (const segment of group.split(",")) {
238
+ if (segment === "")
239
+ continue;
240
+ const [deltaColumn = 0, , deltaLine, deltaOriginalColumn] = decodeVlq(segment);
241
+ generatedColumn += deltaColumn;
242
+ let original = null;
243
+ if (deltaLine !== undefined && deltaOriginalColumn !== undefined) {
244
+ line += deltaLine;
245
+ column += deltaOriginalColumn;
246
+ original = [line, column];
247
+ }
248
+ points.push({ offset: (starts[index] ?? generated.length) + generatedColumn, original });
249
+ }
250
+ }
251
+ return points;
252
+ }
253
+ function encodePoints(code, points) {
254
+ const starts = lineStarts(code);
255
+ const lines = starts.map(() => []);
256
+ const kept = points.filter((point, index) => points[index + 1]?.offset !== point.offset);
257
+ let line = 0;
258
+ let previousColumn = 0;
259
+ let previousLine = 0;
260
+ let previousOriginalColumn = 0;
261
+ for (const point of kept) {
262
+ while (line + 1 < starts.length && starts[line + 1] <= point.offset) {
263
+ line++;
264
+ previousColumn = 0;
265
+ }
266
+ const column = point.offset - starts[line];
267
+ let segment = encodeVlq(column - previousColumn);
268
+ previousColumn = column;
269
+ if (point.original) {
270
+ const [originalLine, originalColumn] = point.original;
271
+ segment += encodeVlq(0) + encodeVlq(originalLine - previousLine) + encodeVlq(originalColumn - previousOriginalColumn);
272
+ previousLine = originalLine;
273
+ previousOriginalColumn = originalColumn;
274
+ }
275
+ lines[line]?.push(segment);
276
+ }
277
+ return lines.map((segments) => segments.join(",")).join(";");
278
+ }
279
+ //# sourceMappingURL=entry.js.map
package/dist/errors.d.ts CHANGED
@@ -1,13 +1,7 @@
1
- /** Errors the facade raises itself; an `Error` from napi (code `E_*`) passes through unwrapped. */
1
+ /** Errors the TypeScript side raises itself; an `Error` from napi (code `E_*`) passes through unwrapped. */
2
2
  export declare class TeacssEngineError extends Error {
3
3
  code: string;
4
4
  details: Record<string, unknown>;
5
5
  constructor(code: string, message: string, details?: Record<string, unknown>);
6
6
  }
7
- /** v02.9 §2.1: the member needs `@teacss/core`, which is not installed. */
8
- export declare function tsRequiredError(member: string): TeacssEngineError;
9
- /** The generator was disposed; the native Engine reports the same code after its own `dispose()`. */
10
- export declare function disposedError(member: string): TeacssEngineError;
11
- /** v02.9 §3.3: the member needs the TypeScript engine, which has not been materialized. */
12
- export declare function tsNotReadyError(member: string): TeacssEngineError;
13
7
  //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- /** Errors the facade raises itself; an `Error` from napi (code `E_*`) passes through unwrapped. */
1
+ /** Errors the TypeScript side raises itself; an `Error` from napi (code `E_*`) passes through unwrapped. */
2
2
  export class TeacssEngineError extends Error {
3
3
  constructor(code, message, details = {}) {
4
4
  super(message);
@@ -7,16 +7,4 @@ export class TeacssEngineError extends Error {
7
7
  this.details = details;
8
8
  }
9
9
  }
10
- /** v02.9 §2.1: the member needs `@teacss/core`, which is not installed. */
11
- export function tsRequiredError(member) {
12
- return new TeacssEngineError("E_TS_REQUIRED", `[@teacss/engine] ${member} requires @teacss/core (optional peer) to be installed`, { member });
13
- }
14
- /** The generator was disposed; the native Engine reports the same code after its own `dispose()`. */
15
- export function disposedError(member) {
16
- return new TeacssEngineError("E_DISPOSED", `[@teacss/engine] ${member} called after dispose()`);
17
- }
18
- /** v02.9 §3.3: the member needs the TypeScript engine, which has not been materialized. */
19
- export function tsNotReadyError(member) {
20
- return new TeacssEngineError("E_TS_REQUIRED", `[@teacss/engine] ${member} needs the TypeScript engine; await generator[Symbol.for('@teacss/engine/materialize')]() first or set engine: 'ts'`, { member });
21
- }
22
10
  //# sourceMappingURL=errors.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The Standard preset's preflight inlines its fonts as data URLs branded with the sha256 of the bytes
3
+ * (`crates/teacss-standard/src/preflight_fonts.rs`); only TeaCSS writes this URL. Groups: quote, digest, base64.
4
+ */
5
+ export declare const BUNDLED_FONT_URL: RegExp;
6
+ export interface BundledFont {
7
+ digest: string;
8
+ bytes: Uint8Array;
9
+ /** What `pathFor` returned for the digest: the URL every `url()` of this font now carries. */
10
+ path: string;
11
+ }
12
+ /**
13
+ * Moves every bundled font out of `css`: each distinct digest is decoded once, checked against its bytes
14
+ * (a mismatch is `E_INVALID_ARGUMENT`) and handed to `pathFor`, whose result replaces the data URL.
15
+ * CSS without a bundled font comes back as the same string with no fonts.
16
+ */
17
+ export declare function extractBundledFonts(css: string, pathFor: (digest: string, bytes: Uint8Array) => string): {
18
+ css: string;
19
+ fonts: BundledFont[];
20
+ };
21
+ //# sourceMappingURL=fonts.d.ts.map
package/dist/fonts.js ADDED
@@ -0,0 +1,32 @@
1
+ import { createHash } from "node:crypto";
2
+ import { TeacssEngineError } from "./errors.js";
3
+ /**
4
+ * The Standard preset's preflight inlines its fonts as data URLs branded with the sha256 of the bytes
5
+ * (`crates/teacss-standard/src/preflight_fonts.rs`); only TeaCSS writes this URL. Groups: quote, digest, base64.
6
+ */
7
+ export const BUNDLED_FONT_URL = /url\(\s*(["']?)data:font\/woff2;teacss=v1-([a-f0-9]{64});base64,([A-Za-z0-9+/]*={0,2})\1\s*\)/g;
8
+ /**
9
+ * Moves every bundled font out of `css`: each distinct digest is decoded once, checked against its bytes
10
+ * (a mismatch is `E_INVALID_ARGUMENT`) and handed to `pathFor`, whose result replaces the data URL.
11
+ * CSS without a bundled font comes back as the same string with no fonts.
12
+ */
13
+ export function extractBundledFonts(css, pathFor) {
14
+ if (!css.includes(";teacss=v1-"))
15
+ return { css, fonts: [] };
16
+ const fonts = new Map();
17
+ const out = css.replace(BUNDLED_FONT_URL, (_, _quote, digest, base64) => {
18
+ let font = fonts.get(digest);
19
+ if (font === undefined) {
20
+ const bytes = Buffer.from(base64, "base64");
21
+ const actual = createHash("sha256").update(bytes).digest("hex");
22
+ if (actual !== digest) {
23
+ throw new TeacssEngineError("E_INVALID_ARGUMENT", `[@teacss/engine] bundled font ${digest} does not match its bytes (sha256 ${actual})`, { path: `teacss=v1-${digest}`, expected: digest, actual });
24
+ }
25
+ font = { digest, bytes, path: pathFor(digest, bytes) };
26
+ fonts.set(digest, font);
27
+ }
28
+ return `url(${JSON.stringify(font.path)})`;
29
+ });
30
+ return { css: out, fonts: [...fonts.values()] };
31
+ }
32
+ //# sourceMappingURL=fonts.js.map
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  export * from "../binding.js";
2
2
  export type * from "../contract.js";
3
- export { CONFIG_ENTRY_PROVENANCE, CONFIG_SOURCE_DECLARATIONS, ENGINE_COMPAT, ENGINE_INSTANCE, ENGINE_MATERIALIZE, ENGINE_PACKAGE_VERSION, ENGINE_STATS, PRESET_OPTIONS, } from "./constants.js";
3
+ export { type EntrySource, replaceEntry, replaceEntryWithMap } from "./entry.js";
4
4
  export { TeacssEngineError } from "./errors.js";
5
- export { createGenerator, engineStats, type FacadeCreateResult, FacadeGenerator, type FacadeHost, reportFallback } from "./generator.js";
6
- export { selectEngineRequest, type TranslateResult, translateConfig } from "./select.js";
7
- export { createCompileClassTransformer } from "./transformer.js";
5
+ export { BUNDLED_FONT_URL, type BundledFont, extractBundledFonts } from "./fonts.js";
6
+ export { CSS_PLACEHOLDER, createProject, type Edit, isCssId, type Project, replayEdits, type ShellOptions, type SourceDirectory, type UnmatchedReport } from "./project.js";
8
7
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,10 +1,7 @@
1
- // Facade modules import `Engine`, `expandClassGroups`, `splitClassTokens` and `version` from
2
- // `../binding.js`, never from `index.js`, which re-exports them (a cycle otherwise).
3
- // This file imports no peer (`@teacss/core`, `@teacss/config`).
1
+ // The package has no runtime dependency beyond the binding.
4
2
  export * from "../binding.js";
5
- export { CONFIG_ENTRY_PROVENANCE, CONFIG_SOURCE_DECLARATIONS, ENGINE_COMPAT, ENGINE_INSTANCE, ENGINE_MATERIALIZE, ENGINE_PACKAGE_VERSION, ENGINE_STATS, PRESET_OPTIONS, } from "./constants.js";
3
+ export { replaceEntry, replaceEntryWithMap } from "./entry.js";
6
4
  export { TeacssEngineError } from "./errors.js";
7
- export { createGenerator, engineStats, FacadeGenerator, reportFallback } from "./generator.js";
8
- export { selectEngineRequest, translateConfig } from "./select.js";
9
- export { createCompileClassTransformer } from "./transformer.js";
5
+ export { BUNDLED_FONT_URL, extractBundledFonts } from "./fonts.js";
6
+ export { CSS_PLACEHOLDER, createProject, isCssId, replayEdits } from "./project.js";
10
7
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,80 @@
1
+ import { Engine } from "../binding.js";
2
+ import type { GenerateResult, LoadProjectFromRootResult, OutputToCssLayersOptions, PresetIconsOptions, PresetStandardOptions, SourceMap, StaticConfig, TransformResult } from "../contract.js";
3
+ /** v10.2 §2.1: a shell's static options. Every field but `minify` maps 1:1 onto `LoadProjectFromRootRequest`. */
4
+ export interface ShellOptions {
5
+ /** Default `process.cwd()`. */
6
+ root?: string;
7
+ entry?: string;
8
+ inline?: StaticConfig;
9
+ /** Default "build". */
10
+ envMode?: "dev" | "build";
11
+ /** Default "warn". */
12
+ unmatched?: "warn" | "error" | "off";
13
+ outputToCssLayers?: boolean | OutputToCssLayersOptions;
14
+ shortcutsLayer?: string;
15
+ presetOptions?: {
16
+ standard?: PresetStandardOptions;
17
+ icons?: PresetIconsOptions;
18
+ };
19
+ /** Default for `Project.css()`: `envMode === "build"`. */
20
+ minify?: boolean;
21
+ }
22
+ export type Edit = TransformResult["edits"][number];
23
+ export interface SourceDirectory {
24
+ /** Absolute path. */
25
+ dir: string;
26
+ /** The rest of the `@source` glob, relative to `dir`. */
27
+ glob: string;
28
+ }
29
+ export interface UnmatchedReport {
30
+ /** The tokens `result` was generated from; default `engine.allTokens()`, what `css()` generates. */
31
+ tokens?: readonly string[];
32
+ /** The host's warning logger. */
33
+ warn(message: string): void;
34
+ }
35
+ /** v10.2 §2.2: what the shells share on top of the binding. */
36
+ export interface Project {
37
+ readonly engine: Engine;
38
+ readonly root: string;
39
+ /** The latest `loadProjectFromRoot` result; `entryPath` is null when the root has no entry. */
40
+ readonly info: LoadProjectFromRootResult;
41
+ /** Runs `loadProjectFromRoot` again; returns whether the programKey changed. */
42
+ reload(): boolean;
43
+ /** `configFiles()` plus `info.sourceFiles`, sorted and deduplicated. */
44
+ watchFiles(): string[];
45
+ /**
46
+ * Where each included `@source` glob can match a file that does not exist yet: the directory before its
47
+ * first glob segment, with the rest of the glob. Sorted by directory; hosts watch these for new files.
48
+ */
49
+ sourceDirectories(): SourceDirectory[];
50
+ /** The tokens of `inline.content.inline`, each item extracted under its id (default `inline:<index>`). */
51
+ inlineTokens(): string[];
52
+ /** `transformStylesheet` for a CSS id, otherwise `transformSource` then `updateModule`. */
53
+ transform(id: string, code: string): {
54
+ code: string;
55
+ map: SourceMap | null;
56
+ changed: boolean;
57
+ };
58
+ /** Ids registered through `transform`, in first-seen order. */
59
+ modules(): string[];
60
+ /** Forgets a module `transform` registered (`engine.removeModule`); returns whether its tokens or definitions changed. */
61
+ removeModule(id: string): boolean;
62
+ /** `generate({ kind: "all" })` with `minify` defaulting to the option; `extendedInfo` fills `matchedInfo`. */
63
+ css(options?: {
64
+ minify?: boolean;
65
+ extendedInfo?: boolean;
66
+ }): GenerateResult;
67
+ /**
68
+ * Applies the `unmatched` option to `result`: the near-miss tokens `diagnoseUnmatched` keeps are
69
+ * warned about once per project with "warn" (and with "error" in dev), and throw `E_UNMATCHED`
70
+ * with "error" in a build. "off" reports nothing.
71
+ */
72
+ reportUnmatched(result: GenerateResult, report: UnmatchedReport): void;
73
+ dispose(): void;
74
+ }
75
+ export declare function createProject(options: ShellOptions): Project;
76
+ /** Splices v02.2 §3 edits (UTF-16 offsets, ascending, non-overlapping) into `code`. */
77
+ export declare function replayEdits(code: string, edits: readonly Edit[]): string;
78
+ export declare const CSS_PLACEHOLDER: string;
79
+ export declare function isCssId(id: string): boolean;
80
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1,136 @@
1
+ import { dirname, isAbsolute, resolve } from "node:path";
2
+ import { Engine, markers } from "../binding.js";
3
+ import { TeacssEngineError } from "./errors.js";
4
+ class ShellProject {
5
+ engine = new Engine();
6
+ root;
7
+ #request;
8
+ #minify;
9
+ #modules = new Set();
10
+ #warnedUnmatched = new Set();
11
+ #info;
12
+ constructor(options) {
13
+ const { minify, root = process.cwd(), envMode = "build", unmatched = "warn", ...rest } = options;
14
+ this.root = root;
15
+ this.#request = { ...rest, root, envMode, unmatched };
16
+ this.#minify = minify ?? envMode === "build";
17
+ this.#info = this.engine.loadProjectFromRoot(this.#request);
18
+ }
19
+ get info() {
20
+ return this.#info;
21
+ }
22
+ reload() {
23
+ this.#info = this.engine.loadProjectFromRoot(this.#request);
24
+ return this.#info.changed;
25
+ }
26
+ watchFiles() {
27
+ return [...new Set([...this.engine.configFiles(), ...this.#info.sourceFiles])].sort();
28
+ }
29
+ sourceDirectories() {
30
+ const { entryPath } = this.#info;
31
+ if (entryPath === null)
32
+ return [];
33
+ const { sources } = this.engine.inspectEntry({ root: this.root, entryPath });
34
+ return sources
35
+ .filter((source) => !source.startsWith("!"))
36
+ .map((source) => splitGlob(this.root, source))
37
+ .sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
38
+ }
39
+ inlineTokens() {
40
+ const tokens = new Set();
41
+ for (const [index, item] of (this.#request.inline?.content?.inline ?? []).entries()) {
42
+ const { code, id } = typeof item === "string" ? { code: item, id: null } : item;
43
+ for (const token of this.engine.extract(code, id ?? `inline:${index}`))
44
+ tokens.add(token);
45
+ }
46
+ return [...tokens];
47
+ }
48
+ transform(id, code) {
49
+ if (isCssId(id)) {
50
+ const result = this.engine.transformStylesheet(id, code);
51
+ return result ? { code: result.code, map: result.map, changed: result.edits.length > 0 } : { code, map: null, changed: false };
52
+ }
53
+ const result = this.engine.transformSource(id, code);
54
+ const next = result?.code ?? code;
55
+ const update = this.engine.updateModule(id, next);
56
+ this.#modules.add(id);
57
+ return { code: next, map: result?.map ?? null, changed: (result?.edits.length ?? 0) > 0 || update.changed };
58
+ }
59
+ modules() {
60
+ return [...this.#modules];
61
+ }
62
+ removeModule(id) {
63
+ this.#modules.delete(id);
64
+ return this.engine.removeModule(id);
65
+ }
66
+ css(options) {
67
+ return this.engine.generate({ kind: "all" }, { minify: options?.minify ?? this.#minify, extendedInfo: options?.extendedInfo, css: true });
68
+ }
69
+ reportUnmatched(result, report) {
70
+ const { unmatched, envMode } = this.#request;
71
+ if (unmatched === "off")
72
+ return;
73
+ const matched = new Set(result.matched === "" ? [] : result.matched.split("\0"));
74
+ const tokens = report.tokens ?? this.engine.allTokens();
75
+ const diagnosed = this.engine.diagnoseUnmatched(tokens.filter((token) => !matched.has(token)));
76
+ if (diagnosed.length === 0)
77
+ return;
78
+ if (unmatched === "error" && envMode === "build") {
79
+ const plural = diagnosed.length === 1 ? "" : "s";
80
+ throw new TeacssEngineError("E_UNMATCHED", `[@teacss/engine] ${diagnosed.length} token${plural} matched no rule: ${diagnosed.map(describeUnmatched).join("; ")}`, { diagnostics: diagnosed });
81
+ }
82
+ for (const diagnostic of diagnosed) {
83
+ if (this.#warnedUnmatched.has(diagnostic.token))
84
+ continue;
85
+ this.#warnedUnmatched.add(diagnostic.token);
86
+ report.warn(`unmatched token ${describeUnmatched(diagnostic)}`);
87
+ }
88
+ }
89
+ dispose() {
90
+ this.engine.dispose();
91
+ }
92
+ }
93
+ /** The text of `teacss build --unmatched error`: `"token"`, plus `(did you mean "a", "b"?)` when there are suggestions. */
94
+ function describeUnmatched({ token, suggestions }) {
95
+ const quoted = `"${token}"`;
96
+ return suggestions.length === 0 ? quoted : `${quoted} (did you mean ${suggestions.map((suggestion) => `"${suggestion}"`).join(", ")}?)`;
97
+ }
98
+ const GLOB_SEGMENT = /[*?[\]{}\\]/;
99
+ /**
100
+ * `glob` (relative to `root`, or absolute) split before its first segment with glob syntax; a plain path splits before its file name.
101
+ * As the engine's `static_base`: an absolute base at the filesystem root is clamped to `root`, the tree the engine scans instead.
102
+ */
103
+ function splitGlob(root, glob) {
104
+ const segments = glob.split("/");
105
+ const first = segments.findIndex((segment) => GLOB_SEGMENT.test(segment));
106
+ const at = first === -1 ? segments.length - 1 : first;
107
+ const rest = segments.slice(at).join("/");
108
+ const base = segments.slice(0, at).join("/");
109
+ if (!isAbsolute(glob))
110
+ return { dir: resolve(root, base), glob: rest };
111
+ const dir = resolve(base || "/");
112
+ return { dir: dirname(dir) === dir ? root : dir, glob: rest };
113
+ }
114
+ export function createProject(options) {
115
+ return new ShellProject(options);
116
+ }
117
+ /** Splices v02.2 §3 edits (UTF-16 offsets, ascending, non-overlapping) into `code`. */
118
+ export function replayEdits(code, edits) {
119
+ let out = "";
120
+ let cursor = 0;
121
+ for (const [index, { start, end, text }] of edits.entries()) {
122
+ if (start < cursor || end < start || end > code.length) {
123
+ throw new TeacssEngineError("E_INVALID_ARGUMENT", `[@teacss/engine] replayEdits: edits[${index}] (${start}, ${end}) overlaps the previous edit or leaves the code (length ${code.length})`, { path: `edits[${index}]`, start, end });
124
+ }
125
+ out += code.slice(cursor, start) + text;
126
+ cursor = end;
127
+ }
128
+ return out + code.slice(cursor);
129
+ }
130
+ export const CSS_PLACEHOLDER = markers().cssPlaceholder;
131
+ // same as teacss-ts core isCssId
132
+ const cssIdMatcher = /\.(css|postcss|sass|scss|less|stylus|styl)($|\?)/;
133
+ export function isCssId(id) {
134
+ return cssIdMatcher.test(id);
135
+ }
136
+ //# sourceMappingURL=project.js.map