discourse/app/assets/javascripts/discourse-common/addon/resolver.js

422 lines
14 KiB
JavaScript

import { dasherize, decamelize } from "@ember/string";
import deprecated from "discourse-common/lib/deprecated";
import { findHelper } from "discourse-common/lib/helpers";
import SuffixTrie from "discourse-common/lib/suffix-trie";
import Resolver from "ember-resolver";
import { buildResolver as buildLegacyResolver } from "discourse-common/lib/legacy-resolver";
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
let _options = {};
let moduleSuffixTrie = null;
const DEPRECATED_MODULES = new Map(
Object.entries({
"controller:discovery.categoryWithID": {
newName: "controller:discovery.category",
since: "2.6.0",
},
"controller:discovery.parentCategory": {
newName: "controller:discovery.category",
since: "2.6.0",
},
"controller:tags-show": { newName: "controller:tag-show", since: "2.6.0" },
"controller:tags.show": { newName: "controller:tag.show", since: "2.6.0" },
"controller:tagsShow": { newName: "controller:tagShow", since: "2.6.0" },
"route:discovery.categoryWithID": {
newName: "route:discovery.category",
since: "2.6.0",
},
"route:discovery.parentCategory": {
newName: "route:discovery.category",
since: "2.6.0",
},
"route:tags-show": { newName: "route:tag-show", since: "2.6.0" },
"route:tags.show": { newName: "route:tag.show", since: "2.6.0" },
"route:tagsShow": { newName: "route:tagShow", since: "2.6.0" },
"app-events:main": {
newName: "service:app-events",
since: "2.4.0",
dropFrom: "2.9.0.beta1",
},
// Deprecations below are silenced because they're in widespread use, and upgrading
// themes/plugins right now would break their compatibility with the stable branch.
// These should be unsilenced for the release of 2.9.0 stable.
"store:main": {
newName: "service:store",
since: "2.8.0.beta8",
dropFrom: "2.9.0.beta1",
silent: true,
},
"search-service:main": {
newName: "service:search",
since: "2.8.0.beta8",
dropFrom: "2.9.0.beta1",
silent: true,
},
"key-value-store:main": {
newName: "service:key-value-store",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"pm-topic-tracking-state:main": {
newName: "service:pm-topic-tracking-state",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"message-bus:main": {
newName: "service:message-bus",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"site-settings:main": {
newName: "service:site-settings",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"current-user:main": {
newName: "service:current-user",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"session:main": {
newName: "service:session",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"site:main": {
newName: "service:site",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"topic-tracking-state:main": {
newName: "service:topic-tracking-state",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
})
);
export function setResolverOption(name, value) {
_options[name] = value;
}
export function getResolverOption(name) {
return _options[name];
}
export function clearResolverOptions() {
_options = {};
}
function lookupModuleBySuffix(suffix) {
if (!moduleSuffixTrie) {
moduleSuffixTrie = new SuffixTrie("/");
const searchPaths = [
"discourse/", // Includes themes/plugins
"discourse-common/",
"select-kit/",
"admin/",
"wizard/",
"truth-helpers/",
];
Object.keys(requirejs.entries).forEach((name) => {
if (
searchPaths.some((s) => name.startsWith(s)) &&
!name.includes("/templates/")
) {
moduleSuffixTrie.add(name);
}
});
}
return moduleSuffixTrie.withSuffix(suffix, 1)[0];
}
export function buildResolver(baseName) {
let LegacyResolver = buildLegacyResolver(baseName);
return class extends Resolver {
LegacyResolver = LegacyResolver;
init(props) {
super.init(props);
this.legacyResolver = this.LegacyResolver.create(props);
}
resolveRouter(/* parsedName */) {
const routerPath = `${baseName}/router`;
if (requirejs.entries[routerPath]) {
const module = requirejs(routerPath, null, null, true);
return module.default;
}
}
// We overwrite this instead of `normalize` so we still get the benefits of the cache.
_normalize(fullName) {
const deprecationInfo = DEPRECATED_MODULES.get(fullName);
if (deprecationInfo) {
if (!deprecationInfo.silent) {
deprecated(
`"${fullName}" is deprecated, use "${deprecationInfo.newName}" instead`,
{
since: deprecationInfo.since,
dropFrom: deprecationInfo.dropFrom,
id: "discourse.resolver-resolutions",
}
);
}
fullName = deprecationInfo.newName;
}
const split = fullName.split(":");
const type = split[0];
let normalized;
if (type === "template" && split[1]?.includes("connectors/")) {
// The default normalize implementation will skip dasherizing component template names
// We need the same for our connector templates names
normalized = "template:" + split[1].replace(/_/g, "-");
} else {
normalized = super._normalize(fullName);
}
// This is code that we don't really want to keep long term. The main situation where we need it is for
// doing stuff like `controllerFor('adminWatchedWordsAction')` where the real route name
// is actually `adminWatchedWords.action`. The default behavior for the former is to
// normalize to `adminWatchedWordsAction` where the latter becomes `adminWatchedWords.action`.
// While these end up looking up the same file ultimately, they are treated as different
// items and so we can end up with two distinct version of the controller!
if (
split.length > 1 &&
(type === "controller" || type === "route" || type === "template")
) {
let corrected;
// This should only apply when there's a dot or slash in the name
if (split[1].includes(".") || split[1].includes("/")) {
// Check to see if the dasherized version exists. If it does we want to
// normalize to that eagerly so the normalized versions of the dotted/slashed and
// dotless/slashless match.
const dashed = dasherize(split[1].replace(/[\.\/]/g, "-"));
const adminBase = `admin/${type}s/`;
const wizardBase = `wizard/${type}s/`;
if (
lookupModuleBySuffix(`${type}s/${dashed}`) ||
requirejs.entries[adminBase + dashed] ||
requirejs.entries[adminBase + dashed.replace(/^admin[-]/, "")] ||
requirejs.entries[
adminBase + dashed.replace(/^admin[-]/, "").replace(/-/g, "_")
] ||
requirejs.entries[wizardBase + dashed] ||
requirejs.entries[wizardBase + dashed.replace(/^wizard[-]/, "")] ||
requirejs.entries[
wizardBase + dashed.replace(/^wizard[-]/, "").replace(/-/g, "_")
]
) {
corrected = type + ":" + dashed;
}
}
if (corrected && corrected !== normalized) {
normalized = corrected;
}
}
return normalized;
}
chooseModuleName(moduleName, parsedName) {
let resolved = super.chooseModuleName(moduleName, parsedName);
if (resolved) {
return resolved;
}
const standard = parsedName.fullNameWithoutType;
let variants = [standard];
if (standard.includes("/")) {
variants.push(parsedName.fullNameWithoutType.replace(/\//g, "-"));
}
for (let name of variants) {
// If we end with the name we want, use it. This allows us to define components within plugins.
const suffix = parsedName.type + "s/" + name;
resolved = lookupModuleBySuffix(dasherize(suffix));
if (resolved) {
return resolved;
}
}
}
resolveOther(parsedName) {
let resolved = super.resolveOther(parsedName);
if (!resolved) {
let legacyParsedName = this.legacyResolver.parseName(
`${parsedName.type}:${parsedName.fullName}`
);
resolved = this.legacyResolver.resolveOther(legacyParsedName);
if (resolved) {
deprecated(
`Unable to resolve with new resolver, but resolved with legacy resolver: ${parsedName.fullName}`,
{ id: "discourse.legacy-resolver-fallback" }
);
}
}
return resolved;
}
resolveHelper(parsedName) {
return findHelper(parsedName.fullNameWithoutType);
}
// If no match is found here, the resolver falls back to `resolveOther`.
resolveRoute(parsedName) {
if (parsedName.fullNameWithoutType === "basic") {
return requirejs("discourse/routes/discourse", null, null, true)
.default;
}
}
resolveTemplate(parsedName) {
return (
this.findMobileTemplate(parsedName) ||
this.findTemplate(parsedName) ||
this.findAdminTemplate(parsedName) ||
this.findWizardTemplate(parsedName) ||
this.findLoadingTemplate(parsedName) ||
this.findConnectorTemplate(parsedName) ||
this.discourseTemplateModule("not_found")
);
}
findLoadingTemplate(parsedName) {
if (parsedName.fullNameWithoutType.match(/loading$/)) {
return this.discourseTemplateModule("loading");
}
}
findConnectorTemplate(parsedName) {
if (parsedName.fullName.startsWith("template:connectors/")) {
const connectorParsedName = this.parseName(
parsedName.fullName
.replace("template:connectors/", "template:")
.replace("components/", "")
);
return this.findTemplate(connectorParsedName);
}
}
findMobileTemplate(parsedName) {
if (_options.mobileView) {
return this.findTemplate(parsedName, "mobile/");
}
}
/**
* Given a template path, this function will return a template, taking into account
* priority rules for theme and plugin overrides. See `lib/discourse-template-map.js`
*/
discourseTemplateModule(name) {
const resolvedName = DiscourseTemplateMap.resolve(name);
if (resolvedName) {
return require(resolvedName).default;
}
}
findTemplate(parsedName, prefix) {
prefix = prefix || "";
const withoutType = parsedName.fullNameWithoutType,
underscored = decamelize(withoutType).replace(/-/g, "_"),
segments = withoutType.split("/");
return (
// Convert dots and dashes to slashes
this.discourseTemplateModule(
prefix + withoutType.replace(/[\.-]/g, "/")
) ||
// Default unmodified behavior of original resolveTemplate.
this.discourseTemplateModule(prefix + withoutType) ||
// Underscored without namespace
this.discourseTemplateModule(prefix + underscored) ||
// Underscored with first segment as directory
this.discourseTemplateModule(prefix + underscored.replace("_", "/")) ||
// Underscore only the last segment
this.discourseTemplateModule(
`${prefix}${segments.slice(0, -1).join("/")}/${segments[
segments.length - 1
].replace(/-/g, "_")}`
) ||
// All dasherized
this.discourseTemplateModule(prefix + withoutType.replace(/\//g, "-"))
);
}
// Try to find a template within a special admin namespace, e.g. adminEmail => admin/templates/email
// (similar to how discourse lays out templates)
findAdminTemplate(parsedName) {
if (parsedName.fullNameWithoutType === "admin") {
return this.discourseTemplateModule("admin/templates/admin");
}
let namespaced, match;
if (parsedName.fullNameWithoutType.startsWith("components/")) {
return (
this.findTemplate(parsedName, "admin/templates/") ||
this.findTemplate(parsedName, "admin/") // Nested under discourse/templates/admin (e.g. from plugins)
);
} else if (/^admin[_\.-]/.test(parsedName.fullNameWithoutType)) {
namespaced = parsedName.fullNameWithoutType.slice(6);
} else if (
(match = parsedName.fullNameWithoutType.match(/^admin([A-Z])(.+)$/))
) {
namespaced = `${match[1].toLowerCase()}${match[2]}`;
}
let resolved;
if (namespaced) {
let adminParsedName = this.parseName(`template:${namespaced}`);
resolved =
this.findTemplate(adminParsedName, "admin/templates/") ||
this.findTemplate(parsedName, "admin/templates/") ||
this.findTemplate(adminParsedName, "admin/"); // Nested under discourse/templates/admin (e.g. from plugin)
}
return resolved;
}
findWizardTemplate(parsedName) {
if (parsedName.fullNameWithoutType === "wizard") {
return this.discourseTemplateModule("wizard/templates/wizard");
}
let namespaced;
if (parsedName.fullNameWithoutType.startsWith("components/")) {
// Look up components as-is
namespaced = parsedName.fullNameWithoutType;
} else if (/^wizard[_\.-]/.test(parsedName.fullNameWithoutType)) {
// This may only get hit for the loading routes and may be removable.
namespaced = parsedName.fullNameWithoutType.slice(7);
}
if (namespaced) {
let wizardParsedName = this.parseName(
`template:wizard/templates/${namespaced}`
);
return this.findTemplate(wizardParsedName);
}
}
};
}