From 549090a5ba72abb17a30541ba36b062d8c7aed57 Mon Sep 17 00:00:00 2001 From: Yuku Kotani Date: Thu, 17 Sep 2020 01:08:44 +0900 Subject: [PATCH] Add ESM distributions for browser (#8983) Co-authored-by: fisker --- changelog_unreleased/api/pr-8983.md | 13 +++++ docs/browser.md | 4 +- jest.config.js | 37 ++++++++++++-- scripts/build/build.js | 16 +++--- scripts/build/bundler.js | 71 ++++++++++++++++++++------- scripts/build/config.js | 8 ++- tests_integration/__tests__/bundle.js | 46 +++++++++++++++++ tests_integration/env.js | 1 + 8 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 changelog_unreleased/api/pr-8983.md create mode 100644 tests_integration/__tests__/bundle.js diff --git a/changelog_unreleased/api/pr-8983.md b/changelog_unreleased/api/pr-8983.md new file mode 100644 index 000000000..8d4578123 --- /dev/null +++ b/changelog_unreleased/api/pr-8983.md @@ -0,0 +1,13 @@ +#### ESM standalone bundles ([#8983](https://github.com/prettier/prettier/pull/8983) by [@Monchi](https://github.com/Monchi), [@fisker](https://github.com/fisker)) + +Now standalone is also bundled as ESM. It works directly on browser. + +```js +import prettier from "https://unpkg.com/prettier/esm/standalone.mjs"; +import parserGraphql from "https://unpkg.com/prettier/esm/parser-graphql.mjs"; + +prettier.format("query { }", { + parser: "graphql", + plugins: [parserGraphql], +}); +``` diff --git a/docs/browser.md b/docs/browser.md index 6f856eb8c..1766b6a44 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -33,8 +33,8 @@ Note that the [`unpkg` field](https://unpkg.com/#examples) in Prettier’s `pack ### ES Modules ```js -import prettier from "prettier/standalone"; -import parserGraphql from "prettier/parser-graphql"; +import prettier from "https://unpkg.com/prettier/esm/standalone.mjs"; +import parserGraphql from "https://unpkg.com/prettier/esm/parser-graphql.mjs"; prettier.format("query { }", { parser: "graphql", diff --git a/jest.config.js b/jest.config.js index 330cf4d54..d4b804135 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,12 +2,41 @@ const installPrettier = require("./scripts/install-prettier"); +const isProduction = process.env.NODE_ENV === "production"; const ENABLE_CODE_COVERAGE = !!process.env.ENABLE_CODE_COVERAGE; -if (process.env.NODE_ENV === "production" || process.env.INSTALL_PACKAGE) { +if (isProduction || process.env.INSTALL_PACKAGE) { process.env.PRETTIER_DIR = installPrettier(); } const { TEST_STANDALONE } = process.env; +const testPathIgnorePatterns = []; +let transform; +if (TEST_STANDALONE) { + testPathIgnorePatterns.push("/tests_integration/"); +} +if (isProduction) { + // `esm` bundles need transform + transform = { + "(?:\\.mjs|codeSamples\\.js)$": [ + "babel-jest", + { + presets: [ + [ + "@babel/env", + // Workaround for https://github.com/babel/babel/issues/11994 + { loose: true }, + ], + ], + }, + ], + }; +} else { + // Only test bundles for production + testPathIgnorePatterns.push( + "/tests_integration/__tests__/bundle.js" + ); +} + module.exports = { setupFiles: ["/tests_config/setup.js"], snapshotSerializers: [ @@ -15,9 +44,7 @@ module.exports = { "jest-snapshot-serializer-ansi", ], testRegex: "jsfmt\\.spec\\.js$|__tests__/.*\\.js$", - testPathIgnorePatterns: TEST_STANDALONE - ? ["/tests_integration/"] - : [], + testPathIgnorePatterns, collectCoverage: ENABLE_CODE_COVERAGE, collectCoverageFrom: ["/src/**/*.js", "/bin/**/*.js"], coveragePathIgnorePatterns: ["/src/document/doc-debug.js"], @@ -28,7 +55,7 @@ module.exports = { }, modulePathIgnorePatterns: ["/dist"], testEnvironment: "node", - transform: {}, + transform, watchPlugins: [ "jest-watch-typeahead/filename", "jest-watch-typeahead/testname", diff --git a/scripts/build/build.js b/scripts/build/build.js index 4eb64acdc..05bb7b25a 100644 --- a/scripts/build/build.js +++ b/scripts/build/build.js @@ -22,7 +22,7 @@ process.on("unhandledRejection", (err) => { process.exit(1); }); -const CACHE_VERSION = "v30"; // This need update when updating build scripts +const CACHE_VERSION = "v31"; // This need update when updating build scripts const CACHED = chalk.bgYellow.black(" CACHED "); const OK = chalk.bgGreen.black(" DONE "); const FAIL = chalk.bgRed.black(" FAIL "); @@ -72,15 +72,18 @@ function handleError(error) { throw error; } -async function cacheFiles() { +async function cacheFiles(cache) { // Copy built files to .cache try { await execa("rm", ["-rf", path.join(".cache", "files")]); await execa("mkdir", ["-p", path.join(".cache", "files")]); - for (const bundleConfig of bundleConfigs) { + await execa("mkdir", ["-p", path.join(".cache", "files", "esm")]); + const manifest = cache.updated; + + for (const file of Object.keys(manifest.files)) { await execa("cp", [ - path.join("dist", bundleConfig.output), - path.join(".cache", "files"), + file, + path.join(".cache", file.replace("dist", "files")), ]); } } catch (err) { @@ -107,6 +110,7 @@ async function preparePackage() { async function run(params) { await execa("rm", ["-rf", "dist"]); await execa("mkdir", ["-p", "dist"]); + await execa("mkdir", ["-p", "dist/esm"]); if (params["purge-cache"]) { await execa("rm", ["-rf", ".cache"]); @@ -120,7 +124,7 @@ async function run(params) { await createBundle(bundleConfig, bundleCache); } - await cacheFiles(); + await cacheFiles(bundleCache); await bundleCache.save(); await preparePackage(); diff --git a/scripts/build/bundler.js b/scripts/build/bundler.js index be86f766a..a2ccbb5c5 100644 --- a/scripts/build/bundler.js +++ b/scripts/build/bundler.js @@ -182,7 +182,13 @@ function getRollupConfig(bundle) { externals(bundle.externals), bundle.target === "universal" && nodeGlobals(), babel(babelConfig), - bundle.minify !== false && bundle.target === "universal" && terser(), + bundle.minify !== false && + bundle.target === "universal" && + terser({ + output: { + ascii_only: true, + }, + }), ].filter(Boolean); if (bundle.target === "node") { @@ -202,11 +208,25 @@ function getRollupOutputOptions(bundle) { if (bundle.target === "node") { options.format = "cjs"; } else if (bundle.target === "universal") { - options.format = "umd"; options.name = bundle.type === "plugin" ? `prettierPlugins.${bundle.name}` : bundle.name; + + if (!bundle.format && bundle.bundler !== "webpack") { + return [ + { + ...options, + format: "umd", + }, + { + ...options, + format: "esm", + file: `dist/esm/${bundle.output.replace(".js", ".mjs")}`, + }, + ]; + } + options.format = bundle.format; } - return options; + return [options]; } function getWebpackConfig(bundle) { @@ -261,32 +281,47 @@ function runWebpack(config) { }); } +async function checkCache(cache, inputOptions, outputOption) { + const useCache = await cache.checkBundle( + outputOption.file, + inputOptions, + outputOption + ); + + if (useCache) { + try { + await execa("cp", [ + path.join(cache.cacheDir, outputOption.file.replace("dist", "files")), + outputOption.file, + ]); + return true; + } catch (err) { + console.log(err); + // Proceed to build + } + } + + return false; +} + module.exports = async function createBundle(bundle, cache) { const inputOptions = getRollupConfig(bundle); const outputOptions = getRollupOutputOptions(bundle); - const useCache = await cache.checkBundle( - bundle.output, - inputOptions, - outputOptions + const checkCacheResults = await Promise.all( + outputOptions.map((outputOption) => + checkCache(cache, inputOptions, outputOption) + ) ); - if (useCache) { - try { - await execa("cp", [ - path.join(cache.cacheDir, "files", bundle.output), - "dist", - ]); - return { cached: true }; - } catch (err) { - // Proceed to build - } + if (checkCacheResults.every((r) => r === true)) { + return { cached: true }; } if (bundle.bundler === "webpack") { await runWebpack(getWebpackConfig(bundle)); } else { const result = await rollup(inputOptions); - await result.write(outputOptions); + await Promise.all(outputOptions.map((option) => result.write(option))); } return { bundled: true }; diff --git a/scripts/build/config.js b/scripts/build/config.js index 444ab78ae..87f46ee81 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -7,7 +7,7 @@ const path = require("path"); * @property {string} input - input of the bundle * @property {string?} output - path of the output file in the `dist/` folder * @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 UMD bundle + * @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` @@ -66,6 +66,11 @@ const parsers = [ }, }, }, + { + input: "dist/parser-postcss.js", + output: "esm/parser-postcss.mjs", + format: "esm", + }, { input: "src/language-graphql/parser-graphql.js", }, @@ -109,6 +114,7 @@ const coreBundles = [ type: "core", output: "doc.js", target: "universal", + format: "umd", minify: false, }, { diff --git a/tests_integration/__tests__/bundle.js b/tests_integration/__tests__/bundle.js new file mode 100644 index 000000000..1a2f85bab --- /dev/null +++ b/tests_integration/__tests__/bundle.js @@ -0,0 +1,46 @@ +"use strict"; + +const path = require("path"); +const globby = require("globby"); +const { projectRoot } = require("../env"); +const coreOptions = require("../../src/main/core-options"); +const codeSamples = require("../../website/playground/codeSamples").default; + +const parserNames = coreOptions.options.parser.choices.map( + ({ value }) => value +); +const distDirectory = path.join(projectRoot, "dist"); + +describe("standalone", () => { + const standalone = require(path.join(distDirectory, "standalone.js")); + const plugins = globby + .sync(["parser-*.js"], { cwd: distDirectory, absolute: true }) + .map((file) => require(file)); + + const esmStandalone = require(path.join(distDirectory, "esm/standalone.mjs")) + .default; + const esmPlugins = globby + .sync(["esm/parser-*.mjs"], { cwd: distDirectory, absolute: true }) + .map((file) => require(file).default); + + for (const parser of parserNames) { + test(parser, () => { + const input = codeSamples(parser); + const umdOutput = standalone.format(input, { + parser, + plugins, + }); + + expect(typeof input).toBe("string"); + expect(typeof umdOutput).toBe("string"); + expect(umdOutput).not.toBe(input); + + const esmOutput = esmStandalone.format(input, { + parser, + plugins: esmPlugins, + }); + + expect(esmOutput).toBe(umdOutput); + }); + } +}); diff --git a/tests_integration/env.js b/tests_integration/env.js index 23845c553..09409e3d0 100644 --- a/tests_integration/env.js +++ b/tests_integration/env.js @@ -18,6 +18,7 @@ const thirdParty = isProduction const projectRoot = path.join(__dirname, ".."); module.exports = { + isProduction, thirdParty, prettierCli, projectRoot,