diff --git a/package-lock.json b/package-lock.json index 1484b0532..759e05790 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "gzip-size": "^5.1.1", "htmlparser2": "^4.0.0", "jsdom": "^16.7.0", + "magic-string": "^0.30.0", "mocha": "^9.2.2", "mocha-chai-jest-snapshot": "^1.1.4", "npm-run-all": "^4.1.5", @@ -8486,6 +8487,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -18884,6 +18897,15 @@ "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==", "dev": true }, + "magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", diff --git a/package.json b/package.json index 4d0fcbccc..e7d808210 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "gzip-size": "^5.1.1", "htmlparser2": "^4.0.0", "jsdom": "^16.7.0", + "magic-string": "^0.30.0", "mocha": "^9.2.2", "mocha-chai-jest-snapshot": "^1.1.4", "npm-run-all": "^4.1.5", diff --git a/scripts/build.ts b/scripts/build.ts index 2135d623c..c6de687b1 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -3,6 +3,7 @@ import rollupTypescript from '@rollup/plugin-typescript'; import CleanCSS from 'clean-css'; import fs from 'fs'; import { mkdir, readFile, readdir, rm, writeFile, } from 'fs/promises'; +import MagicString from 'magic-string'; import path from 'path'; import { rollup } from 'rollup'; import { terser as rollupTerser } from 'rollup-plugin-terser'; @@ -11,7 +12,7 @@ import { toArray } from '../src/shared/util'; import { components } from './components'; import { parallel, runTask, series } from './tasks'; import type { ComponentProto } from '../src/types'; -import type { OutputOptions, Plugin } from 'rollup'; +import type { Plugin, SourceMapInput } from 'rollup'; const SRC_DIR = path.join(__dirname, '../src/'); @@ -41,7 +42,7 @@ async function minifyCSS() { for (const id of pluginIds) { const file = path.join(SRC_DIR, `plugins/${id}/prism-${id}.css`); if (fs.existsSync(file)) { - input[`plugins/${id}/prism-${id}.css`] = file; + input[`plugins/prism-${id}.css`] = file; } } @@ -165,37 +166,43 @@ const dataToInsert = { const dataInsertPlugin: Plugin = { name: 'data-insert', async renderChunk(code, chunk) { - const placeholderPattern = /\/\*\s*(\w+)\[\s*\*\/[\s\S]*?\/\*\s*\]\s*\*\//g; + const pattern = /\/\*\s*(\w+)\[\s*\*\/[\s\S]*?\/\*\s*\]\s*\*\//g; - let result = ''; - let last = 0; + // search for placeholders + const contained = new Set(); let m; + while ((m = pattern.exec(code))) { + contained.add(m[1]); + } - while ((m = placeholderPattern.exec(code))) { - const [, name] = m; - if (name in dataToInsert) { - result += code.slice(last, m.index); - last = m.index + m[0].length; + if (contained.size === 0) { + return null; + } - const data = await dataToInsert[name as keyof typeof dataToInsert](); - result += JSON.stringify(data); + // fetch placeholder data + const dataByName: Record = {}; + for (const name of contained) { + if (name in dataToInsert) { + dataByName[name] = await dataToInsert[name as keyof typeof dataToInsert](); } else { throw new Error(`Unknown placeholder ${name} in ${chunk.fileName}`); } } - if (last < code.length) { - result += code.slice(last); - } - return result; + // replace placeholders + const str = new MagicString(code); + str.replace(pattern, (_, name: string) => { + return JSON.stringify(dataByName[name]); + }); + return toRenderedChunk(str); }, - }; const inlineRegexSourcePlugin: Plugin = { name: 'inline-regex-source', renderChunk(code) { - return code.replace( + const str = new MagicString(code); + str.replace( /\/((?:[^\n\r[\\\/]|\\.|\[(?:[^\n\r\\\]]|\\.)*\])+)\/\s*\.\s*source\b/g, (m, source: string) => { // escape backslashes @@ -219,6 +226,7 @@ const inlineRegexSourcePlugin: Plugin = { return "'" + source + "'"; } ); + return toRenderedChunk(str); }, }; @@ -234,13 +242,22 @@ const inlineRegexSourcePlugin: Plugin = { const lazyGrammarPlugin: Plugin = { name: 'lazy-grammar', renderChunk(code) { - return code.replace( + const str = new MagicString(code); + str.replace( /^(?[ \t]+)grammar: (\{[\s\S]*?^\k\})/m, (m, _, grammar: string) => `\tgrammar: () => (${grammar})` ); + return toRenderedChunk(str); }, }; +function toRenderedChunk(s: MagicString): { code: string; map: SourceMapInput } { + return { + code: s.toString(), + map: s.generateMap({ hires: true }) as SourceMapInput, + }; +} + const terserPlugin = rollupTerser({ ecma: 2015, module: true, @@ -271,28 +288,45 @@ async function buildJS() { input[`languages/prism-${id}`] = path.join(SRC_DIR, `languages/prism-${id}.ts`); } for (const id of pluginIds) { - input[`plugins/${id}/prism-${id}`] = path.join(SRC_DIR, `plugins/${id}/prism-${id}.ts`); + input[`plugins/prism-${id}`] = path.join(SRC_DIR, `plugins/${id}/prism-${id}.ts`); } - const outputOptions: OutputOptions = { - dir: 'dist', - chunkFileNames: '_chunks/[name]-[hash].js', - validate: true, - plugins: [ - lazyGrammarPlugin, - dataInsertPlugin, - inlineRegexSourcePlugin, - terserPlugin - ] - }; - let bundle; try { bundle = await rollup({ input, plugins: [rollupTypescript({ module: 'esnext' })], }); - await bundle.write(outputOptions); + + // ESM + await bundle.write({ + dir: 'dist/esm', + chunkFileNames: '_chunks/[name]-[hash].js', + validate: true, + sourcemap: 'hidden', + plugins: [ + lazyGrammarPlugin, + dataInsertPlugin, + inlineRegexSourcePlugin, + terserPlugin + ] + }); + + // CommonJS + await bundle.write({ + dir: 'dist/cjs', + chunkFileNames: '_chunks/[name]-[hash].js', + validate: true, + sourcemap: 'hidden', + format: 'cjs', + exports: 'named', + plugins: [ + lazyGrammarPlugin, + dataInsertPlugin, + inlineRegexSourcePlugin, + terserPlugin + ] + }); } finally { await bundle?.close(); } diff --git a/tsconfig.json b/tsconfig.json index d64ab40f5..fefe3a942 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */