discourse/app/assets/javascripts/discourse-plugins/index.js

234 lines
6.9 KiB
JavaScript

"use strict";
const path = require("path");
const WatchedDir = require("broccoli-source").WatchedDir;
const Funnel = require("broccoli-funnel");
const mergeTrees = require("broccoli-merge-trees");
const fs = require("fs");
const concat = require("broccoli-concat");
const RawHandlebarsCompiler = require("discourse-hbr/raw-handlebars-compiler");
const DiscoursePluginColocatedTemplateProcessor = require("./colocated-template-compiler");
const EmberApp = require("ember-cli/lib/broccoli/ember-app");
function fixLegacyExtensions(tree) {
return new Funnel(tree, {
getDestinationPath: function (relativePath) {
if (relativePath.endsWith(".es6")) {
return relativePath.slice(0, -4);
} else if (relativePath.endsWith(".raw.hbs")) {
return relativePath.replace(".raw.hbs", ".hbr");
}
return relativePath;
},
});
}
const COLOCATED_CONNECTOR_REGEX =
/^(?<prefix>.*)\/connectors\/(?<outlet>[^\/]+)\/(?<name>[^\/\.]+)\.(?<extension>.+)$/;
// Having connector templates and js in the same directory causes a clash
// when outputting es6 modules. This shim separates colocated connectors
// into separate js / template locations.
function unColocateConnectors(tree) {
return new Funnel(tree, {
getDestinationPath: function (relativePath) {
const match = relativePath.match(COLOCATED_CONNECTOR_REGEX);
if (
match &&
match.groups.extension === "hbs" &&
match.groups.prefix.split("/").pop() !== "templates"
) {
const { prefix, outlet, name } = match.groups;
return `${prefix}/templates/connectors/${outlet}/${name}.hbs`;
}
if (
match &&
match.groups.extension === "js" &&
match.groups.prefix.split("/").pop() === "templates"
) {
// Some plugins are colocating connector JS under `/templates`
const { prefix, outlet, name } = match.groups;
const newPrefix = prefix.slice(0, -"/templates".length);
return `${newPrefix}/connectors/${outlet}/${name}.js`;
}
return relativePath;
},
});
}
function namespaceModules(tree, pluginName) {
return new Funnel(tree, {
getDestinationPath: function (relativePath) {
return `discourse/plugins/${pluginName}/${relativePath}`;
},
});
}
function parsePluginName(pluginRbPath) {
const pluginRb = fs.readFileSync(pluginRbPath, "utf8");
// Match parsing logic in `lib/plugin/metadata.rb`
for (const line of pluginRb.split("\n")) {
if (line.startsWith("#")) {
const [attribute, value] = line.slice(1).split(":", 2);
if (attribute.trim() === "name") {
return value.trim();
}
}
}
throw new Error(
`Unable to parse plugin name from metadata in ${pluginRbPath}`
);
}
module.exports = {
name: require("./package").name,
pluginInfos() {
const root = path.resolve("../../../../plugins");
const pluginDirectories = fs
.readdirSync(root, { withFileTypes: true })
.filter(
(dirent) =>
(dirent.isDirectory() || dirent.isSymbolicLink()) &&
!dirent.name.startsWith(".") &&
fs.existsSync(path.resolve(root, dirent.name, "plugin.rb"))
);
return pluginDirectories.map((directory) => {
const directoryName = directory.name;
const pluginName = parsePluginName(
path.resolve(root, directoryName, "plugin.rb")
);
const jsDirectory = path.resolve(
root,
directoryName,
"assets/javascripts"
);
const adminJsDirectory = path.resolve(
root,
directoryName,
"admin/assets/javascripts"
);
const testDirectory = path.resolve(
root,
directoryName,
"test/javascripts"
);
const configDirectory = path.resolve(root, directoryName, "config");
const hasJs = fs.existsSync(jsDirectory);
const hasAdminJs = fs.existsSync(adminJsDirectory);
const hasTests = fs.existsSync(testDirectory);
const hasConfig = fs.existsSync(configDirectory);
return {
pluginName,
directoryName,
jsDirectory,
adminJsDirectory,
testDirectory,
configDirectory,
hasJs,
hasAdminJs,
hasTests,
hasConfig,
};
});
},
generatePluginsTree() {
const appTree = this._generatePluginAppTree();
const testTree = this._generatePluginTestTree();
const adminTree = this._generatePluginAdminTree();
return mergeTrees([appTree, testTree, adminTree]);
},
_generatePluginAppTree() {
const trees = this.pluginInfos()
.filter((p) => p.hasJs)
.map(({ pluginName, directoryName, jsDirectory }) =>
this._buildAppTree({
directory: jsDirectory,
pluginName,
outputFile: `assets/plugins/${directoryName}.js`,
})
);
return mergeTrees(trees);
},
_generatePluginAdminTree() {
const trees = this.pluginInfos()
.filter((p) => p.hasAdminJs)
.map(({ pluginName, directoryName, adminJsDirectory }) =>
this._buildAppTree({
directory: adminJsDirectory,
pluginName,
outputFile: `assets/plugins/${directoryName}_admin.js`,
})
);
return mergeTrees(trees);
},
_buildAppTree({ directory, pluginName, outputFile }) {
let tree = new WatchedDir(directory);
tree = fixLegacyExtensions(tree);
tree = unColocateConnectors(tree);
tree = namespaceModules(tree, pluginName);
tree = RawHandlebarsCompiler(tree);
const colocateBase = `discourse/plugins/${pluginName}`;
tree = new DiscoursePluginColocatedTemplateProcessor(
tree,
`${colocateBase}/discourse`
);
tree = new DiscoursePluginColocatedTemplateProcessor(
tree,
`${colocateBase}/admin`
);
tree = this.compileTemplates(tree);
tree = this.processedAddonJsFiles(tree);
return concat(mergeTrees([tree]), {
inputFiles: ["**/*.js"],
outputFile,
allowNone: true,
});
},
_generatePluginTestTree() {
const trees = this.pluginInfos()
.filter((p) => p.hasTests)
.map(({ pluginName, directoryName, testDirectory }) => {
let tree = new WatchedDir(testDirectory);
tree = fixLegacyExtensions(tree);
tree = namespaceModules(tree, pluginName);
tree = this.processedAddonJsFiles(tree);
return concat(mergeTrees([tree]), {
inputFiles: ["**/*.js"],
outputFile: `assets/plugins/test/${directoryName}_tests.js`,
allowNone: true,
});
});
return mergeTrees(trees);
},
shouldCompileTemplates() {
// The base Addon implementation checks for template
// files in the addon directories. We need to override that
// check so that the template compiler always runs.
return true;
},
treeFor() {
// This addon doesn't contribute any 'real' trees to the app
return;
},
shouldLoadPluginTestJs() {
return EmberApp.env() === "development" || process.env.LOAD_PLUGINS === "1";
},
};