diff --git a/.gitignore b/.gitignore index cb26594..0ccef3d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,6 @@ ember-cli-build.cjs # deps & caches node_modules/ +package-lock.json .eslintcache .prettiercache diff --git a/src/index.js b/src/index.js index 0b51874..b2a4cf0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,22 @@ +/* global QUnit */ import { registerDeprecationHandler } from '@ember/debug'; +import { VERSION } from '@ember/version'; const LOG_LIMIT = 100; +// Number of minor versions before an upcoming `until` that counts as "approaching" +const APPROACHING_MINOR_WINDOW = 2; + +// Minimum current minor version to be considered approaching the next major version. +// For example, if minor releases go 0–9, versions X.8 and X.9 are within the window. +const CROSS_MAJOR_MINOR_THRESHOLD = 10 - APPROACHING_MINOR_WINDOW; + export default function setupDeprecationWorkflow(config) { self.deprecationWorkflow = self.deprecationWorkflow || {}; self.deprecationWorkflow.deprecationLog = { messages: new Set(), }; + self.deprecationWorkflow.pressingSilenced = new Set(); registerDeprecationHandler((message, options, next) => handleDeprecationWorkflow(config, message, options, next), @@ -16,6 +26,44 @@ export default function setupDeprecationWorkflow(config) { self.deprecationWorkflow.flushDeprecations = (options) => flushDeprecations({ config, ...options }); + + if (typeof QUnit !== 'undefined') { + let pressingSilenced = self.deprecationWorkflow.pressingSilenced; + QUnit.done(() => { + let count = pressingSilenced.size; + if (count > 0) { + console.warn(`Deprecation Workflow: ${count} deprecation(s) silenced.`); + } + }); + } +} + +export function isApproaching(until, currentVersion = VERSION) { + const untilParts = String(until ?? '').split('.'); + const untilMajor = parseInt(untilParts[0], 10); + + if (isNaN(untilMajor)) return false; + + const untilMinor = parseInt(untilParts[1] ?? '0', 10); + + const currentParts = String(currentVersion).split('.'); + const currentMajor = parseInt(currentParts[0], 10); + const currentMinor = parseInt(currentParts[1] ?? '0', 10); + + if (untilMajor === currentMajor + 1) { + // Crossing to the next major: approaching if we're in the last few minor releases + return currentMinor >= CROSS_MAJOR_MINOR_THRESHOLD; + } + + if (untilMajor === currentMajor) { + // Same major: approaching if within the minor window + return ( + untilMinor - currentMinor <= APPROACHING_MINOR_WINDOW && + untilMinor >= currentMinor + ); + } + + return false; } function matchesWorkflow(matcher, value) { @@ -76,9 +124,17 @@ export function handleDeprecationWorkflow(config, message, options, next) { } } else { switch (matchingWorkflow.handler) { - case 'silence': - // no-op + case 'silence': { + if ( + !options || + options.for !== 'ember-source' || + !isApproaching(options.until) + ) + break; + let key = options.id || message; + self.deprecationWorkflow.pressingSilenced.add(key); break; + } case 'log': { let key = (options && options.id) || message; diff --git a/tests/unit/handle-deprecation-workflow-test.js b/tests/unit/handle-deprecation-workflow-test.js index 3e1499d..ba0d09d 100644 --- a/tests/unit/handle-deprecation-workflow-test.js +++ b/tests/unit/handle-deprecation-workflow-test.js @@ -386,4 +386,165 @@ module('handleDeprecationWorkflow', function (hooks) { ); }, 'deprecation throws'); }); + + test('pressing ember-source deprecation is tracked in pressingSilenced Set', function (assert) { + assert.expect(2); + + self.deprecationWorkflow.pressingSilenced = new Set(); + + const config = { + workflow: [{ matchId: /^ember\..*/, handler: 'silence' }], + }; + + handleDeprecationWorkflow( + config, + 'Some ember deprecation', + { + id: 'ember.some-feature', + since: '6.0.0', + until: '7.0', + for: 'ember-source', + }, + () => {}, + ); + + assert.strictEqual( + self.deprecationWorkflow.pressingSilenced.size, + 1, + 'pressing ember-source deprecation is added to the Set', + ); + assert.ok( + self.deprecationWorkflow.pressingSilenced.has('ember.some-feature'), + 'Set contains the deprecation id', + ); + }); + + test('pressing ember-source deprecation is only counted once per unique id', function (assert) { + assert.expect(1); + + self.deprecationWorkflow.pressingSilenced = new Set(); + + const config = { + workflow: [{ matchId: /^ember\..*/, handler: 'silence' }], + }; + + const options = { + id: 'ember.some-feature', + since: '6.0.0', + until: '7.0', + for: 'ember-source', + }; + + handleDeprecationWorkflow( + config, + 'Some ember deprecation', + options, + () => {}, + ); + handleDeprecationWorkflow( + config, + 'Some ember deprecation', + options, + () => {}, + ); + handleDeprecationWorkflow( + config, + 'Some ember deprecation', + options, + () => {}, + ); + + assert.strictEqual( + self.deprecationWorkflow.pressingSilenced.size, + 1, + 'Set contains only one entry after repeated firings of the same deprecation', + ); + }); + + test('multiple distinct pressing ember-source deprecations are all tracked', function (assert) { + assert.expect(1); + + self.deprecationWorkflow.pressingSilenced = new Set(); + + const config = { + workflow: [{ matchId: /^ember\..*/, handler: 'silence' }], + }; + + handleDeprecationWorkflow( + config, + 'First ember deprecation', + { id: 'ember.first', since: '6.0.0', until: '7.0', for: 'ember-source' }, + () => {}, + ); + + handleDeprecationWorkflow( + config, + 'Second ember deprecation', + { id: 'ember.second', since: '6.0.0', until: '7.0', for: 'ember-source' }, + () => {}, + ); + + assert.strictEqual( + self.deprecationWorkflow.pressingSilenced.size, + 2, + 'Set contains both deprecation ids', + ); + }); + + test('deprecation silenced for ember-source with non-approaching until is not tracked', function (assert) { + assert.expect(1); + + self.deprecationWorkflow.pressingSilenced = new Set(); + + const config = { + workflow: [{ matchId: 'ember.far-future', handler: 'silence' }], + }; + + // 8.0 is not approaching from 6.11.0 (two major versions ahead) + handleDeprecationWorkflow( + config, + 'Far future ember deprecation', + { + id: 'ember.far-future', + since: '6.0.0', + until: '8.0', + for: 'ember-source', + }, + () => {}, + ); + + assert.strictEqual( + self.deprecationWorkflow.pressingSilenced.size, + 0, + 'non-approaching deprecation is not tracked', + ); + }); + + test('deprecation silenced for non-ember-source is not tracked', function (assert) { + assert.expect(1); + + self.deprecationWorkflow.pressingSilenced = new Set(); + + const config = { + workflow: [{ matchId: 'some-addon.feature', handler: 'silence' }], + }; + + handleDeprecationWorkflow( + config, + 'Some addon deprecation', + { + id: 'some-addon.feature', + since: '1.0.0', + until: '7.0', + for: 'some-addon', + }, + () => {}, + ); + + assert.strictEqual( + self.deprecationWorkflow.pressingSilenced.size, + 0, + 'non-ember-source deprecation is not tracked', + ); + }); }); diff --git a/tests/unit/is-approaching-test.js b/tests/unit/is-approaching-test.js new file mode 100644 index 0000000..c8c2db5 --- /dev/null +++ b/tests/unit/is-approaching-test.js @@ -0,0 +1,86 @@ +import { module, test } from 'qunit'; +import { isApproaching } from '#src/index.js'; + +module('isApproaching', function () { + test('returns true when until is the next major and current minor is >= 8', function (assert) { + assert.true( + isApproaching('8.0', '7.8.0'), + '7.8 approaches 8.0 (exactly 2 minor steps away)', + ); + assert.true( + isApproaching('8.0', '7.9.0'), + '7.9 approaches 8.0 (1 minor step away)', + ); + assert.true( + isApproaching('8.0.0', '7.8.0'), + 'handles 3-part until version', + ); + assert.true( + isApproaching('8.0', '7.10.0'), + '7.10 approaches 8.0 (minor >= 8)', + ); + }); + + test('returns false when until is the next major but current minor is < 8', function (assert) { + assert.false( + isApproaching('8.0', '7.7.0'), + '7.7 does not approach 8.0 (more than 2 minor steps away)', + ); + assert.false(isApproaching('8.0', '7.0.0'), '7.0 does not approach 8.0'); + }); + + test('returns true when until is within 2 minor versions in the same major', function (assert) { + assert.true( + isApproaching('7.3', '7.1.0'), + '7.1 approaches 7.3 (2 minors away)', + ); + assert.true( + isApproaching('7.2', '7.1.0'), + '7.1 approaches 7.2 (1 minor away)', + ); + assert.true( + isApproaching('7.1', '7.1.0'), + '7.1 approaches 7.1 (same version - overdue)', + ); + }); + + test('returns false when until is more than 2 minor versions ahead in same major', function (assert) { + assert.false( + isApproaching('7.4', '7.1.0'), + '7.1 does not approach 7.4 (3 minors away)', + ); + assert.false(isApproaching('7.10', '7.1.0'), '7.1 does not approach 7.10'); + }); + + test('returns false when until is 2+ majors ahead', function (assert) { + assert.false( + isApproaching('9.0', '7.9.0'), + '7.9 does not approach 9.0 (2 majors away)', + ); + assert.false(isApproaching('10.0', '7.9.0'), '7.9 does not approach 10.0'); + }); + + test('returns false when until is not a valid version', function (assert) { + assert.false( + isApproaching('forever', '7.9.0'), + 'forever is not approaching', + ); + assert.false(isApproaching(null, '7.9.0'), 'null is not approaching'); + assert.false( + isApproaching(undefined, '7.9.0'), + 'undefined is not approaching', + ); + assert.false(isApproaching('', '7.9.0'), 'empty string is not approaching'); + }); + + test('returns false when until is in the past (older major)', function (assert) { + assert.false( + isApproaching('6.0', '7.9.0'), + 'past version is not approaching', + ); + assert.false( + isApproaching('5.0', '7.9.0'), + 'much older version is not approaching', + ); + }); +}); diff --git a/tests/unit/setup-deprecation-workflow-test.js b/tests/unit/setup-deprecation-workflow-test.js new file mode 100644 index 0000000..657aafe --- /dev/null +++ b/tests/unit/setup-deprecation-workflow-test.js @@ -0,0 +1,117 @@ +/* eslint no-console: 0 */ +/* global QUnit */ + +import { module } from 'qunit'; +import test from '../helpers/debug-test'; +import setupDeprecationWorkflow from '#src/index.js'; + +let originalWarn; + +module('setupDeprecationWorkflow', function (hooks) { + hooks.beforeEach(function () { + originalWarn = console.warn; + self.deprecationWorkflow = undefined; + }); + + hooks.afterEach(function () { + console.warn = originalWarn; + }); + + test('initializes pressingSilenced as an empty Set', function (assert) { + setupDeprecationWorkflow({ + workflow: [ + { handler: 'silence', matchId: 'first' }, + { handler: 'silence', matchId: 'second' }, + ], + }); + + assert.ok( + self.deprecationWorkflow.pressingSilenced instanceof Set, + 'pressingSilenced is a Set', + ); + assert.strictEqual( + self.deprecationWorkflow.pressingSilenced.size, + 0, + 'pressingSilenced starts empty', + ); + }); + + test('does not log at setup time', function (assert) { + assert.expect(1); + + let warnMessages = []; + console.warn = function (message) { + warnMessages.push(message); + }; + + setupDeprecationWorkflow({ + workflow: [ + { handler: 'silence', matchId: 'first' }, + { handler: 'silence', matchId: 'second' }, + { handler: 'log', matchId: 'third' }, + ], + }); + + assert.strictEqual( + warnMessages.length, + 0, + 'no warnings are emitted at setup time', + ); + }); + + test('registers a QUnit.done callback that logs silenced pressing deprecation count', function (assert) { + assert.expect(2); + + let registeredCallback; + let originalQUnitDone = QUnit.done; + QUnit.done = function (callback) { + registeredCallback = callback; + }; + + let warnMessages = []; + console.warn = function (message) { + warnMessages.push(message); + }; + + setupDeprecationWorkflow({}); + QUnit.done = originalQUnitDone; + + // Simulate two pressing silenced deprecations discovered during the run + self.deprecationWorkflow.pressingSilenced.add('ember.first'); + self.deprecationWorkflow.pressingSilenced.add('ember.second'); + + assert.ok(registeredCallback, 'QUnit.done was called with a callback'); + registeredCallback(); + assert.ok( + warnMessages.some((m) => m.includes('2 deprecation(s) silenced')), + 'QUnit.done callback logs the count of pressing silenced deprecations', + ); + }); + + test('QUnit.done callback logs nothing when no pressing deprecations are silenced', function (assert) { + assert.expect(1); + + let registeredCallback; + let originalQUnitDone = QUnit.done; + QUnit.done = function (callback) { + registeredCallback = callback; + }; + + let warnMessages = []; + console.warn = function (message) { + warnMessages.push(message); + }; + + setupDeprecationWorkflow({}); + QUnit.done = originalQUnitDone; + + // No pressing silenced deprecations + registeredCallback(); + + assert.strictEqual( + warnMessages.length, + 0, + 'QUnit.done callback logs nothing when pressingSilenced is empty', + ); + }); +});