@purview-dev/astroc4 1.0.0-prerelease.1 → 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/LICENSE.md +9 -9
- package/README.md +2 -2
- package/package.json +22 -14
- package/src/components/labels.ts +5 -1
- package/src/expressive-code.d.mts +0 -28
- package/src/expressive-code.mjs +0 -46
- package/src/fences.mjs +0 -288
- package/src/grammar/langium.mjs +0 -559
- package/src/grammar/like-c4.langium +0 -1357
- package/src/grammar/likec4.tmLanguage.json +0 -327
- package/src/grammar/source.mjs +0 -410
- package/src/grammar/textmate.mjs +0 -439
- package/src/index.d.mts +0 -64
- package/src/index.mjs +0 -228
- package/src/types.d.ts +0 -12
- /package/src/{translations.mjs → translations.ts} +0 -0
package/src/grammar/source.mjs
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
// source.mjs
|
|
2
|
-
// path: src/grammar/source.mjs
|
|
3
|
-
// Resolves the LikeC4 grammar the site asked for: the Langium grammar from
|
|
4
|
-
// likec4/likec4 at a branch, tag or commit, cached on disk, reduced to a
|
|
5
|
-
// TextMate grammar by ./textmate.mjs.
|
|
6
|
-
//
|
|
7
|
-
// This runs from the site's `astro:config:setup`, so `astro dev`, `astro
|
|
8
|
-
// build`, `astro check` and `astro sync` all go through it — a bad ref or an
|
|
9
|
-
// unreadable grammar fails the command instead of quietly shipping
|
|
10
|
-
// unhighlighted code.
|
|
11
|
-
//
|
|
12
|
-
// Network policy: strict by default (`onError: 'fail'`). A *bad ref* is user
|
|
13
|
-
// error and always fails. A *network failure* may fall back to the cache from
|
|
14
|
-
// the last successful fetch, and then to the bundled snapshot, but only when
|
|
15
|
-
// `onError` says so.
|
|
16
|
-
|
|
17
|
-
import { createHash } from 'node:crypto';
|
|
18
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
19
|
-
import { join } from 'node:path';
|
|
20
|
-
import { fileURLToPath } from 'node:url';
|
|
21
|
-
|
|
22
|
-
import { parseLangium } from './langium.mjs';
|
|
23
|
-
import { buildTmLanguage } from './textmate.mjs';
|
|
24
|
-
|
|
25
|
-
/** Upstream repository and the paths the grammar is read from. */
|
|
26
|
-
export const DEFAULT_REPO = 'likec4/likec4';
|
|
27
|
-
export const DEFAULT_REF = 'main';
|
|
28
|
-
export const LANGIUM_PATH = 'packages/language-server/src/like-c4.langium';
|
|
29
|
-
export const DEFAULT_BASE_URL = 'https://raw.githubusercontent.com';
|
|
30
|
-
|
|
31
|
-
/** Shape of a cache file. Bump when the fields change. */
|
|
32
|
-
const CACHE_VERSION = 1;
|
|
33
|
-
|
|
34
|
-
const BUNDLED_GRAMMAR = new URL('./likec4.tmLanguage.json', import.meta.url);
|
|
35
|
-
const BUNDLED_LANGIUM = new URL('./like-c4.langium', import.meta.url);
|
|
36
|
-
|
|
37
|
-
/** The refs a repository actually has: branches, tags, `a1b2c3d`. */
|
|
38
|
-
const REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
39
|
-
|
|
40
|
-
/** @typedef {'fail' | 'warn' | 'silent'} OnError */
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @typedef {object} GrammarSourceOptions
|
|
44
|
-
* @property {string} [repo] Repository to read from. Defaults to likec4/likec4.
|
|
45
|
-
* @property {string} [ref] Branch, tag or commit SHA. Defaults to `main`.
|
|
46
|
-
* @property {boolean} [offline] Use the bundled snapshot, never the network.
|
|
47
|
-
* @property {OnError} [onError] What to do when the grammar cannot be fetched.
|
|
48
|
-
* @property {number} [cacheTtl] Seconds a cached fetch stays fresh.
|
|
49
|
-
* @property {boolean} [refresh] Ignore the cache and refetch.
|
|
50
|
-
* @property {string} [baseUrl] Raw file host, for mirrors and proxies.
|
|
51
|
-
* @property {number} [timeout] Request timeout in milliseconds.
|
|
52
|
-
* @property {string} [root] Site root, for the default cache directory.
|
|
53
|
-
* @property {string} [cacheDir] Where to cache, overriding the default.
|
|
54
|
-
* @property {NodeJS.ProcessEnv} [env] Environment, for `ASTROC4_*` overrides.
|
|
55
|
-
* @property {{ info?: Function, warn?: Function }} [logger] Astro's logger.
|
|
56
|
-
* @property {typeof fetch} [fetchImpl] Injected in tests.
|
|
57
|
-
* @property {string} [bundledLangiumPath] Bundled Langium text, for tests.
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {object} GrammarMeta
|
|
62
|
-
* @property {string} repo
|
|
63
|
-
* @property {string} ref
|
|
64
|
-
* @property {'network' | 'cache' | 'bundled'} source Where it came from.
|
|
65
|
-
* @property {number} fetchedAt
|
|
66
|
-
* @property {string} [etag]
|
|
67
|
-
* @property {string} sha Content hash of the Langium source.
|
|
68
|
-
* @property {string} [reason] Why the bundled snapshot was used.
|
|
69
|
-
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @typedef {object} GrammarSource
|
|
73
|
-
* @property {Record<string, unknown>} grammar The TextMate grammar for Shiki.
|
|
74
|
-
* @property {string} langium The Langium source it was derived from.
|
|
75
|
-
* @property {GrammarMeta} meta Where it came from, for logs and tests.
|
|
76
|
-
*/
|
|
77
|
-
|
|
78
|
-
/** Raised for a ref this repository cannot have — always a user error. */
|
|
79
|
-
export class GrammarRefError extends Error {
|
|
80
|
-
name = 'GrammarRefError';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Raised when the grammar could not be fetched, or came back unusable. */
|
|
84
|
-
export class GrammarFetchError extends Error {
|
|
85
|
-
name = 'GrammarFetchError';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* @param {GrammarSourceOptions} [options]
|
|
90
|
-
* @returns {Promise<GrammarSource>}
|
|
91
|
-
*/
|
|
92
|
-
export async function resolveLikeC4Grammar(options = {}) {
|
|
93
|
-
const env = options.env ?? process.env;
|
|
94
|
-
const repo = options.repo ?? DEFAULT_REPO;
|
|
95
|
-
const ref = env.ASTROC4_REF || options.ref || DEFAULT_REF;
|
|
96
|
-
const offline = options.offline ?? isTruthy(env.ASTROC4_OFFLINE);
|
|
97
|
-
const onError = options.onError ?? 'fail';
|
|
98
|
-
const logger = options.logger;
|
|
99
|
-
|
|
100
|
-
validateRef(ref);
|
|
101
|
-
|
|
102
|
-
if (offline) {
|
|
103
|
-
return bundled('offline: true', options);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const cache = await readCache(options, { repo, ref });
|
|
107
|
-
|
|
108
|
-
if (cache?.fresh && !options.refresh) {
|
|
109
|
-
return derive(cache.langium, { repo, ref, source: 'cache', fetchedAt: cache.fetchedAt, etag: cache.etag });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let fetched;
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
fetched = await fetchLangiumSource({ ...options, repo, ref, etag: cache?.etag });
|
|
116
|
-
} catch (error) {
|
|
117
|
-
if (!(error instanceof Error)) throw error;
|
|
118
|
-
|
|
119
|
-
// A bad ref will fail the same way forever: never paper over it.
|
|
120
|
-
if (error instanceof GrammarRefError) throw error;
|
|
121
|
-
|
|
122
|
-
if (onError === 'fail') throw error;
|
|
123
|
-
|
|
124
|
-
if (onError === 'warn') logger?.warn?.(`[astroc4] ${error.message}`);
|
|
125
|
-
|
|
126
|
-
if (cache) {
|
|
127
|
-
if (onError === 'warn') {
|
|
128
|
-
logger?.warn?.(`[astroc4] using the grammar cached at ${new Date(cache.fetchedAt).toISOString()}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return derive(cache.langium, {
|
|
132
|
-
repo,
|
|
133
|
-
ref,
|
|
134
|
-
source: 'cache',
|
|
135
|
-
fetchedAt: cache.fetchedAt,
|
|
136
|
-
etag: cache.etag,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return bundled(error.message, options);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if ('notModified' in fetched) {
|
|
144
|
-
if (!cache) {
|
|
145
|
-
throw new GrammarFetchError(
|
|
146
|
-
'the grammar host answered 304 for a grammar that is not cached — retry with `refresh: true`',
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
await writeCache(options, { repo, ref }, { ...cache, fresh: true });
|
|
151
|
-
|
|
152
|
-
return derive(cache.langium, { repo, ref, source: 'cache', fetchedAt: cache.fetchedAt, etag: cache.etag });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const result = derive(fetched.text, { repo, ref, source: 'network', etag: fetched.etag });
|
|
156
|
-
await writeCache(options, { repo, ref }, { ...result, langium: fetched.text, etag: fetched.etag });
|
|
157
|
-
|
|
158
|
-
return result;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* @param {string} langium
|
|
163
|
-
* @param {{ repo: string, ref: string, source: 'network' | 'cache' | 'bundled', fetchedAt?: number, etag?: string }} info
|
|
164
|
-
* @returns {GrammarSource}
|
|
165
|
-
*/
|
|
166
|
-
function derive(langium, info) {
|
|
167
|
-
let vocabulary;
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
vocabulary = parseLangium(langium);
|
|
171
|
-
} catch (cause) {
|
|
172
|
-
throw new GrammarFetchError(
|
|
173
|
-
`${info.repo}@${info.ref}: the grammar could not be read — ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
174
|
-
{ cause },
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const sha = grammarSha(langium);
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
grammar: buildTmLanguage(vocabulary, { repo: info.repo, ref: info.ref, sha }),
|
|
182
|
-
langium,
|
|
183
|
-
meta: {
|
|
184
|
-
repo: info.repo,
|
|
185
|
-
ref: info.ref,
|
|
186
|
-
source: info.source,
|
|
187
|
-
fetchedAt: info.fetchedAt ?? Date.now(),
|
|
188
|
-
etag: info.etag,
|
|
189
|
-
sha,
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* The snapshot shipped with this package, for `offline: true` and for falling
|
|
196
|
-
* back when the network is down and `onError` allows it. It is derived from
|
|
197
|
-
* the committed `like-c4.langium`, so it matches `likec4.tmLanguage.json` (the
|
|
198
|
-
* snapshot test asserts exactly that). Its provenance is the snapshot's own,
|
|
199
|
-
* not the ref that was asked for.
|
|
200
|
-
* @param {string} reason Why it is being used, surfaced in logs.
|
|
201
|
-
* @param {GrammarSourceOptions} options
|
|
202
|
-
* @returns {Promise<GrammarSource>}
|
|
203
|
-
*/
|
|
204
|
-
async function bundled(reason, options) {
|
|
205
|
-
const path = options.bundledLangiumPath ?? fileURLToPath(BUNDLED_LANGIUM);
|
|
206
|
-
const snapshot = await bundledProvenance();
|
|
207
|
-
|
|
208
|
-
const source = derive(await readFile(path, 'utf8'), {
|
|
209
|
-
repo: snapshot.repo ?? DEFAULT_REPO,
|
|
210
|
-
ref: snapshot.ref ?? DEFAULT_REF,
|
|
211
|
-
source: 'bundled',
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
source.meta.reason = reason;
|
|
215
|
-
|
|
216
|
-
return source;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** `$generated` from the shipped grammar, so logs name the real snapshot. */
|
|
220
|
-
async function bundledProvenance() {
|
|
221
|
-
try {
|
|
222
|
-
const { $generated } = JSON.parse(await readFile(fileURLToPath(BUNDLED_GRAMMAR), 'utf8'));
|
|
223
|
-
|
|
224
|
-
return { repo: $generated?.repo, ref: $generated?.ref };
|
|
225
|
-
} catch {
|
|
226
|
-
return {};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Short content hash — the identity that actually changes highlighting. */
|
|
231
|
-
export function grammarSha(source) {
|
|
232
|
-
return createHash('sha256').update(source).digest('hex').slice(0, 12);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// region refs
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Rejects anything that cannot name a ref, before it reaches the network.
|
|
239
|
-
* @param {string} ref
|
|
240
|
-
*/
|
|
241
|
-
export function validateRef(ref) {
|
|
242
|
-
if (typeof ref !== 'string' || ref.length === 0) {
|
|
243
|
-
throw new GrammarRefError('the LikeC4 grammar `ref` must be a branch, tag or commit SHA');
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!REF_PATTERN.test(ref) || ref.includes('..') || ref.endsWith('/') || ref.includes('//')) {
|
|
247
|
-
throw new GrammarRefError(
|
|
248
|
-
`"${ref}" is not a usable LikeC4 grammar ref — expected a branch, tag or commit SHA such as "main" or "v1.59.4"`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return ref;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// endregion
|
|
257
|
-
|
|
258
|
-
// region fetch
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* @param {GrammarSourceOptions & { repo: string, ref: string, etag?: string }} options
|
|
262
|
-
* @returns {Promise<{ text: string, etag?: string } | { notModified: true }>}
|
|
263
|
-
*/
|
|
264
|
-
export async function fetchLangiumSource(options) {
|
|
265
|
-
const repo = options.repo;
|
|
266
|
-
const ref = options.ref;
|
|
267
|
-
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
268
|
-
const url = `${baseUrl}/${repo}/${ref}/${LANGIUM_PATH}`;
|
|
269
|
-
const timeout = options.timeout ?? 15_000;
|
|
270
|
-
const request = options.fetchImpl ?? fetch;
|
|
271
|
-
|
|
272
|
-
/** @type {Record<string, string>} */
|
|
273
|
-
const headers = { 'user-agent': 'astroc4', accept: 'text/plain' };
|
|
274
|
-
if (options.etag) headers['if-none-match'] = options.etag;
|
|
275
|
-
|
|
276
|
-
let response;
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
response = await request(url, { headers, redirect: 'follow', signal: AbortSignal.timeout(timeout) });
|
|
280
|
-
} catch (cause) {
|
|
281
|
-
throw new GrammarFetchError(
|
|
282
|
-
`could not fetch the LikeC4 grammar from ${url} — ${cause instanceof Error ? cause.message : String(cause)}. ` +
|
|
283
|
-
'Builds are strict by default: pass `highlighting: { offline: true }` to use the bundled grammar instead.',
|
|
284
|
-
{ cause },
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (response.status === 304) return { notModified: true };
|
|
289
|
-
|
|
290
|
-
if (response.status === 404) {
|
|
291
|
-
throw new GrammarRefError(
|
|
292
|
-
`${repo} has no "${ref}", or it no longer has ${LANGIUM_PATH} (${url}). ` +
|
|
293
|
-
'Use a tag such as "v1.59.4", a commit SHA, or "main".',
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (!response.ok) {
|
|
298
|
-
throw new GrammarFetchError(
|
|
299
|
-
`${url} responded ${response.status} ${response.statusText} — set ` +
|
|
300
|
-
'`highlighting: { onError: "warn" }` to fall back to the bundled grammar.',
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const text = await response.text();
|
|
305
|
-
|
|
306
|
-
if (text.trim().length === 0) {
|
|
307
|
-
throw new GrammarFetchError(`${url} returned an empty file — the grammar is not usable`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return { text, etag: response.headers.get('etag') ?? undefined };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// endregion
|
|
314
|
-
|
|
315
|
-
// region cache
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Cache location: `node_modules/.cache/astroc4`, which every Astro project
|
|
319
|
-
* already ignores. One file per repo@ref, so switching `ref` never thrashes.
|
|
320
|
-
* @param {GrammarSourceOptions} options
|
|
321
|
-
* @param {{ repo: string, ref: string }} identity
|
|
322
|
-
*/
|
|
323
|
-
function cachePath(options, identity) {
|
|
324
|
-
const dir =
|
|
325
|
-
options.cacheDir ?? join(options.root ?? process.cwd(), 'node_modules', '.cache', 'astroc4');
|
|
326
|
-
const key = createHash('sha256').update(`${identity.repo}@${identity.ref}`).digest('hex').slice(0, 16);
|
|
327
|
-
|
|
328
|
-
return join(dir, `grammar-${key}.json`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* @param {GrammarSourceOptions} options
|
|
333
|
-
* @param {{ repo: string, ref: string }} identity
|
|
334
|
-
* @returns {Promise<{ langium: string, etag?: string, fetchedAt: number, fresh: boolean } | undefined>}
|
|
335
|
-
*/
|
|
336
|
-
async function readCache(options, identity) {
|
|
337
|
-
let raw;
|
|
338
|
-
|
|
339
|
-
try {
|
|
340
|
-
raw = await readFile(cachePath(options, identity), 'utf8');
|
|
341
|
-
} catch {
|
|
342
|
-
return undefined;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
const entry = JSON.parse(raw);
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
entry?.version !== CACHE_VERSION ||
|
|
350
|
-
entry.repo !== identity.repo ||
|
|
351
|
-
entry.ref !== identity.ref ||
|
|
352
|
-
typeof entry.langium !== 'string' ||
|
|
353
|
-
typeof entry.fetchedAt !== 'number'
|
|
354
|
-
) {
|
|
355
|
-
// A cache written by a different version of this package, or a
|
|
356
|
-
// truncated file. Refetching is cheaper than debugging it.
|
|
357
|
-
return undefined;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const ttl = options.cacheTtl ?? 3600;
|
|
361
|
-
|
|
362
|
-
return { ...entry, fresh: Date.now() - entry.fetchedAt < ttl * 1000 };
|
|
363
|
-
} catch {
|
|
364
|
-
return undefined;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Best effort: a read-only `node_modules` must not fail a build, it only means
|
|
370
|
-
* the next one has to fetch again.
|
|
371
|
-
* @param {GrammarSourceOptions} options
|
|
372
|
-
* @param {{ repo: string, ref: string }} identity
|
|
373
|
-
* @param {{ langium: string, etag?: string }} entry
|
|
374
|
-
*/
|
|
375
|
-
async function writeCache(options, identity, entry) {
|
|
376
|
-
const path = cachePath(options, identity);
|
|
377
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
await mkdir(join(path, '..'), { recursive: true });
|
|
381
|
-
await writeFile(
|
|
382
|
-
temporary,
|
|
383
|
-
JSON.stringify({
|
|
384
|
-
version: CACHE_VERSION,
|
|
385
|
-
repo: identity.repo,
|
|
386
|
-
ref: identity.ref,
|
|
387
|
-
etag: entry.etag,
|
|
388
|
-
fetchedAt: Date.now(),
|
|
389
|
-
langium: entry.langium,
|
|
390
|
-
}),
|
|
391
|
-
'utf8',
|
|
392
|
-
);
|
|
393
|
-
// Rename rather than write in place: two Astro processes can resolve the
|
|
394
|
-
// same cache entry at once during `astro check && astro build`.
|
|
395
|
-
await rename(temporary, path);
|
|
396
|
-
} catch (error) {
|
|
397
|
-
options.logger?.warn?.(
|
|
398
|
-
`[astroc4] could not cache the LikeC4 grammar in ${path} — ${error instanceof Error ? error.message : String(error)}`,
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// endregion
|
|
404
|
-
|
|
405
|
-
function isTruthy(value) {
|
|
406
|
-
return typeof value === 'string' && /^(1|true|yes|on)$/i.test(value.trim());
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export default resolveLikeC4Grammar;
|
|
410
|
-
|