From 974adfc7c40a9e27e92d3b3be43d9b69aea30216 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Fri, 30 May 2025 02:05:21 -0400 Subject: [PATCH] [wip] --- package.json | 3 +- packages/@ember/engine/index.ts | 18 ++++ packages/@ember/engine/strict-resolver.ts | 121 ++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 packages/@ember/engine/strict-resolver.ts diff --git a/package.json b/package.json index c165fae2d21..d73c1afbec7 100644 --- a/package.json +++ b/package.json @@ -257,6 +257,7 @@ "@ember/engine/instance.js": "ember-source/@ember/engine/instance.js", "@ember/engine/lib/engine-parent.js": "ember-source/@ember/engine/lib/engine-parent.js", "@ember/engine/parent.js": "ember-source/@ember/engine/parent.js", + "@ember/engine/strict-resolver.js": "ember-source/@ember/engine/strict-resolver.js", "@ember/enumerable/index.js": "ember-source/@ember/enumerable/index.js", "@ember/enumerable/mutable.js": "ember-source/@ember/enumerable/mutable.js", "@ember/helper/index.js": "ember-source/@ember/helper/index.js", @@ -400,4 +401,4 @@ } }, "packageManager": "pnpm@10.5.0" -} +} \ No newline at end of file diff --git a/packages/@ember/engine/index.ts b/packages/@ember/engine/index.ts index e16240fa87d..5c913247201 100644 --- a/packages/@ember/engine/index.ts +++ b/packages/@ember/engine/index.ts @@ -15,6 +15,7 @@ import { RoutingService } from '@ember/routing/-internals'; import { ComponentLookup } from '@ember/-internals/views'; import { setupEngineRegistry } from '@ember/-internals/glimmer'; import { RegistryProxyMixin } from '@ember/-internals/runtime'; +import { StrictResolver } from './strict-resolver'; function props(obj: object) { let properties = []; @@ -330,6 +331,20 @@ class Engine extends Namespace.extend(RegistryProxyMixin) { */ declare Resolver: ResolverClass; + /** + Set this to opt-in to using a strict resolver that will only return the + given set of ES modules. The names of the modules should all be relative to + the root of the app and start with "./" + + @property modules + @public + */ + declare modules?: Record>; + + // TODO: I don't think I really want to add this, but I also don't want to + // keep legacy pluralization baked in. + declare plurals?: Record; + init(properties: object | undefined) { super.init(properties); this.buildRegistry(); @@ -462,6 +477,9 @@ class Engine extends Namespace.extend(RegistryProxyMixin) { @return {*} the resolved value for a given lookup */ function resolverFor(namespace: Engine) { + if (namespace.modules) { + return new StrictResolver(namespace.modules, namespace.plurals); + } let ResolverClass = namespace.Resolver; let props = { namespace }; return ResolverClass.create(props); diff --git a/packages/@ember/engine/strict-resolver.ts b/packages/@ember/engine/strict-resolver.ts new file mode 100644 index 00000000000..29c69d5c1bf --- /dev/null +++ b/packages/@ember/engine/strict-resolver.ts @@ -0,0 +1,121 @@ +import type { Factory, Resolver } from '@ember/owner'; + +export class StrictResolver implements Resolver { + #modules = new Map>(); + #plurals = new Map(); + original: any; + constructor( + modules: Record>, + plurals: Record | undefined = undefined + ) { + this.addModules(modules); + this.#plurals.set('config', 'config'); + if (plurals) { + for (let [singular, plural] of Object.entries(plurals)) { + this.#plurals.set(singular, plural); + } + } + } + + addModules(modules: Record>) { + for (let [moduleName, module] of Object.entries(modules)) { + this.#modules.set(this.#normalizeModule(moduleName), module); + } + } + + #normalizeModule(moduleName: string) { + return moduleName.replace(fileExtension, '').replace(leadingDotSlash, ''); + } + + #plural(s: string) { + return this.#plurals.get(s) ?? s + 's'; + } + + resolve(fullName: string): Factory | object | undefined { + let [type, name] = fullName.split(':') as [string, string]; + name = this.#normalizeName(type, name); + for (let strategy of [this.#resolveSelf, this.#mainLookup, this.#defaultLookup]) { + let result = strategy.call(this, type, name); + if (result) { + return this.#extractDefaultExport(result.hit); + } + } + return undefined; + } + + #extractDefaultExport(module: any): Factory | object | undefined { + if (module && module['default']) { + module = module['default']; + } + return module as Factory | object | undefined; + } + + normalize(fullName: `${string}:${string}`): `${string}:${string}` { + let [type, name] = fullName.split(':') as [string, string]; + name = this.#normalizeName(type, name); + return `${type}:${name}`; + } + + #normalizeName(type: string, name: string): string { + if ( + type === 'component' || + type === 'helper' || + type === 'modifier' || + (type === 'template' && name.indexOf('components/') === 0) + ) { + return name.replace(/_/g, '-'); + } else { + return dasherize(name.replace(/\./g, '/')); + } + } + + #resolveSelf(type: string, name: string): Result { + if (type === 'resolver' && name === 'current') { + return { + hit: { + create: () => this, + }, + }; + } + return undefined; + } + + #mainLookup(type: string, name: string): Result { + if (name === 'main') { + let module = this.#modules.get(type); + if (module) { + return { hit: module }; + } + } + return undefined; + } + + #defaultLookup(type: string, name: string): Result { + let dir = this.#plural(type); + let target = `${dir}/${name}`; + let module = this.#modules.get(target); + if (module) { + return { hit: module }; + } + return undefined; + } +} + +const fileExtension = /\.\w{1,4}$/; +const leadingDotSlash = /^\.\//; + +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +const STRING_DASHERIZE_REGEXP = /[ _]/g; +function dasherize(key: string): string { + return decamelize(key).replace(STRING_DASHERIZE_REGEXP, '-'); +} + +type Result = + | { + hit: any; + } + | undefined;