diff --git a/lib/index.js b/lib/index.js index 7b209f811b4..580469b2275 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,6 +10,7 @@ const buildStripClassCallcheckPlugin = require('./build-strip-class-callcheck-pl const injectBabelHelpers = require('./transforms/inject-babel-helpers').injectBabelHelpers; const debugTree = require('broccoli-debug').buildDebugCallback('ember-source:addon'); const vmBabelPlugins = require('@glimmer/vm-babel-plugins'); +const semver = require('semver'); const PRE_BUILT_TARGETS = [ 'last 1 Chrome versions', @@ -40,6 +41,17 @@ add( path.join(__dirname, '..', 'dist', 'ember-template-compiler.js') ); +function* walkAddonTree(project, pathToAddon = []) { + for (let addon of project.addons) { + yield [addon, pathToAddon]; + yield* walkAddonTree(addon, [...pathToAddon, `${addon.name}@${addon.pkg.version}`]); + } +} + +function requirementFor(pkg, deps = {}) { + return deps[pkg]; +} + module.exports = { init() { this._super.init && this._super.init.apply(this, arguments); @@ -64,11 +76,14 @@ module.exports = { name: 'ember-source', paths, absolutePaths, + _bootstrapEmber: "require('@ember/-internals/bootstrap').default();", _jqueryIntegrationEnabled: true, included() { this._super.included.apply(this, arguments); + this._issueGlobalsDeprecation(); + const { has } = require('@ember/edition-utils'); let optionalFeatures = this.project.addons.find((a) => a.name === '@ember/optional-features'); @@ -244,7 +259,7 @@ module.exports = { return new MergeTrees([ concatBundle(emberFiles, { outputFile: 'ember.js', - footer: "require('@ember/-internals/bootstrap');", + footer: this._bootstrapEmber, }), concatBundle(emberTestingFiles, { @@ -309,4 +324,236 @@ module.exports = { return debugTree(new MergeTrees([ember, templateCompiler, jquery]), 'vendor:final'); }, + + _issueGlobalsDeprecation() { + if (process.env.EMBER_ENV === 'production') { + return; + } + + let isYarnProject = ((root) => { + try { + // eslint-disable-next-line node/no-unpublished-require + return require('ember-cli/lib/utilities/is-yarn-project')(root); + } catch { + return undefined; + } + })(this.project.root); + + let groupedByTopLevelAddon = Object.create(null); + let groupedByVersion = Object.create(null); + let projectInfo; + + for (let [addon, pathToAddon] of walkAddonTree(this.project)) { + let version = addon.pkg.version; + + if (addon.name === 'ember-cli-babel' && semver.lt(version, '7.26.6')) { + let info; + + if (addon.parent === this.project) { + let requirement = requirementFor('ember-cli-babel', this.project.pkg.devDependencies); + let compatible = semver.satisfies('7.26.6', requirement); + + info = projectInfo = { + parent: `${this.project.name()} (your app)`, + version, + requirement, + compatible, + dormant: false, + path: pathToAddon, + }; + } else { + let requirement = requirementFor('ember-cli-babel', addon.parent.pkg.dependencies); + let compatible = semver.satisfies('7.26.6', requirement); + let dormant = addon.parent._fileSystemInfo + ? addon.parent._fileSystemInfo().hasJSFiles === false + : false; + + let topLevelAddon = addon.parent; + + while (topLevelAddon.parent !== this.project) { + topLevelAddon = topLevelAddon.parent; + } + + info = { + parent: `${addon.parent.name}@${addon.pkg.version}`, + version, + requirement, + compatible, + dormant, + path: pathToAddon, + }; + + let addons = groupedByTopLevelAddon[topLevelAddon.name] || []; + groupedByTopLevelAddon[topLevelAddon.name] = [...addons, info]; + } + + let group = groupedByVersion[version] || Object.create(null); + groupedByVersion[version] = group; + + let addons = group[info.parent] || []; + group[info.parent] = [...addons, info]; + } + } + + if (Object.keys(groupedByVersion).length === 0) { + return; + } + + let dormantTopLevelAddons = []; + let compatibleTopLevelAddons = []; + let incompatibleTopLevelAddons = []; + + for (let addon of Object.keys(groupedByTopLevelAddon)) { + let group = groupedByTopLevelAddon[addon]; + + if (group.every((info) => info.dormant)) { + dormantTopLevelAddons.push(addon); + } else if (group.every((info) => info.compatible)) { + compatibleTopLevelAddons.push(addon); + } else { + incompatibleTopLevelAddons.push(addon); + } + } + + let message = + 'Usage of the Ember Global is deprecated. ' + + 'You should import the Ember module or the specific API instead.\n\n' + + 'See https://deprecations.emberjs.com/v3.x/#toc_ember-global for details.\n\n' + + 'Usages of the Ember Global may be caused by an outdated ember-cli-babel dependency. ' + + 'The following steps may help:\n\n'; + + let hasActionableSteps = false; + + if (projectInfo) { + message += '* Upgrade your `devDependencies` on `ember-cli-babel` to `^7.26.6`.\n'; + hasActionableSteps = true; + } else if (compatibleTopLevelAddons.length > 0) { + // Only show the compatible addons if the project itself is up-to-date, because updating the + // project's own dependency on ember-cli-babel to latest may also get these addons to use it + // as well. Otherwise, there is an unnecessary copy in the tree and it needs to be deduped. + if (isYarnProject === true) { + message += + '* Run `npx yarn-deduplicate --packages ember-cli-babel` followed by `yarn install`.\n'; + } else if (isYarnProject === false) { + message += '* Run `npm dedupe`.\n'; + } else { + message += + '* If using yarn, run `npx yarn-deduplicate --packages ember-cli-babel` followed by `yarn install`.\n' + + '* If using npm, run `npm dedupe`.\n'; + } + + hasActionableSteps = true; + } + + if (incompatibleTopLevelAddons.length > 0) { + message += '* Upgrade the following addons to the latest version:\n'; + + for (let addon of incompatibleTopLevelAddons) { + message += ` * ${addon}\n`; + } + + hasActionableSteps = true; + } + + if (!hasActionableSteps) { + // Only show the dormant addons if there are nothing else to do because they are unlikely to + // be the problem. + message += '* Upgrade the following addons to the latest version, if available:\n'; + + for (let addon of dormantTopLevelAddons) { + message += ` * ${addon}\n`; + } + } + + if (hasActionableSteps && process.env.EMBER_GLOBAL_DEPRECATIONS !== 'all') { + message += + '\n### Important ###\n\n' + + 'In order to avoid repeatedly showing the same deprecation messages, ' + + 'no further deprecation messages will be shown for usages of the Ember Global ' + + 'until ember-cli-babel is upgraded to v7.26.6 or above.\n\n' + + 'To see all instances of this deprecation message at runtime, ' + + 'set the `EMBER_GLOBAL_DEPRECATIONS` environment variable to "all", ' + + 'e.g. `EMBER_GLOBAL_DEPRECATIONS=all ember test`.\n'; + } + + message += + '\n### Details ###\n\n' + + 'Prior to v7.26.6, ember-cli-babel sometimes transpiled imports into the equivalent Ember Global API, ' + + 'potentially triggering this deprecation message even when you did not directly reference the Ember Global.\n\n' + + 'The following outdated versions are found in your project:\n'; + + let hasDormantAddons = false; + let hasCompatibleAddons = false; + + for (let version of Object.keys(groupedByVersion).sort(semver.compare)) { + message += `\n* ember-cli-babel@${version}, currently used by:\n`; + + for (let parent of Object.keys(groupedByVersion[version]).sort()) { + let info = groupedByVersion[version][parent][0]; + + message += ` * ${parent}`; + + if (info.dormant) { + message += ' (Dormant)\n'; + hasDormantAddons = true; + } else if (info.compatible) { + message += ' (Compatible)\n'; + hasCompatibleAddons = true; + } else { + message += '\n'; + } + + message += ` * Depends on ember-cli-babel@${groupedByVersion[version][parent][0].requirement}\n`; + + for (let info of groupedByVersion[version][parent]) { + let adddedBy = info.path.slice(0, -1); + + if (adddedBy.length) { + message += ` * Added by ${adddedBy.join(' > ')}\n`; + } + + if (info.compatible) { + hasCompatibleAddons = true; + } + } + } + } + + if (hasDormantAddons) { + message += + '\nNote: Addons marked as "Dormant" does not appear to have any JavaScript files. ' + + 'Therefore, even if they are using an old version ember-cli-babel, they are ' + + 'unlikely to be the cuplrit of this deprecation and can likely be ignored.\n'; + } + + if (hasCompatibleAddons) { + message += `\nNote: Addons marked as "Compatible" are already compatible with ember-cli-babel@7.26.6. `; + + if (projectInfo) { + message += 'Try upgrading your `devDependencies` on `ember-cli-babel` to `^7.26.6`.\n'; + } else { + if (isYarnProject === true) { + message += + 'Try running `npx yarn-deduplicate --packages ember-cli-babel` followed by `yarn install`.\n'; + } else if (isYarnProject === false) { + message += 'Try running `npm dedupe`.\n'; + } else { + message += + 'If using yarn, try running `npx yarn-deduplicate --packages ember-cli-babel` followed by `yarn install`.' + + 'If using npm, try running `npm dedupe`.\n'; + } + } + } + + if (hasActionableSteps) { + this.ui.writeWarnLine('[DEPRECATION] ' + message); + } + + this._bootstrapEmber = ` + require('@ember/-internals/bootstrap').default( + ${JSON.stringify(message)}, + ${hasActionableSteps && process.env.EMBER_GLOBAL_DEPRECATIONS !== 'all'} + ); + `; + }, }; diff --git a/packages/@ember/-internals/bootstrap/index.js b/packages/@ember/-internals/bootstrap/index.js index 431992b2e55..05e70d3a721 100644 --- a/packages/@ember/-internals/bootstrap/index.js +++ b/packages/@ember/-internals/bootstrap/index.js @@ -2,8 +2,12 @@ import require from 'require'; import { context } from '@ember/-internals/environment'; import { deprecate } from '@ember/debug'; -(function () { +const DEFAULT_MESSAGE = + 'Usage of the Ember Global is deprecated. You should import the Ember module or the specific API instead.'; + +export default function bootstrap(message = DEFAULT_MESSAGE, once = false) { let Ember; + let disabled = false; function defineEmber(key) { Object.defineProperty(context.exports, key, { @@ -14,19 +18,19 @@ import { deprecate } from '@ember/debug'; Ember = require('ember').default; } - deprecate( - 'Usage of the Ember Global is deprecated. You should import the Ember module or the specific API instead.', - false, - { - id: 'ember-global', - until: '4.0.0', - url: 'https://deprecations.emberjs.com/v3.x/#toc_ember-global', - for: 'ember-source', - since: { - enabled: '3.27.0', - }, - } - ); + deprecate(message, disabled, { + id: 'ember-global', + until: '4.0.0', + url: 'https://deprecations.emberjs.com/v3.x/#toc_ember-global', + for: 'ember-source', + since: { + enabled: '3.27.0', + }, + }); + + if (once) { + disabled = true; + } return Ember; }, @@ -43,4 +47,4 @@ import { deprecate } from '@ember/debug'; // eslint-disable-next-line no-undef module.exports = Ember = require('ember').default; } -})(); +}