Validate doc with __js_expression in playground (#17668)
This commit is contained in:
1 parent
95ef24d1f0
commit
0864823c52
5 files changed
+124
-73
No files matched your search
@@ -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'"
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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] }];
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in new issue
Block a user