blob: e47684ca78640125b5e000f926df40a2ed2e9a54 [file]
// 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('"', '&quot;')}">
<![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>`;
}