@purview-dev/astroc4 1.0.0-prerelease.4 → 1.0.0-prerelease.6

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,11 +1,43 @@
1
1
  // src/index.ts
2
- import { fileURLToPath as fileURLToPath3 } from "node:url";
2
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
3
+
4
+ // src/integration.ts
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";
3
8
  import { LikeC4VitePlugin } from "likec4/vite-plugin";
4
9
 
5
10
  // src/fences.ts
6
11
  import { createHash } from "node:crypto";
12
+ import { readFile } from "node:fs/promises";
13
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
7
14
  import { createRequire } from "node:module";
8
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
9
41
  var require2 = createRequire(import.meta.url);
10
42
  var likec4ApiPromise;
11
43
  function loadLikeC4() {
@@ -23,6 +55,7 @@ var IMPORTS = {
23
55
  [INLINE_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4Fence.astro`,
24
56
  [PROJECT_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4View.astro`
25
57
  };
58
+ var STRINGS = translations.en;
26
59
  var layoutCache = new Map;
27
60
  function parseFenceMeta(meta) {
28
61
  const options = {};
@@ -38,7 +71,7 @@ function parseFenceMeta(meta) {
38
71
  }
39
72
  return options;
40
73
  }
41
- async function layoutSnippet(source) {
74
+ async function layoutSource(source) {
42
75
  const key = createHash("sha256").update(source).digest("hex");
43
76
  const cached = layoutCache.get(key);
44
77
  if (cached)
@@ -90,22 +123,11 @@ function describe(fileURL) {
90
123
  return String(fileURL);
91
124
  }
92
125
  }
93
- function likec4Fences() {
126
+ function likec4Fences({ root, projects } = {}) {
94
127
  return (factory) => {
95
- if (factory.sourceFormat !== "mdx") {
96
- if (!factory.source.includes("```" + FENCE_LANG))
97
- return null;
98
- return {
99
- name: "likec4-fences:warn",
100
- code(node, ctx) {
101
- if (node.lang !== FENCE_LANG)
102
- return;
103
- console.warn(`[likec4-fences] ${describe(ctx.fileURL)} has a \`\`\`${FENCE_LANG} fence, but fences only ` + "render as diagrams in .mdx files. Rename the page to .mdx, or use <LikeC4View /> directly.");
104
- }
105
- };
106
- }
107
128
  if (!factory.source.includes("```" + FENCE_LANG))
108
129
  return null;
130
+ const markdown = factory.sourceFormat !== "mdx";
109
131
  const used = new Set;
110
132
  return {
111
133
  name: "likec4-fences",
@@ -114,7 +136,7 @@ function likec4Fences() {
114
136
  return;
115
137
  const where = `${describe(ctx.fileURL)}: \`\`\`${FENCE_LANG} fence`;
116
138
  const options = parseFenceMeta(node.meta);
117
- const source = (node.value ?? "").trim();
139
+ const source = options.file ? await readFenceFile(options.file, ctx.fileURL, root, where) : (node.value ?? "").trim();
118
140
  const common = {
119
141
  title: options.title,
120
142
  description: options.description,
@@ -126,25 +148,40 @@ function likec4Fences() {
126
148
  if (!options.view) {
127
149
  throw new Error(`${where} is empty, so it needs \`view="<id>"\` to name a view in the project model.`);
128
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
+ }
129
159
  used.add(PROJECT_COMPONENT);
130
160
  ctx.replaceNode(node, {
131
161
  type: "mdxJsxFlowElement",
132
162
  name: PROJECT_COMPONENT,
133
- attributes: attributes({ ...common, viewId: options.view }),
163
+ attributes: attributes({ ...common, viewId: options.view, project: options.project }),
134
164
  children: []
135
165
  });
136
166
  return;
137
167
  }
138
168
  let layout;
139
169
  try {
140
- layout = await layoutSnippet(source);
170
+ layout = await layoutSource(source);
141
171
  } catch (error) {
142
172
  throw new Error(`${where} is not valid LikeC4 — ${error instanceof Error ? error.message : String(error)}`);
143
173
  }
144
- const viewId = options.view ?? layout.viewIds[0];
174
+ const viewId = options.view ?? defaultView(source, layout.viewIds);
145
175
  if (!viewId || !layout.viewIds.includes(viewId)) {
146
176
  throw new Error(`${where} has no view "${viewId ?? "(none)"}". Declared: ${layout.viewIds.join(", ") || "(none)"}.`);
147
177
  }
178
+ if (markdown) {
179
+ ctx.replaceNode(node, {
180
+ raw: fencePlaceholder({ ...placeholderOptions(options), viewId, model: layout.dump }),
181
+ mdxExpressions: false
182
+ });
183
+ return;
184
+ }
148
185
  used.add(INLINE_COMPONENT);
149
186
  ctx.replaceNode(node, {
150
187
  type: "mdxJsxFlowElement",
@@ -161,12 +198,633 @@ function likec4Fences() {
161
198
  };
162
199
  };
163
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
+ }
698
+
699
+ // src/integration.ts
700
+ var PACKAGE_NAME2 = "@purview-dev/astroc4";
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
+ ` import('${PACKAGE_NAME2}/components/md-fences').then((module) => module.mountFences());`,
706
+ `};`,
707
+ `if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mountLikeC4Fences);`,
708
+ `else mountLikeC4Fences();`,
709
+ `document.addEventListener('astro:page-load', mountLikeC4Fences);`
710
+ ].join(`
711
+ `);
712
+ function missingOptimizedDependencies(include, root) {
713
+ const existing = Array.isArray(include) ? include : [];
714
+ return OPTIMIZED_DEPENDENCIES.filter((dependency) => !existing.includes(dependency) && resolvesFrom(dependency, root));
715
+ }
716
+ function resolvesFrom(specifier, root) {
717
+ const name = specifier.split("/", specifier.startsWith("@") ? 2 : 1).join("/");
718
+ for (let dir = resolve5(root);; dir = dirname3(dir)) {
719
+ if (existsSync(join5(dir, "node_modules", name, "package.json")))
720
+ return true;
721
+ if (dirname3(dir) === dir)
722
+ return false;
723
+ }
724
+ }
725
+ function likec4Integration({
726
+ vitePlugin = {},
727
+ projects,
728
+ fences = true
729
+ } = {}) {
730
+ let icons;
731
+ return {
732
+ name: `${PACKAGE_NAME2}/integration`,
733
+ hooks: {
734
+ "astro:config:setup": async ({ command, config, updateConfig, injectScript, logger }) => {
735
+ const root = fileURLToPath4(config.root);
736
+ const optimizeDeps = missingOptimizedDependencies(config.vite?.optimizeDeps?.include, root);
737
+ const { pluginOptions, resolved, stagingDir } = await workspaceFor(root, vitePlugin, projects, logger);
738
+ icons = astroc4IconPlugin({ projects: resolved, base: config.base ?? "/", siteRoot: root, stagingDir });
739
+ updateConfig({
740
+ vite: {
741
+ plugins: [LikeC4VitePlugin(pluginOptions), projectsPlugin(resolved), icons, fileLayoutPlugin()],
742
+ ssr: { noExternal: [PACKAGE_NAME2] },
743
+ ...optimizeDeps.length > 0 ? { optimizeDeps: { include: optimizeDeps } } : {}
744
+ }
745
+ });
746
+ if (fences) {
747
+ await registerFences(config, updateConfig, logger, resolved);
748
+ injectScript("page", MD_FENCES_SCRIPT);
749
+ }
750
+ await ensureReactRenderer(config, updateConfig, logger);
751
+ if (command === "build") {
752
+ await preloadLikeC4();
753
+ }
754
+ },
755
+ "astro:build:done": async ({ dir }) => {
756
+ await icons?.emit(fileURLToPath4(dir));
757
+ }
758
+ }
759
+ };
760
+ }
761
+ async function workspaceFor(root, vitePlugin, configured, logger) {
762
+ const hasConfigured = configured !== undefined && Object.keys(configured).length > 0;
763
+ if (vitePlugin.workspace !== undefined || "languageServices" in vitePlugin) {
764
+ if (hasConfigured) {
765
+ logger.warn("`projects` is ignored because `vitePlugin.workspace` or `vitePlugin.languageServices` is set; remove one of them.");
766
+ }
767
+ return { pluginOptions: vitePlugin, resolved: [] };
768
+ }
769
+ const discovered = await discoverProjects(root);
770
+ if (!hasConfigured) {
771
+ return { pluginOptions: vitePlugin, resolved: discovered };
772
+ }
773
+ const resolved = await resolveProjects({ root, configured, discovered });
774
+ const workspace = await prepareWorkspace(resolved, join5(root, ".astro", "astroc4"));
775
+ return { pluginOptions: { ...vitePlugin, workspace }, resolved, stagingDir: workspace };
776
+ }
777
+ function projectsPlugin(projects) {
778
+ const resolvedId = `\x00${PROJECTS_MODULE_ID}`;
779
+ return {
780
+ name: `${PACKAGE_NAME2}/projects`,
781
+ resolveId: (id) => id === PROJECTS_MODULE_ID ? resolvedId : undefined,
782
+ load: (id) => id === resolvedId ? projectsModule(projects) : undefined
783
+ };
784
+ }
785
+ async function ensureReactRenderer(config, updateConfig, logger) {
786
+ if (config.integrations.some((integration) => integration.name === "@astrojs/react"))
787
+ return;
788
+ try {
789
+ const { default: react } = await import("@astrojs/react");
790
+ updateConfig({ integrations: [react()] });
791
+ logger.info('added @astrojs/react: the LikeC4 diagrams are React islands (client:only="react").');
792
+ } catch {
793
+ logger.warn("@astrojs/react is not installed, so <LikeC4View> cannot render. Install it (`astro add react`).");
794
+ }
795
+ }
796
+ async function registerFences(config, updateConfig, logger, projects) {
797
+ const { satteri, isSatteriProcessor } = await import("@astrojs/markdown-satteri");
798
+ const existing = config.markdown?.processor;
799
+ const fences = () => likec4Fences({
800
+ root: fileURLToPath4(config.root),
801
+ projects: projects.length > 0 ? projects.map((project) => project.name) : undefined
802
+ });
803
+ if (existing === undefined) {
804
+ updateConfig({ markdown: { processor: satteri({ mdastPlugins: [fences()] }) } });
805
+ return;
806
+ }
807
+ if (!isSatteriProcessor(existing)) {
808
+ logger.warn("```likec4 fences are disabled: the site is not using the Sätteri Markdown processor. " + "<LikeC4View /> still works.");
809
+ return;
810
+ }
811
+ const existingOptions = existing.options ?? {};
812
+ updateConfig({
813
+ markdown: {
814
+ processor: satteri({
815
+ ...existingOptions,
816
+ mdastPlugins: [...existingOptions.mdastPlugins ?? [], fences()]
817
+ })
818
+ }
819
+ });
820
+ }
821
+ var integration_default = likec4Integration;
164
822
 
165
823
  // src/grammar/source.ts
166
824
  import { createHash as createHash2 } from "node:crypto";
167
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
168
- import { join } from "node:path";
169
- import { fileURLToPath as fileURLToPath2 } from "node:url";
825
+ import { mkdir as mkdir3, readFile as readFile5, rename, writeFile } from "node:fs/promises";
826
+ import { join as join6 } from "node:path";
827
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
170
828
 
171
829
  // src/grammar/langium.ts
172
830
  var NOT_VALUE_DOMAINS = /Id$/;
@@ -877,9 +1535,9 @@ function derive(langium, info) {
877
1535
  };
878
1536
  }
879
1537
  async function bundled(reason, options) {
880
- const path = options.bundledLangiumPath ?? fileURLToPath2(BUNDLED_LANGIUM);
1538
+ const path = options.bundledLangiumPath ?? fileURLToPath5(BUNDLED_LANGIUM);
881
1539
  const snapshot = await bundledProvenance();
882
- const source = derive(await readFile(path, "utf8"), {
1540
+ const source = derive(await readFile5(path, "utf8"), {
883
1541
  repo: snapshot.repo ?? DEFAULT_REPO,
884
1542
  ref: snapshot.ref ?? DEFAULT_REF,
885
1543
  source: "bundled"
@@ -889,7 +1547,7 @@ async function bundled(reason, options) {
889
1547
  }
890
1548
  async function bundledProvenance() {
891
1549
  try {
892
- const { $generated } = JSON.parse(await readFile(fileURLToPath2(BUNDLED_GRAMMAR), "utf8"));
1550
+ const { $generated } = JSON.parse(await readFile5(fileURLToPath5(BUNDLED_GRAMMAR), "utf8"));
893
1551
  return { repo: $generated?.repo, ref: $generated?.ref };
894
1552
  } catch {
895
1553
  return {};
@@ -938,14 +1596,14 @@ async function fetchLangiumSource(options) {
938
1596
  return { text, etag: response.headers.get("etag") ?? undefined };
939
1597
  }
940
1598
  function cachePath(options, identity) {
941
- const dir = options.cacheDir ?? join(options.root ?? process.cwd(), "node_modules", ".cache", "astroc4");
1599
+ const dir = options.cacheDir ?? join6(options.root ?? process.cwd(), "node_modules", ".cache", "astroc4");
942
1600
  const key = createHash2("sha256").update(`${identity.repo}@${identity.ref}`).digest("hex").slice(0, 16);
943
- return join(dir, `grammar-${key}.json`);
1601
+ return join6(dir, `grammar-${key}.json`);
944
1602
  }
945
1603
  async function readCache(options, identity) {
946
1604
  let raw;
947
1605
  try {
948
- raw = await readFile(cachePath(options, identity), "utf8");
1606
+ raw = await readFile5(cachePath(options, identity), "utf8");
949
1607
  } catch {
950
1608
  return;
951
1609
  }
@@ -964,7 +1622,7 @@ async function writeCache(options, identity, entry) {
964
1622
  const path = cachePath(options, identity);
965
1623
  const temporary = `${path}.${process.pid}.tmp`;
966
1624
  try {
967
- await mkdir(join(path, ".."), { recursive: true });
1625
+ await mkdir3(join6(path, ".."), { recursive: true });
968
1626
  await writeFile(temporary, JSON.stringify({
969
1627
  version: CACHE_VERSION,
970
1628
  repo: identity.repo,
@@ -982,43 +1640,30 @@ function isTruthy(value) {
982
1640
  return typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim());
983
1641
  }
984
1642
 
985
- // src/translations.ts
986
- var translations = {
987
- en: {
988
- "likec4.view.title": "Architecture diagram",
989
- "likec4.view.description": "Interactive LikeC4 diagram for this section.",
990
- "likec4.view.hint": "Click a node in the diagram to open its details.",
991
- "likec4.view.badge": "Click any node to open the modal",
992
- "likec4.view.loading": "Loading architecture diagram…",
993
- "likec4.fence.title": "Diagram",
994
- "likec4.fence.hint": "Click any node to open its details.",
995
- "likec4.fence.loading": "Loading diagram…",
996
- "likec4.error.title": "We couldn’t load this diagram.",
997
- "likec4.error.body": "Please retry, or refresh the page if the issue persists.",
998
- "likec4.error.retry": "Retry",
999
- "likec4.error.viewIdRequired": "A LikeC4 viewId is required to render the diagram.",
1000
- "likec4.error.noProjects": "No LikeC4 projects are available in this workspace.",
1001
- "likec4.error.unknownProject": 'Unknown LikeC4 projectId "{projectId}". Available projects: {available}.',
1002
- "likec4.error.unresolvedProject": "Unable to resolve a LikeC4 project.",
1003
- "likec4.error.loadFailed": 'Unable to load LikeC4 project "{projectId}".'
1004
- }
1005
- };
1006
-
1007
1643
  // src/index.ts
1008
- var PACKAGE_NAME2 = "@purview-dev/astroc4";
1009
1644
  function starlightLikeC4(options = {}) {
1645
+ const integration = likec4Integration({
1646
+ vitePlugin: options.vitePlugin,
1647
+ projects: options.projects,
1648
+ fences: options.fences
1649
+ });
1010
1650
  return {
1011
1651
  name: PACKAGE_NAME2,
1012
1652
  hooks: {
1653
+ ...integration.hooks,
1654
+ "astro:config:setup": async (arguments_) => {
1655
+ warnIfMiswired(arguments_);
1656
+ await integration.hooks["astro:config:setup"]?.(arguments_);
1657
+ },
1013
1658
  "i18n:setup"({ injectTranslations }) {
1014
1659
  injectTranslations(translations);
1015
1660
  },
1016
1661
  async "config:setup"({ config, astroConfig, updateConfig, addIntegration, logger }) {
1017
- addIntegration(likec4Integration(options));
1662
+ addIntegration(integration);
1018
1663
  if (options.highlighting === false)
1019
1664
  return;
1020
1665
  await injectHighlighting({
1021
- root: fileURLToPath3(astroConfig.root),
1666
+ root: fileURLToPath6(astroConfig.root),
1022
1667
  expressiveCode: config.expressiveCode,
1023
1668
  updateConfig,
1024
1669
  logger,
@@ -1028,6 +1673,12 @@ function starlightLikeC4(options = {}) {
1028
1673
  }
1029
1674
  };
1030
1675
  }
1676
+ function warnIfMiswired({ config, logger }) {
1677
+ const starlight = config.integrations.some((integration) => integration.name === "@astrojs/starlight");
1678
+ if (!starlight)
1679
+ return;
1680
+ logger.warn(`${PACKAGE_NAME2} is in \`integrations\`, so this Starlight site gets diagrams only. ` + "Move astroc4() into `starlight({ plugins: [astroc4()] })` to get the translated UI and LikeC4 syntax highlighting.");
1681
+ }
1031
1682
  async function injectHighlighting({
1032
1683
  root,
1033
1684
  expressiveCode,
@@ -1074,52 +1725,11 @@ function isLikeC4Language(entry) {
1074
1725
  function isObject(value) {
1075
1726
  return typeof value === "object" && value !== null && !Array.isArray(value);
1076
1727
  }
1077
- function likec4Integration({ vitePlugin = {}, fences = true }) {
1078
- return {
1079
- name: `${PACKAGE_NAME2}/integration`,
1080
- hooks: {
1081
- "astro:config:setup": async ({ command, config, updateConfig, logger }) => {
1082
- updateConfig({
1083
- vite: {
1084
- plugins: [LikeC4VitePlugin(vitePlugin)],
1085
- ssr: { noExternal: [PACKAGE_NAME2] }
1086
- }
1087
- });
1088
- if (fences) {
1089
- await registerFences(config, updateConfig, logger);
1090
- }
1091
- if (command === "build") {
1092
- await preloadLikeC4();
1093
- }
1094
- }
1095
- }
1096
- };
1097
- }
1098
- async function registerFences(config, updateConfig, logger) {
1099
- const { satteri, isSatteriProcessor } = await import("@astrojs/markdown-satteri");
1100
- const existing = config.markdown?.processor;
1101
- if (existing === undefined) {
1102
- updateConfig({ markdown: { processor: satteri({ mdastPlugins: [likec4Fences()] }) } });
1103
- return;
1104
- }
1105
- if (!isSatteriProcessor(existing)) {
1106
- logger.warn("```likec4 fences are disabled: the site is not using the Sätteri Markdown processor. " + "<LikeC4View /> still works.");
1107
- return;
1108
- }
1109
- const existingOptions = existing.options ?? {};
1110
- updateConfig({
1111
- markdown: {
1112
- processor: satteri({
1113
- ...existingOptions,
1114
- mdastPlugins: [...existingOptions.mdastPlugins ?? [], likec4Fences()]
1115
- })
1116
- }
1117
- });
1118
- }
1119
1728
  export {
1120
1729
  starlightLikeC4 as default,
1730
+ missingOptimizedDependencies,
1121
1731
  withLikeC4Language
1122
1732
  };
1123
1733
 
1124
- //# debugId=BA2D1DBED70A424964756E2164756E21
1734
+ //# debugId=E908D3218F3E812A64756E2164756E21
1125
1735
  //# sourceMappingURL=index.js.map