ember-resolver
Version:
The default modules based resolver for Ember CLI.
489 lines (409 loc) • 13.3 kB
JavaScript
import { dasherize, classify, underscore } from './string';
import classFactory from './utils/class-factory';
export class ModuleRegistry {
constructor(entries) {
this._entries = entries || globalThis.requirejs.entries;
}
moduleNames() {
return Object.keys(this._entries);
}
has(moduleName) {
return moduleName in this._entries;
}
get(...args) {
return globalThis.require(...args);
}
}
/**
* This module defines a subclass of Ember.DefaultResolver that adds two
* important features:
*
* 1) The resolver makes the container aware of es6 modules via the AMD
* output. The loader's _moduleEntries is consulted so that classes can be
* resolved directly via the module loader, without needing a manual
* `import`.
* 2) is able to provide injections to classes that implement `extend`
* (as is typical with Ember).
*/
export default class Resolver {
static moduleBasedResolver = true;
moduleBasedResolver = true;
_deprecatedPodModulePrefix = false;
_normalizeCache = Object.create(null);
static create(props) {
return new this(props);
}
/**
A listing of functions to test for moduleName's based on the provided
`parsedName`. This allows easy customization of additional module based
lookup patterns.
@property moduleNameLookupPatterns
@returns {Ember.Array}
*/
moduleNameLookupPatterns = [
this.podBasedModuleName,
this.podBasedComponentsInSubdir,
this.mainModuleName,
this.defaultModuleName,
this.nestedColocationComponentModuleName,
];
static withModules(explicitModules) {
return class extends this {
static explicitModules = explicitModules;
};
}
constructor(props) {
Object.assign(this, props);
if (!this._moduleRegistry) {
let explicitModules = this.constructor.explicitModules;
if (explicitModules) {
this._moduleRegistry = {
moduleNames() {
return Object.keys(explicitModules);
},
has(name) {
return Boolean(explicitModules[name]);
},
get(name) {
return explicitModules[name];
},
addModules(modules) {
explicitModules = Object.assign({}, explicitModules, modules);
},
};
} else {
if (typeof globalThis.requirejs.entries === 'undefined') {
globalThis.requirejs.entries = globalThis.requirejs._eak_seen;
}
this._moduleRegistry = new ModuleRegistry();
}
}
this.pluralizedTypes = this.pluralizedTypes || Object.create(null);
if (!this.pluralizedTypes.config) {
this.pluralizedTypes.config = 'config';
}
}
makeToString(factory, fullName) {
return '' + this.namespace.modulePrefix + '@' + fullName + ':';
}
shouldWrapInClassFactory(/* module, parsedName */) {
return false;
}
parseName(fullName) {
if (fullName.parsedName === true) {
return fullName;
}
let prefix, type, name;
let fullNameParts = fullName.split('@');
if (fullNameParts.length === 3) {
if (fullNameParts[0].length === 0) {
// leading scoped namespace: `@scope/pkg@type:name`
prefix = `@${fullNameParts[1]}`;
let prefixParts = fullNameParts[2].split(':');
type = prefixParts[0];
name = prefixParts[1];
} else {
// interweaved scoped namespace: `type:@scope/pkg@name`
prefix = `@${fullNameParts[1]}`;
type = fullNameParts[0].slice(0, -1);
name = fullNameParts[2];
}
if (type === 'template:components') {
name = `components/${name}`;
type = 'template';
}
} else if (fullNameParts.length === 2) {
let prefixParts = fullNameParts[0].split(':');
if (prefixParts.length === 2) {
if (prefixParts[1].length === 0) {
type = prefixParts[0];
name = `@${fullNameParts[1]}`;
} else {
prefix = prefixParts[1];
type = prefixParts[0];
name = fullNameParts[1];
}
} else {
let nameParts = fullNameParts[1].split(':');
prefix = fullNameParts[0];
type = nameParts[0];
name = nameParts[1];
}
if (type === 'template' && prefix.lastIndexOf('components/', 0) === 0) {
name = `components/${name}`;
prefix = prefix.slice(11);
}
} else {
fullNameParts = fullName.split(':');
type = fullNameParts[0];
name = fullNameParts[1];
}
let fullNameWithoutType = name;
let namespace = this.namespace;
let root = namespace;
return {
parsedName: true,
fullName: fullName,
prefix: prefix || this.prefix({ type: type }),
type: type,
fullNameWithoutType: fullNameWithoutType,
name: name,
root: root,
resolveMethodName: 'resolve' + classify(type),
};
}
resolveOther(parsedName) {
assert('`modulePrefix` must be defined', this.namespace.modulePrefix);
let normalizedModuleName = this.findModuleName(parsedName);
if (normalizedModuleName) {
let defaultExport = this._extractDefaultExport(
normalizedModuleName,
parsedName
);
if (defaultExport === undefined) {
throw new Error(
` Expected to find: '${parsedName.fullName}' within '${normalizedModuleName}' but got 'undefined'. Did you forget to 'export default' within '${normalizedModuleName}'?`
);
}
if (this.shouldWrapInClassFactory(defaultExport, parsedName)) {
defaultExport = classFactory(defaultExport);
}
return defaultExport;
}
}
normalize(fullName) {
return (
this._normalizeCache[fullName] ||
(this._normalizeCache[fullName] = this._normalize(fullName))
);
}
resolve(fullName) {
if (fullName === 'resolver:current') {
return { create: () => this };
}
let parsedName = this.parseName(fullName);
let resolveMethodName = parsedName.resolveMethodName;
let resolved;
if (typeof this[resolveMethodName] === 'function') {
resolved = this[resolveMethodName](parsedName);
}
if (resolved == null) {
resolved = this.resolveOther(parsedName);
}
return resolved;
}
addModules(modules) {
if (!this._moduleRegistry.addModules) {
throw new Error(
`addModules is only supported when your Resolver has been configured to use static modules via Resolver.withModules()`
);
}
this._moduleRegistry.addModules(modules);
}
_normalize(fullName) {
// A) Convert underscores to dashes
// B) Convert camelCase to dash-case, except for components (their
// templates) and helpers where we want to avoid shadowing camelCase
// expressions
// C) replace `.` with `/` in order to make nested controllers work in the following cases
// 1. `needs: ['posts/post']`
// 2. `{{render "posts/post"}}`
// 3. `this.render('posts/post')` from Route
let split = fullName.split(':');
if (split.length > 1) {
let type = split[0];
if (
type === 'component' ||
type === 'helper' ||
type === 'modifier' ||
(type === 'template' && split[1].indexOf('components/') === 0)
) {
return type + ':' + split[1].replace(/_/g, '-');
} else {
return type + ':' + dasherize(split[1].replace(/\./g, '/'));
}
} else {
return fullName;
}
}
pluralize(type) {
return (
this.pluralizedTypes[type] || (this.pluralizedTypes[type] = type + 's')
);
}
podBasedLookupWithPrefix(podPrefix, parsedName) {
let fullNameWithoutType = parsedName.fullNameWithoutType;
if (parsedName.type === 'template') {
fullNameWithoutType = fullNameWithoutType.replace(/^components\//, '');
}
return podPrefix + '/' + fullNameWithoutType + '/' + parsedName.type;
}
podBasedModuleName(parsedName) {
let podPrefix =
this.namespace.podModulePrefix || this.namespace.modulePrefix;
return this.podBasedLookupWithPrefix(podPrefix, parsedName);
}
podBasedComponentsInSubdir(parsedName) {
let podPrefix =
this.namespace.podModulePrefix || this.namespace.modulePrefix;
podPrefix = podPrefix + '/components';
if (
parsedName.type === 'component' ||
/^components/.test(parsedName.fullNameWithoutType)
) {
return this.podBasedLookupWithPrefix(podPrefix, parsedName);
}
}
resolveEngine(parsedName) {
let engineName = parsedName.fullNameWithoutType;
let engineModule = engineName + '/engine';
if (this._moduleRegistry.has(engineModule)) {
return this._extractDefaultExport(engineModule);
}
}
resolveRouteMap(parsedName) {
let engineName = parsedName.fullNameWithoutType;
let engineRoutesModule = engineName + '/routes';
if (this._moduleRegistry.has(engineRoutesModule)) {
let routeMap = this._extractDefaultExport(engineRoutesModule);
assert(
`The route map for ${engineName} should be wrapped by 'buildRoutes' before exporting.`,
routeMap.isRouteMap
);
return routeMap;
}
}
resolveTemplate(parsedName) {
return this.resolveOther(parsedName);
}
mainModuleName(parsedName) {
if (parsedName.fullNameWithoutType === 'main') {
// if router:main or adapter:main look for a module with just the type first
return parsedName.prefix + '/' + parsedName.type;
}
}
defaultModuleName(parsedName) {
return (
parsedName.prefix +
'/' +
this.pluralize(parsedName.type) +
'/' +
parsedName.fullNameWithoutType
);
}
nestedColocationComponentModuleName(parsedName) {
if (parsedName.type === 'component') {
return (
parsedName.prefix +
'/' +
this.pluralize(parsedName.type) +
'/' +
parsedName.fullNameWithoutType +
'/index'
);
}
}
prefix(parsedName) {
let tmpPrefix = this.namespace.modulePrefix;
if (this.namespace[parsedName.type + 'Prefix']) {
tmpPrefix = this.namespace[parsedName.type + 'Prefix'];
}
return tmpPrefix;
}
findModuleName(parsedName) {
let moduleNameLookupPatterns = this.moduleNameLookupPatterns;
let moduleName;
for (
let index = 0, length = moduleNameLookupPatterns.length;
index < length;
index++
) {
let item = moduleNameLookupPatterns[index];
let tmpModuleName = item.call(this, parsedName);
// allow treat all dashed and all underscored as the same thing
// supports components with dashes and other stuff with underscores.
if (tmpModuleName) {
tmpModuleName = this.chooseModuleName(tmpModuleName);
}
if (tmpModuleName && this._moduleRegistry.has(tmpModuleName)) {
moduleName = tmpModuleName;
}
if (moduleName) {
return moduleName;
}
}
}
chooseModuleName(moduleName) {
let underscoredModuleName = underscore(moduleName);
if (
moduleName !== underscoredModuleName &&
this._moduleRegistry.has(moduleName) &&
this._moduleRegistry.has(underscoredModuleName)
) {
throw new TypeError(
`Ambiguous module names: '${moduleName}' and '${underscoredModuleName}'`
);
}
if (this._moduleRegistry.has(moduleName)) {
return moduleName;
} else if (this._moduleRegistry.has(underscoredModuleName)) {
return underscoredModuleName;
}
}
knownForType(type) {
let moduleKeys = this._moduleRegistry.moduleNames();
let items = Object.create(null);
for (let index = 0, length = moduleKeys.length; index < length; index++) {
let moduleName = moduleKeys[index];
let fullname = this.translateToContainerFullname(type, moduleName);
if (fullname) {
items[fullname] = true;
}
}
return items;
}
translateToContainerFullname(type, moduleName) {
let prefix = this.prefix({ type });
// Note: using string manipulation here rather than regexes for better performance.
// pod modules
// '^' + prefix + '/(.+)/' + type + '$'
let podPrefix = prefix + '/';
let podSuffix = '/' + type;
let start = moduleName.indexOf(podPrefix);
let end = moduleName.indexOf(podSuffix);
if (
start === 0 &&
end === moduleName.length - podSuffix.length &&
moduleName.length > podPrefix.length + podSuffix.length
) {
return type + ':' + moduleName.slice(start + podPrefix.length, end);
}
// non-pod modules
// '^' + prefix + '/' + pluralizedType + '/(.+)$'
let pluralizedType = this.pluralize(type);
let nonPodPrefix = prefix + '/' + pluralizedType + '/';
if (
moduleName.indexOf(nonPodPrefix) === 0 &&
moduleName.length > nonPodPrefix.length
) {
return type + ':' + moduleName.slice(nonPodPrefix.length);
}
}
_extractDefaultExport(normalizedModuleName) {
let module = this._moduleRegistry.get(
normalizedModuleName,
null,
null,
true /* force sync */
);
if (module && module['default']) {
module = module['default'];
}
return module;
}
}
function assert(message, check) {
if (!check) {
throw new Error(message);
}
}