Validate doc with __js_expression in playground (#17668)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-08-05 15:54:42 +08:00
1 parent 95ef24d1f0
commit 0864823c52
5 files changed
+124 -73

No files matched your search

-5
View File
@@ -5,8 +5,3 @@
[build.environment] [build.environment]
NODE_VERSION = "lts/*" NODE_VERSION = "lts/*"
YARN_VERSION = "1.22.22" YARN_VERSION = "1.22.22"
[[headers]]
for = "/worker.js"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-eval'"
+12 -43
View File
@@ -97,10 +97,9 @@ class Playground extends React.Component {
const codeSample = getCodeSample(options.parser); const codeSample = getCodeSample(options.parser);
const content = original.content || codeSample; const content = original.content || codeSample;
const isDocExplorer = options.parser === "doc-explorer"; const isDocExplorer = options.parser === "doc-explorer";
const needsClickForFirstRun = isDocExplorer && content !== codeSample;
const selection = {}; const selection = {};
this.state = { content, options, selection, needsClickForFirstRun }; this.state = { content, options, selection };
this.handleOptionValueChange = this.handleOptionValueChange.bind(this); this.handleOptionValueChange = this.handleOptionValueChange.bind(this);
@@ -171,10 +170,6 @@ class Playground extends React.Component {
? getCodeSample(options.parser) ? getCodeSample(options.parser)
: state.content; : state.content;
if (option.name === "parser") {
state.needsClickForFirstRun = false;
}
return { options, content }; return { options, content };
}); });
} }
@@ -251,7 +246,6 @@ class Playground extends React.Component {
<EditorState> <EditorState>
{(editorState) => ( {(editorState) => (
<PrettierFormat <PrettierFormat
enabled={!this.state.needsClickForFirstRun}
worker={worker} worker={worker}
code={content} code={content}
options={options} options={options}
@@ -465,42 +459,17 @@ class Playground extends React.Component {
/> />
) : null} ) : null}
{editorState.showOutput ? ( {editorState.showOutput ? (
this.state.needsClickForFirstRun ? ( <OutputPanel
<div className="editor disabled-output-panel"> mode={util.getCodemirrorMode(options.parser)}
<div className="explanation"> value={formatted}
<code>doc-explorer</code> involves running code ruler={options.printWidth}
provided by users. overlayStart={
</div> cursorOffset === -1 ? undefined : cursorOffset
<div className="explanation"> }
To stay on the safe side and prevent abuse, an overlayEnd={
explicit user action is required when a direct cursorOffset === -1 ? undefined : cursorOffset + 1
link to a <code>doc-explorer</code> playground is }
opened. />
</div>
<div className="explanation">
Click the button below to start the playground.
</div>
<Button
onClick={() =>
this.setState({ needsClickForFirstRun: false })
}
>
Start
</Button>
</div>
) : (
<OutputPanel
mode={util.getCodemirrorMode(options.parser)}
value={formatted}
ruler={options.printWidth}
overlayStart={
cursorOffset === -1 ? undefined : cursorOffset
}
overlayEnd={
cursorOffset === -1 ? undefined : cursorOffset + 1
}
/>
)
) : null} ) : null}
{editorState.showSecondFormat && !isDocExplorer ? ( {editorState.showSecondFormat && !isDocExplorer ? (
<OutputPanel <OutputPanel
-5
View File
@@ -31,7 +31,6 @@ export default class PrettierFormat extends React.Component {
async format() { async format() {
const { const {
enabled,
worker, worker,
code, code,
options, options,
@@ -43,10 +42,6 @@ export default class PrettierFormat extends React.Component {
rethrowEmbedErrors, rethrowEmbedErrors,
} = this.props; } = this.props;
if (!enabled) {
return;
}
const result = await worker.format(code, options, { const result = await worker.format(code, options, {
ast, ast,
preprocessedAst, preprocessedAst,
@@ -0,0 +1,110 @@
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) {
return (
node.type === "CallExpression" &&
node.callee.type === "MemberExpression" &&
!node.callee.computed &&
!node.callee.optional &&
node.callee.object.type === "Identifier" &&
node.callee.object.name === "Symbol" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "for" &&
node.arguments.length === 1 &&
node.arguments[0].type === "StringLiteral"
);
}
function validateNode(node) {
if (!node?.type) {
return;
}
if (node.type === "ObjectExpression") {
for (const property of node.properties) {
if (
property.type !== "ObjectProperty" ||
property.computed ||
property.shorthand ||
property.method
) {
throw new Error("Unexpected object property.");
}
const { value } = property;
if (isSymbol(value)) {
continue;
}
validateNode(value);
}
return;
}
if (node.type === "Identifier" && !builderNames.has(node.name)) {
throw new Error(`Unexpected identifier name '${node.name}'.`);
}
for (const value of Object.values(node)) {
const children = Array.isArray(value) ? value : [value];
for (const child of children) {
validateNode(child);
}
}
}
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);
}
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] }];
+2 -20
View File
@@ -1,5 +1,6 @@
import prettierPackageManifest from "./lib/package-manifest.mjs"; import prettierPackageManifest from "./lib/package-manifest.mjs";
import * as prettier from "./lib/prettier/standalone.mjs"; import * as prettier from "./lib/prettier/standalone.mjs";
import * as prettierPluginDocExplorer from "./prettier-plugin-doc-explorer.mjs";
const pluginLoadPromises = new Map(); const pluginLoadPromises = new Map();
async function importPlugin(plugin) { async function importPlugin(plugin) {
@@ -38,28 +39,9 @@ function createPlugin(pluginManifest) {
return plugin; return plugin;
} }
const docExplorerPlugin = {
parsers: {
"doc-explorer": {
parse: (text) =>
new Function(
`{ ${Object.keys(prettier.doc.builders)} }`,
`const result = (${text || "''"}\n); return result;`,
)(prettier.doc.builders),
astFormat: "doc-explorer",
},
},
printers: {
"doc-explorer": {
print: (path) => path.getValue(),
},
},
languages: [{ name: "doc-explorer", parsers: ["doc-explorer"] }],
};
const plugins = [ const plugins = [
...prettierPackageManifest.plugins.map((plugin) => createPlugin(plugin)), ...prettierPackageManifest.plugins.map((plugin) => createPlugin(plugin)),
docExplorerPlugin, prettierPluginDocExplorer,
]; ];
self.onmessage = async function (event) { self.onmessage = async function (event) {