Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
60a4066
Draft RFC: per-deprecation early enablement and deprecation shaking
wagenet Jul 16, 2026
55c9801
Add per-deprecation stage configuration via EmberENV.DEPRECATION_STAGES
wagenet Jul 16, 2026
b27e347
Link shakable deprecations to @ember/deprecated-features flags
wagenet Jul 16, 2026
2655f9e
Wire deprecation shaking into the dist build
wagenet Jul 16, 2026
e84cd53
Add ember-source/deprecation-shaking app plugin with smoke coverage
wagenet Jul 16, 2026
39a0ef6
Refine RFC and guard-convention docs for the two guard shapes
wagenet Jul 16, 2026
e129152
Discuss @embroider/macros in the RFC alternatives
wagenet Jul 16, 2026
1aa3d1e
Resolve RFC open questions on API privacy and factory-family flags
wagenet Jul 16, 2026
8d954da
Address review findings in the shaking tooling
wagenet Jul 16, 2026
0e9959f
Create dist/ before writing deprecation-flags.json
wagenet Jul 17, 2026
a8a30f1
Drop the version-coupled isRemoved assertion from the shaking scenario
wagenet Jul 17, 2026
37b9a39
Let except exclude ids from enable in DEPRECATION_STAGES
wagenet Jul 16, 2026
f342cd0
Route framework class definitions through internal non-deprecating al…
wagenet Jul 16, 2026
a738cc9
Deprecate extend, reopen, reopenClass, and Mixin.create (available st…
wagenet Jul 16, 2026
2cd228b
Deprecate computed properties and observers (available stage)
wagenet Jul 16, 2026
097e23e
Deprecate A() (available stage)
wagenet Jul 16, 2026
e0a3d52
Deprecate ObjectProxy and ArrayProxy (available stage)
wagenet Jul 16, 2026
cc22e75
Document the classic object model deprecation wave
wagenet Jul 16, 2026
2a7961d
Fire the computed-macro deprecation per call, not at barrel eval
wagenet Jul 16, 2026
e496574
Correct the RFC appendix for the call-time macro wrappers
wagenet Jul 16, 2026
191b5a1
Address review findings in the wave-1 deprecations
wagenet Jul 17, 2026
7221f6b
Shield excepted ids from the removal simulation
wagenet Jul 17, 2026
b3e8e64
Keep stage-config registry tests out of production runs
wagenet Jul 17, 2026
7173c1c
Keep reopen call sites tree-shakable via internal statics
wagenet Jul 17, 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
46 changes: 43 additions & 3 deletions .github/workflows/ci-jobs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,29 @@ jobs:
fail-fast: false
matrix:
include:
# The classic object model deprecations are excluded from the
# blanket rows: ember's own test suite (and internal-test-helpers)
# still exercises those APIs pervasively. They get targeted
# coverage in packages/@ember/object/tests instead.
- name: "All deprecations enabled"
ALL_DEPRECATIONS_ENABLED: "true"
ENABLED_DEPRECATIONS: "true"
EXCEPT_DEPRECATIONS: "deprecate-ember-object-extend,deprecate-ember-object-reopen,deprecate-ember-mixins,deprecate-computed-properties,deprecate-observers,deprecate-ember-array,deprecate-object-proxy,deprecate-array-proxy"
- name: "All deprecations enabled, with optional features"
ALL_DEPRECATIONS_ENABLED: "true"
ENABLED_DEPRECATIONS: "true"
EXCEPT_DEPRECATIONS: "deprecate-ember-object-extend,deprecate-ember-object-reopen,deprecate-ember-mixins,deprecate-computed-properties,deprecate-observers,deprecate-ember-array,deprecate-object-proxy,deprecate-array-proxy"
ENABLE_OPTIONAL_FEATURES: "true"
- name: "Deprecation compliance declared"
DEPRECATION_COMPLIANCE: "7.2.0"
RAISE_ON_DEPRECATION: "false"
# The classic ids are excepted here too: except also shields an id
# from the OVERRIDE_DEPRECATION_VERSION removal simulation, and the
# suite exercises the classic APIs pervasively.
- name: "Deprecations as errors"
OVERRIDE_DEPRECATION_VERSION: "15.0.0"
EXCEPT_DEPRECATIONS: "deprecate-ember-object-extend,deprecate-ember-object-reopen,deprecate-ember-mixins,deprecate-computed-properties,deprecate-observers,deprecate-ember-array,deprecate-object-proxy,deprecate-array-proxy"
- name: "Deprecations as errors, with optional features"
OVERRIDE_DEPRECATION_VERSION: "15.0.0"
EXCEPT_DEPRECATIONS: "deprecate-ember-object-extend,deprecate-ember-object-reopen,deprecate-ember-mixins,deprecate-computed-properties,deprecate-observers,deprecate-ember-array,deprecate-object-proxy,deprecate-array-proxy"
ENABLE_OPTIONAL_FEATURES: "true"
- name: "Production build"
BUILD: "production"
Expand All @@ -126,7 +140,9 @@ jobs:
NODE_ENV: ${{ matrix.BUILD || 'development' }}
- name: test
env:
ALL_DEPRECATIONS_ENABLED: ${{ matrix.ALL_DEPRECATIONS_ENABLED }}
ENABLED_DEPRECATIONS: ${{ matrix.ENABLED_DEPRECATIONS }}
EXCEPT_DEPRECATIONS: ${{ matrix.EXCEPT_DEPRECATIONS }}
DEPRECATION_COMPLIANCE: ${{ matrix.DEPRECATION_COMPLIANCE }}
OVERRIDE_DEPRECATION_VERSION: ${{ matrix.OVERRIDE_DEPRECATION_VERSION }}
ENABLE_OPTIONAL_FEATURES: ${{ matrix.ENABLE_OPTIONAL_FEATURES }}
RAISE_ON_DEPRECATION: ${{ matrix.RAISE_ON_DEPRECATION }}
Expand Down Expand Up @@ -204,11 +220,35 @@ jobs:
- name: test
env:
OVERRIDE_DEPRECATION_VERSION: "15.0.0"
# The classic ids are shielded from the removal simulation: the
# app-testing ecosystem (@ember/test-helpers, ember-qunit) still
# uses the classic APIs.
EXCEPT_DEPRECATIONS: "deprecate-ember-object-extend,deprecate-ember-object-reopen,deprecate-ember-mixins,deprecate-computed-properties,deprecate-observers,deprecate-ember-array,deprecate-object-proxy,deprecate-array-proxy"
MATRIX_COMMAND: ${{ matrix.command }}
working-directory: smoke-tests/scenarios
run: |
${MATRIX_COMMAND}

deprecation-shaken-dist:
name: Deprecation-shaken dist
runs-on: ubuntu-latest
needs: [basic-test, lint, types]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: build (standard + shaken variant)
env:
EMBER_DEPRECATION_FLAGS: "all=false"
run: pnpm build:js
- name: assert shaken dist is clean and standard dist stays live
env:
EMBER_DEPRECATION_FLAGS: "all=false"
run: node bin/assert-deprecations-shaken.mjs
- name: dist size report
run: du -sh dist/dev dist/prod dist/deprecation-custom/dev dist/deprecation-custom/prod

node-test:
name: Node.js Tests
runs-on: ubuntu-latest
Expand Down
184 changes: 184 additions & 0 deletions bin/assert-deprecations-shaken.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/* eslint-disable no-console */
/*
Verifies deprecation shaking end-to-end:

1. dist/deprecation-custom/prod (built with EMBER_DEPRECATION_FLAGS
disabling flags) must not contain the disabled flag identifiers nor the
per-flag content markers — proof the guarded code paths were eliminated.
2. dist/prod (the standard build) must keep the flags live: the flags
module exists with every const `true`, consumers import it via the
package self-reference, the identifiers and content markers are present,
and dist/deprecation-flags.json matches the manifest.

Run with --report to print findings without failing.
*/
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const { FLAGS, parseFlagsFromEnv, DEFAULT_FLAGS } = require('../broccoli/deprecated-features.cjs');

const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const report = process.argv.includes('--report');

// Content markers are runtime strings inside a guarded branch. deprecateUntil
// message arguments qualify (deprecateUntil is ordinary code, not a stripped
// debug macro) — but only when the call sits inside the guard; entrypoint-style
// stubs keep their message after shaking. Avoid `assert`/`deprecate` call text
// (stripped in prod) and words that appear in doc comments (comments are
// stripped before matching, but only block comments reliably).
const CONTENT_MARKERS = {
DEPRECATE_COMPARABLE_MIXIN: ['The `Comparable` mixin is deprecated'],
// DEPRECATE_IMPORT_INJECT has no content marker: its deprecateUntil message
// intentionally survives shaking as the throwing stub. The flag identifier
// check still proves the guarded implementation was folded away.
DEPRECATE_IMPORT_INJECT: [],
};

const FLAGS_MODULE_SUFFIX = 'packages/@ember/deprecated-features/index.js';
const SELF_REFERENCE = 'ember-source/@ember/deprecated-features/index.js';

let failures = [];

function stripComments(code) {
return code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
}

function walkJs(dir, found = []) {
for (let entry of readdirSync(dir, { withFileTypes: true })) {
let full = join(dir, entry.name);
if (entry.isDirectory()) {
walkJs(full, found);
} else if (entry.name.endsWith('.js')) {
found.push(full);
}
}
return found;
}

function checkShakenDist(disabledFlags) {
let distDir = join(projectRoot, 'dist/deprecation-custom/prod');
if (!existsSync(distDir)) {
failures.push(`missing ${distDir} — build with EMBER_DEPRECATION_FLAGS first`);
return;
}

for (let file of walkJs(distDir)) {
if (file.endsWith(FLAGS_MODULE_SUFFIX)) {
let code = readFileSync(file, 'utf8');
for (let flag of disabledFlags) {
if (!new RegExp(`${flag}\\s*=\\s*false`).test(code)) {
failures.push(`${file}: expected ${flag} = false in shaken flags module`);
}
}
continue;
}

let code = stripComments(readFileSync(file, 'utf8'));
for (let flag of disabledFlags) {
// A flag name may legitimately survive as a DEPRECATIONS registry key
// (`DEPRECATE_X:`) or property access (`DEPRECATIONS.DEPRECATE_X`) —
// those are the runtime registry, not the folded import. Only a
// standalone binding reference means folding failed.
if (new RegExp(`(?<![.\\w$'"])${flag}\\b(?!\\s*:)`).test(code)) {
failures.push(`${file}: contains disabled flag identifier ${flag}`);
}
for (let marker of CONTENT_MARKERS[flag] ?? []) {
if (code.includes(marker)) {
failures.push(`${file}: contains content marker for ${flag}: "${marker}"`);
}
}
}
}
}

function checkStandardDist() {
let distDir = join(projectRoot, 'dist/prod');
if (!existsSync(distDir)) {
failures.push(`missing ${distDir} — run the standard build first`);
return;
}

let flagsModule = join(distDir, FLAGS_MODULE_SUFFIX);
if (!existsSync(flagsModule)) {
failures.push(`missing ${flagsModule} — flags module must ship in the standard dist`);
} else {
let code = readFileSync(flagsModule, 'utf8');
for (let flag of Object.keys(FLAGS)) {
if (!new RegExp(`${flag}\\s*=\\s*true`).test(code)) {
failures.push(`${flagsModule}: expected ${flag} = true`);
}
}
}

let sawSelfReference = false;
let identifiersSeen = new Set();
let markersSeen = new Set();
for (let file of walkJs(distDir)) {
if (file.endsWith(FLAGS_MODULE_SUFFIX)) continue;
let code = readFileSync(file, 'utf8');
if (code.includes(SELF_REFERENCE)) sawSelfReference = true;
let stripped = stripComments(code);
for (let flag of Object.keys(FLAGS)) {
if (new RegExp(`(?<![.\\w$'"])${flag}\\b(?!\\s*:)`).test(stripped)) identifiersSeen.add(flag);
for (let marker of CONTENT_MARKERS[flag] ?? []) {
if (stripped.includes(marker)) markersSeen.add(marker);
}
}
}

if (!sawSelfReference) {
failures.push(`no module in dist/prod imports ${SELF_REFERENCE} — externalization broke`);
}
for (let flag of Object.keys(FLAGS)) {
if (!identifiersSeen.has(flag)) {
failures.push(`dist/prod: no consumer references flag ${flag}`);
}
for (let marker of CONTENT_MARKERS[flag] ?? []) {
if (!markersSeen.has(marker)) {
failures.push(`dist/prod: content marker missing (stale marker?): "${marker}"`);
}
}
}

let metaPath = join(projectRoot, 'dist/deprecation-flags.json');
if (!existsSync(metaPath)) {
failures.push(`missing ${metaPath}`);
} else {
let meta = JSON.parse(readFileSync(metaPath, 'utf8'));
let expected = Object.entries(FLAGS).map(([name, { id, since, until }]) => ({
const: name,
id,
since,
until,
}));
if (JSON.stringify(meta) !== JSON.stringify(expected)) {
failures.push(`${metaPath} does not match broccoli/deprecated-features.cjs FLAGS manifest`);
}
}
}

let disabledFlags = Object.keys(DEFAULT_FLAGS);
if (process.env.EMBER_DEPRECATION_FLAGS) {
let resolved = parseFlagsFromEnv(process.env.EMBER_DEPRECATION_FLAGS);
disabledFlags = Object.keys(resolved).filter((name) => resolved[name] === false);
}

checkShakenDist(disabledFlags);
checkStandardDist();

if (failures.length > 0) {
console.log(`assert-deprecations-shaken: ${failures.length} problem(s):`);
for (let failure of failures) {
console.log(` - ${failure}`);
}
if (!report) {
throw new Error(`assert-deprecations-shaken found ${failures.length} problem(s)`);
}
} else {
console.log(
`assert-deprecations-shaken: OK (${disabledFlags.length} flag(s) verified shaken; standard dist verified live)`
);
}
85 changes: 85 additions & 0 deletions broccoli/deprecated-features.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict';

// Canonical build-time manifest of shakable deprecations. Each key must match
// both an `export const <KEY> = true` in packages/@ember/deprecated-features
// and a DEPRECATIONS registry key in @ember/-internals/deprecations (a
// conformance test enforces the latter pairing).
const FLAGS = Object.freeze({
DEPRECATE_COMPARABLE_MIXIN: Object.freeze({
id: 'deprecate-comparable-mixin',
since: Object.freeze({ available: '7.2.0', enabled: '7.2.0' }),
until: '7.5.0',
}),
DEPRECATE_IMPORT_INJECT: Object.freeze({
id: 'importing-inject-from-ember-service',
since: Object.freeze({ available: '6.2.0', enabled: '6.3.0' }),
until: '7.0.0',
}),
});

const DEFAULT_FLAGS = Object.freeze(
Object.fromEntries(Object.keys(FLAGS).map((name) => [name, true]))
);

function resolveFlags(overrides = {}) {
for (let [name, value] of Object.entries(overrides)) {
if (!(name in DEFAULT_FLAGS)) {
throw new Error(
`Unknown deprecation flag: ${name}. Valid flags: ${Object.keys(DEFAULT_FLAGS).join(', ')}`
);
}
if (typeof value !== 'boolean') {
throw new Error(`Deprecation flag ${name} must be a boolean, got: ${value}`);
}
}
return { ...DEFAULT_FLAGS, ...overrides };
}

// Parses EMBER_DEPRECATION_FLAGS, e.g.
// "DEPRECATE_COMPARABLE_MIXIN=false,DEPRECATE_IMPORT_INJECT=false", with
// "all=false" as shorthand for disabling every flag.
function parseFlagsFromEnv(value) {
let overrides = {};
for (let entry of value.split(',')) {
let trimmed = entry.trim();
if (trimmed === '') continue;
let match = /^(\w+)=(true|false)$/.exec(trimmed);
if (!match) {
throw new Error(
`Cannot parse EMBER_DEPRECATION_FLAGS entry: "${trimmed}" (expected NAME=true or NAME=false)`
);
}
if (match[1] === 'all') {
for (let name of Object.keys(DEFAULT_FLAGS)) {
overrides[name] = match[2] === 'true';
}
} else {
overrides[match[1]] = match[2] === 'true';
}
}
return resolveFlags(overrides);
}

// babel-plugin-debug-macros tuple that folds @ember/deprecated-features
// imports to boolean literals. Only used for shaken variant builds; the
// standard dist keeps the imports live (externalized) so apps can shake.
function deprecatedFeatures(flags = DEFAULT_FLAGS) {
return [
require.resolve('babel-plugin-debug-macros'),
{
flags: [
{
source: '@ember/deprecated-features',
flags: { ...flags },
},
],
},
'debug-macros:deprecated-features',
];
}

module.exports = deprecatedFeatures;
module.exports.FLAGS = FLAGS;
module.exports.DEFAULT_FLAGS = DEFAULT_FLAGS;
module.exports.resolveFlags = resolveFlags;
module.exports.parseFlagsFromEnv = parseFlagsFromEnv;
20 changes: 20 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@
EmberENV['_OVERRIDE_DEPRECATION_VERSION'] = QUnit.urlParams.OVERRIDE_DEPRECATION_VERSION;
}

if (
QUnit.urlParams.ENABLED_DEPRECATIONS ||
QUnit.urlParams.DEPRECATION_COMPLIANCE ||
QUnit.urlParams.EXCEPT_DEPRECATIONS
) {
EmberENV['DEPRECATION_STAGES'] = {};
if (QUnit.urlParams.ENABLED_DEPRECATIONS) {
EmberENV['DEPRECATION_STAGES'].enable =
QUnit.urlParams.ENABLED_DEPRECATIONS === 'true'
? true
: QUnit.urlParams.ENABLED_DEPRECATIONS.split(',');
}
if (QUnit.urlParams.DEPRECATION_COMPLIANCE) {
EmberENV['DEPRECATION_STAGES'].compliance = QUnit.urlParams.DEPRECATION_COMPLIANCE;
}
if (QUnit.urlParams.EXCEPT_DEPRECATIONS) {
EmberENV['DEPRECATION_STAGES'].except = QUnit.urlParams.EXCEPT_DEPRECATIONS.split(',');
}
}

QUnit.config.urlConfig.push({
id: 'OVERRIDE_DEPRECATION_VERSION',
value: ['20.0.0', '6.0.0', '5.12.0'],
Expand Down
Loading
Loading