| // 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 = (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('"', '"')}"> |
| <![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>`; |
| } |