Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple plugin order #16442

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/globals.d.ts
Expand Up @@ -7,5 +7,6 @@ declare function REQUIRED_VERSION(version: string): string;
declare namespace NodeJS {
export interface ProcessEnv {
BABEL_8_BREAKING: string;
IS_PUBLISH: string;
}
}
158 changes: 156 additions & 2 deletions packages/babel-core/src/config/full.ts
Expand Up @@ -26,7 +26,10 @@ import {
checkNoUnwrappedItemOptionPairs,
} from "./validation/options.ts";
import type { PluginItem } from "./validation/options.ts";
import { validatePluginObject } from "./validation/plugins.ts";
import {
type PluginObject,
validatePluginObject,
} from "./validation/plugins.ts";
import { makePluginAPI, makePresetAPI } from "./helpers/config-api.ts";
import type { PluginAPI, PresetAPI } from "./helpers/config-api.ts";

Expand Down Expand Up @@ -56,6 +59,147 @@ export type { Plugin };
export type PluginPassList = Array<Plugin>;
export type PluginPasses = Array<PluginPassList>;

function sortPlugins(plugins: Plugin[]) {
function stableSort(plugins: Plugin[], orderMap: Map<string, number>) {
const buckets = Object.create(null);

// By collecting into buckets, we can guarantee a stable sort.
for (let i = 0; i < plugins.length; i++) {
const n = plugins[i];
const p = 1000 - orderMap.get(n.key);

// In case some plugin is setting an unexpected priority.
const bucket = buckets[p] || (buckets[p] = []);
bucket.push(n);
}

// Sort our keys in descending order. Keys are unique, so we don't have to
// worry about stability.
const keys = Object.keys(buckets)
.map(k => +k)
.sort((a, b) => b - a);

let index = 0;
for (const key of keys) {
const bucket = buckets[key];
for (const n of bucket) {
plugins[index++] = n;
}
}
}

type OrderData = {
version: number;
getData: PluginObject["orderData"]["data"];
list?: string[];
before?: string;
plugins: Plugin[];
};
const orderDataMap: Map<string, OrderData> = new Map();

const pluginsWithPadding: (Plugin | string)[] = [];

for (let i = plugins.length - 1; i >= 0; i--) {
const plugin = plugins[i];
const { orderData } = plugin;
if (orderData) {
let orderData2 = orderDataMap.get(orderData.id);
if ((orderData2?.version || 0) < orderData.version) {
if (orderData2 == null) {
pluginsWithPadding.unshift(orderData.id);
}
orderDataMap.set(
orderData.id,
(orderData2 = {
version: orderData.version,
getData: () => orderData.data(),
plugins: [],
}),
);
}
orderData2.plugins.unshift(plugin);
} else {
pluginsWithPadding.unshift(plugin);
}
}

orderDataMap.forEach(v => {
const data = v.getData();
v.list = data.list;
v.before = data.before;
stableSort(v.plugins, new Map(data.list.map((key, i) => [key, i])));
});

// Detect cycles
const seen = new Set<string>();

function visit(k: string) {
const v = orderDataMap.get(k);
if (!v) return;

if (seen.has(k)) {
throw new Error(
`Plugin/preset order list cycles with '${Array.from(seen.keys()).join(" -> ")}'`,
);
}
seen.add(k);

if (v.before) {
visit(v.before);
}
}

for (const k of orderDataMap.keys()) {
visit(k);
seen.clear();
}

// Sort

// eslint-disable-next-line no-constant-condition
while (true) {
let changed = false;

for (let i = 0; i < pluginsWithPadding.length; i++) {
const plugin = pluginsWithPadding[i];
if (typeof plugin !== "string" || !orderDataMap.has(plugin)) {
continue;
}

const orderData = orderDataMap.get(plugin);
const { before } = orderData;
if (!before) {
continue;
}
const index = pluginsWithPadding.indexOf(before);
if (index === -1) {
continue;
}
if (index < i) {
changed = true;
pluginsWithPadding.splice(i, 1);
pluginsWithPadding.splice(index, 0, plugin);
}
}

if (!changed) {
break;
}
}

const newPlugins: Plugin[] = [];

for (const value of pluginsWithPadding) {
if (typeof value === "string") {
newPlugins.push(...orderDataMap.get(value).plugins);
} else {
newPlugins.push(value);
}
}

return newPlugins;
}

export default gensync(function* loadFullConfig(
inputOpts: unknown,
): Handler<ResolvedConfig | null> {
Expand Down Expand Up @@ -168,7 +312,7 @@ export default gensync(function* loadFullConfig(

if (ignored) return null;

const opts: any = optionDefaults;
const opts: ValidatedOptions = optionDefaults;
mergeOptions(opts, options);

const pluginContext: Context.FullPlugin = {
Expand Down Expand Up @@ -211,6 +355,16 @@ export default gensync(function* loadFullConfig(
.map(plugins => ({ plugins }));
opts.passPerPreset = opts.presets.length > 0;

if (opts.sortPlugins && !opts.passPerPreset) {
opts.plugins = passes[0] = sortPlugins(opts.plugins as Plugin[]);
}

if (!process.env.IS_PUBLISH && process.env.TEST_THROW_PLUGINS) {
throw Object.assign(new Error("TEST_THROW_PLUGINS"), {
plugins: opts.plugins,
});
}

return {
options: opts,
passes: passes,
Expand Down
3 changes: 3 additions & 0 deletions packages/babel-core/src/config/partial.ts
Expand Up @@ -112,6 +112,9 @@ export default function* loadPrivatePartialConfig(
const merged: ValidatedOptions = {
assumptions: {},
};

if (process.env.BABEL_8_BREAKING) merged.sortPlugins = true;

configChain.options.forEach(opts => {
mergeOptions(merged as any, opts);
});
Expand Down
3 changes: 3 additions & 0 deletions packages/babel-core/src/config/plugin.ts
Expand Up @@ -16,6 +16,8 @@ export default class Plugin {

externalDependencies: ReadonlyDeepArray<string>;

orderData?: PluginObject["orderData"];

constructor(
plugin: PluginObject,
options: {},
Expand All @@ -30,6 +32,7 @@ export default class Plugin {
this.visitor = plugin.visitor || {};
this.parserOverride = plugin.parserOverride;
this.generatorOverride = plugin.generatorOverride;
this.orderData = plugin.orderData;

this.options = options;
this.externalDependencies = externalDependencies;
Expand Down
Expand Up @@ -219,7 +219,7 @@ export function assertBoolean(
export function assertObject(
loc: GeneralPath,
value: unknown,
): { readonly [key: string]: unknown } | void {
): { readonly [key: string]: unknown } | undefined {
if (
value !== undefined &&
(typeof value !== "object" || Array.isArray(value) || !value)
Expand Down
3 changes: 3 additions & 0 deletions packages/babel-core/src/config/validation/options.ts
Expand Up @@ -56,6 +56,8 @@ const ROOT_VALIDATORS: ValidatorSet = {
cloneInputAst: assertBoolean as Validator<ValidatedOptions["cloneInputAst"]>,

envName: assertString as Validator<ValidatedOptions["envName"]>,

sortPlugins: assertBoolean as Validator<ValidatedOptions["sortPlugins"]>,
};

const BABELRC_VALIDATORS: ValidatorSet = {
Expand Down Expand Up @@ -192,6 +194,7 @@ export type ValidatedOptions = {
parserOpts?: ParserOptions;
// Deprecate top level generatorOpts
generatorOpts?: GeneratorOptions;
sortPlugins?: boolean;
};

export type NormalizedOptions = {
Expand Down
28 changes: 28 additions & 0 deletions packages/babel-core/src/config/validation/plugins.ts
Expand Up @@ -33,8 +33,27 @@ const VALIDATORS: ValidatorSet = {
generatorOverride: assertFunction as Validator<
PluginObject["generatorOverride"]
>,

orderData: assertOrderData as Validator<PluginObject["orderData"]>,
};

function assertOrderData(loc: OptionPath, value: unknown) {
const obj = assertObject(loc, value);
if (obj) {
if (typeof obj.id !== "string") {
throw new Error(
`${msg(loc)} must have an "id" property that is a string`,
);
}
if (typeof obj.version !== "number") {
throw new Error(
`${msg(loc)} must have a "version" property that is a number`,
);
}
}
return value as PluginObject["orderData"];
}

function assertVisitorMap(loc: OptionPath, value: unknown): Visitor {
const obj = assertObject(loc, value);
if (obj) {
Expand Down Expand Up @@ -95,6 +114,15 @@ export type PluginObject<S extends PluginPass = PluginPass> = {
visitor?: Visitor<S>;
parserOverride?: Function;
generatorOverride?: Function;
orderData?: {
id: string;
version: number;
data: () => {
type: "list";
before?: string;
list: string[];
};
};
};

export function validatePluginObject(obj: {
Expand Down
10 changes: 1 addition & 9 deletions packages/babel-core/src/transformation/index.ts
Expand Up @@ -15,6 +15,7 @@ import generateCode from "./file/generate.ts";
import type File from "./file/file.ts";

import { flattenToSet } from "../config/helpers/deep-array.ts";
import { isThenable } from "../gensync-utils/async.ts";

export type FileResultCallback = {
(err: Error, file: null): void;
Expand Down Expand Up @@ -145,12 +146,3 @@ function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
}
}
}

function isThenable<T extends PromiseLike<any>>(val: any): val is T {
return (
!!val &&
(typeof val === "object" || typeof val === "function") &&
!!val.then &&
typeof val.then === "function"
);
}
8 changes: 5 additions & 3 deletions packages/babel-core/test/api.js
Expand Up @@ -366,9 +366,11 @@ describe("api", function () {
plugins: ["@babel/plugin-syntax-jsx"],
});

expect(result.options.plugins[0].manipulateOptions.toString()).toEqual(
expect.stringContaining("jsx"),
);
expect(
result.options.plugins
.find(v => v.key === "syntax-jsx")
.manipulateOptions.toString(),
).toEqual(expect.stringContaining("jsx"));
});

it("option wrapPluginVisitorMethod", function () {
Expand Down
2 changes: 2 additions & 0 deletions packages/babel-core/test/config-chain.js
Expand Up @@ -4,6 +4,7 @@ import path from "path";
import { fileURLToPath } from "url";
import * as babel from "../lib/index.js";
import rimraf from "rimraf";
import { IS_BABEL_8 } from "$repo-utils";

import _getTargets from "@babel/helper-compilation-targets";
const getTargets = _getTargets.default || _getTargets;
Expand Down Expand Up @@ -1128,6 +1129,7 @@ describe("buildConfigChain", function () {
cloneInputAst: true,
targets: defaultTargets,
assumptions: {},
...(IS_BABEL_8() ? { sortPlugins: true } : {}),
});
const realEnv = process.env.NODE_ENV;
const realBabelEnv = process.env.BABEL_ENV;
Expand Down