Skip to content

Commit 188c4fe

Browse files
committed
don't use resolver to instantiate route classes
1 parent 00b42fd commit 188c4fe

6 files changed

Lines changed: 171 additions & 67 deletions

File tree

packages/@ember/-internals/container/lib/container.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import type {
66
FactoryManager,
77
FullName,
88
} from '@ember/-internals/owner';
9-
import { setOwner } from '@ember/-internals/owner';
9+
import { getOwner, setOwner } from '@ember/-internals/owner';
1010
import { dictionary } from '@ember/-internals/utils';
1111
import { assert } from '@ember/debug';
1212
import { DEBUG } from '@glimmer/env';
1313
import type { DebugRegistry } from './registry';
1414
import type Registry from './registry';
15+
import type EmberRouter from '@ember/routing/router';
1516

1617
interface LeakTracking {
1718
hasContainers(): boolean;
@@ -282,6 +283,23 @@ function lookup(
282283
): InternalFactory<object> | object | undefined {
283284
let normalizedName = fullName;
284285

286+
if (normalizedName.startsWith('route:') && container.owner) {
287+
let router = container.owner.lookup('router:main') as EmberRouter;
288+
let routeName = normalizedName.split('route:')[1];
289+
if (routeName !== undefined) {
290+
// For the main app container, pass the name directly — EmberRouter.getRoute
291+
// resolves engine vs app routes internally via _engineInfoByRoute.
292+
// For engine containers (owner !== router's owner), pass the engine owner
293+
// explicitly so EmberRouter.getRoute uses it directly without a name lookup.
294+
const isEngineContainer = getOwner(router) !== container.owner;
295+
const engineOwner = isEngineContainer ? container.owner : undefined;
296+
return router.getRoute(routeName, engineOwner) as
297+
| InternalFactory<object>
298+
| object
299+
| undefined;
300+
}
301+
}
302+
285303
if (
286304
options.singleton === true ||
287305
(options.singleton === undefined && isSingleton(container, fullName))

packages/@ember/-internals/routing/route-managers/classic-route-manager.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getOwner } from '@ember/-internals/owner';
1+
import { getOwner, setOwner } from '@ember/-internals/owner';
22
import { assert, info } from '@ember/debug';
33
import { get } from '@ember/-internals/metal';
44
import { DEBUG } from '@glimmer/env';
@@ -10,6 +10,7 @@ import { createCapturedArgs, curry, EMPTY_POSITIONAL } from '@glimmer/runtime';
1010
import { dict } from '@glimmer/util';
1111
import { tracked } from '@glimmer/tracking';
1212
import { makeRouteTemplate } from '@ember/-internals/glimmer/lib/component-managers/route-template';
13+
import type Owner from '@ember/owner';
1314
import type Route from '@ember/routing/route';
1415
import type {
1516
RouteManager,
@@ -69,8 +70,16 @@ export interface ClassicInteropArgs {
6970
export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
7071
capabilities: RouteCapabilities = routeCapabilities('1.0', { classicInterop: true });
7172

72-
createRoute(routeInstance: object, args: CreateRouteArgs): ClassicRouteBucket {
73-
let route = routeInstance as Route;
73+
#owner: Owner;
74+
75+
constructor(owner: Owner) {
76+
this.#owner = owner;
77+
}
78+
79+
createRoute(RouteClass: typeof Route, args: CreateRouteArgs): ClassicRouteBucket {
80+
let props = {};
81+
setOwner(props, this.#owner);
82+
let route = RouteClass.create(props);
7483
route._setRouteName(args.name);
7584

7685
return new ClassicRouteBucket(route, args);

packages/@ember/-internals/routing/route-managers/utils.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import type { Owner } from '@glimmer/interfaces';
21
import type { ManagerFactory, RouteManager } from './route-manager';
2+
import { DEBUG } from '@glimmer/env';
3+
import { debugAssert } from '@glimmer/global-context';
34

45
export interface RouteStateBucket {
6+
invokable?: object;
57
instance?: unknown;
68
args: object;
79
}
810

9-
const ROUTE_MANAGERS = new WeakMap<object, RouteManager<RouteStateBucket>>();
11+
const ROUTE_MANAGERS = new WeakMap<object, ManagerFactory<any, RouteManager<any>>>();
1012

1113
/**
1214
* There is also Reflect.getPrototypeOf,
@@ -22,14 +24,31 @@ function setManager<Def extends object>(
2224
manager: object,
2325
obj: Def
2426
): Def {
27+
if (DEBUG) {
28+
debugAssert(
29+
obj !== null && (typeof obj === 'object' || typeof obj === 'function'),
30+
31+
`Attempted to set a manager on a non-object value. Managers can only be associated with objects or functions. Value was ${
32+
obj
33+
}`
34+
);
35+
36+
debugAssert(
37+
!map.has(obj),
38+
`Attempted to set the same type of manager multiple times on a value. You can only associate one manager of each type with a given value. Value was ${
39+
obj
40+
}`
41+
);
42+
}
43+
2544
map.set(obj, manager);
2645
return obj;
2746
}
2847

29-
function getManager<M extends RouteManager<RouteStateBucket>>(
48+
function getManager<M extends ManagerFactory<any, RouteManager<RouteStateBucket>>>(
3049
map: WeakMap<object, M>,
3150
obj: object
32-
): M | undefined {
51+
): ManagerFactory<any, RouteManager<RouteStateBucket>> | undefined {
3352
let pointer: object | null = obj;
3453
while (pointer !== null) {
3554
const manager = map.get(pointer);
@@ -44,22 +63,30 @@ function getManager<M extends RouteManager<RouteStateBucket>>(
4463
return undefined;
4564
}
4665

47-
export function setRouteManager<O extends Owner, T extends object | Function>(
48-
factory: ManagerFactory<O | undefined, RouteManager<RouteStateBucket>>,
66+
export function setRouteManager<T extends object>(
67+
factory: ManagerFactory<any, RouteManager<RouteStateBucket>>,
4968
definition: T
5069
): T {
51-
let manager = factory(undefined);
52-
setManager(ROUTE_MANAGERS, manager, definition);
70+
setManager(ROUTE_MANAGERS, factory, definition);
5371

5472
return definition;
5573
}
5674

5775
export function getRouteManager<T extends object>(
5876
definition: T
59-
): RouteManager<RouteStateBucket> | undefined {
60-
if (!definition) {
61-
return undefined;
77+
): ManagerFactory<any, RouteManager<RouteStateBucket>> | undefined {
78+
let factory = getManager(ROUTE_MANAGERS, definition);
79+
80+
if (factory === undefined) {
81+
if (DEBUG) {
82+
debugAssert(
83+
false,
84+
`Attempted to load a route, but there wasn't a route manager associated with the definition. The definition was: ${
85+
definition
86+
}`
87+
);
88+
}
6289
}
6390

64-
return getManager(ROUTE_MANAGERS, definition);
91+
return factory;
6592
}

packages/@ember/routing/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
export { LinkTo } from '@ember/-internals/glimmer';
2+
export { setRouteManager } from '@ember/-internals/routing/route-managers/utils';
3+
export { ClassicRouteManager } from '@ember/-internals/routing/route-managers/classic-route-manager';
4+
export { routeCapabilities } from '@ember/-internals/routing/route-managers/route-manager';
5+
6+
import type { RouteStateBucket } from '@ember/-internals/routing';
7+
import type { RouteManager as InternalRouteManager } from '@ember/-internals/routing/route-managers/route-manager';
8+
9+
export type RouteManager = InternalRouteManager<RouteStateBucket>;

packages/@ember/routing/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2121,4 +2121,4 @@ export default Route;
21212121
import { setRouteManager } from '@ember/-internals/routing/route-managers/utils';
21222122
import { ClassicRouteManager } from '@ember/-internals/routing/route-managers/classic-route-manager';
21232123

2124-
setRouteManager(() => new ClassicRouteManager(), Route);
2124+
setRouteManager((owner) => new ClassicRouteManager(owner), Route);

packages/@ember/routing/router.ts

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
getFullQueryParams,
3535
hasDefaultSerialize,
3636
} from '@ember/routing/route';
37-
import { getRouteManager } from '@ember/-internals/routing/route-managers/utils';
37+
import { getRouteManager, RouteStateBucket } from '@ember/-internals/routing/route-managers/utils';
3838
import type {
3939
InternalRouteInfo,
4040
ModelFor,
@@ -51,6 +51,7 @@ import type { QueryParams } from 'route-recognizer';
5151
import type { AnyFn, MethodNamesOf, OmitFirst } from '@ember/-internals/utility-types';
5252
import type { Template } from '@glimmer/interfaces';
5353
import type ApplicationInstance from '@ember/application/instance';
54+
import { RouteManager } from '@ember/-internals/routing/route-managers/route-manager';
5455

5556
/**
5657
@module @ember/routing/router
@@ -312,71 +313,112 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented {
312313
this._routerService = routerService;
313314
}
314315

315-
_initRouterJs(): void {
316-
let location = get(this, 'location') as EmberLocation;
317-
let router = this;
318-
const owner = getOwner(this);
319-
assert('Router is unexpectedly missing an owner', owner);
320-
let seen = Object.create(null);
316+
// Both routes and manager instances are keyed first by owner so that engine routes
317+
// (which share local names like 'application' with the main app) are isolated.
318+
#routes = new WeakMap<object, Map<string, RouteStateBucket>>();
319+
#routeManagerInstances = new WeakMap<object, WeakMap<Function, RouteManager<RouteStateBucket>>>();
321320

322-
class PrivateRouter extends Router<Route> {
323-
getRoute(name: string): Route {
324-
let routeName = name;
325-
let routeOwner = owner;
326-
let engineInfo = router._engineInfoByRoute[routeName];
321+
getRoute(name: string, engineOwner?: Owner) {
322+
if (name === 'undefined') {
323+
return;
324+
}
327325

328-
if (engineInfo) {
329-
let engineInstance = router._getEngineInstance(engineInfo);
326+
assert('Name should not start with "route:"', !name.startsWith('route:'));
330327

331-
routeOwner = engineInstance;
332-
routeName = engineInfo.localFullName;
333-
}
328+
// Resolve the owner and local route name.
329+
// If engineOwner is provided (from the container bypass for an engine container),
330+
// use it directly — the name is already the local route name within that engine.
331+
// Otherwise, check _engineInfoByRoute to resolve engine routes by fully-qualified name.
332+
let routeName = name;
333+
let routeOwner: Owner;
334334

335-
let fullRouteName = `route:${routeName}` as const;
336-
337-
assert('Route is unexpectedly missing an owner', routeOwner);
335+
if (engineOwner) {
336+
routeOwner = engineOwner;
337+
} else {
338+
routeOwner = getOwner(this)!;
339+
assert('BUG: Missing owner', routeOwner);
338340

339-
let route = routeOwner.lookup(fullRouteName) as Route | undefined;
341+
const engineInfo = this._engineInfoByRoute[routeName];
342+
if (engineInfo) {
343+
routeOwner = this._getEngineInstance(engineInfo);
344+
routeName = engineInfo.localFullName;
345+
}
346+
}
340347

341-
if (seen[name]) {
342-
assert('seen routes should exist', route);
343-
return route;
344-
}
348+
// Per-owner route cache.
349+
let ownerRoutes = this.#routes.get(routeOwner);
350+
if (!ownerRoutes) {
351+
ownerRoutes = new Map();
352+
this.#routes.set(routeOwner, ownerRoutes);
353+
}
345354

346-
seen[name] = true;
355+
if (!ownerRoutes.has(routeName)) {
356+
let fullRouteName = `route:${routeName}` as const;
357+
let factoryManager = routeOwner.factoryFor(fullRouteName);
347358

348-
if (!route) {
349-
// SAFETY: this is configured in `commonSetupRegistry` in the
350-
// `@ember/application/lib` package.
351-
let DefaultRoute: any = routeOwner.factoryFor('route:basic')!.class;
352-
routeOwner.register(fullRouteName, class extends DefaultRoute {});
353-
route = routeOwner.lookup(fullRouteName) as Route;
359+
// Auto-generate a default route if none is registered.
360+
if (!factoryManager) {
361+
let DefaultRoute: any = routeOwner.factoryFor('route:basic')!.class;
362+
routeOwner.register(fullRouteName, class extends DefaultRoute {});
363+
factoryManager = routeOwner.factoryFor(fullRouteName);
354364

355-
if (DEBUG) {
356-
if (router.namespace.LOG_ACTIVE_GENERATION) {
357-
info(`generated -> ${fullRouteName}`, { fullName: fullRouteName });
358-
}
365+
if (DEBUG) {
366+
if (this.namespace.LOG_ACTIVE_GENERATION) {
367+
info(`generated -> ${fullRouteName}`, { fullName: fullRouteName });
359368
}
360369
}
370+
}
361371

362-
// Look up the route manager and create a bucket.
363-
// This activates the manager-driven code paths in router_js.
364-
let manager = getRouteManager(route.constructor);
365-
assert(`Route ${fullRouteName} does not have a manager`, manager);
372+
assert('BUG: Missing factory for route', factoryManager);
373+
let RouteClass = factoryManager.class;
366374

367-
let bucket = manager.createRoute(route, { name: routeName });
368-
route.manager = manager;
375+
// Engine routes must not define a custom serialize method.
376+
const isEngineRoute = routeOwner !== getOwner(this);
377+
if (isEngineRoute && !hasDefaultSerialize(RouteClass.prototype as any)) {
378+
throw new Error(
379+
'Defining a custom serialize method on an Engine route is not supported.'
380+
);
381+
}
369382

370-
assert('Route manager should return a bucket', bucket);
371-
route.bucket = bucket;
383+
// Look up the manager factory function via prototype walk on the class.
384+
let managerFactory = getRouteManager(RouteClass);
385+
assert('Route manager needs to be defined', managerFactory !== undefined);
386+
387+
// Instantiate the manager once per owner. Keyed first by owner so engine routes
388+
// get their own manager instances, then by factory function so all subclasses
389+
// sharing the same factory share one manager instance within that owner.
390+
let ownerManagerInstances = this.#routeManagerInstances.get(routeOwner);
391+
if (!ownerManagerInstances) {
392+
ownerManagerInstances = new WeakMap();
393+
this.#routeManagerInstances.set(routeOwner, ownerManagerInstances);
394+
}
395+
if (!ownerManagerInstances.has(managerFactory)) {
396+
ownerManagerInstances.set(managerFactory, managerFactory(routeOwner));
397+
}
398+
let routeManagerInstance = ownerManagerInstances.get(managerFactory)!;
372399

373-
if (engineInfo && !hasDefaultSerialize(route)) {
374-
throw new Error(
375-
'Defining a custom serialize method on an Engine route is not supported.'
376-
);
377-
}
400+
// Pass the concrete class to createRoute — the manager is responsible for instantiation.
401+
let routeStateBucket = routeManagerInstance.createRoute(RouteClass, { name: routeName });
402+
403+
assert('Failed to create Route instance', routeStateBucket.instance);
404+
(routeStateBucket.instance as any).bucket = routeStateBucket;
405+
(routeStateBucket.instance as any).manager = routeManagerInstance;
406+
ownerRoutes.set(routeName, routeStateBucket as RouteStateBucket);
407+
}
378408

379-
return route;
409+
return ownerRoutes.get(routeName)!.instance;
410+
}
411+
412+
_initRouterJs(): void {
413+
let location = get(this, 'location') as EmberLocation;
414+
let router = this;
415+
const owner = getOwner(this);
416+
assert('Router is unexpectedly missing an owner', owner);
417+
class PrivateRouter extends Router<Route> {
418+
getRoute(name: string): Route {
419+
// All route instantiation and caching is owned by EmberRouter.getRoute,
420+
// which resolves engine vs main-app owner internally.
421+
return router.getRoute(name) as Route;
380422
}
381423

382424
getSerializer(name: string) {

0 commit comments

Comments
 (0)