| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- #!/usr/bin/env node
- /**
- * Write the TextMate grammars the viewer needs into `dist/textmate/`.
- *
- * Shiki carries about 700 grammars, 11 MB of JSON. The engine indexes about 40
- * languages. Shipping the other 660 to every user of a code-intelligence CLI is
- * not a trade worth making, so `@shikijs/langs` stays a devDependency and this
- * step copies out exactly the closure the viewer can reach: every grammar named
- * in `src/ui-server/highlight/languages.ts`, plus every grammar those embed
- * (`vue` needs html, css, typescript, json and four Vue-specific ones before it
- * will highlight a single-file component).
- *
- * Run from `npm run build`, after `tsc`, because the language table is read
- * from the compiled `dist/ui-server/highlight/languages.js` rather than being
- * duplicated here — one source of truth for what ships.
- *
- * Output:
- * dist/textmate/manifest.json grammar id -> files to load, deps first
- * dist/textmate/<name>.json one TextMate grammar, verbatim
- */
- import { createRequire } from 'node:module';
- import * as fs from 'node:fs';
- import * as path from 'node:path';
- import { fileURLToPath } from 'node:url';
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
- const OUT = path.join(ROOT, 'dist', 'textmate');
- const require = createRequire(import.meta.url);
- function fail(message) {
- process.stderr.write(`[prune-grammars] ${message}\n`);
- process.exit(1);
- }
- const languagesModule = path.join(ROOT, 'dist', 'ui-server', 'highlight', 'languages.js');
- if (!fs.existsSync(languagesModule)) {
- fail(`${path.relative(ROOT, languagesModule)} is missing — run tsc before this script.`);
- }
- const { REQUIRED_GRAMMARS } = require(languagesModule);
- if (!Array.isArray(REQUIRED_GRAMMARS) || REQUIRED_GRAMMARS.length === 0) {
- fail('REQUIRED_GRAMMARS is empty — the language table did not compile as expected.');
- }
- const shikiVersion = JSON.parse(
- fs.readFileSync(path.join(ROOT, 'node_modules', '@shikijs', 'langs', 'package.json'), 'utf-8')
- ).version;
- /**
- * Load one Shiki language module and return its registrations.
- *
- * The default export is already the flattened chain — embedded grammars first,
- * the language itself last — which is exactly the order Shiki's registry needs
- * to resolve `embeddedLangs`. Keeping that order is the whole reason the
- * manifest stores a list rather than a single filename.
- */
- async function loadChain(id) {
- const mod = await import(`@shikijs/langs/${id}`);
- const chain = mod.default;
- if (!Array.isArray(chain) || chain.length === 0) {
- fail(`@shikijs/langs/${id} did not export a grammar array.`);
- }
- return chain;
- }
- fs.rmSync(OUT, { recursive: true, force: true });
- fs.mkdirSync(OUT, { recursive: true });
- const manifest = { shikiVersion, languages: {} };
- const written = new Map();
- let bytes = 0;
- for (const id of REQUIRED_GRAMMARS) {
- let chain;
- try {
- chain = await loadChain(id);
- } catch (err) {
- fail(`could not load the ${id} grammar: ${err?.message ?? err}`);
- }
- const files = [];
- for (const grammar of chain) {
- // A chain can name the same dependency more than once (Vue reaches
- // JavaScript four different ways). Registering it twice is wasted work and
- // a confusing manifest; the FIRST occurrence is the one that keeps the
- // dependencies-before-dependents ordering intact.
- // `name` is the grammar's own id and is unique across the bundle, so two
- // languages that embed html write (and share) exactly one html.json.
- const file = grammar.name;
- if (typeof file !== 'string' || !/^[\w.+-]+$/.test(file)) {
- fail(`the ${id} chain contains a grammar with an unusable name: ${JSON.stringify(file)}`);
- }
- if (files.includes(file)) continue;
- if (!written.has(file)) {
- const json = JSON.stringify(grammar);
- fs.writeFileSync(path.join(OUT, `${file}.json`), json);
- written.set(file, json.length);
- bytes += json.length;
- }
- files.push(file);
- }
- manifest.languages[id] = files;
- }
- fs.writeFileSync(path.join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2));
- process.stdout.write(
- `[prune-grammars] ${REQUIRED_GRAMMARS.length} languages -> ${written.size} grammars, ` +
- `${(bytes / 1024 / 1024).toFixed(1)} MB in dist/textmate (shiki ${shikiVersion})\n`
- );
|