Skip to content

RFC#1234 & RFC#1247: Base Route on a plain class with EMBER_DROP_CLASSIC_FEATURES - #21739

Draft
NullVoxPopuli-ai-agent wants to merge 2 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/plain-route
Draft

NullVoxPopuli-ai-agent wants to merge 2 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/plain-route

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Merge #21726 first. This PR sits on top of it. Review only the last commit, ccc024be55.

With EMBER_DROP_CLASSIC_FEATURES=true, Route from @ember/routing/route no longer extends EmberObject or the ActionHandler mixin. It extends a private plain base class, which extends no other class.

import Route from '@ember/routing/route';
import { service } from '@ember/service';

export default class PostsRoute extends Route {
  @service store;

  queryParams = {
    page: { refreshModel: true },
  };

  constructor(owner) {
    super(owner);
    this.cache = new Map();
  }

  model({ page }) {
    return this.store.query('post', { page });
  }

  willDestroy() {
    super.willDestroy(...arguments);
    this.cache.clear();
  }
}

Behavior with the flag

  • The owner passes itself to the constructor. getOwner(this) and @service work in the constructor.
  • Model hooks, queryParams, @action handlers and send work as before.
  • willDestroy() stays the teardown hook. It runs once, when the owner is destroyed or when route.destroy() is called. isDestroyed(route) is then true.
  • init, get, set, extend, reopen, isDestroying and isDestroyed do not exist on the class.
  • A static create stays, because the container creates every instance through it. A route without an owner is allowed, as on the classic class.

Without the flag, Route does not change. A cast keeps the published type as the classic class, so apps get no new type errors.

Design

The class body is the same in both modes. Only the base class changes with the flag. Two parts of the classic class needed a plain equivalent:

Classic part With the flag
Route.reopen({...}) sets queryParams, templateName, controllerName, send and actions The same object goes on Route.prototype
send asserts on this.isDestroying and this.isDestroyed send uses isDestroying() and isDestroyed() from @glimmer/destroyable, in both modes

The @computed getters _qp and _store and the get() calls in the class body work on plain objects, so they did not change.

The flag build does not merge queryParams across subclasses. The merge comes from Route.extend(), which does not exist with the flag. A native subclass field replaces the queryParams of its parent in both modes.

The plain base has a destroy() method, which calls the destroy function of @glimmer/destroyable, and an empty willDestroy() method. If an instance overrides willDestroy, the constructor registers it as a destructor. Route overrides willDestroy to tear down its views, so that teardown also runs in the flag build.

Tests

Change Why
New @ember/routing/tests/system/route_drop_classic_features_test.js Checks the plain base: no EmberObject, owner in the constructor, willDestroy on both destroy paths, the view teardown of Route, and a route with a model hook and query params in an application. Registers only with the flag.
New ember/tests/without-EMBER_DROP_CLASSIC_FEATURES/route_test.js Checks the classic API in builds without the flag: the EmberObject base, init, properties from create, the queryParams merge of Route.extend, get/set, willDestroy, the destroy flags, and this._super in a hook. Registers only without the flag.
init() to constructor(owner) in Route subclasses of six test files Works on both base classes
this._super(...arguments) to super.setupController(...arguments) Works on both base classes
The route unit tests set properties after create(), read with get(), and destroy with the destroy function Works on both base classes
The test of the queryParams array assertion skips with the flag The assertion comes from the merge of Route.extend()

Checks

Mode Result
Default 9241 tests, 0 failures
ALL_DEPRECATIONS_ENABLED=true 9241 tests, 0 failures
OVERRIDE_DEPRECATION_VERSION=15.0.0 8264 tests, 0 failures
Production build 9078 tests, 0 failures
EMBER_DROP_CLASSIC_FEATURES=true 9240 tests, 0 failures

The type checks of the internals and the published types pass, with and without stable decorators. eslint, prettier and the docs check pass.

🤖 Generated with Claude Code

@NullVoxPopuli
NullVoxPopuli marked this pull request as draft October 17, 2026 19:56
NullVoxPopuli and others added 2 commits October 17, 2026 16:06
`import.meta.env?.EMBER_DROP_CLASSIC_FEATURES` is a build-time flag for
RFC 1247. Later changes read it where a classic feature is defined, and
remove that feature from builds that set the flag.

- `ImportMetaEnv` declares the flag for the type checks.
- `EMBER_DROP_` is in the Vite `envPrefix`, so
  `EMBER_DROP_CLASSIC_FEATURES=true pnpm vite build` sets it for the tests.
- CI has a "Classic features dropped" variant that builds and tests with
  the flag.
- The hello-world smoke test sets the flag with a Vite `define` entry, so
  the package-size workflow measures an app without the classic features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the flag, `Route` from `@ember/routing/route` extends a private plain
base class, not `EmberObject` and the `ActionHandler` mixin. The class body
is the same in both modes. The base sets the owner in the constructor and keeps
a static `create` for the container. A route without an owner is allowed,
as before.

`willDestroy` stays the teardown hook. The base has a `destroy()` method
that calls the `destroy` function of `@glimmer/destroyable`, and an empty
`willDestroy()`. Only a subclass that overrides `willDestroy` gets a
destructor for it. `Route` overrides it to call `teardownViews`, so that
runs with the flag too.

The defaults that `Route.reopen` set (`queryParams`, `templateName`,
`controllerName`, `send` and `actions`) go on the prototype with the flag.
Without the flag, `reopen` still sets them and merges `queryParams`. `send`
reads the destroy state through `@glimmer/destroyable`, so it works on both
bases. A cast keeps the published types.

Tests:
- Route subclasses in tests use a constructor, not `init()`, and `super`,
  not `this._super`.
- The route unit tests set properties after `create()`, read with `get()`,
  and destroy with the `destroy` function.
- The test of the `queryParams` array assertion skips with the flag. The
  assertion comes from the merge of `Route.extend()`.
- A new flag-only module checks the plain base.
- A new suite in `packages/ember/tests/without-EMBER_DROP_CLASSIC_FEATURES`
  checks the classic API in builds without the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent changed the title Base Route on a plain class with EMBER_DROP_CLASSIC_FEATURES RFC#1234 & RFC#1247: Base Route on a plain class with EMBER_DROP_CLASSIC_FEATURES Oct 17, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants