Refactor format test (#18747)
This commit is contained in:
1 parent
7ca5d83e45
commit
8fc02d3671
18 files changed
+693
-547
No files matched your search
@@ -1,3 +1,3 @@
|
||||
import runFormatTest from "./run-format-test.js";
|
||||
import { runFormatTest } from "./format-test/index.js";
|
||||
|
||||
globalThis.runFormatTest = runFormatTest;
|
||||
File renamed without changes.
@@ -0,0 +1,20 @@
|
||||
import path from "node:path";
|
||||
import createEsmUtils from "esm-utils";
|
||||
import { normalizeDirectory } from "./utilities.js";
|
||||
|
||||
const { __dirname } = createEsmUtils(import.meta);
|
||||
|
||||
export const FORMAT_SCRIPT_FILENAME = "format.test.js";
|
||||
|
||||
export const FORMAT_TEST_DIRECTORY = normalizeDirectory(
|
||||
path.join(__dirname, "../../format/"),
|
||||
);
|
||||
|
||||
export const { FULL_TEST, TEST_STANDALONE, NODE_ENV, TEST_RUNTIME } =
|
||||
process.env;
|
||||
export const isProduction = NODE_ENV === "production";
|
||||
export const BOM = "\uFEFF";
|
||||
|
||||
export const CURSOR_PLACEHOLDER = "<|>";
|
||||
export const RANGE_START_PLACEHOLDER = "<<<PRETTIER_RANGE_START>>>";
|
||||
export const RANGE_END_PLACEHOLDER = "<<<PRETTIER_RANGE_END>>>";
|
||||
+2
-4
@@ -1,4 +1,5 @@
|
||||
import { wrap as raw } from "jest-snapshot-serializer-raw";
|
||||
import { CURSOR_PLACEHOLDER } from "./constants.js";
|
||||
import visualizeEndOfLine from "./visualize-end-of-line.js";
|
||||
import visualizeRange from "./visualize-range.js";
|
||||
|
||||
@@ -50,10 +51,7 @@ function printWidthIndicator(printWidth, offset) {
|
||||
return `${before}${" ".repeat(printWidth)}| printWidth`;
|
||||
}
|
||||
|
||||
function createSnapshot(
|
||||
formatResult,
|
||||
{ parsers, formatOptions, CURSOR_PLACEHOLDER },
|
||||
) {
|
||||
function createSnapshot(formatResult, { parsers, formatOptions }) {
|
||||
let {
|
||||
inputWithCursor: input,
|
||||
outputWithCursor: output,
|
||||
+9
-12
@@ -1,8 +1,5 @@
|
||||
import path from "node:path";
|
||||
import createEsmUtils from "esm-utils";
|
||||
|
||||
const { __dirname } = createEsmUtils(import.meta);
|
||||
const fixturesDirectory = path.join(__dirname, "../format/");
|
||||
import { FORMAT_TEST_DIRECTORY } from "./constants.js";
|
||||
|
||||
// TODO: these test files need fix
|
||||
const unstableTests = new Map(
|
||||
@@ -39,7 +36,7 @@ const unstableTests = new Map(
|
||||
const [file, isUnstable = () => true] = Array.isArray(fixture)
|
||||
? fixture
|
||||
: [fixture];
|
||||
return [path.join(fixturesDirectory, file), isUnstable];
|
||||
return [path.join(FORMAT_TEST_DIRECTORY, file), isUnstable];
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -106,17 +103,17 @@ const disabledTests = new Map(
|
||||
],
|
||||
}).map(([parser, tests]) => [
|
||||
parser,
|
||||
new Set(tests.map((file) => path.join(__dirname, "../format", file))),
|
||||
new Set(tests.map((file) => path.join(FORMAT_TEST_DIRECTORY, file))),
|
||||
]),
|
||||
);
|
||||
|
||||
const isUnstable = (filename, options) =>
|
||||
unstableTests.get(filename)?.(options);
|
||||
const isUnstable = (filepath, options) =>
|
||||
unstableTests.get(filepath)?.(options);
|
||||
|
||||
const isAstUnstable = (filename, options) =>
|
||||
unstableAstTests.get(filename)?.(options);
|
||||
const isAstUnstable = (filepath, options) =>
|
||||
unstableAstTests.get(filepath)?.(options);
|
||||
|
||||
const shouldDisable = (dirnameOrFilename, parser) =>
|
||||
disabledTests.get(parser)?.has(dirnameOrFilename);
|
||||
const shouldDisable = (directoryOrFile, parser) =>
|
||||
disabledTests.get(parser)?.has(directoryOrFile);
|
||||
|
||||
export { isAstUnstable, isUnstable, shouldDisable };
|
||||
@@ -0,0 +1,81 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { FORMAT_SCRIPT_FILENAME } from "./constants.js";
|
||||
import { verifyFilename } from "./verify-fixtures.js";
|
||||
import visualizeEndOfLine from "./visualize-end-of-line.js";
|
||||
|
||||
/**
|
||||
@import {Context} from "./run-format-test.js"
|
||||
@typedef {Exclude<ReturnType<ReturnType<getFixtures>["next"]>["value"], void>} Fixture
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {Context} context
|
||||
*/
|
||||
function* getFixtures(context) {
|
||||
yield* getFiles(context);
|
||||
yield* getSnippets(context);
|
||||
}
|
||||
|
||||
/**
|
||||
@param {Context} context
|
||||
*/
|
||||
function* getFiles(context) {
|
||||
const { dirname } = context;
|
||||
for (const file of fs.readdirSync(dirname, { withFileTypes: true })) {
|
||||
const filename = file.name;
|
||||
const filepath = path.join(dirname, filename);
|
||||
if (
|
||||
!file.isFile() ||
|
||||
filename[0] === "." ||
|
||||
filename === FORMAT_SCRIPT_FILENAME ||
|
||||
// VSCode creates this file sometime https://github.com/microsoft/vscode/issues/105191
|
||||
filename === "debug.log" ||
|
||||
path.extname(filename) === ".snap"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = fs.readFileSync(filepath, "utf8");
|
||||
|
||||
verifyFilename(context, filename);
|
||||
|
||||
yield {
|
||||
context,
|
||||
name: filename,
|
||||
filename,
|
||||
filepath,
|
||||
code: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@param {Context} context
|
||||
*/
|
||||
function* getSnippets(context) {
|
||||
for (const [index, snippet] of context.snippets.entries()) {
|
||||
const testCase =
|
||||
typeof snippet === "string"
|
||||
? { code: snippet, context }
|
||||
: { ...snippet, context };
|
||||
|
||||
if (typeof testCase.code !== "string") {
|
||||
throw Object.assign(new Error("Invalid test"), { snippet });
|
||||
}
|
||||
|
||||
if (typeof testCase.output === "string") {
|
||||
testCase.output = visualizeEndOfLine(testCase.output);
|
||||
}
|
||||
|
||||
testCase.name = `snippet: ${testCase.name || `#${index}`}`;
|
||||
|
||||
if (typeof testCase.filename === "string") {
|
||||
testCase.filepath = path.join(context.dirname, testCase.filename);
|
||||
}
|
||||
|
||||
yield testCase;
|
||||
}
|
||||
}
|
||||
|
||||
export { getFixtures };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { shouldDisable } from "./failed-format-tests.js";
|
||||
import { isErrorTest, isLanguage } from "./utilities.js";
|
||||
|
||||
function getParsers(dirname, parsers) {
|
||||
const allParsers = [...parsers];
|
||||
const addParsers = isErrorTest(dirname)
|
||||
? () => {}
|
||||
: (...parsers) => {
|
||||
for (const parser of parsers) {
|
||||
if (!allParsers.includes(parser) && !shouldDisable(dirname, parser)) {
|
||||
allParsers.push(parser);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
parsers.includes("babel") &&
|
||||
(isLanguage(dirname, "js") || isLanguage(dirname, "jsx"))
|
||||
) {
|
||||
addParsers("acorn", "espree", "meriyah", "oxc");
|
||||
}
|
||||
|
||||
if (parsers.includes("typescript")) {
|
||||
addParsers("babel-ts", "oxc-ts");
|
||||
}
|
||||
|
||||
if (parsers.includes("flow")) {
|
||||
addParsers("hermes");
|
||||
}
|
||||
|
||||
if (parsers.includes("babel")) {
|
||||
addParsers("__babel_estree");
|
||||
}
|
||||
|
||||
return allParsers;
|
||||
}
|
||||
|
||||
export { getParsers };
|
||||
@@ -0,0 +1 @@
|
||||
export { runFormatTest } from "./run-format-test.js";
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
CURSOR_PLACEHOLDER,
|
||||
RANGE_END_PLACEHOLDER,
|
||||
RANGE_START_PLACEHOLDER,
|
||||
} from "./constants.js";
|
||||
|
||||
const indexProperties = [
|
||||
{
|
||||
property: "cursorOffset",
|
||||
placeholder: CURSOR_PLACEHOLDER,
|
||||
},
|
||||
{
|
||||
property: "rangeStart",
|
||||
placeholder: RANGE_START_PLACEHOLDER,
|
||||
},
|
||||
{
|
||||
property: "rangeEnd",
|
||||
placeholder: RANGE_END_PLACEHOLDER,
|
||||
},
|
||||
];
|
||||
function replacePlaceholders(originalText, originalOptions) {
|
||||
const indexes = indexProperties
|
||||
.map(({ property, placeholder }) => {
|
||||
const value = originalText.indexOf(placeholder);
|
||||
return value === -1 ? undefined : { property, value, placeholder };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.value - b.value);
|
||||
|
||||
const options = { ...originalOptions };
|
||||
let text = originalText;
|
||||
let offset = 0;
|
||||
for (const { property, value, placeholder } of indexes) {
|
||||
text = text.replace(placeholder, "");
|
||||
options[property] = value + offset;
|
||||
offset -= placeholder.length;
|
||||
}
|
||||
return { text, options };
|
||||
}
|
||||
|
||||
export { replacePlaceholders };
|
||||
@@ -0,0 +1,84 @@
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import { FORMAT_SCRIPT_FILENAME } from "./constants.js";
|
||||
import { getFixtures } from "./get-fixtures.js";
|
||||
import { getParsers } from "./get-parsers.js";
|
||||
import { testFixture } from "./run-test.js";
|
||||
import { stringifyOptionsForTitle } from "./stringify-options-for-title.js";
|
||||
import { isErrorTest, normalizeDirectory } from "./utilities.js";
|
||||
import { verifyParsers } from "./verify-fixtures.js";
|
||||
|
||||
/**
|
||||
@typedef {
|
||||
| string
|
||||
| { code: string; name?: string; filename?: string; output?: string }
|
||||
} Snippet
|
||||
@typedef {{
|
||||
dirname: string,
|
||||
stringifiedOptions: string,
|
||||
parsers: string[],
|
||||
options: any,
|
||||
explicitParsers: string[],
|
||||
rawOptions: any,
|
||||
snippets: Snippet[],
|
||||
}} Context
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {
|
||||
| ImportMeta
|
||||
| { importMeta: ImportMeta, snippets?: Snippet[] }
|
||||
} rawFixtures
|
||||
@param {string[]} explicitParsers
|
||||
@param {any} rawOptions
|
||||
*/
|
||||
function runFormatTest(rawFixtures, explicitParsers, rawOptions) {
|
||||
const { importMeta, snippets = [] } = rawFixtures.importMeta
|
||||
? rawFixtures
|
||||
: { importMeta: rawFixtures };
|
||||
|
||||
const filename = path.basename(new URL(importMeta.url).pathname);
|
||||
if (filename !== FORMAT_SCRIPT_FILENAME) {
|
||||
throw new Error(
|
||||
`Format test should run in file named '${FORMAT_SCRIPT_FILENAME}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
const dirname = normalizeDirectory(
|
||||
path.dirname(url.fileURLToPath(importMeta.url)),
|
||||
);
|
||||
|
||||
let options = {
|
||||
printWidth: 80,
|
||||
...rawOptions,
|
||||
};
|
||||
|
||||
// `IS_ERROR_TEST` mean to watch errors like:
|
||||
// - syntax parser hasn't supported yet
|
||||
// - syntax errors that should throws
|
||||
const IS_ERROR_TEST = isErrorTest(dirname);
|
||||
|
||||
if (IS_ERROR_TEST) {
|
||||
options = { errors: true, ...options };
|
||||
}
|
||||
|
||||
// Make sure tests are in correct location
|
||||
|
||||
const context = {
|
||||
dirname,
|
||||
stringifiedOptions: stringifyOptionsForTitle(rawOptions),
|
||||
parsers: getParsers(dirname, explicitParsers),
|
||||
options,
|
||||
explicitParsers,
|
||||
rawOptions,
|
||||
snippets,
|
||||
};
|
||||
|
||||
verifyParsers(context);
|
||||
|
||||
for (const fixture of getFixtures(context)) {
|
||||
testFixture(fixture);
|
||||
}
|
||||
}
|
||||
|
||||
export { runFormatTest };
|
||||
@@ -0,0 +1,108 @@
|
||||
import fs from "node:fs";
|
||||
import getPrettier from "../get-prettier.js";
|
||||
import {
|
||||
CURSOR_PLACEHOLDER,
|
||||
isProduction,
|
||||
TEST_RUNTIME,
|
||||
TEST_STANDALONE,
|
||||
} from "./constants.js";
|
||||
import { replacePlaceholders } from "./replace-placeholders.js";
|
||||
import { ensurePromise } from "./utilities.js";
|
||||
import visualizeEndOfLine from "./visualize-end-of-line.js";
|
||||
|
||||
async function parse(source, options) {
|
||||
const prettier = await getPrettier();
|
||||
const { ast } = await ensurePromise(
|
||||
prettier.__debug.parse(source, await loadPlugins(options), {
|
||||
massage: true,
|
||||
}),
|
||||
);
|
||||
return ast;
|
||||
}
|
||||
|
||||
async function format(originalText, originalOptions) {
|
||||
const { text: input, options } = replacePlaceholders(
|
||||
originalText,
|
||||
originalOptions,
|
||||
);
|
||||
const inputWithCursor = insertCursor(input, options.cursorOffset);
|
||||
const prettier = await getPrettier();
|
||||
|
||||
const { formatted: output, cursorOffset } = await ensurePromise(
|
||||
prettier.formatWithCursor(input, await loadPlugins(options)),
|
||||
);
|
||||
const outputWithCursor = insertCursor(output, cursorOffset);
|
||||
const eolVisualizedOutput = visualizeEndOfLine(outputWithCursor);
|
||||
|
||||
const changed = outputWithCursor !== inputWithCursor;
|
||||
|
||||
return {
|
||||
changed,
|
||||
options,
|
||||
input,
|
||||
inputWithCursor,
|
||||
output,
|
||||
outputWithCursor,
|
||||
eolVisualizedOutput,
|
||||
};
|
||||
}
|
||||
|
||||
const insertCursor = (text, cursorOffset) =>
|
||||
cursorOffset >= 0
|
||||
? text.slice(0, cursorOffset) +
|
||||
CURSOR_PLACEHOLDER +
|
||||
text.slice(cursorOffset)
|
||||
: text;
|
||||
|
||||
// Move this to prettier installation
|
||||
let externalParsers;
|
||||
async function loadPlugins(options) {
|
||||
if (!isProduction || !options.parser || TEST_RUNTIME === "browser") {
|
||||
return options;
|
||||
}
|
||||
|
||||
const { parser } = options;
|
||||
|
||||
if (!externalParsers) {
|
||||
externalParsers = getExternalPlugins();
|
||||
}
|
||||
externalParsers = await externalParsers;
|
||||
if (!externalParsers.has(parser)) {
|
||||
return options;
|
||||
}
|
||||
const plugin = externalParsers.get(parser);
|
||||
|
||||
const plugins = options.plugins ?? [];
|
||||
plugins.push(TEST_STANDALONE ? plugin.implementation : plugin.url);
|
||||
|
||||
return { ...options, plugins };
|
||||
}
|
||||
|
||||
async function getExternalPlugins() {
|
||||
const distDirectory = new URL("../../../dist/", import.meta.url);
|
||||
const directories = fs.readdirSync(distDirectory);
|
||||
const plugins = await Promise.all(
|
||||
directories
|
||||
.filter((directory) => directory.startsWith("plugin-"))
|
||||
.map(async (directory) => {
|
||||
const url = new URL(`./${directory}/index.mjs`, distDirectory);
|
||||
const implementation = await import(url);
|
||||
return {
|
||||
url,
|
||||
implementation,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const externalParsers = new Map();
|
||||
for (const plugin of plugins) {
|
||||
const { parsers } = plugin.implementation;
|
||||
if (parsers) {
|
||||
for (const parser of Object.keys(parsers)) {
|
||||
externalParsers.set(parser, plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
return externalParsers;
|
||||
}
|
||||
|
||||
export { format, parse };
|
||||
@@ -0,0 +1,210 @@
|
||||
import consistentEndOfLine from "./consistent-end-of-line.js";
|
||||
import { BOM, FULL_TEST } from "./constants.js";
|
||||
import createSnapshot from "./create-snapshot.js";
|
||||
import * as failedTests from "./failed-format-tests.js";
|
||||
import { format, parse } from "./run-prettier.js";
|
||||
import { shouldThrowOnFormat } from "./utilities.js";
|
||||
import visualizeEndOfLine from "./visualize-end-of-line.js";
|
||||
|
||||
/**
|
||||
@import {Fixture} from "./get-fixtures.js"
|
||||
@typedef {ReturnType<getTestCase>} TestCase
|
||||
*/
|
||||
|
||||
/**
|
||||
@param {Fixture} fixture
|
||||
*/
|
||||
function testFixture(fixture) {
|
||||
const { name, context, filepath } = fixture;
|
||||
const { stringifiedOptions, parsers } = context;
|
||||
|
||||
const title = `${name}${
|
||||
stringifiedOptions ? ` - ${stringifiedOptions}` : ""
|
||||
}`;
|
||||
|
||||
const testCases = parsers
|
||||
.filter((parser) => !failedTests.shouldDisable(filepath, parser))
|
||||
.map((parser) => getTestCase(fixture, parser));
|
||||
|
||||
const testCaseForSnapshot = testCases.find(
|
||||
(testCase) =>
|
||||
!testCase.expectFail && typeof testCase.expectedOutput !== "string",
|
||||
);
|
||||
const hasMultipleParsers = testCases.length > 1;
|
||||
|
||||
describe(title, () => {
|
||||
for (const testCase of testCases) {
|
||||
let testTitle = "format";
|
||||
if (testCaseForSnapshot !== testCase && hasMultipleParsers) {
|
||||
testTitle += `[${testCase.parser}]`;
|
||||
}
|
||||
|
||||
test(testTitle, async () => {
|
||||
await runTest(testCase, testCaseForSnapshot);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@param {Fixture} fixture
|
||||
@param {string} parser
|
||||
*/
|
||||
function getTestCase(fixture, parser) {
|
||||
const { code, context, filepath } = fixture;
|
||||
const { options } = context;
|
||||
const formatOptions = { filepath, ...options, parser };
|
||||
const expectFail = shouldThrowOnFormat(fixture, options, parser);
|
||||
/** @type {ReturnType<format> | undefined} */
|
||||
let promise;
|
||||
|
||||
return {
|
||||
context,
|
||||
parser,
|
||||
filepath,
|
||||
code,
|
||||
formatOptions,
|
||||
expectFail,
|
||||
expectedOutput: fixture.output,
|
||||
runFormat: () =>
|
||||
promise === undefined ? (promise = format(code, formatOptions)) : promise,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@param {TestCase} testCase
|
||||
@param {TestCase} testCaseForSnapshot
|
||||
*/
|
||||
async function runTest(testCase, testCaseForSnapshot) {
|
||||
if (testCase.expectFail) {
|
||||
await expect(testCase.runFormat).rejects.toThrowErrorMatchingSnapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
const formatResult = await testCase.runFormat();
|
||||
expect(formatResult).toBeDefined();
|
||||
|
||||
// Make sure output has consistent EOL
|
||||
expect(formatResult.eolVisualizedOutput).toBe(
|
||||
visualizeEndOfLine(consistentEndOfLine(formatResult.outputWithCursor)),
|
||||
);
|
||||
|
||||
if (typeof testCase.expectedOutput === "string") {
|
||||
expect(formatResult.eolVisualizedOutput).toBe(testCase.expectedOutput);
|
||||
} else if (testCase === testCaseForSnapshot) {
|
||||
expect(
|
||||
createSnapshot(formatResult, {
|
||||
parsers: testCase.context.explicitParsers,
|
||||
formatOptions: testCase.formatOptions,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
} else {
|
||||
const { eolVisualizedOutput } = await testCaseForSnapshot.runFormat();
|
||||
expect(formatResult.eolVisualizedOutput).toStrictEqual(eolVisualizedOutput);
|
||||
}
|
||||
|
||||
if (!FULL_TEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filepath, code, formatOptions } = testCase;
|
||||
// Some parsers skip parsing empty files
|
||||
if (formatResult.changed && code.trim()) {
|
||||
const { input, output } = formatResult;
|
||||
const [originalAst, formattedAst] = await Promise.all(
|
||||
[input, output].map((code) => parse(code, formatOptions)),
|
||||
);
|
||||
const isAstUnstableTest = failedTests.isAstUnstable(
|
||||
filepath,
|
||||
formatOptions,
|
||||
);
|
||||
|
||||
if (isAstUnstableTest) {
|
||||
expect(formattedAst).not.toStrictEqual(originalAst);
|
||||
} else {
|
||||
expect(formattedAst).toStrictEqual(originalAst);
|
||||
}
|
||||
}
|
||||
|
||||
// The following cases should only run on main parser
|
||||
if (testCase === testCaseForSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isUnstableTest = failedTests.isUnstable(filepath, formatOptions);
|
||||
if (
|
||||
(formatResult.changed || isUnstableTest) &&
|
||||
// No range and cursor
|
||||
formatResult.input === code
|
||||
) {
|
||||
const { eolVisualizedOutput: firstOutput, output } = formatResult;
|
||||
const { eolVisualizedOutput: secondOutput } = await format(
|
||||
output,
|
||||
formatOptions,
|
||||
);
|
||||
if (isUnstableTest) {
|
||||
if (secondOutput === firstOutput) {
|
||||
throw new Error(
|
||||
`Unstable file '${filepath}' is stable now, please remove from the 'unstableTests' list.`,
|
||||
);
|
||||
}
|
||||
|
||||
// To keep eye on failed tests, this assert never supposed to pass,
|
||||
// if it fails, just remove the file from `unstableTests`
|
||||
expect(secondOutput).not.toBe(firstOutput);
|
||||
} else {
|
||||
expect(secondOutput).toBe(firstOutput);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldSkipEolTest(code, formatResult.options)) {
|
||||
await Promise.all(
|
||||
["\r\n", "\r"].map(async (eol) => {
|
||||
const { eolVisualizedOutput: output } = await format(
|
||||
code.replace(/\n/g, eol),
|
||||
formatOptions,
|
||||
);
|
||||
// Only if `endOfLine: "auto"` the result will be different
|
||||
const expected =
|
||||
formatOptions.endOfLine === "auto"
|
||||
? visualizeEndOfLine(
|
||||
// All `code` use `LF`, so the `eol` of result is always `LF`
|
||||
formatResult.outputWithCursor.replace(/\n/g, eol),
|
||||
)
|
||||
: formatResult.eolVisualizedOutput;
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (code.charAt(0) !== BOM) {
|
||||
const { eolVisualizedOutput: output } = await format(
|
||||
BOM + code,
|
||||
formatOptions,
|
||||
);
|
||||
const expected = BOM + formatResult.eolVisualizedOutput;
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipEolTest(code, options) {
|
||||
if (code.includes("\r")) {
|
||||
return true;
|
||||
}
|
||||
const { requirePragma, checkIgnorePragma, rangeStart, rangeEnd } = options;
|
||||
if (requirePragma || checkIgnorePragma) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof rangeStart === "number" &&
|
||||
typeof rangeEnd === "number" &&
|
||||
rangeStart >= rangeEnd
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export { testFixture };
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
function stringifyOptions(options) {
|
||||
function stringifyOptionsForTitle(options) {
|
||||
const string = JSON.stringify(options || {}, (key, value) => {
|
||||
if (key === "plugins" || key === "errors") {
|
||||
return;
|
||||
@@ -14,4 +14,4 @@ function stringifyOptions(options) {
|
||||
return string === "{}" ? "" : string;
|
||||
}
|
||||
|
||||
export default stringifyOptions;
|
||||
export { stringifyOptionsForTitle };
|
||||
@@ -0,0 +1,48 @@
|
||||
import path from "node:path";
|
||||
import { FORMAT_TEST_DIRECTORY, TEST_STANDALONE } from "./constants.js";
|
||||
|
||||
const normalizeDirectory = (directory) => path.normalize(directory + path.sep);
|
||||
|
||||
const isLanguage = (dirname, language) =>
|
||||
normalizeDirectory(dirname).startsWith(
|
||||
normalizeDirectory(path.join(FORMAT_TEST_DIRECTORY, language)),
|
||||
);
|
||||
|
||||
const isErrorTest = (dirname) =>
|
||||
normalizeDirectory(dirname).includes(`${path.sep}_errors_${path.sep}`);
|
||||
|
||||
const ensurePromise = (value) => {
|
||||
const isPromise = TEST_STANDALONE
|
||||
? // In standalone test, promise is from another context
|
||||
Object.prototype.toString.call(value) === "[object Promise]"
|
||||
: value instanceof Promise;
|
||||
|
||||
if (!isPromise) {
|
||||
throw new TypeError("Expected value to be a 'Promise'.");
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const shouldThrowOnFormat = ({ filename }, options, parser) => {
|
||||
const { errors = {} } = options;
|
||||
if (errors === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const files = errors[parser];
|
||||
|
||||
if (files === true || (Array.isArray(files) && files.includes(filename))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export {
|
||||
ensurePromise,
|
||||
isErrorTest,
|
||||
isLanguage,
|
||||
normalizeDirectory,
|
||||
shouldThrowOnFormat,
|
||||
};
|
||||
+48
-23
@@ -1,13 +1,12 @@
|
||||
import path from "node:path";
|
||||
import createEsmUtils from "esm-utils";
|
||||
import { outdent } from "outdent";
|
||||
import { FORMAT_TEST_DIRECTORY, FULL_TEST } from "./constants.js";
|
||||
|
||||
const { __dirname, __filename } = createEsmUtils(import.meta);
|
||||
|
||||
const TESTS_ROOT = path.join(__dirname, "../../format");
|
||||
const { __filename } = createEsmUtils(import.meta);
|
||||
|
||||
const getCategory = (dirname) =>
|
||||
path.relative(TESTS_ROOT, dirname).split(path.sep).shift();
|
||||
path.relative(FORMAT_TEST_DIRECTORY, dirname).split(path.sep).shift();
|
||||
|
||||
const categoryParsers = new Map([
|
||||
[
|
||||
@@ -166,19 +165,28 @@ const getParserCategories = (parser) => {
|
||||
return categories;
|
||||
};
|
||||
|
||||
const checkParser = ({ dirname, files }, parsers = []) => {
|
||||
const category = getCategory(dirname);
|
||||
const categoryAllowedParsers = categoryParsers.get(category);
|
||||
const verifyParsers = (context) => {
|
||||
if (!FULL_TEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!categoryAllowedParsers) {
|
||||
const { explicitParsers: parsers, dirname } = context;
|
||||
|
||||
if (!Array.isArray(parsers) || parsers.length === 0) {
|
||||
throw new Error(`No parsers were specified for ${dirname}`);
|
||||
}
|
||||
|
||||
const category = getCategory(dirname);
|
||||
const settings = categoryParsers.get(category);
|
||||
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
parsers: allowedParsers = [],
|
||||
verifyParsers: allowedVerifyParsers = [],
|
||||
extensions = [],
|
||||
} = categoryAllowedParsers;
|
||||
} = settings;
|
||||
|
||||
const [parser, ...verifyParsers] = parsers;
|
||||
|
||||
@@ -199,7 +207,9 @@ const checkParser = ({ dirname, files }, parsers = []) => {
|
||||
: outdent`
|
||||
Suggest move your tests to:
|
||||
${suggestCategories
|
||||
.map((category) => `- ${path.join(TESTS_ROOT, category)}`)
|
||||
.map(
|
||||
(category) => `- ${path.join(FORMAT_TEST_DIRECTORY, category)}`,
|
||||
)
|
||||
.join("\n")}
|
||||
|
||||
Or config to allow use this parser in "${__filename}".
|
||||
@@ -224,19 +234,34 @@ const checkParser = ({ dirname, files }, parsers = []) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const { name, filename } of files) {
|
||||
const ext = path.extname(filename);
|
||||
if (!extensions.includes(ext)) {
|
||||
throw new Error(
|
||||
outdent`
|
||||
File "${name}" should not tested in "${dirname}".
|
||||
Allowed extensions: ${extensions.join(",")}.
|
||||
Please rename it or config to allow test "${ext}" file in "${__filename}".
|
||||
`,
|
||||
);
|
||||
}
|
||||
const verifyFilename = (context, filename) => {
|
||||
if (!FULL_TEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dirname } = context;
|
||||
|
||||
const category = getCategory(dirname);
|
||||
const settings = categoryParsers.get(category);
|
||||
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { extensions = [] } = settings;
|
||||
|
||||
const ext = path.extname(filename);
|
||||
if (!extensions.includes(ext)) {
|
||||
throw new Error(
|
||||
outdent`
|
||||
File "${filename}" should not tested in "${dirname}".
|
||||
Allowed extensions: ${extensions.join(",")}.
|
||||
Please rename it or config to allow test "${ext}" file in "${__filename}".
|
||||
`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default checkParser;
|
||||
export { verifyFilename, verifyParsers };
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,505 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import createEsmUtils from "esm-utils";
|
||||
import * as failedTests from "./failed-format-tests.js";
|
||||
import getPrettier from "./get-prettier.js";
|
||||
import checkParsers from "./utilities/check-parsers.js";
|
||||
import consistentEndOfLine from "./utilities/consistent-end-of-line.js";
|
||||
import createSnapshot from "./utilities/create-snapshot.js";
|
||||
import stringifyOptionsForTitle from "./utilities/stringify-options-for-title.js";
|
||||
import visualizeEndOfLine from "./utilities/visualize-end-of-line.js";
|
||||
|
||||
const { __dirname } = createEsmUtils(import.meta);
|
||||
|
||||
const { FULL_TEST, TEST_STANDALONE, NODE_ENV, TEST_RUNTIME } = process.env;
|
||||
const isProduction = NODE_ENV === "production";
|
||||
const BOM = "\uFEFF";
|
||||
|
||||
const CURSOR_PLACEHOLDER = "<|>";
|
||||
const RANGE_START_PLACEHOLDER = "<<<PRETTIER_RANGE_START>>>";
|
||||
const RANGE_END_PLACEHOLDER = "<<<PRETTIER_RANGE_END>>>";
|
||||
|
||||
const shouldThrowOnFormat = (filename, options, parser) => {
|
||||
const { errors = {} } = options;
|
||||
if (errors === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const files = errors[parser];
|
||||
|
||||
if (files === true || (Array.isArray(files) && files.includes(filename))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isTestDirectory = (dirname, name) =>
|
||||
(dirname + path.sep).startsWith(
|
||||
path.join(__dirname, "../format", name) + path.sep,
|
||||
);
|
||||
|
||||
const ensurePromise = (value) => {
|
||||
const isPromise = TEST_STANDALONE
|
||||
? // In standalone test, promise is from another context
|
||||
Object.prototype.toString.call(value) === "[object Promise]"
|
||||
: value instanceof Promise;
|
||||
|
||||
if (!isPromise) {
|
||||
throw new TypeError("Expected value to be a 'Promise'.");
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
function runFormatTest(fixtures, parsers, options = {}) {
|
||||
let { importMeta, snippets = [] } = fixtures.importMeta
|
||||
? fixtures
|
||||
: { importMeta: fixtures };
|
||||
|
||||
const filename = path.basename(new URL(importMeta.url).pathname);
|
||||
if (filename !== "format.test.js") {
|
||||
throw new Error(`Format test should run in file named 'format.test.js'.`);
|
||||
}
|
||||
|
||||
const dirname = path.normalize(
|
||||
path.dirname(url.fileURLToPath(importMeta.url)) + path.sep,
|
||||
);
|
||||
|
||||
// `IS_ERROR_TEST` mean to watch errors like:
|
||||
// - syntax parser hasn't supported yet
|
||||
// - syntax errors that should throws
|
||||
const IS_ERROR_TEST = dirname.includes(`${path.sep}_errors_${path.sep}`);
|
||||
if (IS_ERROR_TEST) {
|
||||
options = { errors: true, ...options };
|
||||
}
|
||||
|
||||
snippets = snippets.map((test, index) => {
|
||||
test = typeof test === "string" ? { code: test } : test;
|
||||
|
||||
if (typeof test.code !== "string") {
|
||||
throw Object.assign(new Error("Invalid test"), { test });
|
||||
}
|
||||
|
||||
return {
|
||||
...test,
|
||||
name: `snippet: ${test.name || `#${index}`}`,
|
||||
};
|
||||
});
|
||||
|
||||
const files = fs
|
||||
.readdirSync(dirname, { withFileTypes: true })
|
||||
.map((file) => {
|
||||
const basename = file.name;
|
||||
const filename = path.join(dirname, basename);
|
||||
if (
|
||||
path.extname(basename) === ".snap" ||
|
||||
!file.isFile() ||
|
||||
basename[0] === "." ||
|
||||
basename === "format.test.js" ||
|
||||
// VSCode creates this file sometime https://github.com/microsoft/vscode/issues/105191
|
||||
basename === "debug.log"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = fs.readFileSync(filename, "utf8");
|
||||
|
||||
return {
|
||||
name: basename,
|
||||
filename,
|
||||
code: text,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Make sure tests are in correct location
|
||||
if (process.env.CHECK_TEST_PARSERS) {
|
||||
if (!Array.isArray(parsers) || parsers.length === 0) {
|
||||
throw new Error(`No parsers were specified for ${dirname}`);
|
||||
}
|
||||
checkParsers({ dirname, files }, parsers);
|
||||
}
|
||||
|
||||
const allParsers = [...parsers];
|
||||
const addParsers = (...parsers) => {
|
||||
if (IS_ERROR_TEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const parser of parsers) {
|
||||
if (
|
||||
!allParsers.includes(parser) &&
|
||||
!failedTests.shouldDisable(dirname, parser)
|
||||
) {
|
||||
allParsers.push(parser);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
parsers.includes("babel") &&
|
||||
(isTestDirectory(dirname, "js") || isTestDirectory(dirname, "jsx"))
|
||||
) {
|
||||
addParsers("acorn", "espree", "meriyah", "oxc");
|
||||
}
|
||||
|
||||
if (parsers.includes("typescript")) {
|
||||
addParsers("babel-ts", "oxc-ts");
|
||||
}
|
||||
|
||||
if (parsers.includes("flow")) {
|
||||
addParsers("hermes");
|
||||
}
|
||||
|
||||
if (parsers.includes("babel")) {
|
||||
addParsers("__babel_estree");
|
||||
}
|
||||
|
||||
const stringifiedOptions = stringifyOptionsForTitle(options);
|
||||
|
||||
for (const { name, filename, code, output } of [...files, ...snippets]) {
|
||||
const title = `${name}${
|
||||
stringifiedOptions ? ` - ${stringifiedOptions}` : ""
|
||||
}`;
|
||||
|
||||
describe(title, () => {
|
||||
const basicOptions = {
|
||||
printWidth: 80,
|
||||
filepath: filename,
|
||||
...options,
|
||||
};
|
||||
|
||||
const testCases = allParsers
|
||||
.filter((parser) => !failedTests.shouldDisable(filename, parser))
|
||||
.map((parser) => {
|
||||
const formatOptions = { ...basicOptions, parser };
|
||||
const expectFail = shouldThrowOnFormat(name, options, parser);
|
||||
return {
|
||||
parser,
|
||||
explicitParsers: parsers,
|
||||
code,
|
||||
formatOptions,
|
||||
expectFail,
|
||||
expectedOutput: output,
|
||||
runFormat: () => format(code, formatOptions),
|
||||
};
|
||||
});
|
||||
const testCaseForSnapshot = testCases.find(
|
||||
(testCase) =>
|
||||
!testCase.expectFail && typeof testCase.expectedOutput !== "string",
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await Promise.all(
|
||||
testCases.map(async (test) => {
|
||||
if (test.expectFail) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
test.formatResult = await test.runFormat();
|
||||
} catch (error) {
|
||||
test.error = error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const hasMultipleParsers = testCases.length > 1;
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const testTitle = `format${testCaseForSnapshot !== testCase && hasMultipleParsers ? `[${testCase.parser}]` : ""}`;
|
||||
|
||||
test(testTitle, async () => {
|
||||
await runTest({
|
||||
filename,
|
||||
code,
|
||||
output,
|
||||
result: testCase,
|
||||
testCaseForSnapshot,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest({
|
||||
filename,
|
||||
code,
|
||||
output,
|
||||
result,
|
||||
testCaseForSnapshot,
|
||||
}) {
|
||||
if (result.expectFail) {
|
||||
await expect(result.runFormat).rejects.toThrowErrorMatchingSnapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, formatResult } = result;
|
||||
if (!formatResult) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
expect(formatResult).toBeDefined();
|
||||
|
||||
// Make sure output has consistent EOL
|
||||
expect(formatResult.eolVisualizedOutput).toBe(
|
||||
visualizeEndOfLine(consistentEndOfLine(formatResult.outputWithCursor)),
|
||||
);
|
||||
|
||||
if (typeof result.expectedOutput === "string") {
|
||||
expect(formatResult.eolVisualizedOutput).toBe(visualizeEndOfLine(output));
|
||||
} else {
|
||||
expect(testCaseForSnapshot).toBeDefined();
|
||||
expect(testCaseForSnapshot.formatResult).toBeDefined();
|
||||
|
||||
if (testCaseForSnapshot === result) {
|
||||
expect(
|
||||
createSnapshot(testCaseForSnapshot.formatResult, {
|
||||
parsers: testCaseForSnapshot.explicitParsers,
|
||||
formatOptions: testCaseForSnapshot.formatOptions,
|
||||
CURSOR_PLACEHOLDER,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
} else {
|
||||
expect(formatResult.eolVisualizedOutput).toStrictEqual(
|
||||
testCaseForSnapshot.formatResult.eolVisualizedOutput,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!FULL_TEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { formatOptions } = result;
|
||||
|
||||
// Some parsers skip parsing empty files
|
||||
if (formatResult.changed && code.trim()) {
|
||||
const { input, output } = formatResult;
|
||||
const [originalAst, formattedAst] = await Promise.all(
|
||||
[input, output].map((code) => parse(code, formatOptions)),
|
||||
);
|
||||
const isAstUnstableTest = failedTests.isAstUnstable(
|
||||
filename,
|
||||
formatOptions,
|
||||
);
|
||||
|
||||
if (isAstUnstableTest) {
|
||||
expect(formattedAst).not.toStrictEqual(originalAst);
|
||||
} else {
|
||||
expect(formattedAst).toStrictEqual(originalAst);
|
||||
}
|
||||
}
|
||||
|
||||
if (testCaseForSnapshot === result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isUnstableTest = failedTests.isUnstable(filename, formatOptions);
|
||||
if (
|
||||
(formatResult.changed || isUnstableTest) &&
|
||||
// No range and cursor
|
||||
formatResult.input === code
|
||||
) {
|
||||
const { eolVisualizedOutput: firstOutput, output } = formatResult;
|
||||
const { eolVisualizedOutput: secondOutput } = await format(
|
||||
output,
|
||||
formatOptions,
|
||||
);
|
||||
if (isUnstableTest) {
|
||||
if (secondOutput === firstOutput) {
|
||||
throw new Error(
|
||||
`Unstable file '${filename}' is stable now, please remove from the 'unstableTests' list.`,
|
||||
);
|
||||
}
|
||||
|
||||
// To keep eye on failed tests, this assert never supposed to pass,
|
||||
// if it fails, just remove the file from `unstableTests`
|
||||
expect(secondOutput).not.toBe(firstOutput);
|
||||
} else {
|
||||
expect(secondOutput).toBe(firstOutput);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldSkipEolTest(code, formatResult.options)) {
|
||||
await Promise.all(
|
||||
["\r\n", "\r"].map(async (eol) => {
|
||||
const { eolVisualizedOutput: output } = await format(
|
||||
code.replace(/\n/g, eol),
|
||||
formatOptions,
|
||||
);
|
||||
// Only if `endOfLine: "auto"` the result will be different
|
||||
const expected =
|
||||
formatOptions.endOfLine === "auto"
|
||||
? visualizeEndOfLine(
|
||||
// All `code` use `LF`, so the `eol` of result is always `LF`
|
||||
formatResult.outputWithCursor.replace(/\n/g, eol),
|
||||
)
|
||||
: formatResult.eolVisualizedOutput;
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (code.charAt(0) !== BOM) {
|
||||
const { eolVisualizedOutput: output } = await format(
|
||||
BOM + code,
|
||||
formatOptions,
|
||||
);
|
||||
const expected = BOM + formatResult.eolVisualizedOutput;
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipEolTest(code, options) {
|
||||
if (code.includes("\r")) {
|
||||
return true;
|
||||
}
|
||||
const { requirePragma, checkIgnorePragma, rangeStart, rangeEnd } = options;
|
||||
if (requirePragma || checkIgnorePragma) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof rangeStart === "number" &&
|
||||
typeof rangeEnd === "number" &&
|
||||
rangeStart >= rangeEnd
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function parse(source, options) {
|
||||
const prettier = await getPrettier();
|
||||
const { ast } = await ensurePromise(
|
||||
prettier.__debug.parse(source, await loadPlugins(options), {
|
||||
massage: true,
|
||||
}),
|
||||
);
|
||||
return ast;
|
||||
}
|
||||
|
||||
const indexProperties = [
|
||||
{
|
||||
property: "cursorOffset",
|
||||
placeholder: CURSOR_PLACEHOLDER,
|
||||
},
|
||||
{
|
||||
property: "rangeStart",
|
||||
placeholder: RANGE_START_PLACEHOLDER,
|
||||
},
|
||||
{
|
||||
property: "rangeEnd",
|
||||
placeholder: RANGE_END_PLACEHOLDER,
|
||||
},
|
||||
];
|
||||
function replacePlaceholders(originalText, originalOptions) {
|
||||
const indexes = indexProperties
|
||||
.map(({ property, placeholder }) => {
|
||||
const value = originalText.indexOf(placeholder);
|
||||
return value === -1 ? undefined : { property, value, placeholder };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.value - b.value);
|
||||
|
||||
const options = { ...originalOptions };
|
||||
let text = originalText;
|
||||
let offset = 0;
|
||||
for (const { property, value, placeholder } of indexes) {
|
||||
text = text.replace(placeholder, "");
|
||||
options[property] = value + offset;
|
||||
offset -= placeholder.length;
|
||||
}
|
||||
return { text, options };
|
||||
}
|
||||
|
||||
const insertCursor = (text, cursorOffset) =>
|
||||
cursorOffset >= 0
|
||||
? text.slice(0, cursorOffset) +
|
||||
CURSOR_PLACEHOLDER +
|
||||
text.slice(cursorOffset)
|
||||
: text;
|
||||
async function format(originalText, originalOptions) {
|
||||
const { text: input, options } = replacePlaceholders(
|
||||
originalText,
|
||||
originalOptions,
|
||||
);
|
||||
const inputWithCursor = insertCursor(input, options.cursorOffset);
|
||||
const prettier = await getPrettier();
|
||||
|
||||
const { formatted: output, cursorOffset } = await ensurePromise(
|
||||
prettier.formatWithCursor(input, await loadPlugins(options)),
|
||||
);
|
||||
const outputWithCursor = insertCursor(output, cursorOffset);
|
||||
const eolVisualizedOutput = visualizeEndOfLine(outputWithCursor);
|
||||
|
||||
const changed = outputWithCursor !== inputWithCursor;
|
||||
|
||||
return {
|
||||
changed,
|
||||
options,
|
||||
input,
|
||||
inputWithCursor,
|
||||
output,
|
||||
outputWithCursor,
|
||||
eolVisualizedOutput,
|
||||
};
|
||||
}
|
||||
|
||||
async function getExternalPlugins() {
|
||||
const distDirectory = new URL("../../dist/", import.meta.url);
|
||||
const directories = fs.readdirSync(distDirectory);
|
||||
const plugins = await Promise.all(
|
||||
directories
|
||||
.filter((directory) => directory.startsWith("plugin-"))
|
||||
.map(async (directory) => {
|
||||
const url = new URL(`./${directory}/index.mjs`, distDirectory);
|
||||
const implementation = await import(url);
|
||||
return {
|
||||
url,
|
||||
implementation,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const externalParsers = new Map();
|
||||
for (const plugin of plugins) {
|
||||
const { parsers } = plugin.implementation;
|
||||
if (parsers) {
|
||||
for (const parser of Object.keys(parsers)) {
|
||||
externalParsers.set(parser, plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
return externalParsers;
|
||||
}
|
||||
|
||||
let externalParsers;
|
||||
async function loadPlugins(options) {
|
||||
if (!isProduction || !options.parser || TEST_RUNTIME === "browser") {
|
||||
return options;
|
||||
}
|
||||
|
||||
const { parser } = options;
|
||||
|
||||
if (!externalParsers) {
|
||||
externalParsers = getExternalPlugins();
|
||||
}
|
||||
externalParsers = await externalParsers;
|
||||
if (!externalParsers.has(parser)) {
|
||||
return options;
|
||||
}
|
||||
const plugin = externalParsers.get(parser);
|
||||
|
||||
const plugins = options.plugins ?? [];
|
||||
plugins.push(TEST_STANDALONE ? plugin.implementation : plugin.url);
|
||||
|
||||
return { ...options, plugins };
|
||||
}
|
||||
|
||||
export default runFormatTest;
|
||||
Reference in new issue
Block a user