diff --git a/.yarn/plugins/plugin-prepare.cjs b/.yarn/plugins/plugin-prepare.cjs index d7484f4e4..52a8087ea 100644 --- a/.yarn/plugins/plugin-prepare.cjs +++ b/.yarn/plugins/plugin-prepare.cjs @@ -1,6 +1,8 @@ const fs = require("node:fs"); const path = require("node:path"); +const root = path.join(__dirname, "../../"); + function setupVscode() { const settingsFile = path.join(__dirname, "../../.vscode/settings.json"); @@ -27,7 +29,11 @@ module.exports = { name: "plugin-prepare", factory: () => ({ hooks: { - async afterAllInstalled() { + async afterAllInstalled({ cwd }) { + if (cwd !== root) { + return; + } + setupVscode(); try { await buildBabelCodeFrameForTest(); diff --git a/scripts/build-website.js b/scripts/build-website.js index 226f6b55a..79a804c20 100644 --- a/scripts/build-website.js +++ b/scripts/build-website.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -import fs from "node:fs/promises"; +import assert from "node:assert/strict"; import path from "node:path"; import url from "node:url"; import esbuild from "esbuild"; @@ -13,58 +13,61 @@ import { PROJECT_ROOT, WEBSITE_DIR, writeFile, - writeJson, } from "./utilities/index.js"; const runYarn = (command, args, options) => spawn("yarn", [command, ...args], { stdio: "inherit", ...options }); + const IS_PULL_REQUEST = process.env.PULL_REQUEST === "true"; -const PACKAGES_DIRECTORY = IS_PULL_REQUEST - ? DIST_DIR - : url.fileURLToPath(new URL("../node_modules", import.meta.url)); -const PLAYGROUND_LIB_DIRECTORY = path.join(WEBSITE_DIR, "static/lib"); +const NODE_MODULES_DIR = url.fileURLToPath( + new URL("../node_modules", import.meta.url), +); async function writeScript(file, code) { const { code: minified } = await esbuild.transform(code, { loader: "js", minify: true, }); - await writeFile(path.join(PLAYGROUND_LIB_DIRECTORY, file), minified.trim()); + await writeFile(file, minified.trim()); } -async function buildPrettier() { - // --- Build prettier for PR --- - const packageJsonFile = path.join(PROJECT_ROOT, "package.json"); - const packageJsonContent = await fs.readFile(packageJsonFile); - const packageJson = JSON.parse(packageJsonContent); - await writeJson(packageJsonFile, { - ...packageJson, - version: `999.999.999-pr.${process.env.REVIEW_ID}`, - }); +/** @param {"stable" | "next"} version */ +async function buildPlaygroundFiles(version) { + assert.ok(version === "stable" || version === "next"); - try { - await runYarn("build", ["--clean", "--playground"], { - cwd: PROJECT_ROOT, - }); - } finally { - // restore - await writeFile(packageJsonFile, packageJsonContent); + let packagesDirectory; + const versionData = { name: version }; + const libDirectory = path.join(WEBSITE_DIR, `static/lib/${version}/`); + + if (version === "stable") { + packagesDirectory = NODE_MODULES_DIR; + const { version: prettierVersion } = await import( + url.pathToFileURL(path.join(packagesDirectory, "prettier/standalone.mjs")) + ); + versionData.version = prettierVersion; + } + + if (version === "next") { + packagesDirectory = DIST_DIR; + versionData.gitTree = await getGitTreeInformation(); + if (IS_PULL_REQUEST) { + versionData.pr = process.env.REVIEW_ID; + } } -} -async function buildPlaygroundFiles() { const pluginFiles = []; // Builtin plugins for (const fileName of await fastGlob(["plugins/*.mjs"], { - cwd: path.join(PACKAGES_DIRECTORY, "prettier"), + cwd: path.join(packagesDirectory, "prettier"), + ignore: ["plugins/flow*"], })) { pluginFiles.push(`prettier/${fileName}`); } // TODO: Support stable version // External plugins - if (IS_PULL_REQUEST) { + if (version === "next") { for (const pluginName of ["plugin-hermes"]) { pluginFiles.push(`${pluginName}/index.mjs`); } @@ -74,6 +77,7 @@ async function buildPlaygroundFiles() { } const packageManifest = { + version: versionData, prettier: { file: "prettier/standalone.mjs", }, @@ -82,7 +86,7 @@ async function buildPlaygroundFiles() { const plugin = { file }; const pluginModule = await import( - url.pathToFileURL(path.join(PACKAGES_DIRECTORY, file)) + url.pathToFileURL(path.join(packagesDirectory, file)) ); for (const property of ["languages", "options", "defaultOptions"]) { @@ -107,24 +111,46 @@ async function buildPlaygroundFiles() { await Promise.all([ ...[packageManifest.prettier, ...packageManifest.plugins].map(({ file }) => copyFile( - path.join(PACKAGES_DIRECTORY, file), - path.join(PLAYGROUND_LIB_DIRECTORY, file), + path.join(packagesDirectory, file), + path.join(libDirectory, file), ), ), writeScript( - "package-manifest.mjs", + path.join(libDirectory, "package-manifest.mjs"), `export default ${serialize(packageManifest, { space: 2 })};`, ), ]); } -if (IS_PULL_REQUEST) { - console.log("Building prettier..."); - await buildPrettier(); +async function getGitTreeInformation() { + const { stdout: branch } = await spawn("git", [ + "rev-parse", + "--abbrev-ref", + "HEAD", + ]); + + const { stdout: diff } = await spawn("git", ["diff", "--name-only"]); + + if (diff) { + return { branch }; + } + + const { stdout: commit } = await spawn("git", [ + "rev-parse", + "--short", + "HEAD", + ]); + return { branch, commit }; } -console.log("Preparing files for playground..."); -await buildPlaygroundFiles(); +console.log("Building prettier..."); +await runYarn("build", ["--clean", "--playground"], { cwd: PROJECT_ROOT }); + +console.log("Preparing files for playground (stable)..."); +await buildPlaygroundFiles("stable"); + +console.log("Preparing files for playground (next)..."); +await buildPlaygroundFiles("next"); // --- Site --- console.log("Installing website dependencies..."); diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 3c304ccef..1e9a61ba3 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -77,6 +77,8 @@ const config = { path: "../docs", sidebarCollapsed: false, breadcrumbs: false, + lastVersion: + process.env.PULL_REQUEST === "true" ? "current" : undefined, versions: { current: { label: "next", diff --git a/website/playground/Playground.jsx b/website/playground/Playground.jsx index 53a762d45..2b46238c8 100644 --- a/website/playground/Playground.jsx +++ b/website/playground/Playground.jsx @@ -162,7 +162,7 @@ function setup(props) { const getMarkdown = ({ formatted, reformatted, full, doc }) => { const { content, options } = state; - const { availableOptions, version } = props; + const { availableOptions } = props; const orderedOptions = orderOptions(availableOptions, [ ...ENABLED_OPTIONS, "rangeStart", @@ -176,7 +176,7 @@ function setup(props) { output: formatted, output2: reformatted, doc, - version, + version: props.version, url: window.location.href, options, cliOptions, @@ -547,7 +547,7 @@ const Playground = { name: "Playground", props: { availableOptions: { type: Array, required: true }, - version: { type: String, required: true }, + version: { type: Object, required: true }, }, setup, }; diff --git a/website/playground/PrettierFormat.js b/website/playground/PrettierFormat.js index b2815a816..21bc77b68 100644 --- a/website/playground/PrettierFormat.js +++ b/website/playground/PrettierFormat.js @@ -1,6 +1,7 @@ import { onMounted, reactive, toRaw, watch } from "vue"; import { settings } from "./composables/playground-settings.js"; import { worker } from "./composables/prettier-worker.js"; +import { version } from "./composables/use-version.js"; function setup(props, { slots }) { const state = reactive({ formatted: "", debug: {} }); @@ -14,6 +15,7 @@ function setup(props, { slots }) { props.code, toRaw(props.options), toRaw(settings), + version.value, ); Object.assign(state, result); @@ -24,6 +26,7 @@ function setup(props, { slots }) { onMounted(componentDidMount); watch(() => props.code, format); watch(() => props.options, format, { deep: true }); + watch(() => version.value, format); // Should not trigger format when these changes const ignoredKeys = new Set(["showSidebar", "showInput"]); diff --git a/website/playground/composables/prettier-worker.js b/website/playground/composables/prettier-worker.js index 90f92dd56..a58284a72 100644 --- a/website/playground/composables/prettier-worker.js +++ b/website/playground/composables/prettier-worker.js @@ -2,14 +2,20 @@ @import {PlaygroundSettings} from "./playground-settings.js" @import {FormatMessage, MetaMessage} from "../../static/worker.mjs" */ - class WorkerApi { - #worker = new Worker("/worker.mjs", { type: "module" }); + #workers = {}; #counter = 0; #handlers = {}; - constructor() { - const worker = this.#worker; + #initWorker(version) { + if (this.#workers[version]) { + return this.#workers[version]; + } + + const worker = new Worker(`/worker.mjs?version=${version}`, { + type: "module", + }); + this.#workers[version] = worker; const handlers = this.#handlers; worker.addEventListener("message", (event) => { @@ -28,20 +34,25 @@ class WorkerApi { resolve(message); } }); + + return worker; } #postMessage(message) { + const worker = this.#initWorker(message.version); const uid = ++this.#counter; - const worker = this.#worker; return new Promise((resolve, reject) => { this.#handlers[uid] = [resolve, reject]; worker.postMessage({ uid, message }); }); } - getMetadata() { + /** + * @param {MetaMessage["version"]} version + */ + getMetadata(version) { /** @type {MetaMessage} */ - const message = { type: "meta" }; + const message = { type: "meta", version }; return this.#postMessage(message); } @@ -49,10 +60,11 @@ class WorkerApi { * @param {FormatMessage["code"]} code * @param {FormatMessage["options"]} options * @param {FormatMessage["settings"]} settings + * @param {FormatMessage["version"]} version */ - format(code, options, settings) { + format(code, options, settings, version) { /** @type {FormatMessage} */ - const message = { type: "format", code, options, settings }; + const message = { type: "format", version, code, options, settings }; return this.#postMessage(message); } } diff --git a/website/playground/composables/use-version.js b/website/playground/composables/use-version.js new file mode 100644 index 000000000..31c924af9 --- /dev/null +++ b/website/playground/composables/use-version.js @@ -0,0 +1,50 @@ +import { ref } from "vue"; + +const ALLOWED_VERSIONS = new Set(["next", "stable"]); +const IS_PULL_REQUEST = import.meta.env.VITE_IS_PR; +const DEFAULT_VERSION = IS_PULL_REQUEST ? "next" : "stable"; +const DOCS_VERSION_KEY = "docs-preferred-version-default"; + +function getVersion() { + const versionInUrl = new URLSearchParams(window.location.search).get( + "version", + ); + + if (ALLOWED_VERSIONS.has(versionInUrl)) { + return versionInUrl; + } + + let docsVersion = localStorage.getItem(DOCS_VERSION_KEY); + if (docsVersion === "current") { + docsVersion = "next"; + } + + if (ALLOWED_VERSIONS.has(docsVersion)) { + return docsVersion; + } + + return DEFAULT_VERSION; +} + +function setVersion(value) { + value = ALLOWED_VERSIONS.has(value) ? value : DEFAULT_VERSION; + + // Save to localStorage, so the docs can respond to version change + localStorage.setItem(DOCS_VERSION_KEY, value === "next" ? "current" : value); + + const url = new URL(window.location); + if (!IS_PULL_REQUEST && value === DEFAULT_VERSION) { + url.searchParams.delete("version"); + } else { + url.searchParams.set("version", value); + } + + window.history.replaceState({}, "", url); + + version.value = value; +} + +const version = ref(undefined); +setVersion(getVersion()); + +export { setVersion, version }; diff --git a/website/playground/header.vue b/website/playground/header.vue index b873cb7d0..28d8c821a 100644 --- a/website/playground/header.vue +++ b/website/playground/header.vue @@ -5,42 +5,67 @@ import VersionLink from "./version-link.vue"; const logo = "/icon.png"; -defineProps({ - version: { type: String, required: true }, +const props = defineProps({ + version: { type: Object, required: true }, }); diff --git a/website/static/prettier-plugin-doc-explorer.mjs b/website/static/prettier-plugin-doc-explorer.mjs index bf620a95c..91f715e3d 100644 --- a/website/static/prettier-plugin-doc-explorer.mjs +++ b/website/static/prettier-plugin-doc-explorer.mjs @@ -1,20 +1,8 @@ -import prettierPackageManifest from "./lib/package-manifest.mjs"; -import * as prettier from "./lib/prettier/standalone.mjs"; - -const builderNames = new Set(Object.keys(prettier.doc.builders)); - const parserName = "doc-explorer"; const languageName = "Doc explorer"; const astFormat = parserName; const expressionParserName = "__js_expression"; -async function getJsExpressionParser() { - const plugin = prettierPackageManifest.plugins.find((plugin) => - plugin.parsers?.includes(expressionParserName), - ); - const pluginModule = await import(`./lib/${plugin.file}`); - return pluginModule.parsers[expressionParserName]; -} // `Symbol.for('description')` function isSymbol(node) { @@ -32,7 +20,7 @@ function isSymbol(node) { ); } -function validateNode(node) { +function validateNode(node, builderNames) { if (!node?.type) { return; } @@ -54,7 +42,7 @@ function validateNode(node) { continue; } - validateNode(value); + validateNode(value, builderNames); } return; } @@ -67,44 +55,61 @@ function validateNode(node) { const children = Array.isArray(value) ? value : [value]; for (const child of children) { - validateNode(child); + validateNode(child, builderNames); } } } -let expressionParser; -async function validateDoc(text) { - expressionParser ??= await getJsExpressionParser(); +export function createDocExplorerPlugin( + prettier, + prettierPackageManifest, + version, +) { + const builderNames = new Set(Object.keys(prettier.doc.builders)); - const ast = expressionParser.parse(text); - if (ast.type !== "JsExpressionRoot") { - throw new Error("Invalid doc"); + async function getJsExpressionParser() { + const plugin = prettierPackageManifest.plugins.find((plugin) => + plugin.parsers?.includes(expressionParserName), + ); + const pluginModule = await import(`./lib/${version}/${plugin.file}`); + return pluginModule.parsers[expressionParserName]; } - validateNode(ast.node); + let expressionParser; + async function validateDoc(text) { + expressionParser ??= await getJsExpressionParser(); + + const ast = expressionParser.parse(text); + if (ast.type !== "JsExpressionRoot") { + throw new Error("Invalid doc"); + } + + validateNode(ast.node, builderNames); + } + + async function parse(text) { + await validateDoc(text); + + const value = new Function( + `{ ${Object.keys(prettier.doc.builders)} }`, + `const result = (${text || "''"}\n); return result;`, + )(prettier.doc.builders); + + return { value }; + } + + return { + parsers: { + [parserName]: { + parse, + astFormat, + }, + }, + printers: { + [astFormat]: { + print: ({ node }) => node.value, + }, + }, + languages: [{ name: languageName, parsers: [parserName] }], + }; } - -async function parse(text) { - await validateDoc(text); - - const value = new Function( - `{ ${Object.keys(prettier.doc.builders)} }`, - `const result = (${text || "''"}\n); return result;`, - )(prettier.doc.builders); - - return { value }; -} - -export const parsers = { - [parserName]: { - parse, - astFormat, - }, -}; -export const printers = { - [astFormat]: { - print: ({ node }) => node.value, - }, -}; - -export const languages = [{ name: languageName, parsers: [parserName] }]; diff --git a/website/static/service-worker.mjs b/website/static/service-worker.mjs index 21a6d9e34..6043dae7c 100644 --- a/website/static/service-worker.mjs +++ b/website/static/service-worker.mjs @@ -1,17 +1,25 @@ import "https://cdnjs.cloudflare.com/ajax/libs/sw-toolbox/3.6.0/sw-toolbox.js"; -import prettierPackageManifest from "./lib/package-manifest.mjs"; +import prettierPackageManifestNext from "./lib/next/package-manifest.mjs"; +import prettierPackageManifestStable from "./lib/stable/package-manifest.mjs"; const { toolbox } = self; -const packageFiles = [ - prettierPackageManifest.prettier, - ...prettierPackageManifest.plugins, -].map(({ file }) => `./lib/${file}`); + +function getPackageFiles(manifest) { + return [manifest.prettier, ...manifest.plugins].map( + ({ file }) => `./lib/${manifest.version.name}/${file}`, + ); +} + +const stableFiles = getPackageFiles(prettierPackageManifestStable); +const nextFiles = getPackageFiles(prettierPackageManifestNext); toolbox.precache([ // Scripts - "lib/package-manifest.mjs", - ...packageFiles, + "lib/stable/package-manifest.mjs", + ...stableFiles, + "lib/next/package-manifest.mjs", + ...nextFiles, "https://cdnjs.cloudflare.com/ajax/libs/sw-toolbox/3.6.0/sw-toolbox.js", // Images diff --git a/website/static/worker.mjs b/website/static/worker.mjs index b05014c3d..d08439b5a 100644 --- a/website/static/worker.mjs +++ b/website/static/worker.mjs @@ -1,22 +1,74 @@ -import prettierPackageManifest from "./lib/package-manifest.mjs"; -import * as prettier from "./lib/prettier/standalone.mjs"; -import * as prettierPluginDocExplorer from "./prettier-plugin-doc-explorer.mjs"; +import { createDocExplorerPlugin } from "./prettier-plugin-doc-explorer.mjs"; /** @import {PlaygroundSettings} from "../playground/composables/playground-settings.js" -@typedef {{ type: "meta" }} MetaMessage +@typedef {{ + type: "meta", + version: "stable" | "next", +}} MetaMessage @typedef {{ type: "format", code: string, options: any, - settings: PlaygroundSettings + settings: PlaygroundSettings, + version: "stable" | "next", }} FormatMessage */ +const workerUrl = new URL(import.meta.url); +const version = + workerUrl.searchParams.get("version") === "next" ? "next" : "stable"; + +let prettierCache; +let prettierPackageManifestCache; +let prettierPluginDocExplorerCache; +let pluginsCache; + +async function loadPrettier() { + if (!prettierCache) { + prettierCache = import(`./lib/${version}/prettier/standalone.mjs`); + } + return await prettierCache; +} + +async function loadPrettierPackageManifest() { + if (!prettierPackageManifestCache) { + prettierPackageManifestCache = import( + `./lib/${version}/package-manifest.mjs` + ).then((m) => m.default); + } + return await prettierPackageManifestCache; +} + +async function loadPrettierPluginDocExplorer() { + if (!prettierPluginDocExplorerCache) { + const prettier = await loadPrettier(); + const prettierPackageManifest = await loadPrettierPackageManifest(); + prettierPluginDocExplorerCache = createDocExplorerPlugin( + prettier, + prettierPackageManifest, + version, + ); + } + return prettierPluginDocExplorerCache; +} + +async function loadPlugins() { + if (!pluginsCache) { + const prettierPackageManifest = await loadPrettierPackageManifest(); + const prettierPluginDocExplorer = await loadPrettierPluginDocExplorer(); + pluginsCache = [ + ...prettierPackageManifest.plugins.map((plugin) => createPlugin(plugin)), + prettierPluginDocExplorer, + ]; + } + return pluginsCache; +} + const pluginLoadPromises = new Map(); async function importPlugin(plugin) { if (!pluginLoadPromises.has(plugin)) { - pluginLoadPromises.set(plugin, import(`./lib/${plugin.file}`)); + pluginLoadPromises.set(plugin, import(`./lib/${version}/${plugin.file}`)); } try { @@ -52,11 +104,6 @@ function createPlugin(pluginManifest) { return plugin; } -const plugins = [ - ...prettierPackageManifest.plugins.map((plugin) => createPlugin(plugin)), - prettierPluginDocExplorer, -]; - self.onmessage = async function (event) { self.postMessage({ uid: event.data.uid, @@ -113,13 +160,17 @@ function handleMessage(message) { } async function handleMetaMessage() { + const prettier = await loadPrettier(); + const prettierPackageManifest = await loadPrettierPackageManifest(); + + const plugins = await loadPlugins(); const supportInfo = await prettier.getSupportInfo({ plugins }); return { type: "meta", // eslint-disable-next-line unicorn/prefer-structured-clone supportInfo: JSON.parse(JSON.stringify(supportInfo)), - version: prettier.version, + version: prettierPackageManifest.version, }; } @@ -127,6 +178,8 @@ async function handleMetaMessage() { @param {FormatMessage} message */ async function handleFormatMessage(message) { + const prettier = await loadPrettier(); + const plugins = await loadPlugins(); let { options, code, settings } = message; options = { ...options, plugins }; @@ -231,6 +284,7 @@ async function handleFormatMessage(message) { @param {PlaygroundSettings} settings */ async function formatCode(text, options, settings = {}) { + const prettier = await loadPrettier(); if (options.parser === "doc-explorer") { options = { ...options, diff --git a/website/vite.config.mjs b/website/vite.config.mjs index df9826770..201bb8e07 100644 --- a/website/vite.config.mjs +++ b/website/vite.config.mjs @@ -43,6 +43,11 @@ export default defineConfig({ outDir: OUT_DIRECTORY, minify: IS_CI, }, + define: { + "import.meta.env.VITE_IS_PR": JSON.stringify( + Boolean(process.env.PULL_REQUEST), + ), + }, }); function buildCdnAlias() {