Add playground for current version (#18830)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: fisker <lionkay@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-02-05 22:29:59 +08:00
1 parent efcc72d91b
commit d9bd8abfeb
17 files changed
+412 -199

No files matched your search

+2
View File
@@ -77,6 +77,8 @@ const config = {
path: "../docs",
sidebarCollapsed: false,
breadcrumbs: false,
lastVersion:
process.env.PULL_REQUEST === "true" ? "current" : undefined,
versions: {
current: {
label: "next",
+3 -3
View File
@@ -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,
};
+3
View File
@@ -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"]);
@@ -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);
}
}
@@ -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 };
+42 -17
View File
@@ -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 },
});
</script>
<template>
<header>
<header class="header">
<a href="/" class="logo-wrapper">
<img class="logo" :src="logo" alt="" />
<h1>Prettier <VersionLink :version></VersionLink></h1>
<h1>Prettier</h1>
</a>
<span class="links">
<GithubButton
href="https://github.com/prettier/prettier"
dataShowCount="true"
class="github-button"
aria-label="Star prettier on GitHub"
>GitHub</GithubButton
>
<VersionLink :version="version" />
<div class="links">
<div class="github-button">
<GithubButton
href="https://github.com/prettier/prettier"
dataShowCount="true"
aria-label="Star prettier on GitHub"
>GitHub</GithubButton
>
</div>
<ThemeToggle></ThemeToggle>
</span>
</div>
</header>
</template>
<style>
.header {
display: flex;
align-items: center;
gap: 10px;
}
.logo-wrapper {
display: flex;
align-items: center;
}
.logo-wrapper h1 {
font-size: 20px;
display: block;
font-family: inherit;
font-weight: 400;
line-height: 18px;
position: relative;
margin: 0;
}
.logo {
height: 34px;
margin-right: 10px;
}
.links {
display: flex;
gap: 8px;
align-items: flex-end;
}
.links > * {
display: flex;
}
.github-button span {
display: flex;
}
+16 -8
View File
@@ -1,26 +1,34 @@
import "./install-service-worker.js";
import { createApp, onMounted, reactive } from "vue";
import { createApp, onMounted, reactive, toRaw, watch } from "vue";
import { settings } from "./composables/playground-settings.js";
import { worker } from "./composables/prettier-worker.js";
import { setVersion, version } from "./composables/use-version.js";
import Header from "./header.vue";
import Playground from "./Playground.jsx";
import { fixPrettierVersion } from "./utilities.js";
import { formatVersion } from "./utilities.js";
const App = {
name: "App",
setup() {
const state = reactive({ loaded: false });
const state = reactive({
loaded: false,
});
const componentDidMount = async () => {
const { supportInfo, version } = await worker.getMetadata();
const updateMetadata = async () => {
const { supportInfo, version: versionData } = await worker.getMetadata(
version.value,
);
Object.assign(state, {
loaded: true,
availableOptions: supportInfo.options.map(augmentOption),
version: fixPrettierVersion(version),
version: formatVersion(versionData),
});
};
watch(() => version.value, updateMetadata);
const render = () => {
const { loaded, availableOptions, version } = state;
@@ -30,13 +38,13 @@ const App = {
return (
<>
<Header version={version}></Header>
<Header version={version} />
<Playground availableOptions={availableOptions} version={version} />
</>
);
};
onMounted(componentDidMount);
onMounted(() => updateMetadata());
return render;
},
};
+1 -1
View File
@@ -14,7 +14,7 @@ function formatMarkdown({
const isIdempotent = !output2 || output === output2;
return [
`**Prettier ${version}**`,
`**Prettier ${version.text}**`,
`[Playground link](${url})`,
optionsString === "" ? null : codeBlock(optionsString, "sh"),
"",
+4 -21
View File
@@ -56,6 +56,10 @@ header {
header {
padding: 10px;
}
.github-button {
display: none;
}
}
header a,
@@ -67,27 +71,6 @@ header a:hover {
text-decoration: none;
}
.logo-wrapper {
display: flex;
align-items: center;
}
.logo {
height: 34px;
margin-right: 10px;
}
header h1 {
font-size: 20px;
display: block;
font-family: inherit;
font-weight: 400;
line-height: 18px;
position: relative;
z-index: 9999;
margin: 0;
}
.theme-icon {
padding-block: 2px;
}
+25 -5
View File
@@ -1,9 +1,29 @@
export function fixPrettierVersion(version) {
const match = version.match(/^\d+\.\d+\.\d+-pr.(\d+)$/);
if (match) {
return `pr-${match[1]}`;
export function formatVersion(version) {
if (version.name === "stable") {
return {
text: `v${version.version}`,
title: `Prettier v${version.version}`,
link: `https://github.com/prettier/prettier/releases/tag/${version.version}`,
};
}
return version;
if (version.name === "next" && version.pr) {
return {
text: `PR #${version.pr}`,
title: `Prettier #${version.pr}`,
link: `https://github.com/prettier/prettier/pull/${version.pr}`,
};
}
const commit = version.gitTree?.commit;
return {
text: commit ?? "uncommitted changes",
title: `Prettier (${commit ?? "uncommitted"})`,
link: commit
? `https://github.com/prettier/prettier/commit/${commit}`
: "https://github.com/prettier/prettier/",
};
}
export function getDefaults(availableOptions, optionNames) {
+38 -32
View File
@@ -1,51 +1,57 @@
<script setup>
import { computed, watch, onMounted } from "vue";
import { watch, onMounted } from "vue";
import {
version as playgroundVersion,
setVersion as setPlaygroundVersion,
} from "./composables/use-version.js";
const props = defineProps({
version: { type: String, required: true },
});
const versionData = computed(() => {
const match = props.version.match(/^pr-(\d+)$/);
let href;
if (match) {
href = `pull/${match[1]}`;
} else if (/\.0$/.test(props.version)) {
href = `releases/tag/${props.version}`;
} else {
href = `blob/main/CHANGELOG.md#${props.version.replaceAll(".", "")}`;
}
return {
href,
formattedVersion: match ? `PR #${match[1]}` : `v${props.version}`,
};
version: { type: Object, required: true },
});
const updateTitle = () => {
document.title = `Prettier ${versionData.value.formattedVersion}`;
document.title = props.version.title;
};
watch(() => props.version, updateTitle);
watch(() => props.version.title, updateTitle);
onMounted(updateTitle);
</script>
<template>
<a
:href="`https://github.com/prettier/prettier/${versionData.href}`"
target="_blank"
rel="noreferrer noopener"
class="version"
>
{{ versionData.formattedVersion }}
</a>
<div class="version-wrapper">
<select
class="channel-select"
:value="playgroundVersion"
@change="
(event) => {
setPlaygroundVersion(event.target.value);
}
"
>
<option value="stable">stable</option>
<option value="next">next</option>
</select>
<a
class="version-link"
:href="version.link"
target="_blank"
rel="noopener noreferrer"
>
{{ version.text }}
</a>
</div>
</template>
<style>
.version {
font-size: 0.5em;
line-height: 0;
.version-wrapper {
display: flex;
align-items: center;
gap: 0.5em;
flex: 1;
}
.version-link {
opacity: 0.5;
}
</style>
+52 -47
View File
@@ -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] }];
+15 -7
View File
@@ -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
+66 -12
View File
@@ -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,
+5
View File
@@ -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() {