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() {