rainbowindex 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +876 -0
  2. package/LICENSE +16 -17
  3. package/NOTICE.md +106 -0
  4. package/README.md +225 -69
  5. package/dist/browser.d.ts +4 -2
  6. package/dist/browser.mjs +12 -6
  7. package/dist/chunk-2T7V5XLK.mjs +912 -0
  8. package/dist/chunk-6OORICWF.mjs +16 -0
  9. package/dist/{chunk-PDORZSQX.mjs → chunk-FJOZJIKB.mjs} +7814 -5174
  10. package/dist/chunk-L56IRO7A.mjs +491 -0
  11. package/dist/chunk-PZDVDEZJ.mjs +196 -0
  12. package/dist/{chunk-4CTJLMYM.mjs → chunk-RC6DDE4L.mjs} +37 -23
  13. package/dist/chunk-TQJYVQPE.mjs +217 -0
  14. package/dist/chunk-W756NVYI.mjs +33 -0
  15. package/dist/chunk-WBESS2ZD.mjs +598 -0
  16. package/dist/{chunk-3HRMFZGE.mjs → chunk-X66Z2YHT.mjs} +2 -1
  17. package/dist/{chunk-4UKFK2GE.mjs → chunk-XQGSG2HK.mjs} +213 -756
  18. package/dist/cli.mjs +1101 -125
  19. package/dist/{context-B9yhJxd5.d.ts → context-DcBtnnan.d.ts} +55 -108
  20. package/dist/editor.d.ts +82 -421
  21. package/dist/editor.mjs +68 -363
  22. package/dist/eslint.d.ts +16 -0
  23. package/dist/eslint.mjs +32 -0
  24. package/dist/{index-Dx-NpFFx.d.ts → imports-C9esHd5Q.d.ts} +98 -81
  25. package/dist/index-CNqdL5U0.d.ts +56 -0
  26. package/dist/index-Czx-EUwh.d.ts +138 -0
  27. package/dist/index-DW8YSxTz.d.ts +104 -0
  28. package/dist/index.d.ts +49 -5
  29. package/dist/index.mjs +40 -13
  30. package/dist/oxlint.d.ts +21 -3
  31. package/dist/oxlint.mjs +19 -1
  32. package/dist/recipe.d.ts +111 -0
  33. package/dist/recipe.mjs +71 -0
  34. package/dist/safelist-CH3_PywB.d.ts +43 -0
  35. package/dist/session-CMaskdB7.d.ts +543 -0
  36. package/dist/tailwind.css +644 -0
  37. package/dist/theme-CIZiGlce.d.ts +115 -0
  38. package/dist/vite.d.ts +10 -1
  39. package/dist/vite.mjs +273 -113
  40. package/package.json +27 -5
  41. package/dist/chunk-F4VCBISU.mjs +0 -1866
  42. package/dist/chunk-RU4756NG.mjs +0 -243
  43. package/dist/safelist-DAkKuxCk.d.ts +0 -96
@@ -0,0 +1,115 @@
1
+ import { E as EditorSession } from './session-CMaskdB7.js';
2
+
3
+ /**
4
+ * Two lint rules over the editor API, in the shape both Oxlint and ESLint read.
5
+ *
6
+ * Neither rule walks the AST for class positions, and that is deliberate. The
7
+ * scanner already decides what a class position is — a `class`/`className`
8
+ * attribute, an argument to a class helper, a `cva`/`tv` config, a safelist —
9
+ * and a rule that re-derived that from the AST would drift from the compiler
10
+ * it is supposed to be describing. So both rules run once per file over the
11
+ * source text, take the scanner's own candidates with their exact spans, and
12
+ * report on those. A class the build compiles and a class the linter checks are
13
+ * then the same set by construction.
14
+ *
15
+ * The rule objects use only the slice of the linter API the two hosts share:
16
+ * `create(context)`, `context.report({ message, node, loc })`, and a visitor
17
+ * keyed by node type. ESLint suggestions are attached where the host supports
18
+ * them, and ignored where it does not.
19
+ */
20
+ interface LintPosition {
21
+ line: number;
22
+ column: number;
23
+ }
24
+ interface LintLoc {
25
+ start: LintPosition;
26
+ end: LintPosition;
27
+ }
28
+ interface LintFix {
29
+ range: [number, number];
30
+ text: string;
31
+ }
32
+ interface LintFixer {
33
+ replaceTextRange(range: [number, number], text: string): LintFix;
34
+ }
35
+ interface LintSuggestion {
36
+ desc: string;
37
+ fix(fixer: LintFixer): LintFix;
38
+ }
39
+ interface LintReport {
40
+ message: string;
41
+ node?: unknown;
42
+ loc?: LintLoc;
43
+ suggest?: LintSuggestion[];
44
+ }
45
+ interface LintSourceCode {
46
+ getText(): string;
47
+ getLocFromIndex(index: number): LintPosition;
48
+ }
49
+ interface LintContext {
50
+ report(report: LintReport): void;
51
+ /** ESLint ≥ 8.40 and Oxlint both expose this; older ESLint used a getter. */
52
+ sourceCode?: LintSourceCode;
53
+ getSourceCode?(): LintSourceCode;
54
+ filename?: string;
55
+ getFilename?(): string;
56
+ options?: readonly unknown[];
57
+ cwd?: string;
58
+ }
59
+ /** Node type → handler. A rule with nothing to do returns an empty one. */
60
+ type LintVisitor = Partial<Record<string, (node: unknown) => void>>;
61
+ interface LintRule {
62
+ meta: {
63
+ type: "problem" | "suggestion";
64
+ docs: {
65
+ description: string;
66
+ };
67
+ hasSuggestions?: boolean;
68
+ schema?: readonly unknown[];
69
+ };
70
+ create(context: LintContext): LintVisitor;
71
+ }
72
+ declare const noUnknownClassRule: LintRule;
73
+ declare const noConflictingClassesRule: LintRule;
74
+ /** The rules both entry points expose, under the names both hosts use. */
75
+ declare const rules: Readonly<Record<string, LintRule>>;
76
+
77
+ /**
78
+ * Finding a project's theme from inside a linter.
79
+ *
80
+ * A lint rule that knows what a class *means* needs the compiled theme, and a
81
+ * linter gives it no build context — just a file and a working directory. So
82
+ * this locates the CSS entry the way the editor toolkit does (the same
83
+ * candidate list, the same activation test), reads it with the same `@import`
84
+ * inliner a build uses, and hands back an `EditorSession`.
85
+ *
86
+ * One session is cached per resolved entry, because a lint run calls into it
87
+ * once per file and rebuilding the theme each time would dominate the run. The
88
+ * cache is invalidated by mtime — on the entry and on every file it imports —
89
+ * so a watching linter picks up a token change without a restart.
90
+ */
91
+
92
+ interface ThemeSourceOptions {
93
+ /**
94
+ * The project's CSS entry, absolute or relative to `cwd`. Set it when the
95
+ * entry is somewhere the candidate list does not look, or to skip the
96
+ * search entirely.
97
+ */
98
+ css?: string;
99
+ /** Where the search starts. Defaults to the linter's working directory. */
100
+ cwd?: string;
101
+ }
102
+ /** The entry the candidate search settled on, or null when there is none. */
103
+ declare function findCSSEntry(cwd: string): string | null;
104
+ /**
105
+ * The session for a project, or null when no Rainbow Index entry was found.
106
+ *
107
+ * Returning null rather than throwing is the point: a rule that cannot find a
108
+ * theme has nothing to say, and a linter run over a repository that does not
109
+ * use Rainbow Index must stay silent rather than fail.
110
+ */
111
+ declare function getSession(options?: ThemeSourceOptions): EditorSession | null;
112
+ /** Drop every cached session. For tests, and for a host that knows better. */
113
+ declare function clearSessionCache(): void;
114
+
115
+ export { type LintRule as L, type ThemeSourceOptions as T, type LintContext as a, type LintFix as b, type LintFixer as c, type LintLoc as d, type LintReport as e, type LintSourceCode as f, type LintSuggestion as g, type LintVisitor as h, clearSessionCache as i, findCSSEntry as j, getSession as k, noUnknownClassRule as l, noConflictingClassesRule as n, rules as r };
package/dist/vite.d.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- declare function rainbowindexVite(): Plugin;
3
+ /**
4
+ * A pair, because the two halves need opposite ends of Vite's pipeline.
5
+ *
6
+ * `rainbowindex` runs `pre`: it has to read a CSS entry before Vite inlines its
7
+ * `@import` at-rules. `rainbowindex:snapshot` runs `post`: it has to prepend an
8
+ * import to real JavaScript, after every framework compiler has turned a
9
+ * component into some. Vite flattens a returned array, so `plugins:
10
+ * [rainbowindex()]` is unchanged for the caller.
11
+ */
12
+ declare function rainbowindexVite(): Plugin[];
4
13
 
5
14
  export { rainbowindexVite as default };
package/dist/vite.mjs CHANGED
@@ -1,26 +1,41 @@
1
1
  import {
2
2
  postcss_default
3
- } from "./chunk-4CTJLMYM.mjs";
3
+ } from "./chunk-RC6DDE4L.mjs";
4
4
  import {
5
+ snapshotFromCSS
6
+ } from "./chunk-6OORICWF.mjs";
7
+ import {
8
+ disableScanChangeTracking,
9
+ enableScanChangeTracking,
5
10
  enableSourceFileListCache,
6
- invalidateSourceFileListCache
7
- } from "./chunk-F4VCBISU.mjs";
11
+ invalidateSourceFileListCache,
12
+ markSourceFileChanged
13
+ } from "./chunk-2T7V5XLK.mjs";
14
+ import {
15
+ createNodeImportResolver
16
+ } from "./chunk-W756NVYI.mjs";
8
17
  import {
9
18
  isSourceFile
10
- } from "./chunk-3HRMFZGE.mjs";
19
+ } from "./chunk-X66Z2YHT.mjs";
11
20
  import {
12
21
  DIRECTIVE_TYPE_NAMES,
13
22
  codepointCompare,
14
- expandApplyGroups,
23
+ expandGroupsInStylesheet,
15
24
  extractClassesFromSource,
16
25
  findClosingBrace,
17
26
  hasRIActivation,
27
+ inlineDirectiveImports,
18
28
  isAtRuleBoundary,
19
- isAtRuleNameChar
20
- } from "./chunk-PDORZSQX.mjs";
29
+ isAtRuleNameChar,
30
+ parseFileDisables,
31
+ scanEntries,
32
+ stripCSSComments,
33
+ warningCode
34
+ } from "./chunk-FJOZJIKB.mjs";
35
+ import "./chunk-L56IRO7A.mjs";
21
36
  import {
22
37
  devWarn
23
- } from "./chunk-4UKFK2GE.mjs";
38
+ } from "./chunk-XQGSG2HK.mjs";
24
39
 
25
40
  // src/integrations/vite.ts
26
41
  import { existsSync } from "fs";
@@ -146,9 +161,72 @@ function rewriteDirectiveBodies(code) {
146
161
  if (last === 0) return code;
147
162
  return out + code.slice(last);
148
163
  }
164
+ var BLOCK_AFTER_DECLARATION_DIRECTIVES = /* @__PURE__ */ new Set([
165
+ "font",
166
+ "animate",
167
+ "color"
168
+ ]);
169
+ var LEGACY_APPLY_GROUP_RE = /@a(?:pply)?\b[^;{}]*[\w@-]+:\{/;
170
+ function usesLegacyDirectiveSyntax(code) {
171
+ if (LEGACY_APPLY_GROUP_RE.test(code)) return true;
172
+ let i = 0;
173
+ while (i < code.length) {
174
+ const at = code.indexOf("@", i);
175
+ if (at === -1) return false;
176
+ if (!isAtRuleBoundary(code, at)) {
177
+ i = at + 1;
178
+ continue;
179
+ }
180
+ let nameEnd = at + 1;
181
+ while (nameEnd < code.length && isAtRuleNameChar(code.charCodeAt(nameEnd))) nameEnd++;
182
+ const name = code.slice(at + 1, nameEnd);
183
+ const removals = REMOVAL_BODY_DIRECTIVES.has(name);
184
+ const keywords = KEYWORD_BODY_DIRECTIVES.has(name);
185
+ const blocks = BLOCK_AFTER_DECLARATION_DIRECTIVES.has(name);
186
+ if (!removals && !keywords && !blocks) {
187
+ i = nameEnd;
188
+ continue;
189
+ }
190
+ let braceIdx = nameEnd;
191
+ while (braceIdx < code.length) {
192
+ const ch = code[braceIdx];
193
+ if (ch === "{" || ch === ";" || ch === "}") break;
194
+ braceIdx++;
195
+ }
196
+ if (code[braceIdx] !== "{") {
197
+ i = braceIdx + 1;
198
+ continue;
199
+ }
200
+ const close = findClosingBrace(code, braceIdx);
201
+ if (close === -1) return true;
202
+ const body = code.slice(braceIdx + 1, close);
203
+ if (removals && matches(REMOVAL_RE, stripBlocks(body))) return true;
204
+ if (keywords && matches(FLUID_KEYWORD_RE, stripBlocks(body))) return true;
205
+ if (name === "color" && matches(COLOR_FLAG_RE, body)) return true;
206
+ if (blocks) {
207
+ for (const entry of scanEntries(stripCSSComments(body), {
208
+ newlineTerminates: name !== "animate"
209
+ })) {
210
+ if (entry.key !== "" && entry.value !== "" && entry.block !== void 0) return true;
211
+ }
212
+ }
213
+ i = close + 1;
214
+ }
215
+ return false;
216
+ }
217
+ function matches(pattern, text) {
218
+ pattern.lastIndex = 0;
219
+ return pattern.test(text);
220
+ }
221
+ function stripBlocks(body) {
222
+ return mapTopLevelSpans(body, keepSpan, (interior) => " ".repeat(interior.length));
223
+ }
149
224
 
150
225
  // src/integrations/vite.ts
151
226
  var CSS_FILE_RE = /\.(?:module\.)?css$/;
227
+ var SNAPSHOT_ID = "virtual:rainbowindex/snapshot";
228
+ var RESOLVED_SNAPSHOT_ID = `\0${SNAPSHOT_ID}`;
229
+ var RI_IMPORT_RE = /from\s*["']rainbowindex["']/;
152
230
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
153
231
  var POSTCSS_CONFIG_FILES = [
154
232
  "postcss.config.js",
@@ -166,127 +244,200 @@ function rainbowindexVite() {
166
244
  let root = process.cwd();
167
245
  let logger;
168
246
  const riCSSFiles = /* @__PURE__ */ new Set();
247
+ const riCSSSources = /* @__PURE__ */ new Map();
248
+ let devServer;
169
249
  const fileVersions = /* @__PURE__ */ new Map();
170
250
  const candidateSignatures = /* @__PURE__ */ new Map();
171
251
  let hotUpdateCount = 0;
172
252
  const PRUNE_INTERVAL = 50;
173
- return {
174
- name: "rainbowindex",
175
- enforce: "pre",
176
- async config(config) {
177
- root = config.root ?? process.cwd();
178
- const patch = {};
179
- const ignorePatterns = await riStylesheetPatterns(root);
180
- if (ignorePatterns.length > 0) {
181
- patch.fmt = { ignorePatterns };
182
- }
183
- if (!hasLocalPostCSSConfig(root)) {
184
- patch.css = {
185
- postcss: {
186
- plugins: [postcss_default()]
187
- }
188
- };
189
- }
190
- return patch;
191
- },
192
- configureServer(server) {
193
- root = server.config?.root ?? process.cwd();
194
- enableSourceFileListCache();
195
- server.watcher?.on("add", invalidateSourceFileListCache);
196
- server.watcher?.on("unlink", invalidateSourceFileListCache);
197
- server.watcher?.on("unlinkDir", invalidateSourceFileListCache);
198
- server.httpServer?.once("close", invalidateSourceFileListCache);
199
- logger = {
200
- info: (msg) => server.config.logger?.info?.(msg, { timestamp: true }),
201
- warn: (msg) => server.config.logger?.warn?.(msg, { timestamp: true })
253
+ const injectSnapshot = {
254
+ name: "rainbowindex:snapshot",
255
+ enforce: "post",
256
+ transform(code, id) {
257
+ if (id === RESOLVED_SNAPSHOT_ID || !RI_IMPORT_RE.test(code)) return null;
258
+ return {
259
+ code: `import ${JSON.stringify(SNAPSHOT_ID)};${code}`,
260
+ map: { mappings: "" }
202
261
  };
203
- if (hasLocalPostCSSConfig(root)) {
204
- logger.info("[rainbowindex] Using local PostCSS config \u2014 skipped auto-injection.");
205
- } else {
206
- logger.info("[rainbowindex] Injected PostCSS plugin (no local postcss.config.* found).");
207
- }
208
- server.httpServer?.once("listening", async () => {
209
- const cssFiles = [];
210
- const fileMap = server.moduleGraph.fileToModulesMap;
211
- if (fileMap) {
212
- for (const [file] of fileMap) {
213
- if (CSS_FILE_RE.test(file)) {
214
- cssFiles.push(file);
262
+ }
263
+ };
264
+ return [
265
+ {
266
+ name: "rainbowindex",
267
+ enforce: "pre",
268
+ async config(config) {
269
+ root = config.root ?? process.cwd();
270
+ const patch = {};
271
+ const ignorePatterns = await riStylesheetPatterns(root);
272
+ if (ignorePatterns.length > 0) {
273
+ patch.fmt = { ignorePatterns };
274
+ }
275
+ if (!hasLocalPostCSSConfig(root)) {
276
+ patch.css = {
277
+ postcss: {
278
+ plugins: [postcss_default()]
215
279
  }
280
+ };
281
+ }
282
+ return patch;
283
+ },
284
+ configureServer(server) {
285
+ root = server.config?.root ?? process.cwd();
286
+ devServer = server;
287
+ enableSourceFileListCache();
288
+ server.watcher?.on("add", invalidateSourceFileListCache);
289
+ server.watcher?.on("unlink", invalidateSourceFileListCache);
290
+ server.watcher?.on("unlinkDir", invalidateSourceFileListCache);
291
+ server.httpServer?.once("close", invalidateSourceFileListCache);
292
+ enableScanChangeTracking();
293
+ server.watcher?.on("change", (file) => markSourceFileChanged(file));
294
+ server.httpServer?.once("close", disableScanChangeTracking);
295
+ logger = {
296
+ info: (msg) => server.config.logger?.info?.(msg, { timestamp: true }),
297
+ warn: (msg) => server.config.logger?.warn?.(msg, { timestamp: true })
298
+ };
299
+ if (hasLocalPostCSSConfig(root)) {
300
+ logger.info("[rainbowindex] Using local PostCSS config \u2014 skipped auto-injection.");
301
+ } else {
302
+ logger.info("[rainbowindex] Injected PostCSS plugin (no local postcss.config.* found).");
303
+ }
304
+ server.httpServer?.once("listening", async () => {
305
+ const cssFiles = [];
306
+ const fileMap = server.moduleGraph.fileToModulesMap;
307
+ if (fileMap) {
308
+ for (const [file] of fileMap) {
309
+ if (CSS_FILE_RE.test(file)) {
310
+ cssFiles.push(file);
311
+ }
312
+ }
313
+ }
314
+ if (cssFiles.length === 0) {
315
+ const diskCSS = await findCSSFilesOnDisk(root);
316
+ cssFiles.push(...diskCSS);
317
+ }
318
+ await Promise.all(cssFiles.map((file) => checkCSSFileAsync(file)));
319
+ if (riCSSFiles.size === 0) {
320
+ logger?.warn(
321
+ `[RI-1602] rainbowindex Vite plugin is registered but no CSS entry with \`@import "rainbowindex"\` was found under ${root}. Create one (e.g. src/index.css) and import it from your app entry, then restart the dev server. Or run \`rainbowindex init\` to wire it up automatically.`
322
+ );
323
+ } else {
324
+ const list = [...riCSSFiles].map((f) => relative(root, f).replaceAll("\\", "/")).join(", ");
325
+ logger?.info(`[rainbowindex] CSS entries: ${list}`);
326
+ }
327
+ });
328
+ },
329
+ resolveId(id) {
330
+ return id === SNAPSHOT_ID ? RESOLVED_SNAPSHOT_ID : null;
331
+ },
332
+ async load(id) {
333
+ if (id !== RESOLVED_SNAPSHOT_ID) return null;
334
+ if (riCSSSources.size === 0) await seedCSSSourcesFromDisk();
335
+ const snapshot = buildClientSnapshot();
336
+ return `import { hydrateSnapshot, publishSnapshot } from "rainbowindex";
337
+ publishSnapshot(hydrateSnapshot(${JSON.stringify(snapshot)}));
338
+ if (import.meta.hot) import.meta.hot.accept();
339
+ `;
340
+ },
341
+ transform(code, id) {
342
+ const file = id.split("?")[0];
343
+ if (CSS_FILE_RE.test(file)) {
344
+ fileVersions.set(file, (fileVersions.get(file) ?? 0) + 1);
345
+ if (hasRIActivation(code)) {
346
+ riCSSFiles.add(file);
347
+ if (riCSSSources.get(file) !== code) {
348
+ riCSSSources.set(file, code);
349
+ invalidateClientSnapshot();
350
+ }
351
+ let safe = rewriteDirectiveBodies(code);
352
+ const expandWarnings = [];
353
+ safe = expandGroupsInStylesheet(safe, expandWarnings, file);
354
+ const suppressed = parseFileDisables(code);
355
+ for (const w of expandWarnings) {
356
+ const warned = warningCode(w);
357
+ if (warned !== null && suppressed.has(warned)) continue;
358
+ (logger?.warn ?? console.warn)(`[rainbowindex] ${w}`);
359
+ }
360
+ return safe !== code ? safe : null;
216
361
  }
362
+ riCSSFiles.delete(file);
363
+ if (riCSSSources.delete(file)) invalidateClientSnapshot();
217
364
  }
218
- if (cssFiles.length === 0) {
219
- const diskCSS = await findCSSFilesOnDisk(root);
220
- cssFiles.push(...diskCSS);
365
+ return null;
366
+ },
367
+ async handleHotUpdate(ctx) {
368
+ const { file, server, modules } = ctx;
369
+ if (++hotUpdateCount % PRUNE_INTERVAL === 0) {
370
+ await pruneDeletedFiles();
221
371
  }
222
- await Promise.all(cssFiles.map((file) => checkCSSFileAsync(file)));
223
- if (riCSSFiles.size === 0) {
224
- logger?.warn(
225
- `[RI-1602] rainbowindex Vite plugin is registered but no CSS entry with \`@import "rainbowindex"\` was found under ${root}. Create one (e.g. src/index.css) and import it from your app entry, then restart the dev server. Or run \`rainbowindex init\` to wire it up automatically.`
226
- );
227
- } else {
228
- const list = [...riCSSFiles].map((f) => relative(root, f).replaceAll("\\", "/")).join(", ");
229
- logger?.info(`[rainbowindex] CSS entries: ${list}`);
372
+ if (CSS_FILE_RE.test(file)) {
373
+ await checkCSSFileAsync(file);
374
+ return;
230
375
  }
231
- });
232
- },
233
- transform(code, id) {
234
- const file = id.split("?")[0];
235
- if (CSS_FILE_RE.test(file)) {
236
- fileVersions.set(file, (fileVersions.get(file) ?? 0) + 1);
237
- if (hasRIActivation(code)) {
238
- riCSSFiles.add(file);
239
- let safe = rewriteDirectiveBodies(code);
240
- const expandWarnings = [];
241
- safe = expandApplyGroups(safe, expandWarnings);
242
- for (const w of expandWarnings) {
243
- (logger?.warn ?? console.warn)(`[rainbowindex] ${w}`);
376
+ if (!isSourceFile(file)) return;
377
+ markSourceFileChanged(file);
378
+ const previous = candidateSignatures.get(file);
379
+ let signature;
380
+ try {
381
+ const content = await ctx.read();
382
+ signature = [...extractClassesFromSource({ path: file, content })].sort(codepointCompare).join(" ");
383
+ } catch {
384
+ candidateSignatures.delete(file);
385
+ }
386
+ if (signature !== void 0) {
387
+ candidateSignatures.set(file, signature);
388
+ if (previous === signature) return;
389
+ }
390
+ const extraModules = [];
391
+ const hmrModules = new Set(modules);
392
+ for (const cssFile of riCSSFiles) {
393
+ const mods = server.moduleGraph.getModulesByFile(cssFile);
394
+ if (mods) {
395
+ for (const mod of mods) {
396
+ if (!hmrModules.has(mod)) {
397
+ extraModules.push(mod);
398
+ }
399
+ }
244
400
  }
245
- return safe !== code ? safe : null;
246
401
  }
247
- riCSSFiles.delete(file);
402
+ if (extraModules.length > 0) {
403
+ return [...modules, ...extraModules];
404
+ }
248
405
  }
249
- return null;
250
406
  },
251
- async handleHotUpdate(ctx) {
252
- const { file, server, modules } = ctx;
253
- if (++hotUpdateCount % PRUNE_INTERVAL === 0) {
254
- await pruneDeletedFiles();
255
- }
256
- if (CSS_FILE_RE.test(file)) {
257
- await checkCSSFileAsync(file);
258
- return;
259
- }
260
- if (!isSourceFile(file)) return;
261
- const previous = candidateSignatures.get(file);
262
- let signature;
407
+ injectSnapshot
408
+ ];
409
+ async function seedCSSSourcesFromDisk() {
410
+ const files = await findCSSFilesOnDisk(root);
411
+ await Promise.all(
412
+ files.map(async (file) => {
413
+ try {
414
+ const content = await readFile(file, "utf-8");
415
+ if (!hasRIActivation(content)) return;
416
+ riCSSFiles.add(file);
417
+ riCSSSources.set(file, content);
418
+ } catch {
419
+ }
420
+ })
421
+ );
422
+ }
423
+ function buildClientSnapshot() {
424
+ const resolveImport = createNodeImportResolver({ cwd: root });
425
+ const css = [...riCSSSources.keys()].sort(codepointCompare).map((file) => {
426
+ const text = riCSSSources.get(file) ?? "";
263
427
  try {
264
- const content = await ctx.read();
265
- signature = [...extractClassesFromSource({ path: file, content })].sort(codepointCompare).join(" ");
428
+ return inlineDirectiveImports(text, { resolve: resolveImport, from: file }).css;
266
429
  } catch {
267
- candidateSignatures.delete(file);
430
+ return text;
268
431
  }
269
- if (signature !== void 0) {
270
- candidateSignatures.set(file, signature);
271
- if (previous === signature) return;
272
- }
273
- const extraModules = [];
274
- const hmrModules = new Set(modules);
275
- for (const cssFile of riCSSFiles) {
276
- const mods = server.moduleGraph.getModulesByFile(cssFile);
277
- if (mods) {
278
- for (const mod of mods) {
279
- if (!hmrModules.has(mod)) {
280
- extraModules.push(mod);
281
- }
282
- }
283
- }
284
- }
285
- if (extraModules.length > 0) {
286
- return [...modules, ...extraModules];
287
- }
288
- }
289
- };
432
+ }).join("\n");
433
+ return snapshotFromCSS(css);
434
+ }
435
+ function invalidateClientSnapshot() {
436
+ const graph = devServer?.moduleGraph;
437
+ if (!graph) return;
438
+ const mod = graph.getModuleById(RESOLVED_SNAPSHOT_ID);
439
+ if (mod) graph.invalidateModule(mod);
440
+ }
290
441
  async function pruneDeletedFiles() {
291
442
  const tracked = /* @__PURE__ */ new Set([...riCSSFiles, ...fileVersions.keys(), ...candidateSignatures.keys()]);
292
443
  const checks = [...tracked].map(async (file) => {
@@ -294,6 +445,7 @@ function rainbowindexVite() {
294
445
  await access(file);
295
446
  } catch {
296
447
  riCSSFiles.delete(file);
448
+ if (riCSSSources.delete(file)) invalidateClientSnapshot();
297
449
  fileVersions.delete(file);
298
450
  candidateSignatures.delete(file);
299
451
  }
@@ -325,7 +477,9 @@ function rainbowindexVite() {
325
477
  const active = await Promise.all(
326
478
  files.map(async (file) => {
327
479
  try {
328
- return hasRIActivation(await readFile(file, "utf-8")) ? file : null;
480
+ const code = await readFile(file, "utf-8");
481
+ if (!hasRIActivation(code)) return null;
482
+ return usesLegacyDirectiveSyntax(code) ? file : null;
329
483
  } catch {
330
484
  return null;
331
485
  }
@@ -345,12 +499,18 @@ function rainbowindexVite() {
345
499
  if ((fileVersions.get(file) ?? 0) !== versionBefore) return;
346
500
  if (hasRIActivation(raw)) {
347
501
  riCSSFiles.add(file);
502
+ if (riCSSSources.get(file) !== raw) {
503
+ riCSSSources.set(file, raw);
504
+ invalidateClientSnapshot();
505
+ }
348
506
  } else {
349
507
  riCSSFiles.delete(file);
508
+ if (riCSSSources.delete(file)) invalidateClientSnapshot();
350
509
  }
351
510
  } catch (_err) {
352
511
  if ((fileVersions.get(file) ?? 0) !== versionBefore) return;
353
512
  riCSSFiles.delete(file);
513
+ if (riCSSSources.delete(file)) invalidateClientSnapshot();
354
514
  }
355
515
  }
356
516
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainbowindex",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "CSS-first system for building and maintaining consistent user interfaces",
5
5
  "keywords": [
6
6
  "css",
@@ -32,18 +32,23 @@
32
32
  "homepage": "https://rainbowindex.dev",
33
33
  "bugs": "https://github.com/rainbowindex/rainbowindex/issues",
34
34
  "type": "module",
35
- "packageManager": "pnpm@10.12.1",
35
+ "packageManager": "pnpm@10.34.5",
36
36
  "main": "./dist/index.mjs",
37
37
  "types": "./dist/index.d.ts",
38
38
  "style": "./dist/index.css",
39
39
  "scripts": {
40
40
  "build": "tsup",
41
41
  "prepublishOnly": "pnpm check",
42
- "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm test:artifacts",
42
+ "check": "pnpm format:check && pnpm typecheck && pnpm lint && pnpm test && pnpm test:artifacts",
43
43
  "test": "vitest run --exclude __tests__/cli/** --exclude __tests__/package.test.ts",
44
44
  "test:coverage": "vitest run --coverage --exclude __tests__/cli/** --exclude __tests__/package.test.ts",
45
45
  "test:artifacts": "pnpm build && vitest run __tests__/cli/cli.test.ts __tests__/package.test.ts",
46
+ "test:golden:update": "vitest run __tests__/golden -u",
46
47
  "test:watch": "vitest",
48
+ "bench": "pnpm --filter @rainbowindex/bench run bench",
49
+ "bench:cli": "pnpm --filter @rainbowindex/bench run bench:cli",
50
+ "bench:parity": "pnpm --filter @rainbowindex/bench run bench:parity",
51
+ "examples:build": "pnpm --filter \"@rainbowindex-example/*\" run build",
47
52
  "typecheck": "tsc --noEmit",
48
53
  "lint": "biome lint .",
49
54
  "lint:fix": "biome lint --write .",
@@ -59,6 +64,7 @@
59
64
  "default": "./dist/index.mjs"
60
65
  },
61
66
  "./index.css": "./dist/index.css",
67
+ "./tailwind.css": "./dist/tailwind.css",
62
68
  "./editor": {
63
69
  "types": "./dist/editor.d.ts",
64
70
  "default": "./dist/editor.mjs"
@@ -71,6 +77,14 @@
71
77
  "types": "./dist/oxlint.d.ts",
72
78
  "default": "./dist/oxlint.mjs"
73
79
  },
80
+ "./eslint": {
81
+ "types": "./dist/eslint.d.ts",
82
+ "default": "./dist/eslint.mjs"
83
+ },
84
+ "./recipe": {
85
+ "types": "./dist/recipe.d.ts",
86
+ "default": "./dist/recipe.mjs"
87
+ },
74
88
  "./package.json": "./package.json"
75
89
  },
76
90
  "bin": {
@@ -78,7 +92,8 @@
78
92
  },
79
93
  "files": [
80
94
  "dist",
81
- "CHANGELOG.md"
95
+ "CHANGELOG.md",
96
+ "NOTICE.md"
82
97
  ],
83
98
  "sideEffects": [
84
99
  "**/*.css"
@@ -106,10 +121,17 @@
106
121
  "@capsizecss/metrics": "^4.2.0",
107
122
  "@types/node": "^25.9.1",
108
123
  "@vitest/coverage-v8": "^4.1.0",
124
+ "esbuild": "^0.27.7",
125
+ "eslint": "^10.9.1",
126
+ "oxfmt": "^0.65.0",
109
127
  "postcss": "^8.5.8",
128
+ "prettier": "^3.9.6",
129
+ "stylelint": "^17.14.1",
110
130
  "tsup": "^8.5.1",
111
131
  "typescript": "^6.0.3",
112
132
  "vite": "8.0.0",
113
- "vitest": "^4.0.17"
133
+ "vitest": "^4.0.17",
134
+ "vscode-css-languageservice": "^6.3.10",
135
+ "vscode-languageserver-textdocument": "^1.0.14"
114
136
  }
115
137
  }