Repository: ember-cli/ember-cli
Pull request #8571 (2019-04-09 11:43:07)

Support npm blueprints with versions
There are various reasons you may want to generate an older blueprint (`ember new -b @ember/octane-app-blueprint@0.17.2`).

The main use I have planned is using this in ember-cli-update https://github.com/ember-cli/ember-cli-update/issues/546.

kellyselden: Hmm my new acceptance test works locally but fails on CI.
kellyselden: @rwjblue Do you have any additional problems with this?

Patch:
diff --git a/lib/tasks/install-blueprint.js b/lib/tasks/install-blueprint.js
index d461d0118..b07a42792 100644
--- a/lib/tasks/install-blueprint.js
+++ b/lib/tasks/install-blueprint.js
@@ -10,7 +10,7 @@ const path = require('path');
 const merge = require('ember-cli-lodash-subset').merge;
 const execa = require('../utilities/execa');
 const SilentError = require('silent-error');
-const validateNpmPackageName = require('validate-npm-package-name');
+const npa = require('npm-package-arg');
 
 const logger = require('heimdalljs-logger')('ember-cli:tasks:install-blueprint');
 
@@ -77,20 +77,27 @@ class InstallBlueprintTask extends Task {
   _handleLookupFailure(name, error) {
     logger.info(`Blueprint lookup for "${name}" failed`);
 
-    if (!validateNpmPackageName(name).validForNewPackages) {
+    let parsed;
+    try {
+      parsed = npa(name);
+    } catch (err) {
       logger.info(`"${name} is not a valid npm package name -> rethrowing original error`);
       throw error;
     }
 
     logger.info(`"${name} is a valid npm package name -> trying npm`);
-    return this._tryNpmBlueprint(name);
+    return this._tryNpmBlueprint(parsed.name, parsed.fetchSpec);
   }
 
-  _tryNpmBlueprint(name) {
+  _tryNpmBlueprint(nameWithoutVersion, version) {
     return this._createTempFolder()
-      .then(pathName => this._npmInstallModule(name, pathName).catch(error => this._handleNpmInstallModuleError(error)))
+      .then(pathName =>
+        this._npmInstallModule(nameWithoutVersion, version, pathName).catch(error =>
+          this._handleNpmInstallModuleError(error)
+        )
+      )
       .then(modulePath => {
-        this._validateNpmModule(modulePath, name);
+        this._validateNpmModule(modulePath, nameWithoutVersion);
         return this._loadBlueprintFromPath(modulePath);
       });
   }
@@ -111,11 +118,12 @@ class InstallBlueprintTask extends Task {
     return execa('npm', ['install'], { cwd });
   }
 
-  _npmInstallModule(module, cwd) {
+  _npmInstallModule(nameWithoutVersion, version, cwd) {
+    let module = `${nameWithoutVersion}@${version}`;
     logger.info(`Running "npm install ${module}" in "${cwd}" ...`);
 
     this._copyNpmrc(cwd);
-    return execa('npm', ['install', module], { cwd }).then(() => path.join(cwd, 'node_modules', module));
+    return execa('npm', ['install', module], { cwd }).then(() => path.join(cwd, 'node_modules', nameWithoutVersion));
   }
 
   _handleNpmInstallModuleError(error) {
diff --git a/package.json b/package.json
index aad7f5ff0..fa82aee8a 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,6 @@
     "tiny-lr": "^1.1.1",
     "tree-sync": "^1.4.0",
     "uuid": "^3.3.2",
-    "validate-npm-package-name": "^3.0.0",
     "walk-sync": "^1.1.3",
     "watch-detector": "^0.1.0",
     "yam": "^1.0.0"


Test patch:
diff --git a/tests/acceptance/new-test.js b/tests/acceptance/new-test.js
index 1e84894c3..faf7e9e2b 100644
--- a/tests/acceptance/new-test.js
+++ b/tests/acceptance/new-test.js
@@ -113,6 +113,15 @@ describe('Acceptance: ember new', function() {
     })
   );
 
+  it(
+    'ember new npm blueprint with old version',
+    co.wrap(function*() {
+      yield ember(['new', 'foo', '--blueprint', '@glimmer/blueprint@0.6.4', '--skip-npm', '--skip-bower']);
+
+      expect(dir('src')).to.exist;
+    })
+  );
+
   it(
     'ember new foo, where foo does not yet exist, works',
     co.wrap(function*() {
diff --git a/tests/unit/tasks/install-blueprint-test.js b/tests/unit/tasks/install-blueprint-test.js
index cc4e5390e..c9c7e854d 100644
--- a/tests/unit/tasks/install-blueprint-test.js
+++ b/tests/unit/tasks/install-blueprint-test.js
@@ -50,17 +50,27 @@ describe('InstallBlueprintTask', function() {
       td.when(task._lookupBlueprint('foobar')).thenReject(error);
 
       let foobarBlueprint = { name: 'foobar npm blueprint' };
-      td.when(task._tryNpmBlueprint('foobar')).thenResolve(foobarBlueprint);
+      td.when(task._tryNpmBlueprint('foobar', 'latest')).thenResolve(foobarBlueprint);
 
       expect(await task._resolveBlueprint('foobar')).to.equal(foobarBlueprint);
     });
 
+    it('tries to resolve "@foo/bar@1.2.3" as npm package with a scope and a version', async function() {
+      let error = new Error('@foo/bar@1.2.3 not found');
+      td.when(task._lookupBlueprint('@foo/bar@1.2.3')).thenReject(error);
+
+      let foobarBlueprint = { name: 'foobar npm blueprint' };
+      td.when(task._tryNpmBlueprint('@foo/bar', '1.2.3')).thenResolve(foobarBlueprint);
+
+      expect(await task._resolveBlueprint('@foo/bar@1.2.3')).to.equal(foobarBlueprint);
+    });
+
     it('rejects if npm module resolution failed', async function() {
       let error = new Error('foobar not found');
       td.when(task._lookupBlueprint('foobar')).thenReject(error);
 
       let npmError = new Error('npm failure');
-      td.when(task._tryNpmBlueprint('foobar')).thenReject(npmError);
+      td.when(task._tryNpmBlueprint('foobar', 'latest')).thenReject(npmError);
 
       await expect(task._resolveBlueprint('foobar')).to.be.rejectedWith(npmError);
     });
@@ -162,12 +172,22 @@ describe('InstallBlueprintTask', function() {
 
     it('resolves with blueprint after successful "npm install"', async function() {
       let modulePath = '/path/to/foobar';
-      td.when(task._npmInstallModule('foobar', task._tempPath)).thenResolve(modulePath);
+      td.when(task._npmInstallModule('foobar', 'latest', task._tempPath)).thenResolve(modulePath);
 
       let foobarBlueprint = { name: 'foobar blueprint' };
       td.when(task._loadBlueprintFromPath(modulePath)).thenResolve(foobarBlueprint);
 
-      expect(await task._tryNpmBlueprint('foobar')).to.equal(foobarBlueprint);
+      expect(await task._tryNpmBlueprint('foobar', 'latest')).to.equal(foobarBlueprint);
+    });
+
+    it('resolves with blueprint after successful "npm install" with a scope and a version', async function() {
+      let modulePath = '/path/to/@foo/bar';
+      td.when(task._npmInstallModule('@foo/bar', '1.2.3', task._tempPath)).thenResolve(modulePath);
+
+      let foobarBlueprint = { name: '@foo/bar blueprint' };
+      td.when(task._loadBlueprintFromPath(modulePath)).thenResolve(foobarBlueprint);
+
+      expect(await task._tryNpmBlueprint('@foo/bar', '1.2.3')).to.equal(foobarBlueprint);
     });
 
     it('rejects with SilentError if npm module "foobar" could not be found', async function() {
@@ -181,9 +201,9 @@ describe('InstallBlueprintTask', function() {
           npm ERR! 404 Note that you can also install from a
           npm ERR! 404 tarball, folder, http url, or git url.`;
 
-      td.when(task._npmInstallModule('foobar', task._tempPath)).thenReject(error);
+      td.when(task._npmInstallModule('foobar', 'latest', task._tempPath)).thenReject(error);
 
-      await expect(task._tryNpmBlueprint('foobar')).to.be.rejectedWith(
+      await expect(task._tryNpmBlueprint('foobar', 'latest')).to.be.rejectedWith(
         SilentError,
         `The package 'foobar' was not found in the npm registry.`
       );
@@ -191,29 +211,29 @@ describe('InstallBlueprintTask', function() {
 
     it('rejects if "npm install" fails', async function() {
       let error = new Error('npm install failed');
-      td.when(task._npmInstallModule('foobar', task._tempPath)).thenReject(error);
+      td.when(task._npmInstallModule('foobar', 'latest', task._tempPath)).thenReject(error);
 
-      await expect(task._tryNpmBlueprint('foobar')).to.be.rejectedWith(error);
+      await expect(task._tryNpmBlueprint('foobar', 'latest')).to.be.rejectedWith(error);
     });
 
     it('rejects if npm module validation fails', async function() {
       let modulePath = '/path/to/foobar';
-      td.when(task._npmInstallModule('foobar', task._tempPath)).thenResolve(modulePath);
+      td.when(task._npmInstallModule('foobar', 'latest', task._tempPath)).thenResolve(modulePath);
 
       let error = new Error('module validation failed');
       td.when(task._validateNpmModule(modulePath, 'foobar')).thenThrow(error);
 
-      await expect(task._tryNpmBlueprint('foobar')).to.be.rejectedWith(error);
+      await expect(task._tryNpmBlueprint('foobar', 'latest')).to.be.rejectedWith(error);
     });
 
     it('rejects if loading blueprint fails', async function() {
       let modulePath = '/path/to/foobar';
-      td.when(task._npmInstallModule('foobar', task._tempPath)).thenResolve(modulePath);
+      td.when(task._npmInstallModule('foobar', 'latest', task._tempPath)).thenResolve(modulePath);
 
       let error = new Error('loading blueprint failed');
       td.when(task._loadBlueprintFromPath(modulePath)).thenReject(error);
 
-      await expect(task._tryNpmBlueprint('foobar')).to.be.rejectedWith(error);
+      await expect(task._tryNpmBlueprint('foobar', 'latest')).to.be.rejectedWith(error);
     });
   });
 });