diff --git a/.try.mjs b/.try.mjs index 61b4553c..359db3f0 100644 --- a/.try.mjs +++ b/.try.mjs @@ -26,10 +26,24 @@ const compatDeps = { export default { scenarios: [ { - name: 'ember-lts-5.8', + name: 'ember-lts-3.28', npm: { devDependencies: { - 'ember-source': '~5.8.0', + 'ember-source': '~3.28.0', + ...compatDeps, + 'ember-cli': '^4.12', + }, + }, + env: { + ENABLE_COMPAT_BUILD: true, + }, + files: compatFiles, + }, + { + name: 'ember-lts-4.12', + npm: { + devDependencies: { + 'ember-source': '~4.12.0', ...compatDeps, }, }, @@ -59,6 +73,22 @@ export default { }, }, }, + { + name: 'ember-lts-6.8', + npm: { + devDependencies: { + 'ember-source': 'npm:ember-source@~6.8.0', + }, + }, + }, + { + name: 'ember-lts-6.12', + npm: { + devDependencies: { + 'ember-source': 'npm:ember-source@~6.12.0', + }, + }, + }, { name: 'ember-latest', npm: { diff --git a/package.json b/package.json index f56f27de..c9efd0bd 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "eslint-plugin-ember": "^12.7.5", "eslint-plugin-import": "^2.32.0", "eslint-plugin-n": "^17.24.0", - "globals": "^17.4.0", + "globals": "^17.5.0", "prettier": "^3.8.2", "prettier-plugin-ember-template-tag": "^2.1.4", "publint": "^0.3.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 476b098b..dcda02e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,8 +112,8 @@ importers: specifier: ^17.24.0 version: 17.24.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) globals: - specifier: ^17.4.0 - version: 17.4.0 + specifier: ^17.5.0 + version: 17.5.0 prettier: specifier: ^3.8.2 version: 3.8.2 @@ -2900,8 +2900,8 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} engines: {node: '>=18'} globalthis@1.0.4: @@ -8558,7 +8558,7 @@ snapshots: globals@15.15.0: {} - globals@17.4.0: {} + globals@17.5.0: {} globalthis@1.0.4: dependencies: diff --git a/src/-private/untrack.ts b/src/-private/untrack.ts new file mode 100644 index 00000000..f6117f3a --- /dev/null +++ b/src/-private/untrack.ts @@ -0,0 +1,108 @@ +import * as glimmerValidator from '@glimmer/validator'; + +// Ember apps can provide @glimmer/validator through different module shapes +// depending on ember-source version and bundling mode. We centralize the +// resolution logic here so modifiers can always call a single untrack API. + +type UntrackCallback = () => void; +type Untrack = (callback: UntrackCallback) => void; +type ValidatorRequire = (moduleName: string) => ValidatorModule; + +interface ValidatorModule { + untrack?: unknown; + beginUntrackFrame?: unknown; + endUntrackFrame?: unknown; +} + +function getCallable unknown>( + target: unknown, + property: string, +): T | undefined { + const value = (target as Record)[property]; + + return typeof value === 'function' ? (value as T) : undefined; +} + +function tryRequire(maybeRequire: ValidatorRequire | undefined) { + if (typeof maybeRequire !== 'function') { + return undefined; + } + + try { + return maybeRequire('@glimmer/validator'); + } catch { + return undefined; + } +} + +function resolveValidatorModule(): ValidatorModule { + const importedModule = glimmerValidator as ValidatorModule; + const importedUntrack = getCallable(importedModule, 'untrack'); + const importedBeginUntrackFrame = getCallable<() => void>( + importedModule, + 'beginUntrackFrame', + ); + + if (importedUntrack || importedBeginUntrackFrame) { + // Modern ESM shape: use the imported namespace directly. + return importedModule; + } + + const globalWithEmber = globalThis as { + Ember?: { + __loader?: { + require?: (moduleName: string) => ValidatorModule; + }; + }; + require?: ValidatorRequire; + requirejs?: ValidatorRequire; + }; + + const requireCandidates: Array = [ + globalWithEmber.Ember?.__loader?.require, + globalWithEmber.require, + globalWithEmber.requirejs, + ]; + + for (const maybeRequire of requireCandidates) { + const module = tryRequire(maybeRequire); + + if (module) { + // Older or compat builds: resolve from runtime module loader. + return module; + } + } + + return {}; +} + +export const untrack: Untrack = (() => { + const validator = resolveValidatorModule(); + const moduleUntrack = getCallable(validator, 'untrack'); + + if (moduleUntrack) { + return moduleUntrack; + } + + const beginUntrackFrame = getCallable<() => void>( + validator, + 'beginUntrackFrame', + ); + const endUntrackFrame = getCallable<() => void>(validator, 'endUntrackFrame'); + + if (beginUntrackFrame && endUntrackFrame) { + // Some validator versions expose frame primitives instead of untrack. + return (callback: UntrackCallback) => { + beginUntrackFrame(); + + try { + callback(); + } finally { + endUntrackFrame(); + } + }; + } + + // Last resort for very old or unexpected runtime shapes. + return (callback: UntrackCallback) => callback(); +})(); diff --git a/src/modifiers/did-update.ts b/src/modifiers/did-update.ts index 298f7a3b..a65143ba 100644 --- a/src/modifiers/did-update.ts +++ b/src/modifiers/did-update.ts @@ -1,5 +1,5 @@ import { setModifierManager, capabilities } from '@ember/modifier'; -import { untrack } from '@glimmer/validator'; +import { untrack } from '../-private/untrack.ts'; import type { ModifierArgs,