Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b4319ca
Phase 1: copy content from ember-resolver so we can more easily asses…
NullVoxPopuli Apr 7, 2026
0c04468
Copy tests from ember-strict-application-resolver
NullVoxPopuli Apr 7, 2026
e55c232
Adapt strict resolver tests to ember's codebase, remove AMD dependencies
NullVoxPopuli Apr 8, 2026
f90e45c
Replace classic resolver with StrictResolver per RFC#1132
NullVoxPopuli Apr 8, 2026
0add00e
Address review: restore string utilities, expand tests, fix setup-res…
NullVoxPopuli Apr 8, 2026
1ab8b52
Address review: revert package.json, add strict-resolver scenario test
NullVoxPopuli Apr 8, 2026
c8c25f4
Expand strict-resolver scenario with full route hierarchy and compone…
NullVoxPopuli Apr 9, 2026
4355bbc
Fix CI failures: lint, type-check, docs coverage, and test cleanup
NullVoxPopuli Apr 18, 2026
a89a953
Shim Engine.Resolver so strict apps work with @ember/test-helpers
NullVoxPopuli Apr 18, 2026
7ffb39c
Address review: inflection, colocation, drop unused helpers
NullVoxPopuli Apr 18, 2026
578bd7b
Merge pull request #21307 from NullVoxPopuli-ai-agent/strict-resolver…
NullVoxPopuli Apr 19, 2026
2609f4f
Scenario test: loading and error substates with strict resolver
NullVoxPopuli Apr 19, 2026
1302135
Address review: drop modulePrefix from scenario app
NullVoxPopuli Apr 19, 2026
c557ce6
Merge pull request #21322 from NullVoxPopuli-ai-agent/scenario-loadin…
NullVoxPopuli Apr 19, 2026
bbd7bd8
Address review: drop cache, simplify pluralize, collapse setup-resolver
NullVoxPopuli Apr 20, 2026
a9cfe43
Fix prettier + drop deleted cache.js from package.json aliases
NullVoxPopuli Apr 20, 2026
a54e687
Drop strictResolver from v2AppScenarios; keep dedicated strict scenario
NullVoxPopuli Apr 20, 2026
f11a87b
Inline dasherize into strict-resolver.ts, drop the string/ directory
NullVoxPopuli Apr 20, 2026
dfe4b5c
Merge pull request #21326 from NullVoxPopuli-ai-agent/strict-resolver…
NullVoxPopuli Apr 20, 2026
9b0f351
Apply suggestion from @NullVoxPopuli
NullVoxPopuli Apr 20, 2026
7b1ca65
Register ember-page-title service in the strict-resolver scenario
NullVoxPopuli Apr 20, 2026
4419a18
Merge pull request #21329 from NullVoxPopuli-ai-agent/fix-strictResol…
NullVoxPopuli Apr 20, 2026
8ec017a
Add tests for the shorthand/`default` selection rule
NullVoxPopuli Apr 21, 2026
cec91b5
Merge pull request #21330 from NullVoxPopuli-ai-agent/shorthand-defau…
NullVoxPopuli Apr 21, 2026
11e0966
Trim redundant tests from strict-resolver basic-test
NullVoxPopuli Apr 21, 2026
aa6e65b
Merge pull request #21331 from NullVoxPopuli-ai-agent/trim-redundant-…
NullVoxPopuli Apr 21, 2026
b6e004b
Trim cross-file redundancies in the strict-resolver tests
NullVoxPopuli Apr 21, 2026
4924964
Fill the nested-route gaps in the strict-resolver scenario
NullVoxPopuli Apr 21, 2026
8bdd546
Merge pull request #21333 from NullVoxPopuli-ai-agent/nested-route-co…
NullVoxPopuli Apr 21, 2026
656f2cd
Fix some logic, re-organize tests
NullVoxPopuli Apr 21, 2026
f57675e
Apply suggestion from @NullVoxPopuli
NullVoxPopuli Apr 28, 2026
a292168
Cleanup
NullVoxPopuli Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@
"@ember/engine/index.js": "ember-source/@ember/engine/index.js",
"@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/lib/strict-resolver.js": "ember-source/@ember/engine/lib/strict-resolver.js",
Comment thread
NullVoxPopuli marked this conversation as resolved.
"@ember/engine/parent.js": "ember-source/@ember/engine/parent.js",
"@ember/enumerable/index.js": "ember-source/@ember/enumerable/index.js",
"@ember/enumerable/mutable.js": "ember-source/@ember/enumerable/mutable.js",
Expand Down
26 changes: 25 additions & 1 deletion packages/@ember/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { EngineInstanceOptions } from '@ember/engine/instance';
import EngineInstance from '@ember/engine/instance';
import { RoutingService } from '@ember/routing/-internals';
import { ComponentLookup } from '@ember/-internals/views';
import { StrictResolver } from './lib/strict-resolver';
import { setupEngineRegistry } from '@ember/-internals/glimmer';
import { RegistryProxyMixin } from '@ember/-internals/runtime';

Expand Down Expand Up @@ -328,7 +329,30 @@ class Engine extends Namespace.extend(RegistryProxyMixin) {
@property resolver
@public
*/
declare Resolver: ResolverClass;
Resolver: ResolverClass = {
create: ({ namespace }: { namespace: Engine }) =>
new StrictResolver(namespace.modules ?? {}, namespace.plurals),
} as unknown as 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<string, unknown>;

/**
Custom pluralization rules for the strict resolver. By default, types are
pluralized by appending 's' (e.g. 'service' -> 'services'). The 'config'
type is pre-mapped to 'config' (no pluralization).

@property plurals
@public
*/
declare plurals?: Record<string, string>;

init(properties: object | undefined) {
super.init(properties);
Expand Down
145 changes: 145 additions & 0 deletions packages/@ember/engine/lib/strict-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { Factory, Resolver } from '@ember/owner';

export class StrictResolver implements Resolver {
// Ember's router uses this flag to decide whether to auto-generate
// `${name}_loading` and `${name}_error` substates for routes defined in
// `Router.map(...)`. Since we always resolve against an ES module registry,
// we unconditionally opt in.
moduleBasedResolver = true;

#modules = new Map<string, unknown>();
#plurals = new Map<string, string>();
original: any;

constructor(
modules: Record<string, unknown>,
plurals: Record<string, string> | 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<string, unknown>) {
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> | 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,
this.#nestedColocationLookup,
]) {
let result = strategy.call(this, type, name);
if (result) {
return this.#extractDefaultExport(result.hit);
}
}
return undefined;
}

#extractDefaultExport(module: any): Factory<object> | object | undefined {
if (module && module['default']) {
module = module['default'];
}
return module as Factory<object> | 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;
}

// Supports the nested colocation pattern where `component:my-widget`
// resolves to `./components/my-widget/index.{js,ts,gjs,gts}`. The index
// file is typically the component class, and it's commonly paired with a
// sibling `index.hbs` inside the same folder.
#nestedColocationLookup(type: string, name: string): Result {
if (type !== 'component') return undefined;

let dir = this.#plural(type);
let target = `${dir}/${name}/index`;
let module = this.#modules.get(target);
if (module) {
return { hit: module };
}
return undefined;
}
}

const fileExtension = /\.\w{1,4}$/;
const leadingDotSlash = /^\.\//;
const camelCaseBoundary = /([a-z\d])([A-Z])/g;
const spacesAndUnderscores = /[ _]/g;

function dasherize(str: string): string {
return str.replace(camelCaseBoundary, '$1_$2').toLowerCase().replace(spacesAndUnderscores, '-');
}

type Result =
| {
hit: any;
}
| undefined;
Loading
Loading