@wojciechpiskorz/astroix 0.0.7 → 0.0.8

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/index.js CHANGED
@@ -4,15 +4,212 @@ import { fileURLToPath as fileURLToPath2 } from "url";
4
4
  import tailwindcss from "@tailwindcss/vite";
5
5
  import react from "@vitejs/plugin-react";
6
6
 
7
+ // src/node/api.ts
8
+ var API_PREFIX = "/__astroix";
9
+ function registerApiEndpoints(server, options) {
10
+ const table = new Map(
11
+ options.handlers.map((handler) => [handlerKey(handler.method, handler.path), handler])
12
+ );
13
+ if (table.size !== options.handlers.length) {
14
+ throw new Error("astroix: duplicate route in the /__astroix handler table");
15
+ }
16
+ const ctx = {
17
+ server,
18
+ root: options.root,
19
+ srcDir: options.srcDir,
20
+ routes: options.routes
21
+ };
22
+ server.middlewares.use(API_PREFIX, (req, res, next) => {
23
+ void dispatchApi(req, res, next, table, ctx);
24
+ });
25
+ }
26
+ async function dispatchApi(req, res, next, table, ctx) {
27
+ try {
28
+ if (isCrossOriginTraffic(req)) {
29
+ json(res, 403, { error: "cross-origin builder traffic is not allowed" });
30
+ return;
31
+ }
32
+ const url = new URL(req.url ?? "/", "http://astroix.internal");
33
+ const handler = table.get(handlerKey(req.method ?? "", url.pathname));
34
+ if (handler === void 0) {
35
+ next();
36
+ return;
37
+ }
38
+ await handler.handle(req, res, url, ctx);
39
+ } catch (error) {
40
+ next(error instanceof Error ? error : new Error(String(error)));
41
+ }
42
+ }
43
+ function handlerKey(method, path) {
44
+ return `${method} ${path}`;
45
+ }
46
+ function isCrossOriginTraffic(req) {
47
+ const secFetchSite = req.headers["sec-fetch-site"];
48
+ return typeof secFetchSite === "string" && secFetchSite !== "same-origin" && secFetchSite !== "none";
49
+ }
50
+ function json(res, status, body) {
51
+ res.statusCode = status;
52
+ res.setHeader("content-type", "application/json; charset=utf-8");
53
+ res.end(JSON.stringify(body));
54
+ }
55
+
56
+ // src/node/routes.ts
57
+ function toRouteInfos(routes) {
58
+ return routes.flatMap((route) => {
59
+ if (route.type !== "page") return [];
60
+ return [
61
+ {
62
+ pattern: route.pattern,
63
+ segments: route.segments.map((segment) => segment.map((part) => ({ ...part }))),
64
+ params: [...route.params]
65
+ }
66
+ ];
67
+ });
68
+ }
69
+ var routesHandlers = [
70
+ { method: "GET", path: "/routes", handle: handleRoutes }
71
+ ];
72
+ async function handleRoutes(_req, res, _url, ctx) {
73
+ json(res, 200, ctx.routes.current);
74
+ }
75
+
76
+ // src/node/source-mode.ts
77
+ import { existsSync } from "fs";
78
+ import { fileURLToPath, URL as NodeURL } from "url";
79
+ var candidates = [
80
+ fileURLToPath(new NodeURL("../../src/client/entry.tsx", import.meta.url)),
81
+ fileURLToPath(new NodeURL("../src/client/entry.tsx", import.meta.url))
82
+ ];
83
+ var clientEntryPath = candidates.find((path) => existsSync(path)) ?? null;
84
+ var chromeArtifactPath = fileURLToPath(
85
+ new NodeURL("./chrome.js", import.meta.url)
86
+ );
87
+ function isSourceMode() {
88
+ return clientEntryPath !== null;
89
+ }
90
+
91
+ // src/node/tailwind-guard.ts
92
+ function hostRegistersTailwind(viteConfig) {
93
+ const seen = /* @__PURE__ */ new Set();
94
+ const walk = (input) => {
95
+ if (Array.isArray(input)) {
96
+ for (const entry of input) walk(entry);
97
+ return;
98
+ }
99
+ if (!input || typeof input !== "object") return;
100
+ const plugin = input;
101
+ if (typeof plugin.name === "string") seen.add(plugin.name);
102
+ };
103
+ walk(viteConfig?.plugins);
104
+ for (const name of seen) {
105
+ if (name.startsWith("@tailwindcss/vite")) return true;
106
+ }
107
+ return false;
108
+ }
109
+
110
+ // src/node/vite-plugin.ts
111
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
112
+ import { join as join3 } from "path";
113
+
114
+ // src/node/chrome-html.ts
115
+ function chromeHtml() {
116
+ return `<!doctype html>
117
+ <html lang="en">
118
+ <head>
119
+ <meta charset="utf-8" />
120
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
121
+ <title>astroix builder</title>
122
+ <style>
123
+ html, body { margin: 0; height: 100%; }
124
+ #astroix-root { display: block; height: 100vh; }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div id="astroix-root"></div>
129
+ <script type="module" src="/virtual:astroix/chrome"></script>
130
+ </body>
131
+ </html>`;
132
+ }
133
+
7
134
  // src/node/content.ts
8
135
  import { existsSync as existsSync2 } from "fs";
9
- import { join as join2 } from "path";
136
+ import { join } from "path";
10
137
  import { createServerModuleRunner } from "vite";
138
+ var contentHandlers = [
139
+ { method: "GET", path: "/collections", handle: handleCollections }
140
+ ];
141
+ async function handleCollections(_req, res, _url, ctx) {
142
+ const runner = createServerModuleRunner(ctx.server.environments.ssr);
143
+ const configPath = findContentConfigPath(ctx.srcDir);
144
+ const configModule = configPath === null ? null : await runner.import(configPath);
145
+ const contentModule = await runner.import("astro:content");
146
+ json(res, 200, await assembleCollectionsPayload(configModule, contentModule));
147
+ }
148
+ async function assembleCollectionsPayload(configModule, contentModule) {
149
+ const definitions = toDefinitionMap(configModule?.collections);
150
+ const collections = [];
151
+ for (const name of Object.keys(definitions).sort()) {
152
+ const entries = await loadEntries(contentModule, name);
153
+ collections.push({ name, hasSchema: definitions[name]?.schema !== void 0, entries });
154
+ }
155
+ return collections;
156
+ }
157
+ function toDefinitionMap(collections) {
158
+ if (typeof collections !== "object" || collections === null) return {};
159
+ const definitions = {};
160
+ for (const [name, definition] of Object.entries(collections)) {
161
+ if (typeof definition === "object" && definition !== null) {
162
+ definitions[name] = definition;
163
+ }
164
+ }
165
+ return definitions;
166
+ }
167
+ async function loadEntries(contentModule, name) {
168
+ const raw = await contentModule.getCollection?.(name) ?? [];
169
+ return raw.filter(
170
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string"
171
+ ).map((entry) => ({
172
+ id: entry.id,
173
+ filePath: typeof entry.filePath === "string" ? entry.filePath : null,
174
+ data: entry.data ?? null,
175
+ body: typeof entry.body === "string" ? entry.body : null
176
+ })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
177
+ }
178
+ function findContentConfigPath(srcDir) {
179
+ const candidates2 = [
180
+ ["content.config.mjs", "content.config.js", "content.config.mts", "content.config.ts"].map(
181
+ (name) => join(srcDir, name)
182
+ ),
183
+ ["config.ts", "config.js", "config.mjs", "config.mts"].map(
184
+ (name) => join(srcDir, "content", name)
185
+ )
186
+ ].flat();
187
+ return candidates2.find((candidate) => existsSync2(candidate)) ?? null;
188
+ }
189
+
190
+ // src/node/document-request.ts
191
+ function isDocumentRequest(input) {
192
+ if (input.method !== "GET" && input.method !== "HEAD") return false;
193
+ if (!(input.accept ?? "").includes("text/html")) return false;
194
+ let url;
195
+ try {
196
+ url = new URL(input.url, "http://astroix.internal");
197
+ } catch {
198
+ return false;
199
+ }
200
+ if (url.searchParams.has("builder")) return false;
201
+ const { pathname } = url;
202
+ if (pathname.startsWith("/@") || pathname.startsWith("/__") || pathname.startsWith("/_astro") || pathname.startsWith("/virtual:")) {
203
+ return false;
204
+ }
205
+ if (/(^|\/)[^/]*\.[a-zA-Z0-9]+$/.test(pathname)) return false;
206
+ return true;
207
+ }
11
208
 
12
209
  // src/node/rest.ts
13
210
  import { createHash } from "crypto";
14
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
15
- import { join, relative, resolve, sep } from "path";
211
+ import { existsSync as existsSync3, readdirSync, readFileSync, writeFileSync } from "fs";
212
+ import { join as join2, relative, resolve, sep } from "path";
16
213
  import postcss2 from "postcss";
17
214
 
18
215
  // src/core/indexer.ts
@@ -115,78 +312,61 @@ function spliceText(content, edit) {
115
312
  }
116
313
 
117
314
  // src/node/rest.ts
118
- var API_PREFIX = "/__astroix";
119
315
  var MAX_BODY_BYTES = 1e6;
120
- function registerRestEndpoints(server, options) {
121
- server.middlewares.use(API_PREFIX, (req, res, next) => {
122
- void handleApiRequest(req, res, next, server, options);
123
- });
124
- }
125
- async function handleApiRequest(req, res, next, server, options) {
316
+ var restHandlers = [
317
+ // The bare mount serves the index payload too (connect hands it as `/`).
318
+ { method: "GET", path: "/", handle: handleIndex },
319
+ { method: "GET", path: "/index", handle: handleIndex },
320
+ { method: "GET", path: "/file", handle: handleFile },
321
+ { method: "POST", path: "/edit", handle: handleEdit }
322
+ ];
323
+ async function handleIndex(_req, res, _url, ctx) {
324
+ const payload = await buildIndexPayload(
325
+ collectSources(ctx.srcDir),
326
+ (file, blockIndex) => resolveCompiledCss(ctx.server, ctx.root, file, blockIndex)
327
+ );
328
+ json(
329
+ res,
330
+ 200,
331
+ payload.map((record) => ({ ...record, file: toRelative(ctx.root, record.file) }))
332
+ );
333
+ }
334
+ async function handleFile(_req, res, url, ctx) {
335
+ const file = url.searchParams.get("file");
336
+ const absPath = file === null ? null : safeResolve(ctx.root, file);
337
+ if (file === null || absPath === null || !existsSync3(absPath)) {
338
+ json(res, 400, { error: `file is missing or outside the project root: ${file ?? ""}` });
339
+ return;
340
+ }
341
+ json(res, 200, { file, contents: readFileSync(absPath, "utf8") });
342
+ }
343
+ async function handleEdit(req, res, _url, ctx) {
344
+ const body = await readJsonBody(req);
345
+ const { file, range, replacement, expected } = parseEditBody(body);
346
+ if (file === null || range === null || replacement === null) {
347
+ json(res, 400, { error: "expected { file, range: { start, end }, replacement }" });
348
+ return;
349
+ }
350
+ const absPath = safeResolve(ctx.root, file);
351
+ if (absPath === null) {
352
+ json(res, 400, { error: `file is outside the project root: ${file}` });
353
+ return;
354
+ }
355
+ const contents = readFileSync(absPath, "utf8");
356
+ if (expected !== null && sha256(contents) !== expected) {
357
+ json(res, 409, { error: "file changed on disk", contents });
358
+ return;
359
+ }
126
360
  try {
127
- if (isCrossOriginTraffic(req)) {
128
- json(res, 403, { error: "cross-origin builder traffic is not allowed" });
129
- return;
130
- }
131
- const url = new URL(req.url ?? "/", "http://astroix.internal");
132
- if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index")) {
133
- const payload = await buildIndexPayload(
134
- collectSources(options.srcDir),
135
- (file, blockIndex) => resolveCompiledCss(server, options.root, file, blockIndex)
136
- );
137
- json(
138
- res,
139
- 200,
140
- payload.map((record) => ({ ...record, file: toRelative(options.root, record.file) }))
141
- );
142
- return;
143
- }
144
- if (req.method === "GET" && url.pathname === "/file") {
145
- const file = url.searchParams.get("file");
146
- const absPath = file === null ? null : safeResolve(options.root, file);
147
- if (file === null || absPath === null || !existsSync(absPath)) {
148
- json(res, 400, { error: `file is missing or outside the project root: ${file ?? ""}` });
149
- return;
150
- }
151
- json(res, 200, { file, contents: readFileSync(absPath, "utf8") });
152
- return;
153
- }
154
- if (req.method === "POST" && url.pathname === "/edit") {
155
- const body = await readJsonBody(req);
156
- const { file, range, replacement, expected } = parseEditBody(body);
157
- if (file === null || range === null || replacement === null) {
158
- json(res, 400, { error: "expected { file, range: { start, end }, replacement }" });
159
- return;
160
- }
161
- const absPath = safeResolve(options.root, file);
162
- if (absPath === null) {
163
- json(res, 400, { error: `file is outside the project root: ${file}` });
164
- return;
165
- }
166
- const contents = readFileSync(absPath, "utf8");
167
- if (expected !== null && sha256(contents) !== expected) {
168
- json(res, 409, { error: "file changed on disk", contents });
169
- return;
170
- }
171
- try {
172
- writeFileSync(
173
- absPath,
174
- spliceText(contents, { start: range[0], end: range[1], replacement })
175
- );
176
- } catch (error) {
177
- if (error instanceof SpliceRangeError) {
178
- json(res, 400, { error: error.message });
179
- return;
180
- }
181
- throw error;
182
- }
183
- json(res, 200, { ok: true });
361
+ writeFileSync(absPath, spliceText(contents, { start: range[0], end: range[1], replacement }));
362
+ } catch (error) {
363
+ if (error instanceof SpliceRangeError) {
364
+ json(res, 400, { error: error.message });
184
365
  return;
185
366
  }
186
- next();
187
- } catch (error) {
188
- next(error instanceof Error ? error : new Error(String(error)));
367
+ throw error;
189
368
  }
369
+ json(res, 200, { ok: true });
190
370
  }
191
371
  async function buildIndexPayload(sources, resolveCompiledCss2) {
192
372
  const payload = buildCssIndex(sources).map((record) => ({
@@ -243,12 +423,12 @@ async function resolveCompiledCss(server, root, file, styleBlockIndex) {
243
423
  return extractCssFromModuleCode(code);
244
424
  }
245
425
  function collectSources(srcDir) {
246
- if (!existsSync(srcDir)) return [];
426
+ if (!existsSync3(srcDir)) return [];
247
427
  const sources = [];
248
428
  const walk = (dir) => {
249
429
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
250
430
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
251
- const full = join(dir, entry.name);
431
+ const full = join2(dir, entry.name);
252
432
  if (entry.isDirectory()) {
253
433
  walk(full);
254
434
  } else if (entry.name.endsWith(".css") || entry.name.endsWith(".astro")) {
@@ -283,15 +463,6 @@ function parseEditBody(body) {
283
463
  function sha256(text) {
284
464
  return createHash("sha256").update(text).digest("hex");
285
465
  }
286
- function isCrossOriginTraffic(req) {
287
- const secFetchSite = req.headers["sec-fetch-site"];
288
- return typeof secFetchSite === "string" && secFetchSite !== "same-origin" && secFetchSite !== "none";
289
- }
290
- function json(res, status, body) {
291
- res.statusCode = status;
292
- res.setHeader("content-type", "application/json; charset=utf-8");
293
- res.end(JSON.stringify(body));
294
- }
295
466
  function readJsonBody(req) {
296
467
  return new Promise((resolveBody, reject) => {
297
468
  const chunks = [];
@@ -316,167 +487,6 @@ function readJsonBody(req) {
316
487
  });
317
488
  }
318
489
 
319
- // src/node/content.ts
320
- function toRouteInfos(routes) {
321
- return routes.flatMap((route) => {
322
- if (route.type !== "page") return [];
323
- return [
324
- {
325
- pattern: route.pattern,
326
- segments: route.segments.map((segment) => segment.map((part) => ({ ...part }))),
327
- params: [...route.params]
328
- }
329
- ];
330
- });
331
- }
332
- function registerContentEndpoints(server, options) {
333
- server.middlewares.use(API_PREFIX, (req, res, next) => {
334
- void handleContentRequest(req, res, next, server, options);
335
- });
336
- }
337
- async function handleContentRequest(req, res, next, server, options) {
338
- try {
339
- if (isCrossOriginTraffic(req)) {
340
- json(res, 403, { error: "cross-origin builder traffic is not allowed" });
341
- return;
342
- }
343
- const url = new URL(req.url ?? "/", "http://astroix.internal");
344
- if (req.method === "GET" && url.pathname === "/collections") {
345
- const runner = createServerModuleRunner(server.environments.ssr);
346
- const configPath = findContentConfigPath(options.srcDir);
347
- const configModule = configPath === null ? null : await runner.import(configPath);
348
- const contentModule = await runner.import("astro:content");
349
- json(res, 200, await assembleCollectionsPayload(configModule, contentModule));
350
- return;
351
- }
352
- if (req.method === "GET" && url.pathname === "/routes") {
353
- json(res, 200, options.routes.current);
354
- return;
355
- }
356
- next();
357
- } catch (error) {
358
- next(error instanceof Error ? error : new Error(String(error)));
359
- }
360
- }
361
- async function assembleCollectionsPayload(configModule, contentModule) {
362
- const definitions = toDefinitionMap(configModule?.collections);
363
- const collections = [];
364
- for (const name of Object.keys(definitions).sort()) {
365
- const entries = await loadEntries(contentModule, name);
366
- collections.push({ name, hasSchema: definitions[name]?.schema !== void 0, entries });
367
- }
368
- return collections;
369
- }
370
- function toDefinitionMap(collections) {
371
- if (typeof collections !== "object" || collections === null) return {};
372
- const definitions = {};
373
- for (const [name, definition] of Object.entries(collections)) {
374
- if (typeof definition === "object" && definition !== null) {
375
- definitions[name] = definition;
376
- }
377
- }
378
- return definitions;
379
- }
380
- async function loadEntries(contentModule, name) {
381
- const raw = await contentModule.getCollection?.(name) ?? [];
382
- return raw.filter(
383
- (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string"
384
- ).map((entry) => ({
385
- id: entry.id,
386
- filePath: typeof entry.filePath === "string" ? entry.filePath : null,
387
- data: entry.data ?? null,
388
- body: typeof entry.body === "string" ? entry.body : null
389
- })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
390
- }
391
- function findContentConfigPath(srcDir) {
392
- const candidates2 = [
393
- ["content.config.mjs", "content.config.js", "content.config.mts", "content.config.ts"].map(
394
- (name) => join2(srcDir, name)
395
- ),
396
- ["config.ts", "config.js", "config.mjs", "config.mts"].map(
397
- (name) => join2(srcDir, "content", name)
398
- )
399
- ].flat();
400
- return candidates2.find((candidate) => existsSync2(candidate)) ?? null;
401
- }
402
-
403
- // src/node/source-mode.ts
404
- import { existsSync as existsSync3 } from "fs";
405
- import { fileURLToPath, URL as NodeURL } from "url";
406
- var candidates = [
407
- fileURLToPath(new NodeURL("../../src/client/entry.tsx", import.meta.url)),
408
- fileURLToPath(new NodeURL("../src/client/entry.tsx", import.meta.url))
409
- ];
410
- var clientEntryPath = candidates.find((path) => existsSync3(path)) ?? null;
411
- var chromeArtifactPath = fileURLToPath(
412
- new NodeURL("./chrome.js", import.meta.url)
413
- );
414
- function isSourceMode() {
415
- return clientEntryPath !== null;
416
- }
417
-
418
- // src/node/tailwind-guard.ts
419
- function hostRegistersTailwind(viteConfig) {
420
- const seen = /* @__PURE__ */ new Set();
421
- const walk = (input) => {
422
- if (Array.isArray(input)) {
423
- for (const entry of input) walk(entry);
424
- return;
425
- }
426
- if (!input || typeof input !== "object") return;
427
- const plugin = input;
428
- if (typeof plugin.name === "string") seen.add(plugin.name);
429
- };
430
- walk(viteConfig?.plugins);
431
- for (const name of seen) {
432
- if (name.startsWith("@tailwindcss/vite")) return true;
433
- }
434
- return false;
435
- }
436
-
437
- // src/node/vite-plugin.ts
438
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
439
- import { join as join3 } from "path";
440
-
441
- // src/node/chrome-html.ts
442
- function chromeHtml() {
443
- return `<!doctype html>
444
- <html lang="en">
445
- <head>
446
- <meta charset="utf-8" />
447
- <meta name="viewport" content="width=device-width, initial-scale=1" />
448
- <title>astroix builder</title>
449
- <style>
450
- html, body { margin: 0; height: 100%; }
451
- #astroix-root { display: block; height: 100vh; }
452
- </style>
453
- </head>
454
- <body>
455
- <div id="astroix-root"></div>
456
- <script type="module" src="/virtual:astroix/chrome"></script>
457
- </body>
458
- </html>`;
459
- }
460
-
461
- // src/node/document-request.ts
462
- function isDocumentRequest(input) {
463
- if (input.method !== "GET" && input.method !== "HEAD") return false;
464
- if (!(input.accept ?? "").includes("text/html")) return false;
465
- let url;
466
- try {
467
- url = new URL(input.url, "http://astroix.internal");
468
- } catch {
469
- return false;
470
- }
471
- if (url.searchParams.has("builder")) return false;
472
- const { pathname } = url;
473
- if (pathname.startsWith("/@") || pathname.startsWith("/__") || pathname.startsWith("/_astro") || pathname.startsWith("/virtual:")) {
474
- return false;
475
- }
476
- if (/(^|\/)[^/]*\.[a-zA-Z0-9]+$/.test(pathname)) return false;
477
- return true;
478
- }
479
-
480
490
  // src/node/watch-sync.ts
481
491
  import { sep as sep2 } from "path";
482
492
  function registerFileSync(server, options) {
@@ -530,8 +540,12 @@ function astroixVitePlugin(options) {
530
540
  })();
531
541
  });
532
542
  const srcDir = options.srcDir ?? join3(server.config.root, "src");
533
- registerRestEndpoints(server, { root: server.config.root, srcDir });
534
- registerContentEndpoints(server, { srcDir, routes: options.routes });
543
+ registerApiEndpoints(server, {
544
+ root: server.config.root,
545
+ srcDir,
546
+ routes: options.routes,
547
+ handlers: [...restHandlers, ...contentHandlers, ...routesHandlers]
548
+ });
535
549
  registerFileSync(server, { root: server.config.root, srcDir });
536
550
  },
537
551
  resolveId(id) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/node/index.ts","../src/node/content.ts","../src/node/rest.ts","../src/core/indexer.ts","../src/core/splice-writer.ts","../src/node/source-mode.ts","../src/node/tailwind-guard.ts","../src/node/vite-plugin.ts","../src/node/chrome-html.ts","../src/node/document-request.ts","../src/node/watch-sync.ts"],"sourcesContent":["import { dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport tailwindcss from '@tailwindcss/vite';\nimport react from '@vitejs/plugin-react';\nimport type { AstroIntegration } from 'astro';\nimport type { Plugin as VitePlugin } from 'vite';\nimport { type RoutesState, toRouteInfos } from './content';\nimport { clientEntryPath, isSourceMode } from './source-mode';\nimport { hostRegistersTailwind } from './tailwind-guard';\nimport { astroixVitePlugin } from './vite-plugin';\n\n/**\n * Canvas script injected into every dev page (the chrome document itself is\n * never Astro-rendered, so this only ever runs inside host pages). It decides\n * on its own whether it is the builder canvas: inside the iframe\n * (`window.parent !== window`) with `?builder=0`, it hides Astro's dev\n * toolbar — the toolbar stays available on normal page loads (spec #2).\n */\nconst CANVAS_SCRIPT = `if (window.parent !== window && new URLSearchParams(location.search).get('builder') === '0') {\n const style = document.createElement('style');\n style.textContent = 'astro-dev-toolbar{display:none!important}';\n document.head.append(style);\n}\n`;\n\n/**\n * Astroix — dev-only visual builder integration.\n *\n * In dev: the astroix Vite plugin serves the builder chrome over every\n * top-level URL (default-on) with the `?builder=0` escape hatch, and the\n * virtual chrome module delivers the app (source mode in this checkout per\n * ADR-0001; the prebuilt bundle lands with the chrome packaging slice).\n * Any other command registers nothing — the dev-only guarantee.\n */\nfunction astroix(): AstroIntegration {\n // Routes captured from the hook below and served at `GET /__astroix/routes`\n // (spec Impl #13). Lives on the integration instance so both hooks —\n // the writer and the plugin that serves the state — share one container\n // across dev restarts (restarts re-run the routes hook on the same instance).\n const routesState: RoutesState = { current: [] };\n return {\n name: 'astroix',\n hooks: {\n 'astro:routes:resolved': ({ routes }) => {\n routesState.current = toRouteInfos(routes);\n },\n 'astro:config:setup': ({ config, command, updateConfig, injectScript, logger }) => {\n if (command !== 'dev') return;\n\n // The resolved config turns dir strings into URLs (trailing slash included) —\n // the plugin wants clean paths.\n const plugins: VitePlugin[] = [\n astroixVitePlugin({ srcDir: fileURLToPath(config.srcDir), routes: routesState }),\n ];\n // The chrome sources live outside the host root and are served via\n // /@fs, which has two consequences fixed below: (a) deps discovered\n // from /@fs files resolve against the importer's location, so `react`\n // can enter the optimizer from two paths and mount twice (Invalid\n // hook call) — dedupe pins every resolution to the host root's React,\n // which in the dev checkout is our own 19; (b) HMR re-fetches carry a\n // `?t=` timestamp that misses the import-chain fs exemption — the\n // checkout root joins the allow list so chrome modules always serve.\n let vitePatch: {\n plugins: VitePlugin[];\n resolve?: { dedupe: string[] };\n server?: { fs: { allow: string[] } };\n } = { plugins };\n\n if (isSourceMode()) {\n // ADR-0001 source mode: chrome from this checkout's source, with\n // fast-refresh scoped to chrome files only (host code untouched).\n const clientDir = dirname(clientEntryPath ?? '');\n // compiler: true = React Compiler via oxc (stack #4: no manual memoization).\n plugins.push(\n ...react({\n include: new RegExp(`^${escapeRegExp(clientDir)}/.*\\\\.tsx?$`),\n compiler: true,\n }),\n );\n if (hostRegistersTailwind(config.vite)) {\n logger.info('host already registers @tailwindcss/vite — reusing it for the chrome');\n } else {\n plugins.push(...tailwindcss());\n }\n vitePatch = {\n plugins,\n resolve: { dedupe: ['react', 'react-dom'] },\n server: { fs: { allow: [dirname(dirname(clientDir))] } },\n };\n }\n\n updateConfig({ vite: vitePatch });\n injectScript('page', CANVAS_SCRIPT);\n },\n },\n };\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport default astroix;\n","import { existsSync } from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { join } from 'node:path';\nimport type { IntegrationResolvedRoute } from 'astro';\nimport { createServerModuleRunner, type ViteDevServer } from 'vite';\nimport type { RouteInfo } from '../core/route-resolver';\nimport { API_PREFIX, isCrossOriginTraffic, json } from './rest';\n\n/** A single collection entry as served to the chrome (core's getCollection shape, JSON-projected). */\nexport interface CollectionEntryRecord {\n /** Slugified source path (glob loader id), e.g. `2024/post`. */\n id: string;\n /** Root-relative posix source path, or null for store entries without one. */\n filePath: string | null;\n /** Parsed frontmatter (zod output). */\n data: unknown;\n /** Raw markdown body, or null for data-only entries. */\n body: string | null;\n}\n\n/** A collection with its entries and schema presence (spec Impl #4 — read side). */\nexport interface CollectionRecord {\n name: string;\n hasSchema: boolean;\n entries: CollectionEntryRecord[];\n}\n\n/** Shared container between the `astro:routes:resolved` hook (writer) and the REST layer (reader). */\nexport interface RoutesState {\n current: RouteInfo[];\n}\n\n/**\n * Projects hook routes to the `RouteInfo` contract of `src/core/route-resolver`\n * (single source of truth per the core-first ruling on PR #77): page routes\n * only — the resolver's contract filters out `endpoint`/`redirect`/`fallback`\n * types at the payload — with Astro's own `segments` parse carried along,\n * deep-copied so no live core object is held between hook runs.\n */\nexport function toRouteInfos(routes: readonly IntegrationResolvedRoute[]): RouteInfo[] {\n return routes.flatMap((route) => {\n if (route.type !== 'page') return [];\n return [\n {\n pattern: route.pattern,\n segments: route.segments.map((segment) => segment.map((part) => ({ ...part }))),\n params: [...route.params],\n },\n ];\n });\n}\n\nexport interface ContentRestOptions {\n /** Absolute Astro src dir (where the content config lives). */\n srcDir: string;\n /** Routes captured by the integration's `astro:routes:resolved` hook. */\n routes: RoutesState;\n}\n\n/**\n * The content read-side endpoints (core-reuse §3):\n *\n * - `GET /__astroix/collections` — collections + entries through core's own\n * `astro:content` module: parsed `data`, `body`, `filePath` per entry, plus\n * schema presence from the content config. **Stateless doctrine**: a fresh\n * module runner per request, no module held between requests — core clears\n * its caches on invalidation, so anything we cache would go stale.\n * - `GET /__astroix/routes` — the routes array captured from\n * `astro:routes:resolved` (re-runs on route changes via dev restarts).\n *\n * Raw entry bytes go through the existing root-confined `GET /__astroix/file`.\n */\nexport function registerContentEndpoints(server: ViteDevServer, options: ContentRestOptions): void {\n server.middlewares.use(API_PREFIX, (req, res, next) => {\n void handleContentRequest(req, res, next, server, options);\n });\n}\n\nasync function handleContentRequest(\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n server: ViteDevServer,\n options: ContentRestOptions,\n): Promise<void> {\n try {\n if (isCrossOriginTraffic(req)) {\n json(res, 403, { error: 'cross-origin builder traffic is not allowed' });\n return;\n }\n\n const url = new URL(req.url ?? '/', 'http://astroix.internal');\n\n if (req.method === 'GET' && url.pathname === '/collections') {\n const runner = createServerModuleRunner(server.environments.ssr);\n const configPath = findContentConfigPath(options.srcDir);\n const configModule =\n configPath === null ? null : ((await runner.import(configPath)) as RawContentConfig);\n const contentModule = (await runner.import('astro:content')) as RawContentModule;\n json(res, 200, await assembleCollectionsPayload(configModule, contentModule));\n return;\n }\n\n if (req.method === 'GET' && url.pathname === '/routes') {\n json(res, 200, options.routes.current);\n return;\n }\n\n next();\n } catch (error) {\n next(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/** The user's `content.config` module as the runner evaluates it. */\nexport interface RawContentConfig {\n collections?: unknown;\n}\n\n/** `astro:content` as the runner evaluates it — only the surface this module consumes. */\nexport interface RawContentModule {\n getCollection?: (name: string) => Promise<unknown[]>;\n}\n\n/**\n * Joins the config's collection definitions (names, schema presence) with\n * core's `getCollection` results. Deterministic: collections and entries are\n * name/id-sorted regardless of store iteration order.\n */\nexport async function assembleCollectionsPayload(\n configModule: RawContentConfig | null,\n contentModule: RawContentModule,\n): Promise<CollectionRecord[]> {\n const definitions = toDefinitionMap(configModule?.collections);\n const collections: CollectionRecord[] = [];\n for (const name of Object.keys(definitions).sort()) {\n const entries = await loadEntries(contentModule, name);\n collections.push({ name, hasSchema: definitions[name]?.schema !== undefined, entries });\n }\n return collections;\n}\n\n/** `Record<string, { schema?: unknown }>` or an empty record — never throws on a malformed config. */\nfunction toDefinitionMap(collections: unknown): Record<string, { schema?: unknown }> {\n if (typeof collections !== 'object' || collections === null) return {};\n const definitions: Record<string, { schema?: unknown }> = {};\n for (const [name, definition] of Object.entries(collections)) {\n if (typeof definition === 'object' && definition !== null) {\n definitions[name] = definition as { schema?: unknown };\n }\n }\n return definitions;\n}\n\nasync function loadEntries(\n contentModule: RawContentModule,\n name: string,\n): Promise<CollectionEntryRecord[]> {\n const raw = (await contentModule.getCollection?.(name)) ?? [];\n return (\n raw\n .filter(\n (entry): entry is { id: string; filePath?: unknown; data?: unknown; body?: unknown } =>\n typeof entry === 'object' &&\n entry !== null &&\n typeof (entry as { id?: unknown }).id === 'string',\n )\n .map((entry) => ({\n id: entry.id,\n filePath: typeof entry.filePath === 'string' ? entry.filePath : null,\n data: entry.data ?? null,\n body: typeof entry.body === 'string' ? entry.body : null,\n }))\n // Code-unit order, like the collection-name sort above — localeCompare\n // follows process collation, which can order ids per machine.\n .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n );\n}\n\n/**\n * The content config path, mirroring core's search order\n * (`src/content.config.{mjs,js,mts,ts}`, then the legacy `src/content/config.*`).\n */\nexport function findContentConfigPath(srcDir: string): string | null {\n const candidates = [\n ['content.config.mjs', 'content.config.js', 'content.config.mts', 'content.config.ts'].map(\n (name) => join(srcDir, name),\n ),\n ['config.ts', 'config.js', 'config.mjs', 'config.mts'].map((name) =>\n join(srcDir, 'content', name),\n ),\n ].flat();\n return candidates.find((candidate) => existsSync(candidate)) ?? null;\n}\n","import { createHash } from 'node:crypto';\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { join, relative, resolve, sep } from 'node:path';\nimport postcss from 'postcss';\nimport type { ViteDevServer } from 'vite';\nimport { buildCssIndex, type SourceFile } from '../core/indexer';\nimport type { IndexPayloadRecord } from '../core/matcher';\nimport { SpliceRangeError, spliceText } from '../core/splice-writer';\n\nexport const API_PREFIX = '/__astroix';\nconst MAX_BODY_BYTES = 1_000_000;\n\nexport interface RestOptions {\n /** Absolute project root (Vite root). */\n root: string;\n /** Absolute Astro src dir holding the css/astro sources to index. */\n srcDir: string;\n}\n\n/**\n * Registers the chrome↔node contract on the Vite connect middleware\n * (core-reuse §2 — like core's `/_astro/status`, not Astro app middleware):\n *\n * - `GET /__astroix/index` — the index payload: edit-truth records joined\n * with compiled scoped forms from the client module graph.\n * - `POST /__astroix/edit` — `{ file, range, replacement }` spliced to disk.\n *\n * Same-origin only: a browser `sec-fetch-site` header that is not\n * same-origin/none is rejected (T2).\n */\nexport function registerRestEndpoints(server: ViteDevServer, options: RestOptions): void {\n server.middlewares.use(API_PREFIX, (req, res, next) => {\n void handleApiRequest(req, res, next, server, options);\n });\n}\n\nasync function handleApiRequest(\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n server: ViteDevServer,\n options: RestOptions,\n): Promise<void> {\n try {\n if (isCrossOriginTraffic(req)) {\n json(res, 403, { error: 'cross-origin builder traffic is not allowed' });\n return;\n }\n\n const url = new URL(req.url ?? '/', 'http://astroix.internal');\n\n // The middleware is mounted at /__astroix (connect strips the prefix),\n // so the GET path arrives as /index.\n if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index')) {\n const payload = await buildIndexPayload(collectSources(options.srcDir), (file, blockIndex) =>\n resolveCompiledCss(server, options.root, file, blockIndex),\n );\n // Payload paths are project-relative; the join worked in absolute space.\n json(\n res,\n 200,\n payload.map((record) => ({ ...record, file: toRelative(options.root, record.file) })),\n );\n return;\n }\n\n // File content for the editor pane — a dedicated endpoint (not payload\n // fields) so contents are fresh exactly when a rule is opened and the\n // payload stays small. Same root confinement as the edit endpoint.\n if (req.method === 'GET' && url.pathname === '/file') {\n const file = url.searchParams.get('file');\n const absPath = file === null ? null : safeResolve(options.root, file);\n if (file === null || absPath === null || !existsSync(absPath)) {\n json(res, 400, { error: `file is missing or outside the project root: ${file ?? ''}` });\n return;\n }\n json(res, 200, { file, contents: readFileSync(absPath, 'utf8') });\n return;\n }\n\n if (req.method === 'POST' && url.pathname === '/edit') {\n const body = await readJsonBody(req);\n const { file, range, replacement, expected } = parseEditBody(body);\n if (file === null || range === null || replacement === null) {\n json(res, 400, { error: 'expected { file, range: { start, end }, replacement }' });\n return;\n }\n const absPath = safeResolve(options.root, file);\n if (absPath === null) {\n json(res, 400, { error: `file is outside the project root: ${file}` });\n return;\n }\n const contents = readFileSync(absPath, 'utf8');\n // Optimistic write check: the chrome sends the hash of the content it\n // based its edit on. A mismatch means the file changed on disk under us\n // (IDE edit racing the debounce) — refuse instead of splicing stale\n // offsets into a shifted file, and hand back the current contents so\n // the editor can reload in one roundtrip.\n if (expected !== null && sha256(contents) !== expected) {\n json(res, 409, { error: 'file changed on disk', contents });\n return;\n }\n try {\n writeFileSync(\n absPath,\n spliceText(contents, { start: range[0], end: range[1], replacement }),\n );\n } catch (error) {\n if (error instanceof SpliceRangeError) {\n json(res, 400, { error: error.message });\n return;\n }\n throw error;\n }\n json(res, 200, { ok: true });\n return;\n }\n\n next();\n } catch (error) {\n next(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/** Supplies the compiled css of a scoped style module, or null when absent. */\nexport type CompiledCssResolver = (file: string, styleBlockIndex: number) => Promise<string | null>;\n\n/**\n * The module-graph hybrid join: static records plus effective selectors for\n * scoped rules. Scoped records of one style block correlate with the compiled\n * rules of module `{file}.astro?astro&type=style&index={N}` in rule order; a\n * block with no compiled module (not loaded on the current route, or a rule\n * count mismatch) stays listed without an effective selector — the liveness\n * line of v1.\n */\nexport async function buildIndexPayload(\n sources: SourceFile[],\n resolveCompiledCss: CompiledCssResolver,\n): Promise<IndexPayloadRecord[]> {\n const payload: IndexPayloadRecord[] = buildCssIndex(sources).map((record) => ({\n ...record,\n effectiveSelector: null,\n }));\n\n const blocks = new Map<string, { file: string; styleBlockIndex: number; positions: number[] }>();\n payload.forEach((record, position) => {\n if (!record.scoped || record.styleBlockIndex === null) return;\n const key = `${record.file}\\u0000${record.styleBlockIndex}`;\n const block = blocks.get(key) ?? {\n file: record.file,\n styleBlockIndex: record.styleBlockIndex,\n positions: [],\n };\n block.positions.push(position);\n blocks.set(key, block);\n });\n\n for (const block of blocks.values()) {\n const css = await resolveCompiledCss(block.file, block.styleBlockIndex);\n if (css === null) continue;\n const selectors = compiledSelectors(css);\n block.positions.forEach((position, ruleOrder) => {\n const effectiveSelector = selectors[ruleOrder];\n const record = payload[position];\n if (effectiveSelector !== undefined && record !== undefined) {\n record.effectiveSelector = effectiveSelector;\n }\n });\n }\n return payload;\n}\n\n/** Selectors of the compiled css in rule order — the join's correlation key. */\nexport function compiledSelectors(css: string): string[] {\n const selectors: string[] = [];\n postcss.parse(css).walkRules((rule) => {\n selectors.push(rule.selector);\n });\n return selectors;\n}\n\n/** Pulls the css text out of a dev-transformed css module's code. */\nexport function extractCssFromModuleCode(code: string): string | null {\n const match = code.match(/__vite__css = (\"(?:[^\"\\\\]|\\\\.)*\")/);\n if (match?.[1] === undefined) return null;\n try {\n return JSON.parse(match[1]) as string;\n } catch {\n return null;\n }\n}\n\nasync function resolveCompiledCss(\n server: ViteDevServer,\n root: string,\n file: string,\n styleBlockIndex: number,\n): Promise<string | null> {\n const moduleUrl = `/${toRelative(root, file)}?astro&type=style&index=${styleBlockIndex}&lang.css`;\n const module = await server.environments.client.moduleGraph.getModuleByUrl(moduleUrl);\n const code = module?.transformResult?.code;\n if (code === undefined || code === null) return null;\n return extractCssFromModuleCode(code);\n}\n\n/** Walks `src/**` collecting the css/astro sources the indexer consumes. */\nexport function collectSources(srcDir: string): SourceFile[] {\n if (!existsSync(srcDir)) return [];\n const sources: SourceFile[] = [];\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name.endsWith('.css') || entry.name.endsWith('.astro')) {\n sources.push({ file: full, contents: readFileSync(full, 'utf8') });\n }\n }\n };\n walk(srcDir);\n return sources;\n}\n\n/** Project-relative posix path — also the file id used by the sync events. */\nexport function toRelative(root: string, file: string): string {\n return relative(root, file).split(sep).join('/');\n}\n\nfunction safeResolve(root: string, file: string): string | null {\n const absPath = resolve(root, file);\n if (absPath !== root && !absPath.startsWith(`${root}${sep}`)) return null;\n return absPath;\n}\n\nfunction parseEditBody(body: unknown): {\n file: string | null;\n range: [number, number] | null;\n replacement: string | null;\n expected: string | null;\n} {\n if (body === null || typeof body !== 'object') {\n return { file: null, range: null, replacement: null, expected: null };\n }\n const { file, range, replacement, expected } = body as Record<string, unknown>;\n const validRange =\n typeof range === 'object' &&\n range !== null &&\n typeof (range as Record<string, unknown>).start === 'number' &&\n typeof (range as Record<string, unknown>).end === 'number'\n ? ([(range as Record<string, unknown>).start, (range as Record<string, unknown>).end] as [\n number,\n number,\n ])\n : null;\n return {\n file: typeof file === 'string' ? file : null,\n range: validRange,\n replacement: typeof replacement === 'string' ? replacement : null,\n expected: typeof expected === 'string' ? expected : null,\n };\n}\n\nfunction sha256(text: string): string {\n return createHash('sha256').update(text).digest('hex');\n}\n\n/**\n * The builder endpoints serve same-origin chrome traffic only: a browser\n * `sec-fetch-site` header that is not same-origin/none means cross-origin\n * (T2). Shared by every `/__astroix` middleware.\n */\nexport function isCrossOriginTraffic(req: IncomingMessage): boolean {\n const secFetchSite = req.headers['sec-fetch-site'];\n return (\n typeof secFetchSite === 'string' && secFetchSite !== 'same-origin' && secFetchSite !== 'none'\n );\n}\n\nexport function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify(body));\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolveBody, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n reject(new Error('request body too large'));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => {\n try {\n resolveBody(JSON.parse(Buffer.concat(chunks).toString('utf8')));\n } catch {\n reject(new Error('request body is not valid JSON'));\n }\n });\n req.on('error', reject);\n });\n}\n","import { extractStylesSync } from '@astrojs/compiler-binding';\nimport postcss from 'postcss';\n\n/** A project CSS source to index: path + raw contents. No IO happens here. */\nexport interface SourceFile {\n file: string;\n contents: string;\n}\n\n/**\n * One rule from the edit-truth index. The range is in character offsets of\n * `file` and covers the rule from its selector through the closing brace\n * (end-exclusive) — the splice-writer edits inside these bounds.\n */\nexport interface CssRuleRecord {\n /** Selector text verbatim from source (source space — no cid synthesis here). */\n selector: string;\n file: string;\n range: { start: number; end: number };\n /**\n * One-based line of the rule's selector in `file`, derived from the range\n * at index time (the indexer holds the contents) — the rule list shows it.\n */\n line: number;\n /** Condition of the nearest `@media` ancestor, or null at the top level. */\n media: string | null;\n /** True for rules from a scoped `<style>` block (the compiler applies the cid). */\n scoped: boolean;\n /**\n * Zero-based style-block index correlating with the module-graph module id\n * `{file}.astro?astro&type=style&index={N}` — the join key for the index\n * payload. Null when the block is not in the module graph (`is:inline`).\n */\n styleBlockIndex: number | null;\n}\n\ninterface BlockMeta {\n scoped: boolean;\n styleBlockIndex: number | null;\n baseOffset: number;\n}\n\nconst STYLE_TAG = /<style\\b[^>]*>([\\s\\S]*?)<\\/style>/g;\n\n/**\n * The indexer: scans project CSS sources into the edit-truth index\n * (selector → file, source range, media condition). Dev generates no CSS\n * sourcemaps, so this static scan is the only mapping to what's on disk —\n * and the only one that sees `is:inline` blocks.\n */\nexport function buildCssIndex(sources: SourceFile[]): CssRuleRecord[] {\n const records: CssRuleRecord[] = [];\n for (const source of sources) {\n const fileRecords = source.file.endsWith('.css')\n ? indexStylesheet(source.file, source.contents, {\n scoped: false,\n styleBlockIndex: null,\n baseOffset: 0,\n })\n : source.file.endsWith('.astro')\n ? indexAstroStyles(source.file, source.contents)\n : [];\n // Lines derive from absolute offsets against the whole file — the .astro\n // blocks were parsed as substrings but carry absolute ranges.\n for (const record of fileRecords) {\n records.push({ ...record, line: lineAt(source.contents, record.range.start) });\n }\n }\n return records;\n}\n\n/** One-based line number of a character offset. */\nfunction lineAt(contents: string, offset: number): number {\n let line = 1;\n for (let i = 0; i < offset && i < contents.length; i++) {\n if (contents[i] === '\\n') line += 1;\n }\n return line;\n}\n\nfunction indexStylesheet(\n file: string,\n css: string,\n meta: BlockMeta,\n): Omit<CssRuleRecord, 'line'>[] {\n const records: Omit<CssRuleRecord, 'line'>[] = [];\n postcss.parse(css).walkRules((rule) => {\n const start = rule.source?.start;\n const end = rule.source?.end;\n if (start === undefined || end === undefined) return;\n records.push({\n selector: rule.selector,\n file,\n range: { start: start.offset + meta.baseOffset, end: end.offset + meta.baseOffset },\n media: nearestMediaCondition(rule),\n scoped: meta.scoped,\n styleBlockIndex: meta.styleBlockIndex,\n });\n });\n return records;\n}\n\nfunction indexAstroStyles(file: string, source: string): Omit<CssRuleRecord, 'line'>[] {\n const records: Omit<CssRuleRecord, 'line'>[] = [];\n // extractStylesSync returns only blocks the compiler would process —\n // `is:inline` (and expression-attribute blocks) never make it there, so the\n // raw tag scan is the edit-truth pass and the compiler blocks supply the\n // module-graph index.\n const processed = extractStylesSync(source);\n let next = 0;\n\n for (const match of source.matchAll(STYLE_TAG)) {\n const content = match[1];\n if (content === undefined) continue;\n const openTag = match[0].slice(0, match[0].indexOf('>') + 1);\n const contentStart = match.index + openTag.length;\n\n const compilerBlock = processed[next];\n if (compilerBlock !== undefined && compilerBlock.content === content) {\n next += 1;\n records.push(\n ...indexStylesheet(file, content, {\n scoped: compilerBlock.attrs['is:global'] === undefined,\n styleBlockIndex: compilerBlock.index,\n baseOffset: contentStart,\n }),\n );\n } else {\n records.push(\n ...indexStylesheet(file, content, {\n scoped: false,\n styleBlockIndex: null,\n baseOffset: contentStart,\n }),\n );\n }\n }\n return records;\n}\n\nfunction nearestMediaCondition(node: postcss.Node): string | null {\n for (let parent = node.parent; parent !== undefined; parent = parent.parent) {\n if (parent.type !== 'atrule') continue;\n const atRule = parent as postcss.AtRule;\n if (atRule.name === 'media') {\n return atRule.params;\n }\n }\n return null;\n}\n","/**\n * A single text edit: replace the half-open range `[start, end)` of the file\n * content with `replacement`. Zero-length ranges insert purely. The editor\n * debounces and sends one edit at a time — this is the only write primitive.\n */\nexport interface SpliceEdit {\n start: number;\n end: number;\n replacement: string;\n}\n\n/** Thrown for ranges that do not fit the content — never produces partial output. */\nexport class SpliceRangeError extends Error {\n constructor(start: number, end: number, contentLength: number) {\n super(`Invalid splice range [${start}, ${end}) for content of length ${contentLength}`);\n this.name = 'SpliceRangeError';\n }\n}\n\n/**\n * The splice-writer primitive: (content, range, replacement) → new content.\n * Text-splice only — never reprints the file. Every byte outside the replaced\n * range stays identical, so formatting, comments and agent conventions\n * survive and the git diff is minimal.\n */\nexport function spliceText(content: string, edit: SpliceEdit): string {\n const { start, end, replacement } = edit;\n if (\n !Number.isInteger(start) ||\n !Number.isInteger(end) ||\n start < 0 ||\n end > content.length ||\n start > end\n ) {\n throw new SpliceRangeError(start, end, content.length);\n }\n return content.slice(0, start) + replacement + content.slice(end);\n}\n\n/**\n * Append a rule at EOF with exactly one added line, regardless of whether the\n * original ends with a newline (no accidental blank runs; the file's\n * trailing-newline convention is preserved).\n */\nexport function appendRule(content: string, rule: string): string {\n if (content === '') {\n return rule;\n }\n const endsWithNewline = content.endsWith('\\n');\n const body = endsWithNewline ? content : `${content}\\n`;\n return `${body}${rule}${endsWithNewline ? '\\n' : ''}`;\n}\n","import { existsSync } from 'node:fs';\nimport { fileURLToPath, URL as NodeURL } from 'node:url';\n\n/**\n * Dev-checkout detection for the ADR-0001 mode switch: the chrome client\n * sources exist next to the integration only when the package runs from this\n * repo (an installed package ships no `src/`). Two candidate depths because\n * this module executes from `src/node/` during development and from the\n * bundled `dist/index.js` at runtime. `URL` comes from `node:url` because\n * happy-dom (unit tests) patches the global `URL` with its own class, which\n * `fileURLToPath` rejects.\n */\nconst candidates = [\n fileURLToPath(new NodeURL('../../src/client/entry.tsx', import.meta.url)),\n fileURLToPath(new NodeURL('../src/client/entry.tsx', import.meta.url)),\n];\n\nexport const clientEntryPath: string | null = candidates.find((path) => existsSync(path)) ?? null;\n\n/**\n * The prebuilt chrome bundle (ADR-0001): a self-contained ESM shipped inside\n * `dist/`. Served by the virtual chrome module when the dev-checkout sources\n * are absent — the consumer-facing delivery mode.\n */\nexport const chromeArtifactPath: string = fileURLToPath(\n new NodeURL('./chrome.js', import.meta.url),\n);\n\nexport function isSourceMode(): boolean {\n return clientEntryPath !== null;\n}\n","import type { AstroUserConfig } from 'astro';\n\n/**\n * Guard: skip our Tailwind plugin when the host already registered one.\n * The `@tailwindcss/vite` factory returns an array whose plugin names start\n * with `@tailwindcss/vite:` (verified on 4.x) — that prefix is the check.\n */\nexport function hostRegistersTailwind(viteConfig: AstroUserConfig['vite']): boolean {\n const seen = new Set<string>();\n const walk = (input: unknown): void => {\n if (Array.isArray(input)) {\n for (const entry of input) walk(entry);\n return;\n }\n if (!input || typeof input !== 'object') return;\n const plugin = input as { name?: unknown };\n if (typeof plugin.name === 'string') seen.add(plugin.name);\n };\n walk(viteConfig?.plugins);\n for (const name of seen) {\n if (name.startsWith('@tailwindcss/vite')) return true;\n }\n return false;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Plugin, ViteDevServer } from 'vite';\nimport { chromeHtml } from './chrome-html';\nimport type { RoutesState } from './content';\nimport { registerContentEndpoints } from './content';\nimport { isDocumentRequest } from './document-request';\nimport { registerRestEndpoints } from './rest';\nimport { chromeArtifactPath, clientEntryPath } from './source-mode';\nimport { registerFileSync } from './watch-sync';\n\nexport const VIRTUAL_CHROME_ID = 'virtual:astroix/chrome';\n\nexport interface AstroixPluginOptions {\n /** Absolute Astro src dir with the sources to index; defaults to `<root>/src`. */\n srcDir?: string;\n /** Routes captured by the integration's `astro:routes:resolved` hook, served at `/__astroix/routes`. */\n routes: RoutesState;\n}\n\n/**\n * The astroix Vite plugin: default-on chrome over every top-level dev URL.\n * The middleware is registered in the body of `configureServer` (pre-internal)\n * because Astro's dev handler lives in a post-hook and never calls `next()` —\n * this is the only position that sees every request (core-reuse §1). The\n * chrome HTML passes through `server.transformIndexHtml` (the plugin hook\n * never fires for Astro pages, the server API does) which injects the Vite\n * client and the plugin-react preamble.\n */\nexport function astroixVitePlugin(options: AstroixPluginOptions): Plugin {\n return {\n name: 'astroix',\n configureServer(server: ViteDevServer) {\n server.middlewares.use((req, res, next) => {\n void (async () => {\n try {\n const url = req.url ?? '/';\n if (\n !isDocumentRequest({ method: req.method ?? 'GET', url, accept: req.headers.accept })\n ) {\n next();\n return;\n }\n const html = await server.transformIndexHtml(url, chromeHtml());\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(html);\n } catch (error) {\n next(error instanceof Error ? error : new Error(String(error)));\n }\n })();\n });\n const srcDir = options.srcDir ?? join(server.config.root, 'src');\n registerRestEndpoints(server, { root: server.config.root, srcDir });\n registerContentEndpoints(server, { srcDir, routes: options.routes });\n registerFileSync(server, { root: server.config.root, srcDir });\n },\n resolveId(id) {\n // The HTML references `/virtual:astroix/chrome`; imports may use the bare id.\n if (id === VIRTUAL_CHROME_ID || id === `/${VIRTUAL_CHROME_ID}`) {\n return VIRTUAL_CHROME_ID;\n }\n return null;\n },\n load(id) {\n if (id !== VIRTUAL_CHROME_ID) return null;\n if (clientEntryPath !== null) {\n // ADR-0001 source mode: the chrome loads from this checkout's source,\n // so the host dev server transforms it (fast-refresh, Tailwind).\n return `import { mountChrome } from '/@fs${clientEntryPath}';\\nmountChrome();\\n`;\n }\n // ADR-0001 prebuilt mode: serve the shipped bundle — a self-contained\n // ESM with react, the compiled CSS and CodeMirror inside, so foreign\n // hosts resolve none of our chrome dependencies. Missing artifact =\n // broken package build; fail loudly, never silently (ADR-0001).\n if (!existsSync(chromeArtifactPath)) {\n throw new Error(\n 'astroix: prebuilt chrome bundle is missing from the package build (expected dist/chrome.js)',\n );\n }\n return readFileSync(chromeArtifactPath, 'utf8');\n },\n };\n}\n","/**\n * The chrome document shell: a mount point and the virtual-module reference.\n * Layout lives inside the shadow root (React app); the document only resets\n * geometry so the shadow host can fill the viewport.\n */\nexport function chromeHtml(): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>astroix builder</title>\n <style>\n html, body { margin: 0; height: 100%; }\n #astroix-root { display: block; height: 100vh; }\n </style>\n </head>\n <body>\n <div id=\"astroix-root\"></div>\n <script type=\"module\" src=\"/virtual:astroix/chrome\"></script>\n </body>\n</html>`;\n}\n","/**\n * Is this request a top-level document navigation the builder should wrap?\n * Deliberately conservative: anything asset-like, internal to Vite/Astro, or\n * carrying an explicit `builder` param falls through to the host.\n */\nexport function isDocumentRequest(input: {\n method: string;\n url: string;\n accept?: string | undefined;\n}): boolean {\n if (input.method !== 'GET' && input.method !== 'HEAD') return false;\n if (!(input.accept ?? '').includes('text/html')) return false;\n\n let url: URL;\n try {\n url = new URL(input.url, 'http://astroix.internal');\n } catch {\n return false;\n }\n if (url.searchParams.has('builder')) return false;\n\n const { pathname } = url;\n if (\n pathname.startsWith('/@') ||\n pathname.startsWith('/__') ||\n pathname.startsWith('/_astro') ||\n pathname.startsWith('/virtual:')\n ) {\n return false;\n }\n // A dot in the last path segment reads as an asset (`home.css`, `foo.png`).\n if (/(^|\\/)[^/]*\\.[a-zA-Z0-9]+$/.test(pathname)) return false;\n return true;\n}\n","import { sep } from 'node:path';\nimport type { ViteDevServer } from 'vite';\nimport { toRelative } from './rest';\n\n/**\n * The file→chrome half of the sync (spec #13): the host watcher is the only\n * FS subscriber; css/astro changes under the project's src dir are debounced\n * per file and pushed to the chrome as `astroix:file-changed` over the Vite\n * WebSocket — the same channel Astro uses for its own events. The chrome\n * refetches content/payload on receipt; its own writes echo back as no-ops\n * (content compare client-side).\n */\nexport function registerFileSync(\n server: ViteDevServer,\n options: { root: string; srcDir: string },\n): void {\n // astro hands srcDir as a URL that keeps a trailing slash — strip it or\n // the startsWith filter below never matches\n const srcDir = options.srcDir.split(sep).join('/').replace(/\\/+$/, '');\n const pending = new Map<string, ReturnType<typeof setTimeout>>();\n\n const push = (file: string): void => {\n const timer = pending.get(file);\n if (timer !== undefined) clearTimeout(timer);\n pending.set(\n file,\n setTimeout(() => {\n pending.delete(file);\n server.ws.send('astroix:file-changed', { file: toRelative(options.root, file) });\n }, 100),\n );\n };\n\n const isWatchedSource = (file: string): boolean => {\n const norm = file.split(sep).join('/');\n if (!norm.startsWith(`${srcDir}/`)) return false;\n return norm.endsWith('.css') || norm.endsWith('.astro');\n };\n\n // `add` matters too: IDE atomic saves (write temp + rename) can surface as\n // add instead of change depending on the editor\n server.watcher.on('change', (file) => {\n if (isWatchedSource(file)) push(file);\n });\n server.watcher.on('add', (file) => {\n if (isWatchedSource(file)) push(file);\n });\n}\n"],"mappings":"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","names":["fileURLToPath","existsSync","join","postcss","resolveCompiledCss","postcss","candidates","join","existsSync","existsSync","existsSync","readFileSync","join","sep","sep","join","existsSync","readFileSync","fileURLToPath"]}
1
+ {"version":3,"sources":["../src/node/index.ts","../src/node/api.ts","../src/node/routes.ts","../src/node/source-mode.ts","../src/node/tailwind-guard.ts","../src/node/vite-plugin.ts","../src/node/chrome-html.ts","../src/node/content.ts","../src/node/document-request.ts","../src/node/rest.ts","../src/core/indexer.ts","../src/core/splice-writer.ts","../src/node/watch-sync.ts"],"sourcesContent":["import { dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport tailwindcss from '@tailwindcss/vite';\nimport react from '@vitejs/plugin-react';\nimport type { AstroIntegration } from 'astro';\nimport type { Plugin as VitePlugin } from 'vite';\nimport { type RoutesState, toRouteInfos } from './routes';\nimport { clientEntryPath, isSourceMode } from './source-mode';\nimport { hostRegistersTailwind } from './tailwind-guard';\nimport { astroixVitePlugin } from './vite-plugin';\n\n/**\n * Canvas script injected into every dev page (the chrome document itself is\n * never Astro-rendered, so this only ever runs inside host pages). It decides\n * on its own whether it is the builder canvas: inside the iframe\n * (`window.parent !== window`) with `?builder=0`, it hides Astro's dev\n * toolbar — the toolbar stays available on normal page loads (spec #2).\n */\nconst CANVAS_SCRIPT = `if (window.parent !== window && new URLSearchParams(location.search).get('builder') === '0') {\n const style = document.createElement('style');\n style.textContent = 'astro-dev-toolbar{display:none!important}';\n document.head.append(style);\n}\n`;\n\n/**\n * Astroix — dev-only visual builder integration.\n *\n * In dev: the astroix Vite plugin serves the builder chrome over every\n * top-level URL (default-on) with the `?builder=0` escape hatch, and the\n * virtual chrome module delivers the app (source mode in this checkout per\n * ADR-0001; the prebuilt bundle lands with the chrome packaging slice).\n * Any other command registers nothing — the dev-only guarantee.\n */\nfunction astroix(): AstroIntegration {\n // Routes captured from the hook below and served at `GET /__astroix/routes`\n // (spec Impl #13). Lives on the integration instance so both hooks —\n // the writer and the plugin that serves the state — share one container\n // across dev restarts (restarts re-run the routes hook on the same instance).\n const routesState: RoutesState = { current: [] };\n return {\n name: 'astroix',\n hooks: {\n 'astro:routes:resolved': ({ routes }) => {\n routesState.current = toRouteInfos(routes);\n },\n 'astro:config:setup': ({ config, command, updateConfig, injectScript, logger }) => {\n if (command !== 'dev') return;\n\n // The resolved config turns dir strings into URLs (trailing slash included) —\n // the plugin wants clean paths.\n const plugins: VitePlugin[] = [\n astroixVitePlugin({ srcDir: fileURLToPath(config.srcDir), routes: routesState }),\n ];\n // The chrome sources live outside the host root and are served via\n // /@fs, which has two consequences fixed below: (a) deps discovered\n // from /@fs files resolve against the importer's location, so `react`\n // can enter the optimizer from two paths and mount twice (Invalid\n // hook call) — dedupe pins every resolution to the host root's React,\n // which in the dev checkout is our own 19; (b) HMR re-fetches carry a\n // `?t=` timestamp that misses the import-chain fs exemption — the\n // checkout root joins the allow list so chrome modules always serve.\n let vitePatch: {\n plugins: VitePlugin[];\n resolve?: { dedupe: string[] };\n server?: { fs: { allow: string[] } };\n } = { plugins };\n\n if (isSourceMode()) {\n // ADR-0001 source mode: chrome from this checkout's source, with\n // fast-refresh scoped to chrome files only (host code untouched).\n const clientDir = dirname(clientEntryPath ?? '');\n // compiler: true = React Compiler via oxc (stack #4: no manual memoization).\n plugins.push(\n ...react({\n include: new RegExp(`^${escapeRegExp(clientDir)}/.*\\\\.tsx?$`),\n compiler: true,\n }),\n );\n if (hostRegistersTailwind(config.vite)) {\n logger.info('host already registers @tailwindcss/vite — reusing it for the chrome');\n } else {\n plugins.push(...tailwindcss());\n }\n vitePatch = {\n plugins,\n resolve: { dedupe: ['react', 'react-dom'] },\n server: { fs: { allow: [dirname(dirname(clientDir))] } },\n };\n }\n\n updateConfig({ vite: vitePatch });\n injectScript('page', CANVAS_SCRIPT);\n },\n },\n };\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport default astroix;\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { ViteDevServer } from 'vite';\nimport type { RoutesState } from './routes';\n\nconst API_PREFIX = '/__astroix';\n\n/** The shared server context every `/__astroix` handler may touch. */\nexport interface ApiContext {\n server: ViteDevServer;\n /** Absolute project root (Vite root) — the confinement root for file paths. */\n root: string;\n /** Absolute Astro src dir (css/astro sources to index, content config). */\n srcDir: string;\n /** Routes captured by the integration's `astro:routes:resolved` hook. */\n routes: RoutesState;\n}\n\n/**\n * One endpoint of the builder API. `path` is mount-relative (connect strips\n * the `/__astroix` prefix, so `GET /__astroix/index` arrives as `/index`).\n * A handler answers its request fully or throws — it never calls `next()`.\n */\nexport interface ApiHandler {\n method: 'GET' | 'POST';\n path: string;\n handle(req: IncomingMessage, res: ServerResponse, url: URL, ctx: ApiContext): Promise<void>;\n}\n\nexport interface ApiOptions {\n root: string;\n srcDir: string;\n routes: RoutesState;\n /** Endpoint handlers, contributed by the owning modules (rest, content, routes). */\n handlers: readonly ApiHandler[];\n}\n\n/**\n * Registers the chrome↔node contract on the Vite connect middleware\n * (core-reuse §2 — like core's `/_astro/status`, not Astro app middleware):\n * one `/__astroix` mount dispatching over a handler table keyed by\n * `method + path`, so the same-origin invariant is structural (checked once,\n * in the dispatcher) instead of conventional (per registrar). Handlers are\n * contributed by the owning modules — see each module for its endpoints.\n *\n * Same-origin only: a browser `sec-fetch-site` header that is not\n * same-origin/none is rejected (T2).\n */\nexport function registerApiEndpoints(server: ViteDevServer, options: ApiOptions): void {\n const table = new Map(\n options.handlers.map((handler) => [handlerKey(handler.method, handler.path), handler]),\n );\n // Two modules registering the same route would silently shadow — fail at\n // boot instead of at request time.\n if (table.size !== options.handlers.length) {\n throw new Error('astroix: duplicate route in the /__astroix handler table');\n }\n const ctx: ApiContext = {\n server,\n root: options.root,\n srcDir: options.srcDir,\n routes: options.routes,\n };\n server.middlewares.use(API_PREFIX, (req, res, next) => {\n void dispatchApi(req, res, next, table, ctx);\n });\n}\n\nasync function dispatchApi(\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n table: ReadonlyMap<string, ApiHandler>,\n ctx: ApiContext,\n): Promise<void> {\n try {\n if (isCrossOriginTraffic(req)) {\n json(res, 403, { error: 'cross-origin builder traffic is not allowed' });\n return;\n }\n const url = new URL(req.url ?? '/', 'http://astroix.internal');\n // The middleware is mounted at /__astroix (connect strips the prefix),\n // so GET /__astroix/index arrives as /index.\n const handler = table.get(handlerKey(req.method ?? '', url.pathname));\n if (handler === undefined) {\n next();\n return;\n }\n await handler.handle(req, res, url, ctx);\n } catch (error) {\n next(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\nfunction handlerKey(method: string, path: string): string {\n return `${method} ${path}`;\n}\n\n/**\n * The builder endpoints serve same-origin chrome traffic only: a browser\n * `sec-fetch-site` header that is not same-origin/none means cross-origin\n * (T2). Enforced once, by the `/__astroix` dispatcher.\n */\nexport function isCrossOriginTraffic(req: IncomingMessage): boolean {\n const secFetchSite = req.headers['sec-fetch-site'];\n return (\n typeof secFetchSite === 'string' && secFetchSite !== 'same-origin' && secFetchSite !== 'none'\n );\n}\n\nexport function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify(body));\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { IntegrationResolvedRoute } from 'astro';\nimport type { RouteInfo } from '../core/route-resolver';\nimport { type ApiContext, type ApiHandler, json } from './api';\n\n/** Shared container between the `astro:routes:resolved` hook (writer) and the REST layer (reader). */\nexport interface RoutesState {\n current: RouteInfo[];\n}\n\n/**\n * Projects hook routes to the `RouteInfo` contract of `src/core/route-resolver`\n * (single source of truth per the core-first ruling on PR #77): page routes\n * only — the resolver's contract filters out `endpoint`/`redirect`/`fallback`\n * types at the payload — with Astro's own `segments` parse carried along,\n * deep-copied so no live core object is held between hook runs.\n */\nexport function toRouteInfos(routes: readonly IntegrationResolvedRoute[]): RouteInfo[] {\n return routes.flatMap((route) => {\n if (route.type !== 'page') return [];\n return [\n {\n pattern: route.pattern,\n segments: route.segments.map((segment) => segment.map((part) => ({ ...part }))),\n params: [...route.params],\n },\n ];\n });\n}\n\n/**\n * `GET /__astroix/routes` — the routes array captured from\n * `astro:routes:resolved` (re-runs on route changes via dev restarts).\n * A routing concern, not a content one: the concept grows from here — the\n * resolver (#69) and the overrides-file naming both key on route patterns.\n */\nexport const routesHandlers: readonly ApiHandler[] = [\n { method: 'GET', path: '/routes', handle: handleRoutes },\n];\n\nasync function handleRoutes(\n _req: IncomingMessage,\n res: ServerResponse,\n _url: URL,\n ctx: ApiContext,\n): Promise<void> {\n json(res, 200, ctx.routes.current);\n}\n","import { existsSync } from 'node:fs';\nimport { fileURLToPath, URL as NodeURL } from 'node:url';\n\n/**\n * Dev-checkout detection for the ADR-0001 mode switch: the chrome client\n * sources exist next to the integration only when the package runs from this\n * repo (an installed package ships no `src/`). Two candidate depths because\n * this module executes from `src/node/` during development and from the\n * bundled `dist/index.js` at runtime. `URL` comes from `node:url` because\n * happy-dom (unit tests) patches the global `URL` with its own class, which\n * `fileURLToPath` rejects.\n */\nconst candidates = [\n fileURLToPath(new NodeURL('../../src/client/entry.tsx', import.meta.url)),\n fileURLToPath(new NodeURL('../src/client/entry.tsx', import.meta.url)),\n];\n\nexport const clientEntryPath: string | null = candidates.find((path) => existsSync(path)) ?? null;\n\n/**\n * The prebuilt chrome bundle (ADR-0001): a self-contained ESM shipped inside\n * `dist/`. Served by the virtual chrome module when the dev-checkout sources\n * are absent — the consumer-facing delivery mode.\n */\nexport const chromeArtifactPath: string = fileURLToPath(\n new NodeURL('./chrome.js', import.meta.url),\n);\n\nexport function isSourceMode(): boolean {\n return clientEntryPath !== null;\n}\n","import type { AstroUserConfig } from 'astro';\n\n/**\n * Guard: skip our Tailwind plugin when the host already registered one.\n * The `@tailwindcss/vite` factory returns an array whose plugin names start\n * with `@tailwindcss/vite:` (verified on 4.x) — that prefix is the check.\n */\nexport function hostRegistersTailwind(viteConfig: AstroUserConfig['vite']): boolean {\n const seen = new Set<string>();\n const walk = (input: unknown): void => {\n if (Array.isArray(input)) {\n for (const entry of input) walk(entry);\n return;\n }\n if (!input || typeof input !== 'object') return;\n const plugin = input as { name?: unknown };\n if (typeof plugin.name === 'string') seen.add(plugin.name);\n };\n walk(viteConfig?.plugins);\n for (const name of seen) {\n if (name.startsWith('@tailwindcss/vite')) return true;\n }\n return false;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Plugin, ViteDevServer } from 'vite';\nimport { registerApiEndpoints } from './api';\nimport { chromeHtml } from './chrome-html';\nimport { contentHandlers } from './content';\nimport { isDocumentRequest } from './document-request';\nimport { restHandlers } from './rest';\nimport { type RoutesState, routesHandlers } from './routes';\nimport { chromeArtifactPath, clientEntryPath } from './source-mode';\nimport { registerFileSync } from './watch-sync';\n\nexport const VIRTUAL_CHROME_ID = 'virtual:astroix/chrome';\n\nexport interface AstroixPluginOptions {\n /** Absolute Astro src dir with the sources to index; defaults to `<root>/src`. */\n srcDir?: string;\n /** Routes captured by the integration's `astro:routes:resolved` hook, served at `/__astroix/routes`. */\n routes: RoutesState;\n}\n\n/**\n * The astroix Vite plugin: default-on chrome over every top-level dev URL.\n * The middleware is registered in the body of `configureServer` (pre-internal)\n * because Astro's dev handler lives in a post-hook and never calls `next()` —\n * this is the only position that sees every request (core-reuse §1). The\n * chrome HTML passes through `server.transformIndexHtml` (the plugin hook\n * never fires for Astro pages, the server API does) which injects the Vite\n * client and the plugin-react preamble.\n */\nexport function astroixVitePlugin(options: AstroixPluginOptions): Plugin {\n return {\n name: 'astroix',\n configureServer(server: ViteDevServer) {\n server.middlewares.use((req, res, next) => {\n void (async () => {\n try {\n const url = req.url ?? '/';\n if (\n !isDocumentRequest({ method: req.method ?? 'GET', url, accept: req.headers.accept })\n ) {\n next();\n return;\n }\n const html = await server.transformIndexHtml(url, chromeHtml());\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(html);\n } catch (error) {\n next(error instanceof Error ? error : new Error(String(error)));\n }\n })();\n });\n const srcDir = options.srcDir ?? join(server.config.root, 'src');\n registerApiEndpoints(server, {\n root: server.config.root,\n srcDir,\n routes: options.routes,\n handlers: [...restHandlers, ...contentHandlers, ...routesHandlers],\n });\n registerFileSync(server, { root: server.config.root, srcDir });\n },\n resolveId(id) {\n // The HTML references `/virtual:astroix/chrome`; imports may use the bare id.\n if (id === VIRTUAL_CHROME_ID || id === `/${VIRTUAL_CHROME_ID}`) {\n return VIRTUAL_CHROME_ID;\n }\n return null;\n },\n load(id) {\n if (id !== VIRTUAL_CHROME_ID) return null;\n if (clientEntryPath !== null) {\n // ADR-0001 source mode: the chrome loads from this checkout's source,\n // so the host dev server transforms it (fast-refresh, Tailwind).\n return `import { mountChrome } from '/@fs${clientEntryPath}';\\nmountChrome();\\n`;\n }\n // ADR-0001 prebuilt mode: serve the shipped bundle — a self-contained\n // ESM with react, the compiled CSS and CodeMirror inside, so foreign\n // hosts resolve none of our chrome dependencies. Missing artifact =\n // broken package build; fail loudly, never silently (ADR-0001).\n if (!existsSync(chromeArtifactPath)) {\n throw new Error(\n 'astroix: prebuilt chrome bundle is missing from the package build (expected dist/chrome.js)',\n );\n }\n return readFileSync(chromeArtifactPath, 'utf8');\n },\n };\n}\n","/**\n * The chrome document shell: a mount point and the virtual-module reference.\n * Layout lives inside the shadow root (React app); the document only resets\n * geometry so the shadow host can fill the viewport.\n */\nexport function chromeHtml(): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>astroix builder</title>\n <style>\n html, body { margin: 0; height: 100%; }\n #astroix-root { display: block; height: 100vh; }\n </style>\n </head>\n <body>\n <div id=\"astroix-root\"></div>\n <script type=\"module\" src=\"/virtual:astroix/chrome\"></script>\n </body>\n</html>`;\n}\n","import { existsSync } from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { join } from 'node:path';\nimport { createServerModuleRunner } from 'vite';\nimport { type ApiContext, type ApiHandler, json } from './api';\n\n/** A single collection entry as served to the chrome (core's getCollection shape, JSON-projected). */\nexport interface CollectionEntryRecord {\n /** Slugified source path (glob loader id), e.g. `2024/post`. */\n id: string;\n /** Root-relative posix source path, or null for store entries without one. */\n filePath: string | null;\n /** Parsed frontmatter (zod output). */\n data: unknown;\n /** Raw markdown body, or null for data-only entries. */\n body: string | null;\n}\n\n/** A collection with its entries and schema presence (spec Impl #4 — read side). */\nexport interface CollectionRecord {\n name: string;\n hasSchema: boolean;\n entries: CollectionEntryRecord[];\n}\n\n/** The content read-side endpoint (core-reuse §3). */\nexport const contentHandlers: readonly ApiHandler[] = [\n { method: 'GET', path: '/collections', handle: handleCollections },\n];\n\n/**\n * `GET /__astroix/collections` — collections + entries through core's own\n * `astro:content` module: parsed `data`, `body`, `filePath` per entry, plus\n * schema presence from the content config. **Stateless doctrine**: a fresh\n * module runner per request, no module held between requests — core clears\n * its caches on invalidation, so anything we cache would go stale. Raw entry\n * bytes go through the root-confined `GET /__astroix/file` (rest.ts).\n */\nasync function handleCollections(\n _req: IncomingMessage,\n res: ServerResponse,\n _url: URL,\n ctx: ApiContext,\n): Promise<void> {\n const runner = createServerModuleRunner(ctx.server.environments.ssr);\n const configPath = findContentConfigPath(ctx.srcDir);\n const configModule =\n configPath === null ? null : ((await runner.import(configPath)) as RawContentConfig);\n const contentModule = (await runner.import('astro:content')) as RawContentModule;\n json(res, 200, await assembleCollectionsPayload(configModule, contentModule));\n}\n\n/** The user's `content.config` module as the runner evaluates it. */\nexport interface RawContentConfig {\n collections?: unknown;\n}\n\n/** `astro:content` as the runner evaluates it — only the surface this module consumes. */\nexport interface RawContentModule {\n getCollection?: (name: string) => Promise<unknown[]>;\n}\n\n/**\n * Joins the config's collection definitions (names, schema presence) with\n * core's `getCollection` results. Deterministic: collections and entries are\n * name/id-sorted regardless of store iteration order.\n */\nexport async function assembleCollectionsPayload(\n configModule: RawContentConfig | null,\n contentModule: RawContentModule,\n): Promise<CollectionRecord[]> {\n const definitions = toDefinitionMap(configModule?.collections);\n const collections: CollectionRecord[] = [];\n for (const name of Object.keys(definitions).sort()) {\n const entries = await loadEntries(contentModule, name);\n collections.push({ name, hasSchema: definitions[name]?.schema !== undefined, entries });\n }\n return collections;\n}\n\n/** `Record<string, { schema?: unknown }>` or an empty record — never throws on a malformed config. */\nfunction toDefinitionMap(collections: unknown): Record<string, { schema?: unknown }> {\n if (typeof collections !== 'object' || collections === null) return {};\n const definitions: Record<string, { schema?: unknown }> = {};\n for (const [name, definition] of Object.entries(collections)) {\n if (typeof definition === 'object' && definition !== null) {\n definitions[name] = definition as { schema?: unknown };\n }\n }\n return definitions;\n}\n\nasync function loadEntries(\n contentModule: RawContentModule,\n name: string,\n): Promise<CollectionEntryRecord[]> {\n const raw = (await contentModule.getCollection?.(name)) ?? [];\n return (\n raw\n .filter(\n (entry): entry is { id: string; filePath?: unknown; data?: unknown; body?: unknown } =>\n typeof entry === 'object' &&\n entry !== null &&\n typeof (entry as { id?: unknown }).id === 'string',\n )\n .map((entry) => ({\n id: entry.id,\n filePath: typeof entry.filePath === 'string' ? entry.filePath : null,\n data: entry.data ?? null,\n body: typeof entry.body === 'string' ? entry.body : null,\n }))\n // Code-unit order, like the collection-name sort above — localeCompare\n // follows process collation, which can order ids per machine.\n .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n );\n}\n\n/**\n * The content config path, mirroring core's search order\n * (`src/content.config.{mjs,js,mts,ts}`, then the legacy `src/content/config.*`).\n */\nexport function findContentConfigPath(srcDir: string): string | null {\n const candidates = [\n ['content.config.mjs', 'content.config.js', 'content.config.mts', 'content.config.ts'].map(\n (name) => join(srcDir, name),\n ),\n ['config.ts', 'config.js', 'config.mjs', 'config.mts'].map((name) =>\n join(srcDir, 'content', name),\n ),\n ].flat();\n return candidates.find((candidate) => existsSync(candidate)) ?? null;\n}\n","/**\n * Is this request a top-level document navigation the builder should wrap?\n * Deliberately conservative: anything asset-like, internal to Vite/Astro, or\n * carrying an explicit `builder` param falls through to the host.\n */\nexport function isDocumentRequest(input: {\n method: string;\n url: string;\n accept?: string | undefined;\n}): boolean {\n if (input.method !== 'GET' && input.method !== 'HEAD') return false;\n if (!(input.accept ?? '').includes('text/html')) return false;\n\n let url: URL;\n try {\n url = new URL(input.url, 'http://astroix.internal');\n } catch {\n return false;\n }\n if (url.searchParams.has('builder')) return false;\n\n const { pathname } = url;\n if (\n pathname.startsWith('/@') ||\n pathname.startsWith('/__') ||\n pathname.startsWith('/_astro') ||\n pathname.startsWith('/virtual:')\n ) {\n return false;\n }\n // A dot in the last path segment reads as an asset (`home.css`, `foo.png`).\n if (/(^|\\/)[^/]*\\.[a-zA-Z0-9]+$/.test(pathname)) return false;\n return true;\n}\n","import { createHash } from 'node:crypto';\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { join, relative, resolve, sep } from 'node:path';\nimport postcss from 'postcss';\nimport type { ViteDevServer } from 'vite';\nimport { buildCssIndex, type SourceFile } from '../core/indexer';\nimport type { IndexPayloadRecord } from '../core/matcher';\nimport { SpliceRangeError, spliceText } from '../core/splice-writer';\nimport { type ApiContext, type ApiHandler, json } from './api';\n\nconst MAX_BODY_BYTES = 1_000_000;\n\n/** The css endpoints: the index payload, root-confined file reads, disk splices. */\nexport const restHandlers: readonly ApiHandler[] = [\n // The bare mount serves the index payload too (connect hands it as `/`).\n { method: 'GET', path: '/', handle: handleIndex },\n { method: 'GET', path: '/index', handle: handleIndex },\n { method: 'GET', path: '/file', handle: handleFile },\n { method: 'POST', path: '/edit', handle: handleEdit },\n];\n\nasync function handleIndex(\n _req: IncomingMessage,\n res: ServerResponse,\n _url: URL,\n ctx: ApiContext,\n): Promise<void> {\n const payload = await buildIndexPayload(collectSources(ctx.srcDir), (file, blockIndex) =>\n resolveCompiledCss(ctx.server, ctx.root, file, blockIndex),\n );\n // Payload paths are project-relative; the join worked in absolute space.\n json(\n res,\n 200,\n payload.map((record) => ({ ...record, file: toRelative(ctx.root, record.file) })),\n );\n}\n\n// File content for the editor pane — a dedicated endpoint (not payload\n// fields) so contents are fresh exactly when a rule is opened and the\n// payload stays small. Same root confinement as the edit endpoint.\nasync function handleFile(\n _req: IncomingMessage,\n res: ServerResponse,\n url: URL,\n ctx: ApiContext,\n): Promise<void> {\n const file = url.searchParams.get('file');\n const absPath = file === null ? null : safeResolve(ctx.root, file);\n if (file === null || absPath === null || !existsSync(absPath)) {\n json(res, 400, { error: `file is missing or outside the project root: ${file ?? ''}` });\n return;\n }\n json(res, 200, { file, contents: readFileSync(absPath, 'utf8') });\n}\n\nasync function handleEdit(\n req: IncomingMessage,\n res: ServerResponse,\n _url: URL,\n ctx: ApiContext,\n): Promise<void> {\n const body = await readJsonBody(req);\n const { file, range, replacement, expected } = parseEditBody(body);\n if (file === null || range === null || replacement === null) {\n json(res, 400, { error: 'expected { file, range: { start, end }, replacement }' });\n return;\n }\n const absPath = safeResolve(ctx.root, file);\n if (absPath === null) {\n json(res, 400, { error: `file is outside the project root: ${file}` });\n return;\n }\n const contents = readFileSync(absPath, 'utf8');\n // Optimistic write check: the chrome sends the hash of the content it\n // based its edit on. A mismatch means the file changed on disk under us\n // (IDE edit racing the debounce) — refuse instead of splicing stale\n // offsets into a shifted file, and hand back the current contents so\n // the editor can reload in one roundtrip.\n if (expected !== null && sha256(contents) !== expected) {\n json(res, 409, { error: 'file changed on disk', contents });\n return;\n }\n try {\n writeFileSync(absPath, spliceText(contents, { start: range[0], end: range[1], replacement }));\n } catch (error) {\n if (error instanceof SpliceRangeError) {\n json(res, 400, { error: error.message });\n return;\n }\n throw error;\n }\n json(res, 200, { ok: true });\n}\n\n/** Supplies the compiled css of a scoped style module, or null when absent. */\nexport type CompiledCssResolver = (file: string, styleBlockIndex: number) => Promise<string | null>;\n\n/**\n * The module-graph hybrid join: static records plus effective selectors for\n * scoped rules. Scoped records of one style block correlate with the compiled\n * rules of module `{file}.astro?astro&type=style&index={N}` in rule order; a\n * block with no compiled module (not loaded on the current route, or a rule\n * count mismatch) stays listed without an effective selector — the liveness\n * line of v1.\n */\nexport async function buildIndexPayload(\n sources: SourceFile[],\n resolveCompiledCss: CompiledCssResolver,\n): Promise<IndexPayloadRecord[]> {\n const payload: IndexPayloadRecord[] = buildCssIndex(sources).map((record) => ({\n ...record,\n effectiveSelector: null,\n }));\n\n const blocks = new Map<string, { file: string; styleBlockIndex: number; positions: number[] }>();\n payload.forEach((record, position) => {\n if (!record.scoped || record.styleBlockIndex === null) return;\n const key = `${record.file}\\u0000${record.styleBlockIndex}`;\n const block = blocks.get(key) ?? {\n file: record.file,\n styleBlockIndex: record.styleBlockIndex,\n positions: [],\n };\n block.positions.push(position);\n blocks.set(key, block);\n });\n\n for (const block of blocks.values()) {\n const css = await resolveCompiledCss(block.file, block.styleBlockIndex);\n if (css === null) continue;\n const selectors = compiledSelectors(css);\n block.positions.forEach((position, ruleOrder) => {\n const effectiveSelector = selectors[ruleOrder];\n const record = payload[position];\n if (effectiveSelector !== undefined && record !== undefined) {\n record.effectiveSelector = effectiveSelector;\n }\n });\n }\n return payload;\n}\n\n/** Selectors of the compiled css in rule order — the join's correlation key. */\nexport function compiledSelectors(css: string): string[] {\n const selectors: string[] = [];\n postcss.parse(css).walkRules((rule) => {\n selectors.push(rule.selector);\n });\n return selectors;\n}\n\n/** Pulls the css text out of a dev-transformed css module's code. */\nexport function extractCssFromModuleCode(code: string): string | null {\n const match = code.match(/__vite__css = (\"(?:[^\"\\\\]|\\\\.)*\")/);\n if (match?.[1] === undefined) return null;\n try {\n return JSON.parse(match[1]) as string;\n } catch {\n return null;\n }\n}\n\nasync function resolveCompiledCss(\n server: ViteDevServer,\n root: string,\n file: string,\n styleBlockIndex: number,\n): Promise<string | null> {\n const moduleUrl = `/${toRelative(root, file)}?astro&type=style&index=${styleBlockIndex}&lang.css`;\n const module = await server.environments.client.moduleGraph.getModuleByUrl(moduleUrl);\n const code = module?.transformResult?.code;\n if (code === undefined || code === null) return null;\n return extractCssFromModuleCode(code);\n}\n\n/** Walks `src/**` collecting the css/astro sources the indexer consumes. */\nexport function collectSources(srcDir: string): SourceFile[] {\n if (!existsSync(srcDir)) return [];\n const sources: SourceFile[] = [];\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name.endsWith('.css') || entry.name.endsWith('.astro')) {\n sources.push({ file: full, contents: readFileSync(full, 'utf8') });\n }\n }\n };\n walk(srcDir);\n return sources;\n}\n\n/** Project-relative posix path — also the file id used by the sync events. */\nexport function toRelative(root: string, file: string): string {\n return relative(root, file).split(sep).join('/');\n}\n\nfunction safeResolve(root: string, file: string): string | null {\n const absPath = resolve(root, file);\n if (absPath !== root && !absPath.startsWith(`${root}${sep}`)) return null;\n return absPath;\n}\n\nfunction parseEditBody(body: unknown): {\n file: string | null;\n range: [number, number] | null;\n replacement: string | null;\n expected: string | null;\n} {\n if (body === null || typeof body !== 'object') {\n return { file: null, range: null, replacement: null, expected: null };\n }\n const { file, range, replacement, expected } = body as Record<string, unknown>;\n const validRange =\n typeof range === 'object' &&\n range !== null &&\n typeof (range as Record<string, unknown>).start === 'number' &&\n typeof (range as Record<string, unknown>).end === 'number'\n ? ([(range as Record<string, unknown>).start, (range as Record<string, unknown>).end] as [\n number,\n number,\n ])\n : null;\n return {\n file: typeof file === 'string' ? file : null,\n range: validRange,\n replacement: typeof replacement === 'string' ? replacement : null,\n expected: typeof expected === 'string' ? expected : null,\n };\n}\n\nfunction sha256(text: string): string {\n return createHash('sha256').update(text).digest('hex');\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolveBody, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n reject(new Error('request body too large'));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => {\n try {\n resolveBody(JSON.parse(Buffer.concat(chunks).toString('utf8')));\n } catch {\n reject(new Error('request body is not valid JSON'));\n }\n });\n req.on('error', reject);\n });\n}\n","import { extractStylesSync } from '@astrojs/compiler-binding';\nimport postcss from 'postcss';\n\n/** A project CSS source to index: path + raw contents. No IO happens here. */\nexport interface SourceFile {\n file: string;\n contents: string;\n}\n\n/**\n * One rule from the edit-truth index. The range is in character offsets of\n * `file` and covers the rule from its selector through the closing brace\n * (end-exclusive) — the splice-writer edits inside these bounds.\n */\nexport interface CssRuleRecord {\n /** Selector text verbatim from source (source space — no cid synthesis here). */\n selector: string;\n file: string;\n range: { start: number; end: number };\n /**\n * One-based line of the rule's selector in `file`, derived from the range\n * at index time (the indexer holds the contents) — the rule list shows it.\n */\n line: number;\n /** Condition of the nearest `@media` ancestor, or null at the top level. */\n media: string | null;\n /** True for rules from a scoped `<style>` block (the compiler applies the cid). */\n scoped: boolean;\n /**\n * Zero-based style-block index correlating with the module-graph module id\n * `{file}.astro?astro&type=style&index={N}` — the join key for the index\n * payload. Null when the block is not in the module graph (`is:inline`).\n */\n styleBlockIndex: number | null;\n}\n\ninterface BlockMeta {\n scoped: boolean;\n styleBlockIndex: number | null;\n baseOffset: number;\n}\n\nconst STYLE_TAG = /<style\\b[^>]*>([\\s\\S]*?)<\\/style>/g;\n\n/**\n * The indexer: scans project CSS sources into the edit-truth index\n * (selector → file, source range, media condition). Dev generates no CSS\n * sourcemaps, so this static scan is the only mapping to what's on disk —\n * and the only one that sees `is:inline` blocks.\n */\nexport function buildCssIndex(sources: SourceFile[]): CssRuleRecord[] {\n const records: CssRuleRecord[] = [];\n for (const source of sources) {\n const fileRecords = source.file.endsWith('.css')\n ? indexStylesheet(source.file, source.contents, {\n scoped: false,\n styleBlockIndex: null,\n baseOffset: 0,\n })\n : source.file.endsWith('.astro')\n ? indexAstroStyles(source.file, source.contents)\n : [];\n // Lines derive from absolute offsets against the whole file — the .astro\n // blocks were parsed as substrings but carry absolute ranges.\n for (const record of fileRecords) {\n records.push({ ...record, line: lineAt(source.contents, record.range.start) });\n }\n }\n return records;\n}\n\n/** One-based line number of a character offset. */\nfunction lineAt(contents: string, offset: number): number {\n let line = 1;\n for (let i = 0; i < offset && i < contents.length; i++) {\n if (contents[i] === '\\n') line += 1;\n }\n return line;\n}\n\nfunction indexStylesheet(\n file: string,\n css: string,\n meta: BlockMeta,\n): Omit<CssRuleRecord, 'line'>[] {\n const records: Omit<CssRuleRecord, 'line'>[] = [];\n postcss.parse(css).walkRules((rule) => {\n const start = rule.source?.start;\n const end = rule.source?.end;\n if (start === undefined || end === undefined) return;\n records.push({\n selector: rule.selector,\n file,\n range: { start: start.offset + meta.baseOffset, end: end.offset + meta.baseOffset },\n media: nearestMediaCondition(rule),\n scoped: meta.scoped,\n styleBlockIndex: meta.styleBlockIndex,\n });\n });\n return records;\n}\n\nfunction indexAstroStyles(file: string, source: string): Omit<CssRuleRecord, 'line'>[] {\n const records: Omit<CssRuleRecord, 'line'>[] = [];\n // extractStylesSync returns only blocks the compiler would process —\n // `is:inline` (and expression-attribute blocks) never make it there, so the\n // raw tag scan is the edit-truth pass and the compiler blocks supply the\n // module-graph index.\n const processed = extractStylesSync(source);\n let next = 0;\n\n for (const match of source.matchAll(STYLE_TAG)) {\n const content = match[1];\n if (content === undefined) continue;\n const openTag = match[0].slice(0, match[0].indexOf('>') + 1);\n const contentStart = match.index + openTag.length;\n\n const compilerBlock = processed[next];\n if (compilerBlock !== undefined && compilerBlock.content === content) {\n next += 1;\n records.push(\n ...indexStylesheet(file, content, {\n scoped: compilerBlock.attrs['is:global'] === undefined,\n styleBlockIndex: compilerBlock.index,\n baseOffset: contentStart,\n }),\n );\n } else {\n records.push(\n ...indexStylesheet(file, content, {\n scoped: false,\n styleBlockIndex: null,\n baseOffset: contentStart,\n }),\n );\n }\n }\n return records;\n}\n\nfunction nearestMediaCondition(node: postcss.Node): string | null {\n for (let parent = node.parent; parent !== undefined; parent = parent.parent) {\n if (parent.type !== 'atrule') continue;\n const atRule = parent as postcss.AtRule;\n if (atRule.name === 'media') {\n return atRule.params;\n }\n }\n return null;\n}\n","/**\n * A single text edit: replace the half-open range `[start, end)` of the file\n * content with `replacement`. Zero-length ranges insert purely. The editor\n * debounces and sends one edit at a time — this is the only write primitive.\n */\nexport interface SpliceEdit {\n start: number;\n end: number;\n replacement: string;\n}\n\n/** Thrown for ranges that do not fit the content — never produces partial output. */\nexport class SpliceRangeError extends Error {\n constructor(start: number, end: number, contentLength: number) {\n super(`Invalid splice range [${start}, ${end}) for content of length ${contentLength}`);\n this.name = 'SpliceRangeError';\n }\n}\n\n/**\n * The splice-writer primitive: (content, range, replacement) → new content.\n * Text-splice only — never reprints the file. Every byte outside the replaced\n * range stays identical, so formatting, comments and agent conventions\n * survive and the git diff is minimal.\n */\nexport function spliceText(content: string, edit: SpliceEdit): string {\n const { start, end, replacement } = edit;\n if (\n !Number.isInteger(start) ||\n !Number.isInteger(end) ||\n start < 0 ||\n end > content.length ||\n start > end\n ) {\n throw new SpliceRangeError(start, end, content.length);\n }\n return content.slice(0, start) + replacement + content.slice(end);\n}\n\n/**\n * Append a rule at EOF with exactly one added line, regardless of whether the\n * original ends with a newline (no accidental blank runs; the file's\n * trailing-newline convention is preserved).\n */\nexport function appendRule(content: string, rule: string): string {\n if (content === '') {\n return rule;\n }\n const endsWithNewline = content.endsWith('\\n');\n const body = endsWithNewline ? content : `${content}\\n`;\n return `${body}${rule}${endsWithNewline ? '\\n' : ''}`;\n}\n","import { sep } from 'node:path';\nimport type { ViteDevServer } from 'vite';\nimport { toRelative } from './rest';\n\n/**\n * The file→chrome half of the sync (spec #13): the host watcher is the only\n * FS subscriber; css/astro changes under the project's src dir are debounced\n * per file and pushed to the chrome as `astroix:file-changed` over the Vite\n * WebSocket — the same channel Astro uses for its own events. The chrome\n * refetches content/payload on receipt; its own writes echo back as no-ops\n * (content compare client-side).\n */\nexport function registerFileSync(\n server: ViteDevServer,\n options: { root: string; srcDir: string },\n): void {\n // astro hands srcDir as a URL that keeps a trailing slash — strip it or\n // the startsWith filter below never matches\n const srcDir = options.srcDir.split(sep).join('/').replace(/\\/+$/, '');\n const pending = new Map<string, ReturnType<typeof setTimeout>>();\n\n const push = (file: string): void => {\n const timer = pending.get(file);\n if (timer !== undefined) clearTimeout(timer);\n pending.set(\n file,\n setTimeout(() => {\n pending.delete(file);\n server.ws.send('astroix:file-changed', { file: toRelative(options.root, file) });\n }, 100),\n );\n };\n\n const isWatchedSource = (file: string): boolean => {\n const norm = file.split(sep).join('/');\n if (!norm.startsWith(`${srcDir}/`)) return false;\n return norm.endsWith('.css') || norm.endsWith('.astro');\n };\n\n // `add` matters too: IDE atomic saves (write temp + rename) can surface as\n // add instead of change depending on the editor\n server.watcher.on('change', (file) => {\n if (isWatchedSource(file)) push(file);\n });\n server.watcher.on('add', (file) => {\n if (isWatchedSource(file)) push(file);\n });\n}\n"],"mappings":"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","names":["fileURLToPath","existsSync","readFileSync","join","existsSync","candidates","existsSync","existsSync","join","postcss","existsSync","resolveCompiledCss","postcss","join","sep","sep","join","existsSync","readFileSync","fileURLToPath"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wojciechpiskorz/astroix",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Dev-only Astro 7 integration — a visual builder for Content Collections content and repo-mapped CSS",
5
5
  "type": "module",
6
6
  "license": "MIT",