| // 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 { glob } from 'glob'; |
| |
| /// 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) => { |
| // TODO(fxbug.dev/119360): reenable this |
| // 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 = (testFilesGlob) => { |
| return { |
| name: 'tests', |
| setup(build) { |
| build.onResolve({ filter: /^all-tests$/ }, () => { |
| return { path: './all-tests.js', namespace: 'tests' }; |
| }); |
| build.onLoad({ filter: /./, namespace: 'tests' }, async () => { |
| const files = await glob(testFilesGlob); |
| return { |
| contents: files.map((file) => { |
| const relPath = file.startsWith('.') ? file : `./${file}`; |
| return `import ${JSON.stringify(relPath)};\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 in the 'syntax' directory and returns them as pseudo-imports. |
| * |
| * @returns {Promise<string[]>} A promise that resolves to an array of import paths |
| * (e.g., 'syntax/cpp'). |
| */ |
| 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}`); |
| } |
| |
| /** |
| * Formats esbuild build results for machine consumption (e.g., VS Code problems). |
| * |
| * @param {Object} results - The build results. |
| * @param {import('esbuild').Message[]} [results.errors] - Array of esbuild error messages. |
| * @param {import('esbuild').Message[]} [results.warnings] - Array of esbuild warning messages. |
| * @param {string} name - The name of the build target. |
| * @returns {string} The formatted output string. |
| */ |
| 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 consumption. |
| * |
| * @param {import('esbuild').Message} msg - The esbuild message. |
| * @param {string} kind - The kind of message ('error' or 'warning'). |
| * @param {string} name - The name of the build target. |
| * @returns {string} The formatted message string. |
| */ |
| function formatMessageForMachine(msg, kind, name) { |
| // column is 0-based, vscode wants it 1-based |
| const loc = msg.location; |
| const pos = `${loc.file}(${loc.line},${loc.column + 1})`; |
| return `${pos}:${kind}:[${name}] ${msg.text}`; |
| } |
| |
| /** |
| * Configures watch mode for esbuild based on command line arguments and environment variables. |
| * |
| * @param {string} name - The name of the build target. |
| * @returns {boolean|Object} Returns an object with an `onRebuild` callback if in machine watch |
| * mode, `true` if in standard watch mode, or `false` if not in watch mode. |
| */ |
| 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; |
| } |
| } |
| |
| /** |
| * Converts esbuild results for multiple targets into a JUnit-style XML report. |
| * |
| * @param {Array<[string, { errors: esbuild.Message[], warnings: esbuild.Message[] }]>} results - |
| * The build results array of [name, result] pairs. |
| * @returns {string} The XML representation of the results. |
| */ |
| export function resultsToXML(results) { |
| const formatMessage = (kind, msg) => { |
| const tag = kind === 'error' ? 'error' : 'failure'; |
| return `<${tag} message="${msg.text.replaceAll('"', '"')}"> |
| <![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(([, { errors }]) => errors.length !== 0).length; |
| const numWarns = results.filter(([, { warnings }]) => warnings.length !== 0).length; |
| return `<testsuites> |
| <testsuite name="esbuild" tests="${results.length}" errors="${numErrs}" failures="${numWarns}"> |
| ${cases.join('\n')} |
| </testsuite> |
| </testsuites>`; |
| } |