| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as path from 'path'; |
| import * as vm from 'vm'; |
| import * as fs from 'fs/promises'; |
| |
| import * as esbuild from 'esbuild'; |
| import * as oniguruma from 'oniguruma'; |
| import peggy from 'peggy'; // not esm |
| import glob from 'glob'; |
| |
| /// processes peggy .pegjs files --> to js (esm) source |
| export const peggyPlugin = { |
| name: 'peggy', |
| setup(build) { |
| build.onLoad({filter: /.pegjs$/}, async (args) => { |
| let pegSource = await fs.readFile(args.path, 'utf8'); |
| return { |
| contents: peggy.generate(pegSource, {output: 'source', format: 'es'}), |
| loader: 'js', |
| } |
| }); |
| }, |
| } |
| |
| /// processes syntax-generating scripts to equivalent json files |
| export const syntaxPlugin = { |
| name: 'syntax', |
| setup(build) { |
| build.onResolve({filter: /^syntax\/.+$/}, async (args) => { |
| let lang = path.basename(args.path); |
| // resolve pseudo-imports to json file names, so that we write the |
| // correct file names, but keep the base language name around for onLoad |
| return {path: `syntax/${lang}.tmLanguage.json`, namespace: 'syntax', pluginData: {lang: lang}}; |
| }); |
| |
| build.onLoad({filter: /./, namespace: 'syntax'}, async (args) => { |
| // first, load & compile the script from ts to js |
| const srcFile = path.join("syntax", `${args.pluginData.lang}.ts`); |
| let tsContents = await fs.readFile(srcFile, 'utf8'); |
| let jsContents = await build.esbuild.transform( |
| tsContents, |
| { |
| loader: 'ts', |
| platform: 'node', |
| target: 'node18', |
| format: 'cjs', |
| }); |
| |
| // then, execute the script |
| const scr = new vm.Script(jsContents.code); |
| const modRes = { |
| module: {exports: {}}, |
| require: (name) => { |
| if (name === 'oniguruma') { |
| return oniguruma; |
| } else { |
| throw new Error(`unknown syntax script import ${name}`); |
| } |
| }, |
| }; |
| const ctx = vm.createContext(modRes); |
| scr.runInContext(ctx); |
| |
| // finally, return the results of the script (a blob of json) |
| return { |
| contents: JSON.stringify(modRes.module.exports.default), |
| loader: 'copy', |
| watchFiles: [srcFile], |
| }; |
| }); |
| } |
| }; |
| |
| /// locates and bundles all test files |
| export const testsPlugin = { |
| name: 'tests', |
| setup(build) { |
| build.onResolve({filter: /^all-tests$/}, (args) => { |
| return {path: './all-tests.js', namespace: 'tests'}; |
| }); |
| build.onLoad({filter: /./, namespace: 'tests'}, async (args) => { |
| const files = await allTests(); |
| return { |
| contents: files.map((file) => `import '${file}';\n`).join(""), |
| loader: 'js', |
| resolveDir: '.', |
| // we *may* need to make this watchDirs for adding tests, but stick |
| // with files for now |
| watchFiles: files, |
| }; |
| }); |
| }, |
| } |
| |
| /// finds all syntax files as `syntax/${name}` imports |
| export async function allSyntax() { |
| return (await fs.readdir('syntax')). |
| filter((file) => path.extname(file) === '.ts'). |
| map((file) => path.basename(file).slice(0, -3)). |
| map((name) => `syntax/${name}`); |
| } |
| |
| /// finds all tests in this project |
| function allTests() { |
| return new Promise((resolve, reject) => { |
| glob('./{src,webviews}/**/*.test.ts', (err, files) => { |
| if (err) { |
| reject(err); |
| return; |
| } |
| |
| resolve(files); |
| }); |
| }); |
| } |
| |
| /// formats output for running in vscode |
| export function formatMachineOutput({errors, warnings}, name) { |
| return [ |
| ...(errors ?? []).map((err) => formatMessageForMachine(err, 'error', name)), |
| ...(warnings ?? []).map((warning) => formatMessageForMachine(warning, 'warning', name)), |
| ].join("\n"); |
| } |
| |
| /// formats a single esbuild message for machine output |
| function formatMessageForMachine(msg, kind, name) { |
| // column is 0-based, vscode wants it 1-based |
| return `${msg.location.file}(${msg.location.line},${msg.location.column+1}):${kind}:[${name}] ${msg.text}`; |
| } |
| |
| /// makes a watch that prints out machine-readable error messages |
| export function makeWatch(name) { |
| if (process.argv.length === 3 && process.argv[2] === 'watch') { |
| const isMachine = process.env.MACHINE === 'true'; |
| if (isMachine) { |
| return { |
| onRebuild(error, result) { |
| // TODO(fxbug.dev/106861): to use this with vscode, we'd need to have |
| // `watch start` and `watch end` lines, but because builds work in |
| // parallel, this would mean that we couldn't use the built-in watch |
| // mechanism of esbuild, and would have to build something custom |
| // on the incremental API. |
| if (error) { |
| const out = formatMachineOutput(error, name); |
| if (out) console.error(out); |
| } |
| else { |
| const out = formatMachineOutput(result, name); |
| if (out) console.log(out); |
| } |
| }, |
| }; |
| } else { |
| return true; |
| } |
| } else { |
| return false; |
| } |
| } |
| |
| /// formats esbuild results as test XML |
| export function resultsToXML(results) { |
| const formatMessage = (kind, msg) => { |
| const tag = kind === 'error' ? 'error' : 'failure'; |
| return `<${tag} message="${msg.text.replace(/"/g, '"')}"> |
| <![CDATA[${esbuild.formatMessagesSync([msg], {color: false, kind})}]]> |
| </${tag}> |
| `; |
| }; |
| let cases = results. |
| map(([name, {errors, warnings}]) => { |
| return ` <testcase name="${name}" status="run" result="completed"> |
| ${errors.map((msg) => formatMessage('error', msg))} |
| ${warnings.map((msg) => formatMessage('warning', msg))} |
| </testcase>`; |
| }); |
| |
| const numErrs = results.filter(([_n, {errors}]) => errors.length !== 0).length; |
| const numWarns = results.filter(([_n, {warnings}]) => warnings.length !== 0).length; |
| return `<testsuites> |
| <testsuite name="esbuild" tests="${results.length}" errors="${numErrs}" failures="${numWarns}"> |
| ${cases.join("\n")} |
| </testsuite> |
| </testsuites>`; |
| } |