Build: Switch to esbuild (backport #12052 and #10006) (#12055)

This commit is contained in:
fisker Cheung authored and GitHub committed 2022-01-14 15:58:13 +08:00
1 parent 6ccc462409
commit cc6856ae20
26 files changed
+832 -1158

No files matched your search

-12
View File
@@ -23,17 +23,6 @@ jobs:
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Cache Build Results
id: build-cache
uses: actions/cache@v2.1.7
with:
path: .cache
key: v2-build-cache-${{ hashFiles('yarn.lock') }}-${{ hashFiles('scripts/build/**/*') }}-${{ github.ref }}-
restore-keys: |
v2-build-cache-${{ hashFiles('yarn.lock') }}-${{ hashFiles('scripts/build/**/*') }}-${{ github.ref }}-
v2-build-cache-${{ hashFiles('yarn.lock') }}-${{ hashFiles('scripts/build/**/*') }}-refs/heads/${{ github.base_ref }}-
v2-build-cache-${{ hashFiles('yarn.lock') }}-${{ hashFiles('scripts/build/**/*') }}-refs/heads/main-
- name: Build Package
run: yarn build
@@ -45,7 +34,6 @@ jobs:
# This step calls `git reset`
# It should be the last step
# The cache step might saving the result of main branch, need investigate
- name: Check Sizes
if: ${{ github.event_name == 'pull_request' && startsWith(github.head_ref, 'dependabot/npm_and_yarn/') }}
uses: preactjs/compressed-size-action@2.3.0
+4
View File
@@ -93,6 +93,7 @@
"eqeqeq",
"Ericsburgh",
"Esben",
"esbuild",
"eslintignore",
"eslump",
"espree",
@@ -188,6 +189,7 @@
"mdast",
"Memberish",
"meriyah",
"metafile",
"Michał",
"Microsyntax",
"Mikael",
@@ -227,6 +229,8 @@
"onwarn",
"Oopsy",
"outdent",
"outfile",
"outro",
"overparenthesization",
"Panasenko",
"pandoc",
+9 -14
View File
@@ -91,24 +91,24 @@
"@babel/core": "7.16.7",
"@babel/preset-env": "7.16.7",
"@babel/types": "7.16.7",
"@esbuild-plugins/node-globals-polyfill": "0.1.1",
"@esbuild-plugins/node-modules-polyfill": "0.1.4",
"@glimmer/reference": "0.83.1",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-babel": "5.3.0",
"@rollup/plugin-commonjs": "21.0.1",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "13.1.3",
"@rollup/plugin-replace": "3.0.1",
"@types/estree": "0.0.50",
"@types/jest": "27.4.0",
"babel-jest": "27.4.6",
"@typescript-eslint/eslint-plugin": "5.9.0",
"babel-loader": "8.2.3",
"benchmark": "2.1.4",
"browserslist": "4.19.1",
"builtin-modules": "3.2.0",
"core-js": "3.20.2",
"cross-env": "7.0.3",
"cspell": "5.15.1",
"enquirer": "2.3.6",
"esbuild": "0.14.10",
"esbuild-plugin-browserslist": "0.4.1",
"esbuild-plugin-text-replace": "1.1.3",
"eslint": "8.6.0",
"eslint-config-prettier": "8.3.0",
"eslint-formatter-friendly": "7.0.0",
@@ -132,14 +132,9 @@
"prettier": "2.5.1",
"pretty-bytes": "5.6.0",
"rimraf": "3.0.2",
"rollup": "2.63.0",
"rollup-plugin-polyfill-node": "0.8.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-license": "2.2.0",
"snapshot-diff": "0.9.0",
"string-replace-loader": "3.1.0",
"tempy": "1.0.1",
"terser-webpack-plugin": "5.3.0",
"webpack": "5.49.0"
"tempy": "1.0.1"
},
"scripts": {
"prepublishOnly": "echo \"Error: must publish from dist/\" && exit 1",
@@ -163,7 +158,7 @@
"fix": "run-s fix:eslint fix:prettier",
"fix:eslint": "yarn lint:eslint --fix",
"fix:prettier": "yarn lint:prettier --write",
"build": "node --max-old-space-size=4096 ./scripts/build/build.mjs",
"build": "node ./scripts/build/build.mjs",
"build:website": "node ./scripts/build-website.mjs"
},
"browserslist": [
-8
View File
@@ -12,14 +12,6 @@ yarn build
## Flags
### `--no-cache` flag
Run script with `--no-cache` flag will clean up the cache directory (`.cache` in project root) before bundling files.
```sh
yarn build --no-cache
```
### `--playground`
Run script with `--playground` flag will only build files needed for the website.
+19 -31
View File
@@ -9,7 +9,6 @@ import prettyBytes from "pretty-bytes";
import rimraf from "rimraf";
import {
PROJECT_ROOT,
BUILD_CACHE_DIR,
DIST_DIR,
readJson,
writeJson,
@@ -17,7 +16,7 @@ import {
} from "../utils/index.mjs";
import bundler from "./bundler.mjs";
import bundleConfigs from "./config.mjs";
import Cache from "./cache.mjs";
import saveLicenses from "./save-licenses.mjs";
// Errors in promises should be fatal.
const loggedErrors = new Set();
@@ -29,9 +28,7 @@ process.on("unhandledRejection", (err) => {
process.exit(1);
});
const CACHE_VERSION = "v38"; // This need update when updating build scripts
const statusConfig = [
{ color: "bgYellow", text: "CACHED" },
{ color: "bgGreen", text: "DONE" },
{ color: "bgRed", text: "FAIL" },
{ color: "bgGray", text: "SKIPPED" },
@@ -58,22 +55,17 @@ function fitTerminal(input, suffix = "") {
return input;
}
async function createBundle(bundleConfig, cache, options) {
async function createBundle(bundleConfig, options) {
const { output, target, format, type } = bundleConfig;
process.stdout.write(fitTerminal(output));
try {
const { cached, skipped } = await bundler(bundleConfig, cache, options);
const { skipped } = await bundler(bundleConfig, options);
if (skipped) {
console.log(status.SKIPPED);
return;
}
if (cached) {
console.log(status.CACHED);
return;
}
const file = path.join(DIST_DIR, output);
// Files including U+FFEE can't load in Chrome Extension
@@ -143,9 +135,10 @@ async function preparePackage() {
}
async function run(params) {
const shouldUseCache = params.cache && !params.file && params.minify === null;
const shouldPreparePackage =
!params.playground && !params.file && params.minify === null;
const shouldSaveBundledPackagesLicenses = shouldPreparePackage;
let configs = bundleConfigs;
if (params.file) {
configs = configs.filter(({ output }) => output === params.file);
@@ -153,39 +146,34 @@ async function run(params) {
rimraf.sync(DIST_DIR);
}
if (!params.cache) {
rimraf.sync(BUILD_CACHE_DIR);
}
let bundleCache;
if (shouldUseCache) {
bundleCache = new Cache({
cacheDir: BUILD_CACHE_DIR,
distDir: DIST_DIR,
version: CACHE_VERSION,
});
await bundleCache.load();
const licenses = [];
if (shouldSaveBundledPackagesLicenses) {
params.onLicenseFound = (dependencies) => licenses.push(...dependencies);
}
console.log(chalk.inverse(" Building packages "));
for (const bundleConfig of configs) {
await createBundle(bundleConfig, bundleCache, params);
}
if (shouldUseCache) {
await bundleCache.save();
await createBundle(bundleConfig, params);
}
if (shouldPreparePackage) {
await preparePackage();
}
if (shouldSaveBundledPackagesLicenses) {
await saveLicenses(licenses);
} else {
console.warn(
chalk.red("Bundled packages licenses not included in `dist/LICENSE`.")
);
}
}
run(
minimist(process.argv.slice(2), {
boolean: ["cache", "playground", "print-size", "minify"],
boolean: ["playground", "print-size", "minify"],
string: ["file"],
default: { cache: true, playground: false, printSize: false, minify: null },
default: { playground: false, printSize: false, minify: null },
})
);
+118 -293
View File
@@ -1,58 +1,28 @@
import fs from "node:fs/promises";
import path from "node:path";
import fs from "node:fs";
import { rollup } from "rollup";
import webpack from "webpack";
import { nodeResolve as rollupPluginNodeResolve } from "@rollup/plugin-node-resolve";
import rollupPluginAlias from "@rollup/plugin-alias";
import rollupPluginCommonjs from "@rollup/plugin-commonjs";
import rollupPluginPolyfillNode from "rollup-plugin-polyfill-node";
import rollupPluginJson from "@rollup/plugin-json";
import rollupPluginReplace from "@rollup/plugin-replace";
import { terser as rollupPluginTerser } from "rollup-plugin-terser";
import { babel as rollupPluginBabel } from "@rollup/plugin-babel";
import WebpackPluginTerser from "terser-webpack-plugin";
import createEsmUtils from "esm-utils";
import builtinModules from "builtin-modules";
import browserslist from "browserslist";
import * as babel from "@babel/core";
import esbuild from "esbuild";
import { NodeModulesPolyfillPlugin as esbuildPluginNodeModulePolyfills } from "@esbuild-plugins/node-modules-polyfill";
import { NodeGlobalsPolyfillPlugin as esbuildPluginNodeGlobalsPolyfills } from "@esbuild-plugins/node-globals-polyfill";
import esbuildPluginTextReplace from "esbuild-plugin-text-replace";
import { resolveToEsbuildTarget } from "esbuild-plugin-browserslist";
import { PROJECT_ROOT, DIST_DIR } from "../utils/index.mjs";
import rollupPluginExecutable from "./rollup-plugins/executable.mjs";
import rollupPluginEvaluate from "./rollup-plugins/evaluate.mjs";
import rollupPluginReplaceModule from "./rollup-plugins/replace-module.mjs";
import esbuildPluginEvaluate from "./esbuild-plugins/evaluate.mjs";
import esbuildPluginReplaceModule from "./esbuild-plugins/replace-module.mjs";
import esbuildPluginLicense from "./esbuild-plugins/license.mjs";
import esbuildPluginUmd from "./esbuild-plugins/umd.mjs";
import bundles from "./config.mjs";
const { __dirname, json } = createEsmUtils(import.meta);
const packageJson = json.loadSync("../../package.json");
const entries = [
// Force using the CJS file, instead of ESM; i.e. get the file
// from `"main"` instead of `"module"` (rollup default) of package.json
{
find: "@angular/compiler/src",
replacement: path.resolve(
`${PROJECT_ROOT}/node_modules/@angular/compiler/esm2015/src`
),
},
];
function webpackNativeShims(config, modules) {
if (!config.resolve) {
config.resolve = {};
}
const { resolve } = config;
resolve.alias = resolve.alias || {};
resolve.fallback = resolve.fallback || {};
for (const module of modules) {
if (module in resolve.alias || module in resolve.fallback) {
throw new Error(`fallback/alias for "${module}" already exists.`);
}
const file = path.join(__dirname, `shims/${module}.mjs`);
if (fs.existsSync(file)) {
resolve.alias[module] = file;
} else {
resolve.fallback[module] = false;
}
}
return config;
}
const umdTarget = resolveToEsbuildTarget(
browserslist(packageJson.browserslist),
{ printUnknownTargets: false }
);
function getBabelConfig(bundle) {
const config = {
@@ -62,7 +32,7 @@ function getBabelConfig(bundle) {
},
sourceType: "unambiguous",
plugins: bundle.babelPlugins || [],
compact: bundle.type === "plugin" ? false : "auto",
compact: false,
exclude: [/\/core-js\//],
};
const targets = { node: "10" };
@@ -75,7 +45,9 @@ function getBabelConfig(bundle) {
{
targets,
exclude: [
"es.array.unscopables.flat",
"es.array.unscopables.flat-map",
"es.array.sort",
"es.promise",
"es.promise.finally",
"es.string.replace",
@@ -99,40 +71,12 @@ function getBabelConfig(bundle) {
return config;
}
function getRollupConfig(bundle) {
const config = {
input: path.join(PROJECT_ROOT, bundle.input),
onwarn(warning) {
if (
// ignore `MIXED_EXPORTS` warn
warning.code === "MIXED_EXPORTS" ||
(warning.code === "CIRCULAR_DEPENDENCY" &&
(warning.importer.startsWith("node_modules") ||
warning.importer.startsWith("\x00polyfill-node."))) ||
warning.code === "SOURCEMAP_ERROR" ||
warning.code === "THIS_IS_UNDEFINED"
) {
return;
}
// web bundle can't have external requires
if (
warning.code === "UNRESOLVED_IMPORT" &&
bundle.target === "universal"
) {
throw new Error(
`Unresolved dependency in universal bundle: ${warning.source}`
);
}
console.warn(warning);
},
external: [],
};
function* getEsbuildOptions(bundle, options) {
const replaceStrings = {
"process.env.PRETTIER_TARGET": JSON.stringify(bundle.target),
"process.env.NODE_ENV": JSON.stringify("production"),
// `tslib` exports global variables
"createExporter(root": "createExporter({}",
};
if (bundle.target === "universal") {
// We can't reference `process` in UMD bundles and this is
@@ -148,12 +92,6 @@ function getRollupConfig(bundle) {
"/prettier-security-dirname-placeholder"
);
}
Object.assign(replaceStrings, bundle.replace);
const babelConfig = { babelHelpers: "bundled", ...getBabelConfig(bundle) };
const alias = { ...bundle.alias };
alias.entries = [...entries, ...(alias.entries || [])];
const replaceModule = {};
// Replace other bundled files
@@ -171,7 +109,7 @@ function getRollupConfig(bundle) {
// Universal bundle only use version info from package.json
// Replace package.json with `{version: "{VERSION}"}`
replaceModule[path.join(PROJECT_ROOT, "package.json")] = {
code: `export default ${JSON.stringify({
code: `module.exports = ${JSON.stringify({
version: packageJson.version,
})};`,
};
@@ -186,238 +124,125 @@ function getRollupConfig(bundle) {
"src/language-markdown/parsers.js",
"src/language-yaml/parsers.js",
]) {
replaceModule[path.join(PROJECT_ROOT, file)] = {
code: "export default undefined;",
};
replaceModule[path.join(PROJECT_ROOT, file)] = { code: "" };
}
}
Object.assign(replaceModule, bundle.replaceModule);
config.plugins = [
rollupPluginReplace({
values: replaceStrings,
delimiters: ["", ""],
preventAssignment: true,
}),
rollupPluginExecutable(),
rollupPluginEvaluate(),
rollupPluginJson({
exclude: Object.keys(replaceModule)
.filter((file) => file.endsWith(".json"))
.map((file) => path.relative(PROJECT_ROOT, file)),
}),
rollupPluginAlias(alias),
rollupPluginNodeResolve({
extensions: [".js", ".json"],
preferBuiltins: bundle.target === "node",
mainFields: ["main"],
}),
rollupPluginCommonjs({
ignoreGlobal: bundle.target === "node",
ignore:
bundle.type === "plugin"
? undefined
: (id) => /\.\/parser-.*?/.test(id),
requireReturnsDefault: "preferred",
ignoreDynamicRequires: true,
ignoreTryCatch: bundle.target === "node",
...bundle.commonjs,
}),
rollupPluginReplaceModule(replaceModule),
bundle.target === "universal" && rollupPluginPolyfillNode(),
rollupPluginBabel(babelConfig),
].filter(Boolean);
if (bundle.target === "node") {
config.external.push(...builtinModules);
}
if (bundle.external) {
config.external.push(...bundle.external);
}
return config;
}
function getRollupOutputOptions(bundle, buildOptions) {
const options = {
// Avoid warning form #8797
exports: "auto",
file: path.join(DIST_DIR, bundle.output),
name: bundle.name,
plugins: [],
};
let shouldMinify = buildOptions.minify;
let shouldMinify = options.minify;
if (typeof shouldMinify !== "boolean") {
shouldMinify = bundle.minify !== false && bundle.target === "universal";
}
if (shouldMinify) {
options.plugins.push(
rollupPluginTerser({
output: {
ascii_only: true,
},
})
);
}
if (bundle.target === "node") {
options.format = "cjs";
} else if (bundle.target === "universal") {
if (!bundle.format && bundle.bundler !== "webpack") {
return [
{
...options,
format: "umd",
},
!buildOptions.playground && {
...options,
format: "esm",
file: path.join(
DIST_DIR,
`esm/${bundle.output.replace(".js", ".mjs")}`
),
},
].filter(Boolean);
}
options.format = bundle.format;
}
if (buildOptions.playground && bundle.bundler !== "webpack") {
return { skipped: true };
}
return [options];
}
function getWebpackConfig(bundle, buildOptions) {
if (bundle.type !== "plugin" || bundle.target !== "universal") {
throw new Error("Must use rollup for this bundle");
}
const config = {
mode: "production",
performance: { hints: false },
entry: path.resolve(PROJECT_ROOT, bundle.input),
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: getBabelConfig(bundle),
const esbuildOptions = {
entryPoints: [path.join(PROJECT_ROOT, bundle.input)],
bundle: true,
metafile: true,
plugins: [
bundle.target === "universal" && esbuildPluginNodeGlobalsPolyfills(),
bundle.target === "universal" && esbuildPluginNodeModulePolyfills(),
esbuildPluginEvaluate(),
esbuildPluginReplaceModule({ ...replaceModule, ...bundle.replaceModule }),
esbuildPluginTextReplace({
include: /\.[cm]?js$/,
// TODO[@fisker]: Use RegExp when possible
pattern: Object.entries({ ...replaceStrings, ...bundle.replace }),
}),
options.onLicenseFound &&
esbuildPluginLicense({
cwd: PROJECT_ROOT,
thirdParty: {
includePrivate: true,
output: options.onLicenseFound,
},
},
{
test: /\.js$/,
use: {
loader: "string-replace-loader",
options: {
multiple: Object.entries(bundle.replace).map(
([search, replace]) => ({ search, replace })
),
},
},
},
],
},
output: {
path: DIST_DIR,
filename: bundle.output,
library: {
type: "umd",
name: bundle.name.split("."),
},
// https://github.com/webpack/webpack/issues/6642
globalObject: 'new Function("return this")()',
},
optimization: {},
resolve: {
// Webpack@5 can't resolve "postcss/lib/parser" and "postcss/lib/stringifier"" imported by `postcss-scss`
// Ignore `exports` field to fix bundle script
exportsFields: [],
},
}),
].filter(Boolean),
minify: shouldMinify,
legalComments: "none",
external: [...(bundle.external || [])],
// Disable esbuild auto discover `tsconfig.json` file
tsconfig: path.join(__dirname, "empty-tsconfig.json"),
mainFields: ["main"],
target: ["node10"],
logLevel: "error",
};
let shouldMinify = buildOptions.minify;
if (typeof shouldMinify !== "boolean") {
shouldMinify = true;
}
if (bundle.target === "universal") {
esbuildOptions.target.push(...umdTarget);
if (shouldMinify) {
config.optimization.minimizer = [
new WebpackPluginTerser({
// prevent terser generate extra .LICENSE file
extractComments: false,
terserOptions: {
// prevent U+FFFE in the output
output: {
ascii_only: true,
},
},
}),
];
yield {
...esbuildOptions,
outfile: path.join(DIST_DIR, bundle.output),
plugins: [
esbuildPluginUmd({ name: bundle.name }),
...esbuildOptions.plugins,
],
format: "umd",
};
if (!bundle.format && !options.playground) {
yield {
...esbuildOptions,
outfile: path.join(
DIST_DIR,
`esm/${bundle.output.replace(".js", ".mjs")}`
),
format: "esm",
};
}
} else {
config.optimization.minimize = false;
}
esbuildOptions.external.push(
...builtinModules,
"./package.json*",
...bundles
.filter((item) => item.input !== bundle.input)
.map((item) => `./${item.output}*`)
);
return webpackNativeShims(config, ["os", "path", "util", "url", "fs"]);
yield {
...esbuildOptions,
outfile: path.join(DIST_DIR, bundle.output),
format: "cjs",
};
}
}
function runWebpack(config) {
return new Promise((resolve, reject) => {
webpack(config, (error, stats) => {
if (error) {
reject(error);
return;
}
async function runBuild(bundle, esbuildOptions) {
const { format, plugins, outfile } = esbuildOptions;
if (stats.hasErrors()) {
const { errors } = stats.toJson();
const error = new Error(errors[0].message);
error.errors = errors;
reject(error);
return;
}
await esbuild.build({
...esbuildOptions,
plugins: plugins.filter(({ name }) => name !== "umd"),
format: format === "umd" ? "cjs" : format,
minify: false,
});
if (stats.hasWarnings()) {
const { warnings } = stats.toJson();
console.warn(warnings);
}
const text = await fs.readFile(outfile);
resolve();
});
const { code } = await babel.transformAsync(text, {
filename: outfile,
...getBabelConfig(bundle),
});
await fs.writeFile(outfile, code);
await esbuild.build({
...esbuildOptions,
plugins: plugins.filter(({ name }) => name === "umd"),
entryPoints: [outfile],
allowOverwrite: true,
});
}
async function createBundle(bundle, cache, options) {
const inputOptions = getRollupConfig(bundle);
const outputOptions = getRollupOutputOptions(bundle, options);
if (!Array.isArray(outputOptions) && outputOptions.skipped) {
async function createBundle(bundle, options) {
if (
options.playground &&
(bundle.target !== "universal" || bundle.output === "doc.js")
) {
return { skipped: true };
}
if (
cache &&
(
await Promise.all(
outputOptions.map((outputOption) =>
cache.isCached(inputOptions, outputOption)
)
)
).every((cached) => cached)
) {
return { cached: true };
}
if (bundle.bundler === "webpack") {
await runWebpack(getWebpackConfig(bundle, options));
} else {
const result = await rollup(inputOptions);
await Promise.all(outputOptions.map((option) => result.write(option)));
const esbuildOptions = getEsbuildOptions(bundle, options);
for (const options of esbuildOptions) {
await runBuild(bundle, options);
}
return { bundled: true };
-160
View File
@@ -1,160 +0,0 @@
import { strict as assert } from "node:assert";
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { rollup } from "rollup";
import {
PROJECT_ROOT,
readJson,
writeJson,
copyFile,
} from "../utils/index.mjs";
class Cache {
constructor({ cacheDir, distDir, version }) {
this.cacheDir = cacheDir || required("cacheDir");
this.distDir = distDir || required("distDir");
this.manifest = path.join(cacheDir, "manifest.json");
this.version = version || required("version");
this.checksums = {};
this.files = {};
this.updated = {
version: this.version,
checksums: {},
files: {},
};
}
// Loads the manifest.json file with the information from the last build
async load() {
// This should never throw, if it does, let it fail the build
const lockfile = await fs.readFile(
path.join(PROJECT_ROOT, "yarn.lock"),
"utf8"
);
const lockfileHash = hashString(lockfile);
this.updated.checksums["yarn.lock"] = lockfileHash;
try {
const { version, checksums, files } = await readJson(this.manifest);
// Ignore the cache if the version changed
assert.strictEqual(this.version, version);
assert.ok(typeof checksums === "object");
// If yarn.lock changed, rebuild everything
assert.strictEqual(lockfileHash, checksums["yarn.lock"]);
assert.ok(typeof files === "object");
for (const modules of Object.values(files)) {
assert.ok(Array.isArray(modules));
}
this.checksums = checksums;
this.files = files;
} catch {
// noop
}
}
// Run rollup to get the list of files included in the bundle and check if
// any (or the list itself) have changed.
// This takes the same rollup config used for bundling to include files that are
// resolved by specific plugins.
async checkBundle(id, inputOptions, outputOptions) {
const files = new Set(this.files[id]);
const newFiles = (this.updated.files[id] = []);
let dirty = false;
const bundle = await rollup(getRollupConfig(inputOptions));
const { output } = await bundle.generate(outputOptions);
const modules = output
.filter((mod) => !/\0/.test(mod.facadeModuleId))
.map((mod) => [
path.relative(PROJECT_ROOT, mod.facadeModuleId),
mod.code,
]);
for (const [id, code] of modules) {
newFiles.push(id);
// If we already checked this file for another bundle, reuse the hash
if (!this.updated.checksums[id]) {
this.updated.checksums[id] = hashString(code);
}
const hash = this.updated.checksums[id];
// Check if this file changed
if (!this.checksums[id] || this.checksums[id] !== hash) {
dirty = true;
}
// Check if this file is new
if (!files.delete(id)) {
dirty = true;
}
}
// Final check: if any file was removed, `files` is not empty
return !dirty && files.size === 0;
}
async save() {
const { manifest, updated } = this;
await writeJson(manifest, updated);
const files = Object.keys(updated.files);
for (const file of files) {
await copyFile(
path.join(this.distDir, file),
path.join(this.cacheDir, "files", file)
);
}
}
async isCached(inputOptions, outputOption) {
const file = path.relative(this.distDir, outputOption.file);
const useCache = await this.checkBundle(file, inputOptions, outputOption);
if (useCache) {
try {
await copyFile(
path.join(this.cacheDir, "files", file),
path.join(this.distDir, file)
);
return true;
} catch {
// Proceed to build
}
}
return false;
}
}
function required(name) {
throw new Error(name + " is required");
}
function hashString(string) {
return crypto.createHash("md5").update(string).digest("hex");
}
function getRollupConfig(rollupConfig) {
return {
...rollupConfig,
onwarn() {},
plugins: rollupConfig.plugins.filter(
(plugin) =>
// We're not interested in dependencies, we already check `yarn.lock`
plugin.name !== "node-resolve" &&
// This is really slow, we need this "preflight" to be fast
plugin.name !== "babel"
),
};
}
export default Cache;
+11 -10
View File
@@ -1,4 +1,7 @@
import path from "node:path";
import createEsmUtils from "esm-utils";
const { require } = createEsmUtils(import.meta);
/**
* @typedef {Object} Bundle
@@ -7,8 +10,6 @@ import path from "node:path";
* @property {string?} name - name for the UMD bundle (for plugins, it'll be `prettierPlugins.${name}`)
* @property {'node' | 'universal'} target - should generate a CJS only for node or universal bundle
* @property {'core' | 'plugin'} type - it's a plugin bundle or core part of prettier
* @property {'rollup' | 'webpack'} [bundler='rollup'] - define which bundler to use
* @property {CommonJSConfig} [commonjs={}] - options for `rollup-plugin-commonjs`
* @property {string[]} external - array of paths that should not be included in the final bundle
* @property {Object.<string, string | {code: string}>} replaceModule - module replacement path or code
* @property {Object.<string, string>} replace - map of strings to replace when processing the bundle
@@ -46,6 +47,7 @@ const parsers = [
// `rollup-plugin-polyfill-node` don't have polyfill for these modules
'require("perf_hooks")': "{}",
'require("inspector")': "{}",
"_fs.realpathSync.native": "_fs.realpathSync && _fs.realpathSync.native",
// Dynamic `require()`s
"ts.sys && ts.sys.require": "false",
"require(etwModulePath)": "undefined",
@@ -66,6 +68,9 @@ const parsers = [
},
{
input: "src/language-js/parse/angular.js",
replace: {
"@angular/compiler/src": "@angular/compiler/esm2015/src",
},
},
{
input: "src/language-css/parser-postcss.js",
@@ -77,22 +82,18 @@ const parsers = [
"node.constructor.name === 'Word'": "node.type === 'word'",
},
},
{
input: "dist/parser-postcss.js",
output: "esm/parser-postcss.mjs",
format: "esm",
},
{
input: "src/language-graphql/parser-graphql.js",
},
{
input: "src/language-markdown/parser-markdown.js",
replaceModule: {
[require.resolve("parse-entities/decode-entity.browser.js")]:
require.resolve("parse-entities/decode-entity.js"),
},
},
{
input: "src/language-handlebars/parser-glimmer.js",
commonjs: {
ignore: ["source-map"],
},
},
{
input: "src/language-html/parser-html.js",
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1,22 @@
import createEsmUtils from "esm-utils";
const { require } = createEsmUtils(import.meta);
export default function esbuildPluginEvaluate() {
return {
name: "evaluate",
setup(build) {
build.onLoad({ filter: /\.evaluate\.js$/ }, ({ path }) => {
const json = JSON.stringify(require(path), (_, v) => {
if (typeof v === "function") {
throw new TypeError("Cannot evaluate functions.");
}
return v;
});
return { loader: "json", contents: json };
});
},
};
}
+24
View File
@@ -0,0 +1,24 @@
import rollupPluginLicense from "rollup-plugin-license";
export default function esbuildPluginLicense(options) {
const plugin = rollupPluginLicense(options);
return {
name: "license",
setup(build) {
build.initialOptions.metafile = true;
build.onEnd((result) => {
const files = Object.keys(result.metafile.inputs);
const chunk = {
modules: Object.fromEntries(
files.map((file) => [file, { renderedLength: 1 }])
),
};
plugin.renderChunk("", chunk);
plugin.generateBundle();
});
},
};
}
@@ -0,0 +1,34 @@
import fs from "node:fs";
export default function esbuildPluginReplaceModule(replacements = {}) {
return {
name: "replace-module",
setup(build) {
build.onLoad({ filter: /./ }, ({ path }) => {
if (!Reflect.has(replacements, path)) {
return;
}
const replacement = replacements[path];
if (typeof replacement === "string") {
return {
contents: `module.exports = require(${JSON.stringify(
replacement
)});`,
};
}
if (replacement.file) {
return {
contents: fs.readFileSync(replacement.file, "utf8"),
};
}
return {
contents: replacement.code,
};
});
},
};
}
+115
View File
@@ -0,0 +1,115 @@
import fs from "node:fs";
import camelCase from "camelcase";
import { outdent } from "outdent";
function getUmdWrapper(name, build) {
const path = name.split(".");
const { minify } = build.initialOptions;
const temporaryName = minify ? "m" : camelCase(name);
const placeholder = "/*! bundled code !*/";
let globalObjectText = [];
for (let index = 0; index < path.length; index++) {
const object = ["root", ...path.slice(0, index + 1)].join(".");
if (index === path.length - 1) {
globalObjectText.push(`${object} = factory();`);
} else {
globalObjectText.push(`${object} = ${object} || {};`);
}
}
globalObjectText = globalObjectText
.map((text) => `${" ".repeat(4)}${text}`)
.join("\n");
let wrapper = outdent`
(function (factory) {
if (typeof exports === "object" && typeof module === "object") {
module.exports = factory();
} else if (typeof define === "function" && define.amd) {
define(factory);
} else {
var root =
typeof globalThis !== "undefined"
? globalThis
: typeof global !== "undefined"
? global
: typeof self !== "undefined"
? self
: this || {};
${globalObjectText.trimStart()}
}
})(function() {
${placeholder}
return ${temporaryName};
});
`;
if (minify) {
wrapper = build.esbuild
.buildSync({ stdin: { contents: wrapper }, minify, write: false })
.outputFiles[0].text.trim();
}
const [intro, outro] = wrapper.split(placeholder);
return {
name: temporaryName,
intro,
outro,
expectedOutput: `"use strict";${minify ? "" : "\n"}var ${temporaryName}${
minify ? "=" : " = "
}`,
};
}
export default function esbuildPluginUmd({ name }) {
return {
name: "umd",
setup(build) {
const options = build.initialOptions;
const { globalName, format, outfile } = options;
if (globalName) {
throw new Error("'globalName' in options cannot be set.");
}
if (format !== "umd") {
throw new Error("'format' options must be 'umd'.");
}
if (!outfile) {
throw new Error("'outfile' options is required.");
}
const {
name: temporaryName,
intro,
outro,
expectedOutput,
} = getUmdWrapper(name, build);
options.globalName = temporaryName;
options.format = "iife";
build.onEnd(() => {
if (!fs.existsSync(outfile)) {
throw new Error(`${outfile} not exists`);
}
const text = fs.readFileSync(outfile, "utf8");
const actualOutput = text.slice(0, expectedOutput.length);
if (actualOutput !== expectedOutput) {
console.log();
console.error(outdent`
Expected output starts with:
${expectedOutput}
Got:
${actualOutput}
`);
throw new Error("Unexpected output");
}
fs.writeFileSync(outfile, intro + text.trim() + outro);
});
},
};
}
-30
View File
@@ -1,30 +0,0 @@
import createEsmUtils from "esm-utils";
const { require } = createEsmUtils(import.meta);
export default function rollupPluginEvaluate() {
return {
name: "evaluate",
transform(_text, id) {
if (!/\.evaluate\.js$/.test(id)) {
return;
}
const json = JSON.stringify(
require(id.replace(/^\0commonjs-proxy:/, "")),
(_, v) => {
if (typeof v === "function") {
throw new TypeError("Cannot evaluate functions.");
}
return v;
}
);
return {
code: `export default ${json};`,
map: { mappings: "" },
};
},
};
}
@@ -1,47 +0,0 @@
import fs from "node:fs";
import path from "node:path";
export default function rollupPluginExecutable() {
let banner;
let entry;
let file;
return {
name: "executable",
options(inputOptions) {
entry = path.resolve(inputOptions.input);
},
generateBundle(outputOptions) {
file = outputOptions.file;
},
load(id) {
if (id !== entry) {
return;
}
const source = fs.readFileSync(id, "utf-8");
const match = source.match(/^\s*(#!.*)/);
if (match) {
banner = match[1];
return (
source.slice(0, match.index) +
source.slice(match.index + banner.length)
);
}
},
renderChunk(code) {
if (banner) {
return { code: banner + "\n" + code };
}
},
writeBundle() {
if (banner && file) {
fs.chmodSync(file, 0o755 & ~process.umask());
}
},
};
}
@@ -1,19 +0,0 @@
export default function rollupPluginReplaceModule(replacements = {}) {
return {
name: "replace-module",
load(importee) {
if (!Reflect.has(replacements, importee)) {
return;
}
const replacement = replacements[importee];
if (typeof replacement === "string") {
return `export default require(${JSON.stringify(replacement)});`;
}
return replacement.code;
},
};
}
+87
View File
@@ -0,0 +1,87 @@
import path from "node:path";
import { promises as fs } from "node:fs";
import { outdent } from "outdent";
import { DIST_DIR } from "../utils/index.mjs";
const file = path.join(DIST_DIR, "LICENSE");
const separator = `\n${"-".repeat(40)}\n\n`;
async function saveLicenses(dependencies) {
// Unique by `name` and `version`
dependencies = dependencies.filter(
(dependency, index) =>
index ===
dependencies.findIndex(
({ name, version }) =>
dependency.name === name && dependency.version === version
)
);
dependencies.sort(
(dependencyA, dependencyB) =>
dependencyA.name.localeCompare(dependencyB.name) ||
dependencyA.version.localeCompare(dependencyB.version)
);
const prettierLicense = await fs.readFile(file, "utf8");
const licenses = [
...new Set(
dependencies
.filter(({ license }) => license)
.map(({ license }) => license)
),
];
const text = outdent`
# Prettier license
Prettier is released under the MIT license:
${prettierLicense.trim()}
## Licenses of bundled dependencies
The published Prettier artifact additionally contains code with the following licenses:
${licenses.join(", ")}
## Bundled dependencies
`;
const content = dependencies
.map((dependency) => {
let text = `### ${dependency.name}@v${dependency.version}\n`;
const meta = [];
if (dependency.license) {
meta.push(`License: ${dependency.license}`);
}
if (dependency.author?.name) {
meta.push(`By: ${dependency.author.name}`);
}
if (dependency.repository?.url) {
meta.push(`Repository: <${dependency.repository.url}>`);
}
if (meta.length > 0) {
text += "\n" + meta.join("\n") + "\n";
}
if (dependency.licenseText) {
text +=
"\n" +
dependency.licenseText
.trim()
.split("\n")
.map((line) => (line ? `> ${line}` : ">"))
.join("\n") +
"\n";
}
return text;
})
.join(separator);
await fs.writeFile(file, text + "\n\n" + content);
}
export default saveLicenses;
-4
View File
@@ -1,4 +0,0 @@
function assert() {}
assert.ok = function () {};
assert.strictEqual = function () {};
export default assert;
-1
View File
@@ -1 +0,0 @@
export class EventEmitter {}
-4
View File
@@ -1,4 +0,0 @@
export const existsSync = () => false;
export const readFileSync = () => "";
const fs = { existsSync, readFileSync };
export default fs;
-6
View File
@@ -1,6 +0,0 @@
const os = {
EOL: "\n",
platform: () => "browser",
cpus: () => [{ model: "Prettier" }],
};
export default os;
-2
View File
@@ -1,2 +0,0 @@
export * from "path-browserify";
export { default } from "path-browserify";
-6
View File
@@ -1,6 +0,0 @@
const tty = {
isatty() {
return false;
},
};
export default tty;
+1 -1
View File
@@ -2,7 +2,7 @@ import chalk from "chalk";
import { runYarn, logPromise, readJson } from "../utils.js";
export default async function generateBundles({ version }) {
await logPromise("Generating bundles", runYarn(["build", "--no-cache"]));
await logPromise("Generating bundles", runYarn(["build"]));
const builtPkg = await readJson("dist/package.json");
if (builtPkg.version !== version) {
-2
View File
@@ -28,14 +28,12 @@ async function writeFile(file, content) {
}
const PROJECT_ROOT = url.fileURLToPath(new URL("../../", import.meta.url));
const BUILD_CACHE_DIR = path.join(PROJECT_ROOT, ".cache");
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
const WEBSITE_DIR = path.join(PROJECT_ROOT, "website");
export {
PROJECT_ROOT,
DIST_DIR,
BUILD_CACHE_DIR,
WEBSITE_DIR,
readJson,
writeJson,
+387 -508
View File
File diff suppressed because it is too large. Load diff