@purview-dev/astroc4 1.0.0-prerelease.2 → 1.0.0-prerelease.3

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/README.md CHANGED
@@ -139,7 +139,7 @@ with the integration options, so its own languages and the plugin's coexist.
139
139
 
140
140
  ### Translating
141
141
 
142
- Add any key from `src/translations.mjs` to the site's
142
+ Add any key from `src/translations.ts` to the site's
143
143
  `src/content/i18n/<lang>.json`. Placeholders are `{name}` — single braces,
144
144
  because some are filled in by the browser, not by i18next.
145
145
 
@@ -161,7 +161,7 @@ bun run test:site # pack the tarball and build a real Starlight site from i
161
161
  bun run check # astro check over playground/ and src/
162
162
  bun run grammar:check # fail if the committed grammar snapshot is stale
163
163
  bun run build && bun run preview
164
- bun scripts/smoke.mjs http://localhost:4321 # headless: hydration, icons, highlighting, i18n
164
+ bun scripts/smoke.ts http://localhost:4321 # headless: hydration, icons, highlighting, i18n
165
165
  ```
166
166
 
167
167
  `Justfile` wraps each of those (`just verify` runs what the PR workflow runs).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purview-dev/astroc4",
3
- "version": "1.0.0-prerelease.2",
3
+ "version": "1.0.0-prerelease.3",
4
4
  "description": "Interactive LikeC4 architecture diagrams for Astro and Starlight: <LikeC4View>, ```likec4 fences, and LikeC4 syntax highlighting derived from the live Langium grammar.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -32,29 +32,37 @@
32
32
  ],
33
33
  "type": "module",
34
34
  "exports": {
35
- ".": "./src/index.mjs",
36
- "./expressive-code": "./src/expressive-code.mjs",
37
- "./grammar/likec4.tmLanguage.json": "./src/grammar/likec4.tmLanguage.json",
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./expressive-code": {
40
+ "types": "./dist/expressive-code.d.ts",
41
+ "import": "./dist/expressive-code.js"
42
+ },
43
+ "./grammar/likec4.tmLanguage.json": "./dist/likec4.tmLanguage.json",
38
44
  "./components/LikeC4View.astro": "./src/components/LikeC4View.astro",
39
45
  "./components/LikeC4Fence.astro": "./src/components/LikeC4Fence.astro"
40
46
  },
41
47
  "files": [
42
- "src",
43
- "!src/**/__tests__",
44
- "!src/**/__fixtures__"
48
+ "dist",
49
+ "src/components",
50
+ "!src/components/__tests__",
51
+ "src/translations.ts"
45
52
  ],
46
53
  "scripts": {
47
- "dev": "cd playground && astro dev",
48
- "build": "cd playground && astro build",
54
+ "build:lib": "bun scripts/build.ts",
55
+ "dev": "bun run build:lib && cd playground && astro dev",
56
+ "build": "bun run build:lib && cd playground && astro build",
49
57
  "preview": "cd playground && astro preview",
50
- "check": "cd playground && astro check",
58
+ "check": "bun run build:lib && cd playground && astro check && cd .. && bunx tsc -p tsconfig.scripts.json --noEmit",
51
59
  "test": "vitest run",
52
60
  "test:watch": "vitest",
53
61
  "test:network": "ASTROC4_NETWORK=1 vitest run src/grammar/__tests__/network.test.ts",
54
- "test:site": "bun scripts/test-site.mjs",
55
- "test:smoke": "node scripts/smoke.mjs",
56
- "grammar:sync": "bun scripts/sync-grammar.mjs",
57
- "grammar:check": "bun scripts/sync-grammar.mjs --check",
62
+ "test:site": "bun run build:lib && bun scripts/test-site.ts",
63
+ "test:smoke": "bun scripts/smoke.ts",
64
+ "grammar:sync": "bun scripts/sync-grammar.ts",
65
+ "grammar:check": "bun scripts/sync-grammar.ts --check",
58
66
  "likec4:validate": "likec4 validate playground/src/likec4"
59
67
  },
60
68
  "peerDependencies": {
@@ -4,7 +4,11 @@
4
4
  // the server and pass the islands plain strings, because `Astro.locals.t` does
5
5
  // not exist in the browser.
6
6
 
7
- import { translations } from '../translations.mjs';
7
+ // Ships alongside this file: `files` publishes `src/components` *and*
8
+ // `src/translations.ts`, so the components stay self-contained source. The
9
+ // specifier keeps its `.js` extension because that is what the emitted
10
+ // declarations carry and what the site's bundler maps back to this `.ts` file.
11
+ import { translations } from '../translations.js';
8
12
 
9
13
  export type TranslationKey = keyof (typeof translations)['en'];
10
14
  export type Translate = (key: TranslationKey) => string;
@@ -1,28 +0,0 @@
1
- // expressive-code.d.mts
2
- // path: src/expressive-code.d.mts
3
- // Types for src/expressive-code.mjs — see src/index.d.mts for why they exist.
4
-
5
- import type { LanguageRegistration } from 'shiki';
6
-
7
- /** Shiki registration for LikeC4, under `likec4` with a `c4` alias. */
8
- export declare const likec4Language: LanguageRegistration & { aliases: string[] };
9
-
10
- /**
11
- * Resolve the grammar from likec4/likec4 at the requested ref and return it as
12
- * a Shiki language registration.
13
- */
14
- export declare function loadLikeC4Language(options?: {
15
- ref?: string;
16
- repo?: string;
17
- offline?: boolean;
18
- onError?: 'fail' | 'warn' | 'silent';
19
- cacheTtl?: number;
20
- refresh?: boolean;
21
- cacheDir?: string;
22
- baseUrl?: string;
23
- timeout?: number;
24
- root?: string;
25
- }): Promise<LanguageRegistration & { aliases: string[] }>;
26
-
27
- export default likec4Language;
28
-
@@ -1,46 +0,0 @@
1
- // expressive-code.mjs
2
- // path: src/expressive-code.mjs
3
- // LikeC4 syntax highlighting for Expressive Code (Starlight's code blocks).
4
- // Shiki ships no LikeC4 grammar, so without this .c4 source renders as plain
5
- // text.
6
- //
7
- // The Starlight plugin injects this language into the site's Expressive Code
8
- // options for you (see src/index.mjs) resolved from the live Langium grammar.
9
- // The exports here cover the two cases the plugin cannot:
10
- //
11
- // - a site that already has an `ec.config.mjs`, which can add the bundled
12
- // grammar with one line:
13
- //
14
- // import { likec4Language } from '@purview-dev/astroc4/expressive-code';
15
- // export default defineEcConfig({ shiki: { langs: [likec4Language] } });
16
- //
17
- // - anything else that needs the grammar programmatically, e.g. a Markdown
18
- // pipeline of its own: `await loadLikeC4Language({ ref: 'v1.59.4' })`.
19
- //
20
- // `likec4Language` is the bundled snapshot, so it works without network
21
- // access; `loadLikeC4Language` fetches the ref you ask for.
22
-
23
- import grammar from './grammar/likec4.tmLanguage.json' with { type: 'json' };
24
- import { resolveLikeC4Grammar } from './grammar/source.mjs';
25
- import { LANGUAGE_ALIASES, LANGUAGE_NAME } from './grammar/textmate.mjs';
26
-
27
- /** @type {import('shiki').LanguageRegistration} */
28
- export const likec4Language = {
29
- ...grammar,
30
- name: LANGUAGE_NAME,
31
- aliases: [...LANGUAGE_ALIASES],
32
- };
33
-
34
- /**
35
- * Resolve the grammar from likec4/likec4 at the ref you ask for and return it
36
- * as a Shiki language registration.
37
- * @param {import('./grammar/source.mjs').GrammarSourceOptions} [options]
38
- * @returns {Promise<import('shiki').LanguageRegistration>}
39
- */
40
- export async function loadLikeC4Language(options) {
41
- const { grammar: resolved } = await resolveLikeC4Grammar(options);
42
-
43
- return { ...resolved, name: LANGUAGE_NAME, aliases: [...LANGUAGE_ALIASES] };
44
- }
45
-
46
- export default likec4Language;
package/src/fences.mjs DELETED
@@ -1,288 +0,0 @@
1
- // fences.mjs
2
- // path: src/fences.mjs
3
- // Turns a ```likec4 code fence into a rendered diagram instead of a code block.
4
- //
5
- // Two forms:
6
- //
7
- // ```likec4 view="requestPath" title="Request path"
8
- // ```
9
- // -> renders a view from the site's LikeC4 project (any likec4.config.json).
10
- //
11
- // ```likec4 title="Example"
12
- // specification { element system }
13
- // model { a = system 'A' b = system 'B' a -> b }
14
- // views { view index { include * } }
15
- // ```
16
- // -> compiles and lays out this snippet at build time, then renders it.
17
- //
18
- // Layout needs LikeC4's WASM graph engine, so it has to happen during the
19
- // build; the browser only rehydrates the resulting JSON.
20
- //
21
- // This is a Sätteri mdast plugin, not a remark one. Astro 7 made Sätteri the
22
- // default Markdown processor, and `remarkPlugins` is a legacy option that is
23
- // silently ignored unless @astrojs/markdown-remark is installed. The plugin's
24
- // integration (src/index.mjs) registers it; sites do not add it themselves.
25
-
26
- import { createHash } from 'node:crypto';
27
- import { createRequire } from 'node:module';
28
- import { fileURLToPath } from 'node:url';
29
-
30
- const require = createRequire(import.meta.url);
31
-
32
- // `likec4`'s Node API is deliberately NOT imported at module scope. It pulls in
33
- // Langium, chevrotain, an LSP and the WASM layout engine, and this module is
34
- // loaded as part of the site's astro.config.mjs — on a cold start that was
35
- // enough to make Astro report "Configuration file updated. Restarting...", and the restart
36
- // killed the content-layer transport, leaving every page a 500. Load it lazily
37
- // so the cost is paid only when a page actually contains an inline fence.
38
- let likec4ApiPromise;
39
-
40
- function loadLikeC4() {
41
- // A plain `require('likec4')` rather than `import('likec4')`: Vite's SSR
42
- // module runner intercepts dynamic `import()` from any module it loaded
43
- // (this one included) to route it through the runner's own module graph —
44
- // and on Windows that runner can already be mid-teardown by the time a
45
- // page's ```likec4 fence triggers this, crashing the whole `astro build`
46
- // (see `preloadLikeC4` below). `require()` goes through Node's own
47
- // resolver instead, which Node 22+ supports for ESM-only packages like
48
- // this one, sidestepping the runner entirely.
49
- likec4ApiPromise ??= Promise.resolve(require('likec4').LikeC4);
50
-
51
- return likec4ApiPromise;
52
- }
53
-
54
- /**
55
- * Kick off (and await) the same lazy load `loadLikeC4()` uses, so a later
56
- * call inside Satteri's processing reuses the already-resolved promise
57
- * instead of issuing a fresh `import('likec4')`.
58
- *
59
- * `astro build` on Windows crashes mid-build ("Vite module runner has been
60
- * closed", followed by a native libuv assertion that kills the process) if
61
- * that first `import('likec4')` happens while Vite is tearing down the SSR
62
- * module runner used for the content layer — which is exactly when the first
63
- * page containing a ```likec4 fence is rendered. Finishing the import before
64
- * that point removes the race. src/index.mjs calls this from
65
- * `astro:config:setup` for builds only — the module-scope comment above
66
- * `loadLikeC4` explains why eager-loading it in dev is unsafe.
67
- */
68
- export function preloadLikeC4() {
69
- return loadLikeC4();
70
- }
71
-
72
- const FENCE_LANG = 'likec4';
73
-
74
- /** Component the fence is rewritten into, per form. */
75
- const INLINE_COMPONENT = '__LikeC4Fence';
76
- const PROJECT_COMPONENT = '__LikeC4View';
77
-
78
- // Package specifiers, not relative paths: the import is written into the page
79
- // being compiled, so it has to resolve from the site, not from this file.
80
- /** The published package name; see the comment on `PACKAGE_NAME` in index.mjs. */
81
- const PACKAGE_NAME = '@purview-dev/astroc4';
82
-
83
- const IMPORTS = {
84
- [INLINE_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4Fence.astro`,
85
- [PROJECT_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4View.astro`,
86
- };
87
-
88
- /**
89
- * Compiling a snippet costs ~250ms (a Langium workspace per call) and the same
90
- * page is re-rendered on every dev edit, so key on the source: a build pays
91
- * that once per distinct snippet, not once per render.
92
- */
93
- const layoutCache = new Map();
94
-
95
- /** Parse `key="value"` / `key=value` pairs from the fence's meta string. */
96
- export function parseFenceMeta(meta) {
97
- const options = {};
98
-
99
- if (!meta) return options;
100
-
101
- const pattern = /(\w+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g;
102
- let match;
103
-
104
- while ((match = pattern.exec(meta)) !== null) {
105
- options[match[1]] = match[2] ?? match[3] ?? match[4] ?? '';
106
- }
107
-
108
- return options;
109
- }
110
-
111
- async function layoutSnippet(source) {
112
- const key = createHash('sha256').update(source).digest('hex');
113
- const cached = layoutCache.get(key);
114
-
115
- if (cached) return cached;
116
-
117
- const LikeC4 = await loadLikeC4();
118
- const likec4 = await LikeC4.fromSource(source);
119
-
120
- if (likec4.hasErrors()) {
121
- // Only the first line of each error — LikeC4's parser errors enumerate
122
- // every token it expected, which runs to hundreds of lines.
123
- const detail = likec4
124
- .getErrors()
125
- .map((error) => String(error.message).split('\n')[0].trim())
126
- .slice(0, 3)
127
- .join('; ');
128
-
129
- throw new Error(detail || 'the snippet could not be parsed');
130
- }
131
-
132
- const model = await likec4.layoutedModel();
133
- // `$data` is the plain serialisable model; `$model` is its deprecated alias.
134
- const dump = JSON.parse(JSON.stringify(model.$data));
135
-
136
- const result = { dump, viewIds: Object.keys(dump.views ?? {}) };
137
- layoutCache.set(key, result);
138
-
139
- return result;
140
- }
141
-
142
- /** ESTree for `import <local> from '<source>';`, as the MDX compiler expects. */
143
- function importNode(local, source) {
144
- return {
145
- type: 'mdxjsEsm',
146
- value: `import ${local} from ${JSON.stringify(source)};`,
147
- data: {
148
- estree: {
149
- type: 'Program',
150
- sourceType: 'module',
151
- body: [
152
- {
153
- type: 'ImportDeclaration',
154
- specifiers: [
155
- { type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name: local } },
156
- ],
157
- source: { type: 'Literal', value: source, raw: JSON.stringify(source) },
158
- attributes: [],
159
- },
160
- ],
161
- },
162
- },
163
- };
164
- }
165
-
166
- function attributes(pairs) {
167
- return Object.entries(pairs)
168
- .filter(([, value]) => value !== undefined && value !== null && value !== '')
169
- .map(([name, value]) => ({ type: 'mdxJsxAttribute', name, value: String(value) }));
170
- }
171
-
172
- function describe(fileURL) {
173
- if (!fileURL) return 'a content file';
174
-
175
- try {
176
- return fileURLToPath(fileURL);
177
- } catch {
178
- return String(fileURL);
179
- }
180
- }
181
-
182
- // region plugin
183
- /**
184
- * @returns {import('satteri').MdastPluginEntry}
185
- */
186
- export function likec4Fences() {
187
- // A factory runs once per document, so `used` is safely per-file.
188
- return (factory) => {
189
- // Astro only supports components in .mdx. Leaving the fence as a code
190
- // block is the graceful outcome, but say so — a silent code block where
191
- // a diagram was intended is easy to miss in review.
192
- if (factory.sourceFormat !== 'mdx') {
193
- if (!factory.source.includes('```' + FENCE_LANG)) return null;
194
-
195
- return {
196
- name: 'likec4-fences:warn',
197
- code(node, ctx) {
198
- if (node.lang !== FENCE_LANG) return;
199
-
200
- console.warn(
201
- `[likec4-fences] ${describe(ctx.fileURL)} has a \`\`\`${FENCE_LANG} fence, but fences only ` +
202
- 'render as diagrams in .mdx files. Rename the page to .mdx, or use <LikeC4View /> directly.',
203
- );
204
- },
205
- };
206
- }
207
-
208
- if (!factory.source.includes('```' + FENCE_LANG)) return null;
209
-
210
- const used = new Set();
211
-
212
- return {
213
- name: 'likec4-fences',
214
-
215
- async code(node, ctx) {
216
- if (node.lang !== FENCE_LANG) return;
217
-
218
- const where = `${describe(ctx.fileURL)}: \`\`\`${FENCE_LANG} fence`;
219
- const options = parseFenceMeta(node.meta);
220
- const source = (node.value ?? '').trim();
221
-
222
- const common = {
223
- title: options.title,
224
- description: options.description,
225
- height: options.height,
226
- collapsible: options.collapsible,
227
- defaultOpen: options.defaultOpen,
228
- };
229
-
230
- if (source.length === 0) {
231
- if (!options.view) {
232
- throw new Error(`${where} is empty, so it needs \`view="<id>"\` to name a view in the project model.`);
233
- }
234
-
235
- used.add(PROJECT_COMPONENT);
236
-
237
- ctx.replaceNode(node, {
238
- type: 'mdxJsxFlowElement',
239
- name: PROJECT_COMPONENT,
240
- attributes: attributes({ ...common, viewId: options.view }),
241
- children: [],
242
- });
243
-
244
- return;
245
- }
246
-
247
- let layout;
248
-
249
- try {
250
- layout = await layoutSnippet(source);
251
- } catch (error) {
252
- throw new Error(`${where} is not valid LikeC4 — ${error.message}`);
253
- }
254
-
255
- const viewId = options.view ?? layout.viewIds[0];
256
-
257
- if (!viewId || !layout.viewIds.includes(viewId)) {
258
- throw new Error(
259
- `${where} has no view "${viewId ?? '(none)'}". Declared: ${layout.viewIds.join(', ') || '(none)'}.`,
260
- );
261
- }
262
-
263
- used.add(INLINE_COMPONENT);
264
-
265
- ctx.replaceNode(node, {
266
- type: 'mdxJsxFlowElement',
267
- name: INLINE_COMPONENT,
268
- attributes: attributes({ ...common, viewId, model: JSON.stringify(layout.dump) }),
269
- children: [],
270
- });
271
- },
272
-
273
- // Imports go in once, after every fence on the page is known.
274
- after(root, ctx) {
275
- if (used.size === 0) return;
276
-
277
- ctx.insertChildAt(
278
- root,
279
- 0,
280
- [...used].map((local) => importNode(local, IMPORTS[local])),
281
- );
282
- },
283
- };
284
- };
285
- }
286
- // endregion
287
-
288
- export default likec4Fences;