blob: 57d48045c1ea3bc4a9e827afcbed0fb151037731 [file] [log] [blame]
// 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 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) => {
// 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$/ }, (args) => {
return { path: './all-tests.js', namespace: 'tests' };
});
build.onLoad({ filter: /./, namespace: 'tests' }, async (args) => {
const files = await allTests(testFilesGlob);
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,
};
});
},
}
};
/// updates package.json's configuration section with feature flags
/// in src/all-feature-flags.ts
export async function generateFlags() {
// first, we load the flags definition file...
const flagsTS = await fs.readFile('./src/all-feature-flags.ts', 'utf8');
// ... and compile it so that we can run it...
const flagsJS = await esbuild.transform(
flagsTS,
{
loader: 'ts',
platform: 'node',
target: 'node18',
format: 'cjs',
});
// ... then we run it to get access to it!
const scr = new vm.Script(flagsJS.code);
const modRes = {
module: {exports: {}},
require: (_name) => { throw new Error('flags file should not have imports'); },
};
const ctx = vm.createContext(modRes);
scr.runInContext(ctx);
// now we just extract the flags, inject them into package.json, and write out the result
const flags = modRes.module.exports.features;
const pkgJSON = JSON.parse(await fs.readFile('./package.json', 'utf8'));
updatePackageJSON(pkgJSON, flags);
await fs.writeFile('./package.json', JSON.stringify(pkgJSON, null, 2 /* 2-space indent */));
}
/// replaces feature flags section in the package.json structure
function updatePackageJSON(pkgJSON, flags) {
// find the right spot
const targetSection = pkgJSON.contributes.configuration.find((section) => section.id === 'fuchsia.features');
if (!targetSection) {
throw new Error('unable to find the "fuchsia.features" section of configuration in package.json');
}
// reset to empty
targetSection.properties = {};
// and add in our flags
for (const [name, flag] of Object.entries(flags)) {
targetSection.properties[`fuchsia.features.${name}`] = {
scope: flag.scope ?? "window",
type: "string",
enum: ["enabled", "disabled", "auto"],
markdownEnumDescriptions: [
"(force) enable this feature",
"(force) disable this feature",
"defer to `#fuchsia.featurePreview#`",
],
markdownDeprecationMessage: flag.deprecationMessage,
default: "auto",
description: flag.description,
}
}
}
/// 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(testFilesGlob) {
return new Promise((resolve, reject) => {
glob(testFilesGlob, (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.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(([_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>`;
}