@purview-dev/astroc4 1.0.0-prerelease.5 → 1.0.0-prerelease.7

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
@@ -1,16 +1,43 @@
1
1
  // src/index.ts
2
- import { fileURLToPath as fileURLToPath4 } from "node:url";
2
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
3
3
 
4
4
  // src/integration.ts
5
- import { createRequire as createRequire2 } from "node:module";
6
- import { join } from "node:path";
7
- import { fileURLToPath as fileURLToPath2 } from "node:url";
5
+ import { existsSync } from "node:fs";
6
+ import { dirname as dirname3, join as join5, resolve as resolve5 } from "node:path";
7
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
8
8
  import { LikeC4VitePlugin } from "likec4/vite-plugin";
9
9
 
10
10
  // src/fences.ts
11
11
  import { createHash } from "node:crypto";
12
+ import { readFile } from "node:fs/promises";
13
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
12
14
  import { createRequire } from "node:module";
13
15
  import { fileURLToPath } from "node:url";
16
+
17
+ // src/translations.ts
18
+ var translations = {
19
+ en: {
20
+ "likec4.view.title": "Architecture diagram",
21
+ "likec4.view.description": "Interactive LikeC4 diagram for this section.",
22
+ "likec4.view.hint": "Click a node in the diagram to open its details.",
23
+ "likec4.view.badge": "Click any node to open the modal",
24
+ "likec4.view.loading": "Loading architecture diagram…",
25
+ "likec4.fence.title": "Diagram",
26
+ "likec4.fence.hint": "Click any node to open its details.",
27
+ "likec4.fence.loading": "Loading diagram…",
28
+ "likec4.error.title": "We couldn’t load this diagram.",
29
+ "likec4.error.body": "Please retry, or refresh the page if the issue persists.",
30
+ "likec4.error.retry": "Retry",
31
+ "likec4.error.viewIdRequired": "A LikeC4 viewId is required to render the diagram.",
32
+ "likec4.error.noProjects": "No LikeC4 projects are available in this workspace.",
33
+ "likec4.error.unknownProject": 'Unknown LikeC4 projectId "{projectId}". Available projects: {available}.',
34
+ "likec4.error.projectRequired": 'Several LikeC4 projects exist ({available}); pass project="…".',
35
+ "likec4.error.unresolvedProject": "Unable to resolve a LikeC4 project.",
36
+ "likec4.error.loadFailed": 'Unable to load LikeC4 project "{projectId}".'
37
+ }
38
+ };
39
+
40
+ // src/fences.ts
14
41
  var require2 = createRequire(import.meta.url);
15
42
  var likec4ApiPromise;
16
43
  function loadLikeC4() {
@@ -28,6 +55,7 @@ var IMPORTS = {
28
55
  [INLINE_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4Fence.astro`,
29
56
  [PROJECT_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4View.astro`
30
57
  };
58
+ var STRINGS = translations.en;
31
59
  var layoutCache = new Map;
32
60
  function parseFenceMeta(meta) {
33
61
  const options = {};
@@ -43,7 +71,7 @@ function parseFenceMeta(meta) {
43
71
  }
44
72
  return options;
45
73
  }
46
- async function layoutSnippet(source) {
74
+ async function layoutSource(source) {
47
75
  const key = createHash("sha256").update(source).digest("hex");
48
76
  const cached = layoutCache.get(key);
49
77
  if (cached)
@@ -95,26 +123,11 @@ function describe(fileURL) {
95
123
  return String(fileURL);
96
124
  }
97
125
  }
98
- var CONSOLE_REPORTER = {
99
- warn: (message) => console.warn(`[likec4-fences] ${message}`)
100
- };
101
- function likec4Fences({ reporter = CONSOLE_REPORTER, mdx = true } = {}) {
126
+ function likec4Fences({ root, projects } = {}) {
102
127
  return (factory) => {
103
- if (factory.sourceFormat !== "mdx") {
104
- if (!factory.source.includes("```" + FENCE_LANG))
105
- return null;
106
- const fix = mdx ? "Rename the page to .mdx, or use <LikeC4View /> directly." : "Add @astrojs/mdx (`astro add mdx`) and rename the page to .mdx, or use <LikeC4View /> directly.";
107
- return {
108
- name: "likec4-fences:warn",
109
- code(node, ctx) {
110
- if (node.lang !== FENCE_LANG)
111
- return;
112
- reporter.warn(`${describe(ctx.fileURL)} has a \`\`\`${FENCE_LANG} fence, but fences only ` + `render as diagrams in .mdx files. ${fix}`);
113
- }
114
- };
115
- }
116
128
  if (!factory.source.includes("```" + FENCE_LANG))
117
129
  return null;
130
+ const markdown = factory.sourceFormat !== "mdx";
118
131
  const used = new Set;
119
132
  return {
120
133
  name: "likec4-fences",
@@ -123,7 +136,7 @@ function likec4Fences({ reporter = CONSOLE_REPORTER, mdx = true } = {}) {
123
136
  return;
124
137
  const where = `${describe(ctx.fileURL)}: \`\`\`${FENCE_LANG} fence`;
125
138
  const options = parseFenceMeta(node.meta);
126
- const source = (node.value ?? "").trim();
139
+ const source = options.file ? await readFenceFile(options.file, ctx.fileURL, root, where) : (node.value ?? "").trim();
127
140
  const common = {
128
141
  title: options.title,
129
142
  description: options.description,
@@ -135,25 +148,40 @@ function likec4Fences({ reporter = CONSOLE_REPORTER, mdx = true } = {}) {
135
148
  if (!options.view) {
136
149
  throw new Error(`${where} is empty, so it needs \`view="<id>"\` to name a view in the project model.`);
137
150
  }
151
+ checkProject(options.project, projects, where);
152
+ if (markdown) {
153
+ ctx.replaceNode(node, {
154
+ raw: fencePlaceholder({ ...placeholderOptions(options), viewId: options.view, project: options.project }),
155
+ mdxExpressions: false
156
+ });
157
+ return;
158
+ }
138
159
  used.add(PROJECT_COMPONENT);
139
160
  ctx.replaceNode(node, {
140
161
  type: "mdxJsxFlowElement",
141
162
  name: PROJECT_COMPONENT,
142
- attributes: attributes({ ...common, viewId: options.view }),
163
+ attributes: attributes({ ...common, viewId: options.view, project: options.project }),
143
164
  children: []
144
165
  });
145
166
  return;
146
167
  }
147
168
  let layout;
148
169
  try {
149
- layout = await layoutSnippet(source);
170
+ layout = await layoutSource(source);
150
171
  } catch (error) {
151
172
  throw new Error(`${where} is not valid LikeC4 — ${error instanceof Error ? error.message : String(error)}`);
152
173
  }
153
- const viewId = options.view ?? layout.viewIds[0];
174
+ const viewId = options.view ?? defaultView(source, layout.viewIds);
154
175
  if (!viewId || !layout.viewIds.includes(viewId)) {
155
176
  throw new Error(`${where} has no view "${viewId ?? "(none)"}". Declared: ${layout.viewIds.join(", ") || "(none)"}.`);
156
177
  }
178
+ if (markdown) {
179
+ ctx.replaceNode(node, {
180
+ raw: fencePlaceholder({ ...placeholderOptions(options), viewId, model: layout.dump }),
181
+ mdxExpressions: false
182
+ });
183
+ return;
184
+ }
157
185
  used.add(INLINE_COMPONENT);
158
186
  ctx.replaceNode(node, {
159
187
  type: "mdxJsxFlowElement",
@@ -170,49 +198,593 @@ function likec4Fences({ reporter = CONSOLE_REPORTER, mdx = true } = {}) {
170
198
  };
171
199
  };
172
200
  }
201
+ function checkProject(project, projects, where) {
202
+ if (projects === undefined)
203
+ return;
204
+ const requested = project?.trim();
205
+ const available = projects.join(", ");
206
+ if (!requested) {
207
+ if (projects.length > 1) {
208
+ throw new Error(`${where}: ${STRINGS["likec4.error.projectRequired"].replace("{available}", available)}`);
209
+ }
210
+ return;
211
+ }
212
+ if (!projects.includes(requested)) {
213
+ throw new Error(`${where}: ${STRINGS["likec4.error.unknownProject"].replace("{projectId}", requested).replace("{available}", available || "(none)")}`);
214
+ }
215
+ }
216
+ function defaultView(source, viewIds) {
217
+ for (const match of source.matchAll(/(?<![\w-])view\s+([\w-]+)/g)) {
218
+ if (viewIds.includes(match[1]))
219
+ return match[1];
220
+ }
221
+ return viewIds[0];
222
+ }
223
+ var token = {
224
+ border: "var(--likec4-border, var(--sl-color-gray-5, rgba(128, 128, 128, 0.35)))",
225
+ surface: "var(--likec4-surface, var(--sl-color-bg, transparent))",
226
+ surfaceAlt: "var(--likec4-surface-alt, var(--sl-color-bg-nav, rgba(128, 128, 128, 0.06)))",
227
+ text: "var(--likec4-text, var(--sl-color-white, inherit))",
228
+ textMuted: "var(--likec4-text-muted, var(--sl-color-gray-2, rgba(128, 128, 128, 0.95)))"
229
+ };
230
+ function fencePlaceholder({
231
+ viewId,
232
+ model,
233
+ project,
234
+ title = STRINGS["likec4.fence.title"],
235
+ description,
236
+ height = 420,
237
+ collapsible = true,
238
+ defaultOpen = true
239
+ }) {
240
+ const data = [
241
+ `data-astroc4-fence`,
242
+ `data-view-id="${escapeHtml(viewId)}"`,
243
+ project ? `data-project="${escapeHtml(project)}"` : "",
244
+ model === undefined ? "" : `data-model="${escapeHtml(JSON.stringify(model))}"`
245
+ ].filter(Boolean).join(" ");
246
+ const headerStyle = `display: block; padding: 1em 1.25rem; border-bottom: 1px solid ${token.border}; background: ${token.surface};`;
247
+ const heading = [
248
+ `<h3 style="display: inline; margin: 0; font-size: 1rem; font-weight: 600; color: ${token.text};">${escapeHtml(title)}</h3>`,
249
+ description ? `<p style="margin: 0.25rem 0 0; font-size: 0.875rem; color: ${token.textMuted};">${escapeHtml(description)}</p>` : ""
250
+ ].join("");
251
+ const body = [
252
+ `<div style="padding: 1em 1.25rem;">`,
253
+ `<div class="likec4-frame" style="position: relative; width: 100%; height: ${height}px; min-height: ${height}px;">`,
254
+ `<p class="likec4-loading" aria-live="polite" style="color: ${token.textMuted};">${escapeHtml(STRINGS["likec4.fence.loading"])}</p>`,
255
+ `</div>`,
256
+ `</div>`
257
+ ].join("");
258
+ const shell = collapsible ? `<details${defaultOpen ? " open" : ""}><summary style="cursor: pointer; ${headerStyle}">${heading}</summary>${body}</details>` : `<header style="${headerStyle}">${heading}</header>${body}`;
259
+ return [
260
+ `<section class="not-content likec4-md-fence" ${data} style="margin: 1.5rem 0; overflow: hidden; border: 1px solid ${token.border}; border-radius: 0.75rem; background: ${token.surfaceAlt};">`,
261
+ shell,
262
+ `</section>`
263
+ ].join("");
264
+ }
265
+ function placeholderOptions(options) {
266
+ const height = Number.parseInt(options.height ?? "", 10);
267
+ return {
268
+ title: options.title || undefined,
269
+ description: options.description || undefined,
270
+ height: Number.isFinite(height) ? height : undefined,
271
+ collapsible: options.collapsible !== "false",
272
+ defaultOpen: options.defaultOpen !== "false"
273
+ };
274
+ }
275
+ function escapeHtml(value) {
276
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
277
+ }
278
+ async function readFenceFile(file, pageURL, root, where) {
279
+ if (!pageURL || !root) {
280
+ throw new Error(`${where} uses file="${file}", which needs the page's location and the site root.`);
281
+ }
282
+ const path = resolve(dirname(fileURLToPath(pageURL)), file);
283
+ const fromRoot = relative(resolve(root), path);
284
+ if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
285
+ throw new Error(`${where} uses file="${file}", which is outside the site (${path}).`);
286
+ }
287
+ try {
288
+ return (await readFile(path, "utf8")).trim();
289
+ } catch {
290
+ throw new Error(`${where} uses file="${file}", which does not exist (${path}).`);
291
+ }
292
+ }
293
+
294
+ // src/file-layout.ts
295
+ import { readFile as readFile2 } from "node:fs/promises";
296
+ var FILE_LAYOUT_QUERY = "?astroc4-layout";
297
+ function fileLayoutPlugin() {
298
+ return {
299
+ name: "@purview-dev/astroc4/file-layout",
300
+ async load(id) {
301
+ if (!id.endsWith(FILE_LAYOUT_QUERY))
302
+ return null;
303
+ const file = id.slice(0, -FILE_LAYOUT_QUERY.length);
304
+ this.addWatchFile(file);
305
+ let layout;
306
+ try {
307
+ layout = await layoutSource(await readFile2(file, "utf8"));
308
+ } catch (error) {
309
+ throw new Error(`[astroc4] ${file} is not valid LikeC4 — ${error.message}`);
310
+ }
311
+ return `export default ${JSON.stringify(layout)};`;
312
+ }
313
+ };
314
+ }
315
+
316
+ // src/icon-plugin.ts
317
+ import { readFile as readFile4, copyFile, mkdir } from "node:fs/promises";
318
+ import { dirname as dirname2, extname, join as join3 } from "node:path";
319
+ import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "node:url";
320
+
321
+ // src/icon-assets.ts
322
+ import { realpathSync } from "node:fs";
323
+ import { isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve3, sep } from "node:path";
324
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
325
+
326
+ // src/projects.ts
327
+ import { readdir, readFile as readFile3, stat } from "node:fs/promises";
328
+ import { basename, join, resolve as resolve2 } from "node:path";
329
+ var CONFIG_FILES = [
330
+ ".likec4rc",
331
+ ".likec4.config.json",
332
+ "likec4.config.json",
333
+ "likec4.config.js",
334
+ "likec4.config.cjs",
335
+ "likec4.config.mjs",
336
+ "likec4.config.ts",
337
+ "likec4.config.cts",
338
+ "likec4.config.mts"
339
+ ];
340
+ var JSON_CONFIG_FILES = CONFIG_FILES.slice(0, 3);
341
+
342
+ class Astroc4ConfigError extends Error {
343
+ constructor(message) {
344
+ super(`[astroc4] ${message}`);
345
+ this.name = "Astroc4ConfigError";
346
+ }
347
+ }
348
+ async function discoverProjects(root) {
349
+ const found = [];
350
+ async function walk(folder) {
351
+ const entries = await readdir(folder, { withFileTypes: true });
352
+ const file = configFileIn(entries.filter((entry) => entry.isFile()).map((entry) => entry.name));
353
+ if (file) {
354
+ const config = await readConfig(folder, file);
355
+ found.push(toProject(config.name, folder, config, "discovered"));
356
+ }
357
+ for (const entry of entries) {
358
+ if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith("."))
359
+ continue;
360
+ await walk(join(folder, entry.name));
361
+ }
362
+ }
363
+ await walk(resolve2(root));
364
+ return found;
365
+ }
366
+ async function resolveProjects(options) {
367
+ const configured = [];
368
+ for (const [name, path] of Object.entries(options.configured ?? {})) {
369
+ const folder = resolve2(options.root, path);
370
+ if (!await isDirectory(folder)) {
371
+ throw new Astroc4ConfigError(`project "${name}": folder ${folder} does not exist.`);
372
+ }
373
+ const file = configFileIn(await readdir(folder));
374
+ if (!file) {
375
+ throw new Astroc4ConfigError(`project "${name}": no LikeC4 config file (likec4.config.json, .likec4rc, likec4.config.ts, …) in ${folder}.`);
376
+ }
377
+ configured.push(toProject(name, folder, await readConfig(folder, file), "configured"));
378
+ }
379
+ const configuredFolders = new Set(configured.map((project) => project.folder));
380
+ const projects = [
381
+ ...configured,
382
+ ...options.discovered.filter((project) => !configuredFolders.has(project.folder))
383
+ ];
384
+ const byName = new Map;
385
+ for (const project of projects) {
386
+ const existing = byName.get(project.name);
387
+ if (existing) {
388
+ throw new Astroc4ConfigError(`two LikeC4 projects are named "${project.name}": ${existing.folder} and ${project.folder}.`);
389
+ }
390
+ byName.set(project.name, project);
391
+ }
392
+ const byId = new Map;
393
+ for (const project of projects) {
394
+ const existing = byId.get(project.likec4Id);
395
+ if (existing) {
396
+ throw new Astroc4ConfigError(`two LikeC4 projects declare "name": "${project.likec4Id}" in their configs: ${existing.folder} and ${project.folder}. Rename one.`);
397
+ }
398
+ byId.set(project.likec4Id, project);
399
+ }
400
+ return projects;
401
+ }
402
+ function configFileIn(names) {
403
+ return CONFIG_FILES.find((file) => names.includes(file));
404
+ }
405
+ async function readConfig(folder, file) {
406
+ const path = join(folder, file);
407
+ let config;
408
+ try {
409
+ config = JSON_CONFIG_FILES.includes(file) ? await readJsonConfig(path) : await evaluateConfig(path, folder);
410
+ } catch (error) {
411
+ throw new Astroc4ConfigError(`${path} could not be read: ${error.message}`);
412
+ }
413
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
414
+ throw new Astroc4ConfigError(`${path} must define a config object.`);
415
+ }
416
+ const { name = basename(folder), imageAliases } = config;
417
+ if (typeof name !== "string" || name.trim().length === 0) {
418
+ throw new Astroc4ConfigError(`${path} has an empty "name".`);
419
+ }
420
+ return { name, imageAliases: isRecord(imageAliases) ? imageAliases : undefined };
421
+ }
422
+ async function readJsonConfig(path) {
423
+ const { default: JSON5 } = await import("json5");
424
+ const text = (await readFile3(path, "utf8")).trim();
425
+ return JSON5.parse(text || "{}");
426
+ }
427
+ async function evaluateConfig(path, folder) {
428
+ const { bundleRequire } = await import("bundle-require");
429
+ const { mod } = await bundleRequire({
430
+ filepath: path,
431
+ cwd: folder,
432
+ esbuildOptions: {
433
+ resolveExtensions: [".ts", ".mts", ".cts", ".mjs", ".js", ".cjs"],
434
+ plugins: [
435
+ {
436
+ name: "astroc4-likec4-config",
437
+ setup(build) {
438
+ build.onResolve({ filter: /^@?likec4\/config$/ }, (args) => ({
439
+ path: args.path,
440
+ namespace: "astroc4-likec4-config"
441
+ }));
442
+ build.onLoad({ filter: /.*/, namespace: "astroc4-likec4-config" }, () => ({
443
+ contents: [
444
+ "const identity = (value) => value;",
445
+ "export { identity as defineConfig, identity as defineGenerators, identity as defineStyle, identity as defineTheme, identity as defineThemeColor };"
446
+ ].join(`
447
+ `),
448
+ loader: "js"
449
+ }));
450
+ }
451
+ }
452
+ ]
453
+ }
454
+ });
455
+ return mod.default ?? mod;
456
+ }
457
+ function toProject(name, folder, config, source) {
458
+ const imageAliases = Object.fromEntries(Object.entries(config.imageAliases ?? {}).map(([alias, path]) => [alias, resolve2(folder, path)]));
459
+ return { name, folder: resolve2(folder), likec4Id: config.name, source, imageAliases };
460
+ }
461
+ function isRecord(value) {
462
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
463
+ }
464
+ async function isDirectory(path) {
465
+ return (await stat(path).catch(() => null))?.isDirectory() ?? false;
466
+ }
467
+
468
+ // src/icon-assets.ts
469
+ var ICON_ROUTE = "/_astroc4/";
470
+ var UP = "__up";
471
+ function iconAssetFor(project, fileUrl, base, siteRoot) {
472
+ const file = realFile(fileUrl);
473
+ const prefix = base.replace(/\/+$/, "");
474
+ const publicFolder = tryRealpath(join2(siteRoot, "public"));
475
+ if (isInside(file, publicFolder)) {
476
+ const path = split(relative2(publicFolder, file)).map(encodeURIComponent).join("/");
477
+ return { file, url: `${prefix}/${path}`, projectName: project.name, public: true };
478
+ }
479
+ const segments = aliasSegments(project, file) ?? relativeSegments(tryRealpath(project.folder), file);
480
+ const path = [project.name, ...segments].map(encodeURIComponent).join("/");
481
+ return { file, url: `${prefix}${ICON_ROUTE}${path}`, projectName: project.name };
482
+ }
483
+ function unstage(fileUrl, project, stagingDir) {
484
+ if (stagingDir === undefined)
485
+ return fileUrl;
486
+ let path;
487
+ try {
488
+ path = fileURLToPath2(decodeDrive(fileUrl));
489
+ } catch {
490
+ return fileUrl;
491
+ }
492
+ const fromLink = relative2(join2(stagingDir, project.name), path);
493
+ return pathToFileURL(resolve3(project.folder, fromLink)).href;
494
+ }
495
+ function buildIconManifest(entries, base, siteRoot) {
496
+ const manifest = new Map;
497
+ const byUrl = new Map;
498
+ for (const { project, fileUrl, source } of entries) {
499
+ let asset;
500
+ try {
501
+ asset = iconAssetFor(project, fileUrl, base, siteRoot);
502
+ } catch (error) {
503
+ throw new Astroc4ConfigError(`${source}: ${error.message.replace(/^\[astroc4\] /, "")}`);
504
+ }
505
+ if (asset === null)
506
+ continue;
507
+ const existing = byUrl.get(asset.url);
508
+ if (existing && !samePath(existing.asset.file, asset.file)) {
509
+ throw new Astroc4ConfigError(`two icons would be served at ${asset.url}: ${existing.asset.file} (${existing.source}) and ${asset.file} (${source}). Rename one, or give its alias another name.`);
510
+ }
511
+ byUrl.set(asset.url, { asset, source });
512
+ manifest.set(fileUrl, asset);
513
+ }
514
+ return manifest;
515
+ }
516
+ function aliasSegments(project, file) {
517
+ let best = null;
518
+ for (const [alias, folder] of Object.entries(project.imageAliases)) {
519
+ const real = tryRealpath(folder);
520
+ if (isInside(file, real) && (best === null || real.length > best.folder.length)) {
521
+ best = { folder: real, alias };
522
+ }
523
+ }
524
+ if (best === null)
525
+ return null;
526
+ const name = best.alias.replace(/^@/, "");
527
+ return [...name ? [name] : [], ...split(relative2(best.folder, file))];
528
+ }
529
+ function relativeSegments(folder, file) {
530
+ return split(relative2(folder, file)).map((segment) => segment === ".." ? UP : segment);
531
+ }
532
+ function realFile(fileUrl) {
533
+ let path;
534
+ try {
535
+ path = fileURLToPath2(decodeDrive(fileUrl));
536
+ } catch {
537
+ throw new Astroc4ConfigError(`icon ${fileUrl} is not a file URL.`);
538
+ }
539
+ try {
540
+ return realpathSync(path);
541
+ } catch {
542
+ throw new Astroc4ConfigError(`icon ${path} does not exist.`);
543
+ }
544
+ }
545
+ function decodeDrive(fileUrl) {
546
+ return fileUrl.replace(/^file:\/\/\/([A-Za-z])%3A/i, "file:///$1:");
547
+ }
548
+ function tryRealpath(path) {
549
+ try {
550
+ return realpathSync(path);
551
+ } catch {
552
+ return path;
553
+ }
554
+ }
555
+ function isInside(file, folder) {
556
+ const path = relative2(folder, file);
557
+ return path.length > 0 && !path.startsWith("..") && !isAbsolute2(path);
558
+ }
559
+ function samePath(a, b) {
560
+ return sep === "\\" ? a.toLowerCase() === b.toLowerCase() : a === b;
561
+ }
562
+ function split(path) {
563
+ return path.split(/[\\/]+/).filter((segment) => segment.length > 0);
564
+ }
565
+
566
+ // src/icon-plugin.ts
567
+ var LIKEC4_MODULE = /likec4:plugin\/(?<projectId>[^/]+)\/[^/]+$/;
568
+ var FILE_URL = /file:(?:\\u002F|\/){3}[^"'`\s]+/g;
569
+ var CONTENT_TYPES = {
570
+ ".svg": "image/svg+xml",
571
+ ".png": "image/png",
572
+ ".jpg": "image/jpeg",
573
+ ".jpeg": "image/jpeg",
574
+ ".gif": "image/gif",
575
+ ".webp": "image/webp",
576
+ ".ico": "image/x-icon"
577
+ };
578
+ function astroc4IconPlugin(state) {
579
+ const manifest = new Map;
580
+ const byUrl = new Map;
581
+ const base = state.base.replace(/\/+$/, "");
582
+ function gather(project, fileUrl) {
583
+ const known = manifest.get(fileUrl);
584
+ if (known)
585
+ return known;
586
+ const entries = [
587
+ ...[...byUrl.values()].map((asset) => ({
588
+ project: state.projects.find((p) => p.name === asset.projectName),
589
+ fileUrl: pathToFileURL2(asset.file).href,
590
+ source: `LikeC4 project "${asset.projectName}"`
591
+ })),
592
+ { project, fileUrl, source: `LikeC4 project "${project.name}"` }
593
+ ];
594
+ const asset = buildIconManifest(entries, state.base, state.siteRoot).get(fileUrl);
595
+ if (!asset)
596
+ return;
597
+ manifest.set(fileUrl, asset);
598
+ if (!asset.public)
599
+ byUrl.set(asset.url, asset);
600
+ return asset;
601
+ }
602
+ return {
603
+ name: "@purview-dev/astroc4/icons",
604
+ enforce: "post",
605
+ transform(code, id) {
606
+ const match = LIKEC4_MODULE.exec(id);
607
+ if (!match)
608
+ return null;
609
+ const project = state.projects.find((p) => p.likec4Id === match.groups.projectId);
610
+ if (!project || !code.includes("file:"))
611
+ return null;
612
+ const rewritten = code.replace(FILE_URL, (found) => {
613
+ const escapedForm = found.includes("\\u002F");
614
+ const [fileUrl, query] = found.replaceAll("\\u002F", "/").split("?", 2);
615
+ const real = unstage(fileUrl, project, state.stagingDir);
616
+ if (query !== undefined) {
617
+ return `${fileURLToPath3(real).replaceAll("\\", "/")}?${query}`;
618
+ }
619
+ const asset = gather(project, real);
620
+ if (!asset)
621
+ return found;
622
+ return escapedForm ? asset.url.replaceAll("/", "\\u002F") : asset.url;
623
+ });
624
+ return rewritten === code ? null : { code: rewritten, map: null };
625
+ },
626
+ configureServer(server) {
627
+ const prefix = `${base}${ICON_ROUTE}`;
628
+ server.middlewares.use(async (req, res, next) => {
629
+ const url = req.url?.split("?")[0];
630
+ if (!url?.startsWith(prefix))
631
+ return next();
632
+ const asset = byUrl.get(url);
633
+ if (!asset) {
634
+ res.statusCode = 404;
635
+ res.end();
636
+ return;
637
+ }
638
+ res.setHeader("Content-Type", CONTENT_TYPES[extname(asset.file).toLowerCase()] ?? "application/octet-stream");
639
+ res.end(await readFile4(asset.file));
640
+ });
641
+ },
642
+ manifest: () => manifest,
643
+ async emit(outDir) {
644
+ for (const asset of byUrl.values()) {
645
+ const target = join3(outDir, ...decodeURIComponent(asset.url.slice(base.length)).split("/"));
646
+ await mkdir(dirname2(target), { recursive: true });
647
+ await copyFile(asset.file, target);
648
+ }
649
+ }
650
+ };
651
+ }
652
+
653
+ // src/workspace.ts
654
+ import { lstat, mkdir as mkdir2, readdir as readdir2, readlink, realpath, rm, symlink } from "node:fs/promises";
655
+ import { join as join4, resolve as resolve4 } from "node:path";
656
+ var PROJECTS_MODULE_ID = "virtual:astroc4/projects";
657
+ async function prepareWorkspace(projects, cacheDir) {
658
+ const staging = join4(cacheDir, "workspace");
659
+ await mkdir2(staging, { recursive: true });
660
+ const wanted = new Map(projects.map((project) => [project.name, resolve4(project.folder)]));
661
+ for (const entry of await readdir2(staging)) {
662
+ const target = wanted.get(entry);
663
+ if (target === undefined || !await pointsAt(join4(staging, entry), target)) {
664
+ await rm(join4(staging, entry), { recursive: false, force: true });
665
+ }
666
+ }
667
+ for (const [name, folder] of wanted) {
668
+ const link = join4(staging, name);
669
+ if (await exists(link))
670
+ continue;
671
+ await symlink(folder, link, "junction");
672
+ }
673
+ return staging;
674
+ }
675
+ function projectsModule(projects) {
676
+ const names = projects.map((project) => project.name);
677
+ const ids = Object.fromEntries(projects.map((project) => [project.name, project.likec4Id]));
678
+ return [
679
+ `export const projectNames = ${JSON.stringify(names)};`,
680
+ `export const likec4IdFor = ${JSON.stringify(ids)};`
681
+ ].join(`
682
+ `);
683
+ }
684
+ async function pointsAt(link, target) {
685
+ try {
686
+ const stats = await lstat(link);
687
+ if (!stats.isSymbolicLink())
688
+ return false;
689
+ await readlink(link);
690
+ return await realpath(link) === await realpath(target);
691
+ } catch {
692
+ return false;
693
+ }
694
+ }
695
+ async function exists(path) {
696
+ return await lstat(path).catch(() => null) !== null;
697
+ }
173
698
 
174
699
  // src/integration.ts
175
700
  var PACKAGE_NAME2 = "@purview-dev/astroc4";
176
701
  var OPTIMIZED_DEPENDENCIES = ["react/compiler-runtime"];
702
+ var MD_FENCES_SCRIPT = [
703
+ `const mountLikeC4Fences = () => {`,
704
+ ` if (!document.querySelector('[data-astroc4-fence]:not([data-astroc4-mounted])')) return;`,
705
+ ` const preamble = window.$RefreshReg$ ? Promise.resolve() : import('@vitejs/plugin-react/preamble');`,
706
+ ` preamble`,
707
+ ` .then(() => import('${PACKAGE_NAME2}/components/md-fences'))`,
708
+ ` .then((module) => module.mountFences());`,
709
+ `};`,
710
+ `if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mountLikeC4Fences);`,
711
+ `else mountLikeC4Fences();`,
712
+ `document.addEventListener('astro:page-load', mountLikeC4Fences);`
713
+ ].join(`
714
+ `);
177
715
  function missingOptimizedDependencies(include, root) {
178
716
  const existing = Array.isArray(include) ? include : [];
179
717
  return OPTIMIZED_DEPENDENCIES.filter((dependency) => !existing.includes(dependency) && resolvesFrom(dependency, root));
180
718
  }
181
719
  function resolvesFrom(specifier, root) {
182
- try {
183
- createRequire2(join(root, "package.json")).resolve(specifier);
184
- return true;
185
- } catch {
186
- return false;
720
+ const name = specifier.split("/", specifier.startsWith("@") ? 2 : 1).join("/");
721
+ for (let dir = resolve5(root);; dir = dirname3(dir)) {
722
+ if (existsSync(join5(dir, "node_modules", name, "package.json")))
723
+ return true;
724
+ if (dirname3(dir) === dir)
725
+ return false;
187
726
  }
188
727
  }
189
728
  function likec4Integration({
190
729
  vitePlugin = {},
730
+ projects,
191
731
  fences = true
192
732
  } = {}) {
733
+ let icons;
193
734
  return {
194
735
  name: `${PACKAGE_NAME2}/integration`,
195
736
  hooks: {
196
- "astro:config:setup": async ({ command, config, updateConfig, logger }) => {
197
- const optimizeDeps = missingOptimizedDependencies(config.vite?.optimizeDeps?.include, fileURLToPath2(config.root));
737
+ "astro:config:setup": async ({ command, config, updateConfig, injectScript, logger }) => {
738
+ const root = fileURLToPath4(config.root);
739
+ const optimizeDeps = missingOptimizedDependencies(config.vite?.optimizeDeps?.include, root);
740
+ const { pluginOptions, resolved, stagingDir } = await workspaceFor(root, vitePlugin, projects, logger);
741
+ icons = astroc4IconPlugin({ projects: resolved, base: config.base ?? "/", siteRoot: root, stagingDir });
198
742
  updateConfig({
199
743
  vite: {
200
- plugins: [LikeC4VitePlugin(vitePlugin)],
744
+ plugins: [LikeC4VitePlugin(pluginOptions), projectsPlugin(resolved), icons, fileLayoutPlugin()],
201
745
  ssr: { noExternal: [PACKAGE_NAME2] },
202
746
  ...optimizeDeps.length > 0 ? { optimizeDeps: { include: optimizeDeps } } : {}
203
747
  }
204
748
  });
205
749
  if (fences) {
206
- await registerFences(config, updateConfig, logger);
750
+ await registerFences(config, updateConfig, logger, resolved);
751
+ injectScript("page", MD_FENCES_SCRIPT);
207
752
  }
208
753
  await ensureReactRenderer(config, updateConfig, logger);
209
754
  if (command === "build") {
210
755
  await preloadLikeC4();
211
756
  }
757
+ },
758
+ "astro:build:done": async ({ dir }) => {
759
+ await icons?.emit(fileURLToPath4(dir));
212
760
  }
213
761
  }
214
762
  };
215
763
  }
764
+ async function workspaceFor(root, vitePlugin, configured, logger) {
765
+ const hasConfigured = configured !== undefined && Object.keys(configured).length > 0;
766
+ if (vitePlugin.workspace !== undefined || "languageServices" in vitePlugin) {
767
+ if (hasConfigured) {
768
+ logger.warn("`projects` is ignored because `vitePlugin.workspace` or `vitePlugin.languageServices` is set; remove one of them.");
769
+ }
770
+ return { pluginOptions: vitePlugin, resolved: [] };
771
+ }
772
+ const discovered = await discoverProjects(root);
773
+ if (!hasConfigured) {
774
+ return { pluginOptions: vitePlugin, resolved: discovered };
775
+ }
776
+ const resolved = await resolveProjects({ root, configured, discovered });
777
+ const workspace = await prepareWorkspace(resolved, join5(root, ".astro", "astroc4"));
778
+ return { pluginOptions: { ...vitePlugin, workspace }, resolved, stagingDir: workspace };
779
+ }
780
+ function projectsPlugin(projects) {
781
+ const resolvedId = `\x00${PROJECTS_MODULE_ID}`;
782
+ return {
783
+ name: `${PACKAGE_NAME2}/projects`,
784
+ resolveId: (id) => id === PROJECTS_MODULE_ID ? resolvedId : undefined,
785
+ load: (id) => id === resolvedId ? projectsModule(projects) : undefined
786
+ };
787
+ }
216
788
  async function ensureReactRenderer(config, updateConfig, logger) {
217
789
  if (config.integrations.some((integration) => integration.name === "@astrojs/react"))
218
790
  return;
@@ -224,16 +796,13 @@ async function ensureReactRenderer(config, updateConfig, logger) {
224
796
  logger.warn("@astrojs/react is not installed, so <LikeC4View> cannot render. Install it (`astro add react`).");
225
797
  }
226
798
  }
227
- async function registerFences(config, updateConfig, logger) {
799
+ async function registerFences(config, updateConfig, logger, projects) {
228
800
  const { satteri, isSatteriProcessor } = await import("@astrojs/markdown-satteri");
229
801
  const existing = config.markdown?.processor;
230
- const fences = () => {
231
- const fenceLogger = logger.fork("likec4-fences");
232
- return likec4Fences({
233
- reporter: { warn: (message) => fenceLogger.warn(message) },
234
- mdx: config.integrations.some((integration) => integration.name === "@astrojs/mdx")
235
- });
236
- };
802
+ const fences = () => likec4Fences({
803
+ root: fileURLToPath4(config.root),
804
+ projects: projects.length > 0 ? projects.map((project) => project.name) : undefined
805
+ });
237
806
  if (existing === undefined) {
238
807
  updateConfig({ markdown: { processor: satteri({ mdastPlugins: [fences()] }) } });
239
808
  return;
@@ -256,9 +825,9 @@ var integration_default = likec4Integration;
256
825
 
257
826
  // src/grammar/source.ts
258
827
  import { createHash as createHash2 } from "node:crypto";
259
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
260
- import { join as join2 } from "node:path";
261
- import { fileURLToPath as fileURLToPath3 } from "node:url";
828
+ import { mkdir as mkdir3, readFile as readFile5, rename, writeFile } from "node:fs/promises";
829
+ import { join as join6 } from "node:path";
830
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
262
831
 
263
832
  // src/grammar/langium.ts
264
833
  var NOT_VALUE_DOMAINS = /Id$/;
@@ -969,9 +1538,9 @@ function derive(langium, info) {
969
1538
  };
970
1539
  }
971
1540
  async function bundled(reason, options) {
972
- const path = options.bundledLangiumPath ?? fileURLToPath3(BUNDLED_LANGIUM);
1541
+ const path = options.bundledLangiumPath ?? fileURLToPath5(BUNDLED_LANGIUM);
973
1542
  const snapshot = await bundledProvenance();
974
- const source = derive(await readFile(path, "utf8"), {
1543
+ const source = derive(await readFile5(path, "utf8"), {
975
1544
  repo: snapshot.repo ?? DEFAULT_REPO,
976
1545
  ref: snapshot.ref ?? DEFAULT_REF,
977
1546
  source: "bundled"
@@ -981,7 +1550,7 @@ async function bundled(reason, options) {
981
1550
  }
982
1551
  async function bundledProvenance() {
983
1552
  try {
984
- const { $generated } = JSON.parse(await readFile(fileURLToPath3(BUNDLED_GRAMMAR), "utf8"));
1553
+ const { $generated } = JSON.parse(await readFile5(fileURLToPath5(BUNDLED_GRAMMAR), "utf8"));
985
1554
  return { repo: $generated?.repo, ref: $generated?.ref };
986
1555
  } catch {
987
1556
  return {};
@@ -1030,14 +1599,14 @@ async function fetchLangiumSource(options) {
1030
1599
  return { text, etag: response.headers.get("etag") ?? undefined };
1031
1600
  }
1032
1601
  function cachePath(options, identity) {
1033
- const dir = options.cacheDir ?? join2(options.root ?? process.cwd(), "node_modules", ".cache", "astroc4");
1602
+ const dir = options.cacheDir ?? join6(options.root ?? process.cwd(), "node_modules", ".cache", "astroc4");
1034
1603
  const key = createHash2("sha256").update(`${identity.repo}@${identity.ref}`).digest("hex").slice(0, 16);
1035
- return join2(dir, `grammar-${key}.json`);
1604
+ return join6(dir, `grammar-${key}.json`);
1036
1605
  }
1037
1606
  async function readCache(options, identity) {
1038
1607
  let raw;
1039
1608
  try {
1040
- raw = await readFile(cachePath(options, identity), "utf8");
1609
+ raw = await readFile5(cachePath(options, identity), "utf8");
1041
1610
  } catch {
1042
1611
  return;
1043
1612
  }
@@ -1056,7 +1625,7 @@ async function writeCache(options, identity, entry) {
1056
1625
  const path = cachePath(options, identity);
1057
1626
  const temporary = `${path}.${process.pid}.tmp`;
1058
1627
  try {
1059
- await mkdir(join2(path, ".."), { recursive: true });
1628
+ await mkdir3(join6(path, ".."), { recursive: true });
1060
1629
  await writeFile(temporary, JSON.stringify({
1061
1630
  version: CACHE_VERSION,
1062
1631
  repo: identity.repo,
@@ -1074,31 +1643,13 @@ function isTruthy(value) {
1074
1643
  return typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim());
1075
1644
  }
1076
1645
 
1077
- // src/translations.ts
1078
- var translations = {
1079
- en: {
1080
- "likec4.view.title": "Architecture diagram",
1081
- "likec4.view.description": "Interactive LikeC4 diagram for this section.",
1082
- "likec4.view.hint": "Click a node in the diagram to open its details.",
1083
- "likec4.view.badge": "Click any node to open the modal",
1084
- "likec4.view.loading": "Loading architecture diagram…",
1085
- "likec4.fence.title": "Diagram",
1086
- "likec4.fence.hint": "Click any node to open its details.",
1087
- "likec4.fence.loading": "Loading diagram…",
1088
- "likec4.error.title": "We couldn’t load this diagram.",
1089
- "likec4.error.body": "Please retry, or refresh the page if the issue persists.",
1090
- "likec4.error.retry": "Retry",
1091
- "likec4.error.viewIdRequired": "A LikeC4 viewId is required to render the diagram.",
1092
- "likec4.error.noProjects": "No LikeC4 projects are available in this workspace.",
1093
- "likec4.error.unknownProject": 'Unknown LikeC4 projectId "{projectId}". Available projects: {available}.',
1094
- "likec4.error.unresolvedProject": "Unable to resolve a LikeC4 project.",
1095
- "likec4.error.loadFailed": 'Unable to load LikeC4 project "{projectId}".'
1096
- }
1097
- };
1098
-
1099
1646
  // src/index.ts
1100
1647
  function starlightLikeC4(options = {}) {
1101
- const integration = likec4Integration({ vitePlugin: options.vitePlugin, fences: options.fences });
1648
+ const integration = likec4Integration({
1649
+ vitePlugin: options.vitePlugin,
1650
+ projects: options.projects,
1651
+ fences: options.fences
1652
+ });
1102
1653
  return {
1103
1654
  name: PACKAGE_NAME2,
1104
1655
  hooks: {
@@ -1115,7 +1666,7 @@ function starlightLikeC4(options = {}) {
1115
1666
  if (options.highlighting === false)
1116
1667
  return;
1117
1668
  await injectHighlighting({
1118
- root: fileURLToPath4(astroConfig.root),
1669
+ root: fileURLToPath6(astroConfig.root),
1119
1670
  expressiveCode: config.expressiveCode,
1120
1671
  updateConfig,
1121
1672
  logger,
@@ -1183,5 +1734,5 @@ export {
1183
1734
  withLikeC4Language
1184
1735
  };
1185
1736
 
1186
- //# debugId=0517D2E2C36FC4B664756E2164756E21
1737
+ //# debugId=B9FCAC3D36CE7F6464756E2164756E21
1187
1738
  //# sourceMappingURL=index.js.map