mirror of
https://github.com/discourse/discourse.git
synced 2026-08-04 10:39:43 +08:00
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>
132 lines
4 KiB
JavaScript
Vendored
132 lines
4 KiB
JavaScript
Vendored
import { readDiscourseImportMode } from "./discourse-import-attribute";
|
|
|
|
export default function (babel) {
|
|
const { types: t } = babel;
|
|
|
|
function rewriteReferences(binding, buildValue) {
|
|
for (const reference of [...binding.referencePaths]) {
|
|
const parent = reference.parentPath;
|
|
|
|
if (parent.isExportSpecifier()) {
|
|
throw reference.buildCodeFrameError(
|
|
"Re-exporting a cross-plugin import is not supported. Import and reference it directly instead."
|
|
);
|
|
} else if (
|
|
parent.isObjectProperty({ shorthand: true, value: reference.node })
|
|
) {
|
|
parent.node.shorthand = false;
|
|
reference.replaceWith(buildValue());
|
|
} else if (parent.isCallExpression({ callee: reference.node })) {
|
|
// `(0, ...)` keeps `this === undefined` for the call.
|
|
reference.replaceWith(
|
|
t.sequenceExpression([t.numericLiteral(0), buildValue()])
|
|
);
|
|
} else {
|
|
reference.replaceWith(buildValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cross-plugin imports are required unless explicitly marked optional.
|
|
function isOptionalPluginImport(path) {
|
|
return readDiscourseImportMode(path) === "optional";
|
|
}
|
|
|
|
return {
|
|
manipulateOptions(_opts, parserOpts) {
|
|
// Allow the `with { discourseImport: ... }` attribute to parse.
|
|
if (!parserOpts.plugins.includes("importAttributes")) {
|
|
parserOpts.plugins.push("importAttributes");
|
|
}
|
|
},
|
|
pre() {
|
|
this.pluginImports = new Map();
|
|
},
|
|
visitor: {
|
|
Program: {
|
|
exit(path) {
|
|
const declarations = [];
|
|
for (const [importSource, localId] of this.pluginImports) {
|
|
declarations.push(
|
|
t.importDeclaration(
|
|
[t.importDefaultSpecifier(t.identifier(localId))],
|
|
t.stringLiteral(importSource)
|
|
)
|
|
);
|
|
}
|
|
path.node.body.unshift(...declarations);
|
|
},
|
|
},
|
|
|
|
ImportDeclaration(path) {
|
|
const moduleName = path.node.source.value;
|
|
if (!moduleName.startsWith("discourse/plugins/")) {
|
|
return;
|
|
}
|
|
|
|
const optional = isOptionalPluginImport(path);
|
|
|
|
const parts = moduleName.split("/");
|
|
const pluginName = parts[2];
|
|
const compatModuleName = parts.slice(3).join("/");
|
|
|
|
// Add a ? suffix for optional cross-plugin imports
|
|
const importSource = `discourse/plugins/${pluginName}${optional ? "?" : ""}`;
|
|
|
|
let localId = this.pluginImports.get(importSource);
|
|
if (!localId) {
|
|
localId = path.scope.generateUid(
|
|
`plugin_${pluginName}${optional ? "_optional" : ""}`
|
|
);
|
|
this.pluginImports.set(importSource, localId);
|
|
}
|
|
|
|
const compatModule = () => {
|
|
const primary = t.memberExpression(
|
|
t.identifier(localId),
|
|
t.stringLiteral(compatModuleName),
|
|
true
|
|
);
|
|
|
|
if (
|
|
!compatModuleName ||
|
|
compatModuleName === "index" ||
|
|
compatModuleName.endsWith("/index")
|
|
) {
|
|
return primary;
|
|
}
|
|
|
|
const indexFallback = t.memberExpression(
|
|
t.identifier(localId),
|
|
t.stringLiteral(`${compatModuleName}/index`),
|
|
true
|
|
);
|
|
|
|
return t.logicalExpression("||", primary, indexFallback);
|
|
};
|
|
|
|
for (const specifier of path.node.specifiers) {
|
|
const localName = specifier.local.name;
|
|
|
|
let exportedName;
|
|
if (specifier.type === "ImportDefaultSpecifier") {
|
|
exportedName = "default";
|
|
} else if (specifier.type === "ImportNamespaceSpecifier") {
|
|
exportedName = null;
|
|
} else {
|
|
exportedName = specifier.imported.name;
|
|
}
|
|
|
|
const buildValue = () =>
|
|
exportedName
|
|
? t.memberExpression(compatModule(), t.identifier(exportedName))
|
|
: compatModule();
|
|
|
|
rewriteReferences(path.scope.getBinding(localName), buildValue);
|
|
}
|
|
|
|
path.remove();
|
|
},
|
|
},
|
|
};
|
|
}
|