0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/frontend/asset-processor/babel-resolve-plugin-imports.test.mjs
David Taylor e3054ef590
DEV: Improve cross-plugin/theme import handling (#40939)
1. Remove 'federated exports' system, which was named entrypoint exports
for every module inside the plugin. Replace it with a single import of
the target plugin's 'compatModules', and then update call sites to do a
'just in time' lookup of the module and export. This is implemented in a
new `babel-resolve-plugin-imports` plugin

2. Update theme & plugin build systems to produce a list of external
plugins which are imported. For plugins, it's stored in the manifest.
For themes, it's stored in a new column of the javascript_caches table.

3. Refactor theme extra_js loading to use a more structured data model,
and move the HTML generation to the erb template

4. Update core importmap to identify any missing plugin dependencies and
add a fake placeholder module for them. This allows optional imports to
exist without causing a boot error. The imported values will resolve to
'null'.

5. Update core plugins to remove use of `optionalRequire`, and replace
it with regular imports, and a `with { discourseImport: "optional" }`
suffix. This is functionally equivalent to optionalRequire, but without
leaning on the legacy `loader.js` system of core

6. Add support for `with { discourseImport: "optional" }` for
plugins/themes importing core modules. This is useful when a
theme/plugin needs to target multiple versions of Discourse core.

Co-authored-by: Jarek Radosz <jarek@cvx.dev>
2026-06-30 16:11:38 +01:00

163 lines
5.1 KiB
JavaScript
Vendored

/* eslint-disable qunit/require-expect */
import { transformSync } from "@babel/core";
import { expect, test } from "vitest";
import BabelResolvePluginImports from "./babel-resolve-plugin-imports.js";
function compile(input) {
return transformSync(input, {
configFile: false,
plugins: [BabelResolvePluginImports],
}).code;
}
test("rewrites cross-plugin imports to the compatModules map", () => {
expect(
compile(`
import SharedThing, { somethingShared, other as renamed } from "discourse/plugins/other/lib/shared";
import * as Helpers from "discourse/plugins/other/lib/helpers";
SharedThing();
somethingShared(1);
renamed.property;
Helpers.doThing();
`)
).toMatchInlineSnapshot(`
"import _plugin_other from "discourse/plugins/other";
(0, (_plugin_other["lib/shared"] || _plugin_other["lib/shared/index"]).default)();
(0, (_plugin_other["lib/shared"] || _plugin_other["lib/shared/index"]).somethingShared)(1);
(_plugin_other["lib/shared"] || _plugin_other["lib/shared/index"]).other.property;
(_plugin_other["lib/helpers"] || _plugin_other["lib/helpers/index"]).doThing();"
`);
});
test("de-dupes the default import per plugin", () => {
expect(
compile(`
import { a } from "discourse/plugins/other/lib/one";
import { b } from "discourse/plugins/other/lib/two";
a();
b();
`)
).toMatchInlineSnapshot(`
"import _plugin_other from "discourse/plugins/other";
(0, (_plugin_other["lib/one"] || _plugin_other["lib/one/index"]).a)();
(0, (_plugin_other["lib/two"] || _plugin_other["lib/two/index"]).b)();"
`);
});
test("expands shorthand object properties inline", () => {
expect(
compile(`
import { foo, bar } from "discourse/plugins/other/lib/shared";
foo();
const obj = { bar };
`)
).toMatchInlineSnapshot(`
"import _plugin_other from "discourse/plugins/other";
(0, (_plugin_other["lib/shared"] || _plugin_other["lib/shared/index"]).foo)();
const obj = {
bar: (_plugin_other["lib/shared"] || _plugin_other["lib/shared/index"]).bar
};"
`);
});
test("throws when a cross-plugin import is re-exported", () => {
expect(() =>
compile(`
import { foo } from "discourse/plugins/other/lib/shared";
export { foo };
`)
).toThrow(/Re-exporting a cross-plugin import is not supported/);
});
test("imports are required by default, consolidating to the plain specifier", () => {
expect(
compile(`
import ChatChannel from "discourse/plugins/chat/models/chat-channel";
ChatChannel.create();
`)
).toMatchInlineSnapshot(`
"import _plugin_chat from "discourse/plugins/chat";
(_plugin_chat["models/chat-channel"] || _plugin_chat["models/chat-channel/index"]).default.create();"
`);
});
test('`discourseImport: "optional"` consolidates to the `?` specifier', () => {
expect(
compile(`
import ChatChannel from "discourse/plugins/chat/models/chat-channel" with { discourseImport: "optional" };
ChatChannel.create();
`)
).toMatchInlineSnapshot(`
"import _plugin_chat_optional from "discourse/plugins/chat?";
(_plugin_chat_optional["models/chat-channel"] || _plugin_chat_optional["models/chat-channel/index"]).default.create();"
`);
});
test("optional and required imports of the same plugin get separate specifiers", () => {
expect(
compile(`
import { a } from "discourse/plugins/chat/lib/one" with { discourseImport: "optional" };
import { b } from "discourse/plugins/chat/lib/two";
a();
b();
`)
).toMatchInlineSnapshot(`
"import _plugin_chat_optional from "discourse/plugins/chat?";
import _plugin_chat from "discourse/plugins/chat";
(0, (_plugin_chat_optional["lib/one"] || _plugin_chat_optional["lib/one/index"]).a)();
(0, (_plugin_chat["lib/two"] || _plugin_chat["lib/two/index"]).b)();"
`);
});
test('`discourseImport: "required"` consolidates to the plain specifier', () => {
expect(
compile(`
import ChatChannel from "discourse/plugins/chat/models/chat-channel" with { discourseImport: "required" };
ChatChannel.create();
`)
).toMatchInlineSnapshot(`
"import _plugin_chat from "discourse/plugins/chat";
(_plugin_chat["models/chat-channel"] || _plugin_chat["models/chat-channel/index"]).default.create();"
`);
});
test("recognises the quoted attribute key form", () => {
expect(
compile(`
import ChatChannel from "discourse/plugins/chat/models/chat-channel" with { "discourseImport": "required" };
ChatChannel.create();
`)
).toMatchInlineSnapshot(`
"import _plugin_chat from "discourse/plugins/chat";
(_plugin_chat["models/chat-channel"] || _plugin_chat["models/chat-channel/index"]).default.create();"
`);
});
test("throws on an unknown `discourseImport` value", () => {
expect(() =>
compile(`
import ChatChannel from "discourse/plugins/chat/models/chat-channel" with { discourseImport: "maybe" };
ChatChannel.create();
`)
).toThrow(/Invalid `discourseImport` import attribute "maybe"/);
});
test("leaves relative and core imports untouched", () => {
expect(
compile(`
import sibling from "./sibling";
import concatClass from "discourse/helpers/concat-class";
`)
).toMatchInlineSnapshot(`
"import sibling from "./sibling";
import concatClass from "discourse/helpers/concat-class";"
`);
});