From 67e3e3d0ed0c286d446ef764bd08afa38ee87d93 Mon Sep 17 00:00:00 2001 From: Hagar Fisher Date: Mon, 22 May 2023 23:08:50 +0300 Subject: [PATCH 01/13] changing regex to match against npx and npm init --- src/content/registry/npm.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js index 53045dd..e689401 100644 --- a/src/content/registry/npm.js +++ b/src/content/registry/npm.js @@ -1,12 +1,14 @@ import { createParseCommand } from './shared'; -const npmInstall = /(npm|yarn)( -g)?( global)? (install|i|add|update) /; +const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update))\s/; +const npmInit = /(npx|npm init)\b/; const packageName = /(?[a-z0-9_@][a-z0-9_./-]*)/; const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_-]+)?)/; const packageLabel = /@[a-z0-9_-]+/; const fullPackage = new RegExp(String.raw`^${packageName.source}(${packageVersion.source}|${packageLabel.source})?$`); const parsePackageString = (str) => { + console.log('hi'); const match = str.match(fullPackage); if (!match) return null; return { @@ -18,7 +20,7 @@ const parsePackageString = (str) => { export const parseCommand = createParseCommand( 'npm', - (line) => line.match(npmInstall), + (line) => line.match(npmInstall) || line.match(npmInit), (word) => word.length + 1, parsePackageString ); From 436ebc5e73eec3166a4d06ecb0e761c5b1e74d5c Mon Sep 17 00:00:00 2001 From: Hagar Fisher Date: Mon, 22 May 2023 23:10:07 +0300 Subject: [PATCH 02/13] removing console log --- src/content/registry/npm.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js index e689401..7186ae8 100644 --- a/src/content/registry/npm.js +++ b/src/content/registry/npm.js @@ -8,7 +8,6 @@ const packageLabel = /@[a-z0-9_-]+/; const fullPackage = new RegExp(String.raw`^${packageName.source}(${packageVersion.source}|${packageLabel.source})?$`); const parsePackageString = (str) => { - console.log('hi'); const match = str.match(fullPackage); if (!match) return null; return { From 018ea68c59fe2d92f6d4497fad6c8b28fa1c988b Mon Sep 17 00:00:00 2001 From: Hagar Fisher Date: Mon, 22 May 2023 23:54:48 +0300 Subject: [PATCH 03/13] disregard additional arguments if npx or npm init --- src/content/registry/npm.js | 5 ++++- src/content/registry/shared.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js index 7186ae8..0a435a3 100644 --- a/src/content/registry/npm.js +++ b/src/content/registry/npm.js @@ -7,7 +7,10 @@ const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_ const packageLabel = /@[a-z0-9_-]+/; const fullPackage = new RegExp(String.raw`^${packageName.source}(${packageVersion.source}|${packageLabel.source})?$`); -const parsePackageString = (str) => { +const parsePackageString = (str, baseCommand, packagesLength) => { + if ((baseCommand === 'npx' || baseCommand === 'npm init') && packagesLength > 0) { + return null; + } const match = str.match(fullPackage); if (!match) return null; return { diff --git a/src/content/registry/shared.js b/src/content/registry/shared.js index 6d51fa5..27847de 100644 --- a/src/content/registry/shared.js +++ b/src/content/registry/shared.js @@ -10,7 +10,7 @@ const finishAllWords = (argsAndPackagesWords) => { * @param {string} registryName `type` in result element * @param {(line: string) => RegExpMatchArray} getBaseCommandMatch get the base command match for a line (examples: `pip install`, `yarn add`) * @param {(word: string, argsAndPackagesWords: string[]) => number} handleArgument add a length to the counter to move it to the next argument - * @param {(word: string) => { packageName: string, packageVersion?: string, packagePart: string } | null } parsePackageWord parse a word to its package name and its whole part (includes the version) + * @param {(word: string, baseCommand?: string, packagesLength?: number) => { packageName: string, packageVersion?: string, packagePart: string } | null } parsePackageWord parse a word to its package name and its whole part (includes the version) */ export const createParseCommand = (registryName, getBaseCommandMatch, handleArgument, parsePackageWord) => @@ -50,7 +50,7 @@ export const createParseCommand = continue; } - const packageMatch = parsePackageWord(word); + const packageMatch = parsePackageWord(word, baseCommandMatch[0], packages.length); if (!packageMatch) { counterIndex += word.length + 1; continue; From 128c187bbde26f3162119290ff17b90929ba46f5 Mon Sep 17 00:00:00 2001 From: Hagar Fisher Date: Mon, 29 May 2023 23:05:10 +0300 Subject: [PATCH 04/13] reverting npmInstall regex change, replacing \s with literal space --- src/content/registry/npm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js index 0a435a3..249884d 100644 --- a/src/content/registry/npm.js +++ b/src/content/registry/npm.js @@ -1,6 +1,6 @@ import { createParseCommand } from './shared'; -const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update))\s/; +const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update)) /; const npmInit = /(npx|npm init)\b/; const packageName = /(?[a-z0-9_@][a-z0-9_./-]*)/; const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_-]+)?)/; From 05a00d371d2b24b6905432618b55c7926a8da640 Mon Sep 17 00:00:00 2001 From: Hagar Fisher Date: Mon, 29 May 2023 23:08:10 +0300 Subject: [PATCH 05/13] renaming packagesLength to foundPackages --- src/content/registry/npm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js index 249884d..22ed87b 100644 --- a/src/content/registry/npm.js +++ b/src/content/registry/npm.js @@ -7,8 +7,8 @@ const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_ const packageLabel = /@[a-z0-9_-]+/; const fullPackage = new RegExp(String.raw`^${packageName.source}(${packageVersion.source}|${packageLabel.source})?$`); -const parsePackageString = (str, baseCommand, packagesLength) => { - if ((baseCommand === 'npx' || baseCommand === 'npm init') && packagesLength > 0) { +const parsePackageString = (str, baseCommand, foundPackages) => { + if ((baseCommand === 'npx' || baseCommand === 'npm init') && foundPackages > 0) { return null; } const match = str.match(fullPackage); From 06fb8b4bcc386057039fd836e49cef29e7d6e7f4 Mon Sep 17 00:00:00 2001 From: Hagar Fisher Date: Mon, 29 May 2023 23:30:17 +0300 Subject: [PATCH 06/13] removing npm init from regex for now and updating test yaml --- src/content/registry/npm.js | 2 +- .../real-examples/real-examples-results.yaml | 123 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js index 22ed87b..bbb2e2d 100644 --- a/src/content/registry/npm.js +++ b/src/content/registry/npm.js @@ -1,7 +1,7 @@ import { createParseCommand } from './shared'; const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update)) /; -const npmInit = /(npx|npm init)\b/; +const npmInit = /(npx)\b/; const packageName = /(?[a-z0-9_@][a-z0-9_./-]*)/; const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_-]+)?)/; const packageLabel = /@[a-z0-9_-]+/; diff --git a/tests/real-examples/real-examples-results.yaml b/tests/real-examples/real-examples-results.yaml index 36ad119..36076b5 100644 --- a/tests/real-examples/real-examples-results.yaml +++ b/tests/real-examples/real-examples-results.yaml @@ -1013,31 +1013,130 @@ https://stackoverflow.com/questions/59188624: - range: npm install -g create-react-app type: npm name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app + - range: |- + npx create-react-app my-app + npm init react-app my-app + yarn create react-app my-app + + type: npm + name: create-react-app - range: npm install -g create-react-app type: npm name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app - range: |- npm install create-react-app@latest type: npm name: create-react-app + - range: |- + npx create-react-app my-app + + type: npm + name: create-react-app + - range: |- + npx create-react-app my-app [--template typescript] + + type: npm + name: create-react-app + - range: npx create-react-app@latest + type: npm + name: create-react-app + - range: npx create-react-app@latest + type: npm + name: create-react-app + - range: npx create-react-app@latest ./client + type: npm + name: create-react-app - range: npm install -g create-react-app type: npm name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app + - range: npx --ignore-existing create-react-app + type: npm + name: create-react-app + - range: npx create-react-app@latest your-project-name + type: npm + name: create-react-app + - range: |- + npx create-react-app project-name + + type: npm + name: create-react-app + - range: |- + sudo npm uninstall -g create-react-app + npx create-react-app my-test-app + + type: npm + name: create-react-app + - range: npx create-react-app app + type: npm + name: create-react-app - range: npm update -g create-react-app type: npm name: create-react-app + - range: npx create-react-app + type: npm + name: create-react-app - range: npm install -g create-react-app type: npm name: create-react-app + - range: npx create-react-app app_name + type: npm + name: create-react-app - range: npm install -g create-react-app type: npm name: create-react-app + - range: |- + npx create-react-app my-app + + type: npm + name: create-react-app - range: |- npm install create-react-app@latest type: npm name: create-react-app + - range: |- + npx create-react-app my-app + + type: npm + name: create-react-app + - range: |- + npx create-react-app my-app --template typescript + + type: npm + name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app + - range: |- + npx create-react-app my-app-name + + type: npm + name: create-react-app + - range: npx create-react-app my-app + type: npm + name: create-react-app + - range: npx create-react-app my-app --template typescript + type: npm + name: create-react-app - range: npm i -g create-react-app type: npm name: create-react-app @@ -1046,9 +1145,33 @@ https://stackoverflow.com/questions/59188624: type: npm name: create-react-app + - range: |- + npx create-react-app my-app + + type: npm + name: create-react-app - range: npm install create-react-app type: npm name: create-react-app + - range: npx create-react-app name-of-app + type: npm + name: create-react-app + - range: |- + npx --ignore-existing create-react-app [project name] + + type: npm + name: create-react-app + - range: npx create-react-app [project name] + type: npm + name: create-react-app + - range: |- + npx create-react-app@latest {project name} + + type: npm + name: create-react-app + - range: npx clear-npx-cache + type: npm + name: clear-npx-cache - range: npm i -g create-react-app type: npm name: create-react-app From 07bb04e1a6639beadc836876cff0c82833d0a4d7 Mon Sep 17 00:00:00 2001 From: Baruch Odem Date: Thu, 8 Jun 2023 12:21:26 +0300 Subject: [PATCH 07/13] add failing tests --- src/content/registry/npm.test.js | 17 +-- src/content/stackoverflow/finder.test.js | 12 +- .../real-examples/real-examples-results.yaml | 105 ++++++++++++++++++ tests/real-examples/real-examples.yaml | 8 ++ 4 files changed, 134 insertions(+), 8 deletions(-) diff --git a/src/content/registry/npm.test.js b/src/content/registry/npm.test.js index c4c3264..7c75bf8 100644 --- a/src/content/registry/npm.test.js +++ b/src/content/registry/npm.test.js @@ -43,18 +43,21 @@ describe('npm', () => { expect(packagePosition).toStrictEqual(expectedPackages); }); - it('should range the package with the version part', () => { - const command = 'yarn add -D react@^12.5.0'; + it.each(['yarn add -D ', 'npx my-app'])('should range the package with the version part', (command) => { + const name = 'create-react-app'; + const version = '^12.5.0'; + const packagePart = `${name}@${version}`; + const startIndex = command.indexOf(''); const expectedPackages = [ packageResult({ - name: 'react', - version: '^12.5.0', - startIndex: 12, - endIndex: 12 + 'react@^12.5.0'.length, + name, + version, + startIndex, + endIndex: startIndex + packagePart.length, }), ]; - const packagePosition = parseCommand(command); + const packagePosition = parseCommand(command.replace('', packagePart)); expect(packagePosition).toStrictEqual(expectedPackages); }); diff --git a/src/content/stackoverflow/finder.test.js b/src/content/stackoverflow/finder.test.js index d6ef280..615cf42 100644 --- a/src/content/stackoverflow/finder.test.js +++ b/src/content/stackoverflow/finder.test.js @@ -77,9 +77,18 @@ describe(findRanges.name, () => { 'npm i --save-dev ', 'npm i --save-dev', 'npm update ', + 'npm exec ', + 'npm exec --package= command', ]; - const yarnVariants = ['yarn add ', 'yarn add -D ', 'yarn global add ']; + const npxVariants = ['npx ', 'npx -p command', 'npx --package= command']; + + const yarnVariants = [ + 'yarn add ', + 'yarn add -D ', + 'yarn global add ', + 'yarn create argument', + ]; const pipVariants = [ 'pip install ', @@ -91,6 +100,7 @@ describe(findRanges.name, () => { it.each([ ...npmVariants.map((cmd) => [cmd, 'npm']), ...yarnVariants.map((cmd) => [cmd, 'npm']), + ...npxVariants.map((cmd) => [cmd, 'npm']), ...pipVariants.map((cmd) => [cmd, 'pypi']), ])(`'%s' inside
`, (installCommand, type) => {
       const commandPackageName = 'my-package-name';
diff --git a/tests/real-examples/real-examples-results.yaml b/tests/real-examples/real-examples-results.yaml
index 36076b5..88cef76 100644
--- a/tests/real-examples/real-examples-results.yaml
+++ b/tests/real-examples/real-examples-results.yaml
@@ -1175,6 +1175,111 @@ https://stackoverflow.com/questions/59188624:
     - range: npm i -g create-react-app
       type: npm
       name: create-react-app
+https://stackoverflow.com/questions/50605219:
+  comment: npx dummy package, npx -p
+  post: answer
+  registry: npm
+  results:
+    - range: >-
+        https://www.npmjs.com/package/npx
+      type: npm
+      name: npx
+    - range: npx create-react-app my-app
+      type: npm
+      name: create-react-app
+    - range: npx create-react-app my-app
+      type: npm
+      name: create-react-app
+    - range: |-
+        npm install some-package
+        
+      type: npm
+      name: some-package
+    - range: |-
+        npx some-package
+        
+      type: npm
+      name: some-package
+    - range: |-
+        $ npx create-react-app my-app
+        
+      type: npm
+      name: create-react-app
+    - range: >-
+        npx   -p uglify-js         uglifyjs --output app.min.js app.js
+        common.js
+              +----------------+   +--------------------------------------------+
+              package (optional)   command, followed by arguments
+        
+      type: npm
+      name: uglify-js
+    - range: >-
+        Start a HTTP Server      : npx http-server
+
+        Lint code                : npx eslint ./src
+                                 # Run uglifyjs command in the package uglify-js
+        Minify JS                : npx -p uglify-js uglifyjs -o app.min.js
+        app.js common.js
+
+        Minify CSS               : npx clean-css-cli -o style.min.css
+        css/bootstrap.css style.css
+
+        Minify HTML              : npx html-minifier index-2.html -o index.html
+        --remove-comments --collapse-whitespace
+
+        Scan for open ports      : npx evilscan 192.168.1.10 --port=10-9999
+
+        Cast video to Chromecast : npx castnow
+        http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4
+
+        
+      type: npm
+      name: http-server
+    - range: |-
+        npm install -g npx
+        
+      type: npm
+      name: npx
+    - range: |-
+        npx your-package-name
+        
+      type: npm
+      name: your-package-name
+https://stackoverflow.com/questions/70358643:
+  comment: npx with version and argument
+  post: answer
+  registry: npm
+  results:
+    - range: |-
+        npx clear-npx-cache
+        
+      type: npm
+      name: clear-npx-cache
+    - range: npx create-react-app@5.0.0 my-app
+      type: npm
+      name: create-react-app
+      version: 5.0.0
+    - range: npx clear-npx-cache
+      type: npm
+      name: clear-npx-cache
+    - range: npx clear-npx-cache
+      type: npm
+      name: clear-npx-cache
+    - range: |-
+        npx create-react-app@latest my-app --use-npm
+        
+      type: npm
+      name: create-react-app
+    - range: |-
+        npx create-react-app@5.0.0 my-app
+        
+      type: npm
+      name: create-react-app
+      version: 5.0.0
+    - range: npx create-react-app my-app
+      type: npm
+      name: create-react-app
 https://stackoverflow.com/questions/22563281:
   comment: with semver ~
   post: answer
diff --git a/tests/real-examples/real-examples.yaml b/tests/real-examples/real-examples.yaml
index 347466b..e044284 100644
--- a/tests/real-examples/real-examples.yaml
+++ b/tests/real-examples/real-examples.yaml
@@ -64,6 +64,14 @@ links:
     comment: npx
     post: question
     registry: npm
+  https://stackoverflow.com/questions/50605219:
+    comment: npx dummy package, npx -p
+    post: answer
+    registry: npm
+  https://stackoverflow.com/questions/70358643:
+    comment: npx with version and argument
+    post: answer
+    registry: npm
   https://stackoverflow.com/questions/22563281:
     comment: with semver ~
     post: answer

From 9ca539051250011cc4594bf87eae90059eee8a94 Mon Sep 17 00:00:00 2001
From: Baruch Odem 
Date: Thu, 8 Jun 2023 12:23:14 +0300
Subject: [PATCH 08/13] test future support is find nothing

---
 src/content/stackoverflow/finder.test.js | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/content/stackoverflow/finder.test.js b/src/content/stackoverflow/finder.test.js
index 615cf42..2d81a31 100644
--- a/src/content/stackoverflow/finder.test.js
+++ b/src/content/stackoverflow/finder.test.js
@@ -343,15 +343,16 @@ describe(findRanges.name, () => {
     });
 
     // issue #37, #38
-    it.skip.each(['npm install git://github.com/user-c/dep-2#node0.8.0', 'npx create-react-app my-app'])(
-      `Future support '%s`,
-      (command) => {
-        const { body } = createCodeBlock(command);
+    it.each([
+      'npm install git://github.com/user-c/dep-2#node0.8.0',
+      'npm init react-app my-app', // create-react-app
+      'npm exec create-react-app',
+    ])(`Future support '%s`, (command) => {
+      const { body } = createCodeBlock(command);
 
-        const foundElements = findRanges(body);
+      const foundElements = findRanges(body);
 
-        expect(foundElements.length).toBe(1);
-      }
-    );
+      expect(foundElements.length).toBe(0);
+    });
   });
 });

From 8c47da4bb1e2c64f96c06ea6c1389139050e5a08 Mon Sep 17 00:00:00 2001
From: Baruch Odem 
Date: Thu, 8 Jun 2023 13:35:23 +0300
Subject: [PATCH 09/13] split npm parse commands

---
 src/content/registry/npm.js         | 28 +++++++++++++++++++++-------
 src/content/registry/npm.test.js    |  4 +++-
 src/content/registry/shared.js      |  4 ++--
 src/content/stackoverflow/finder.js |  2 +-
 4 files changed, 27 insertions(+), 11 deletions(-)

diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js
index bbb2e2d..94c77d9 100644
--- a/src/content/registry/npm.js
+++ b/src/content/registry/npm.js
@@ -1,16 +1,13 @@
 import { createParseCommand } from './shared';
 
 const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update)) /;
-const npmInit = /(npx)\b/;
+const npxInstall = /(npx)\b/;
 const packageName = /(?[a-z0-9_@][a-z0-9_./-]*)/;
 const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_-]+)?)/;
 const packageLabel = /@[a-z0-9_-]+/;
 const fullPackage = new RegExp(String.raw`^${packageName.source}(${packageVersion.source}|${packageLabel.source})?$`);
 
-const parsePackageString = (str, baseCommand, foundPackages) => {
-  if ((baseCommand === 'npx' || baseCommand === 'npm init') && foundPackages > 0) {
-    return null;
-  }
+const parsePackageString = (str) => {
   const match = str.match(fullPackage);
   if (!match) return null;
   return {
@@ -20,13 +17,30 @@ const parsePackageString = (str, baseCommand, foundPackages) => {
   };
 };
 
-export const parseCommand = createParseCommand(
+const parseOnlyFirstPackage = (str, foundPackages) => {
+  if (foundPackages > 0) {
+    return null;
+  }
+
+  return parsePackageString(str);
+};
+
+const parseNpmCommand = createParseCommand(
   'npm',
-  (line) => line.match(npmInstall) || line.match(npmInit),
+  (line) => line.match(npmInstall),
   (word) => word.length + 1,
   parsePackageString
 );
 
+const parseNpxCommand = createParseCommand(
+  'npm',
+  (line) => line.match(npxInstall),
+  (word) => word.length + 1,
+  parseOnlyFirstPackage
+);
+
+export const parseCommands = [parseNpmCommand, parseNpxCommand];
+
 const urlParser = ({ pathname }) => {
   if (!pathname.startsWith('/package/')) return;
   const [name, version] = pathname.replace('/package/', '').split('/v/');
diff --git a/src/content/registry/npm.test.js b/src/content/registry/npm.test.js
index 7c75bf8..9471191 100644
--- a/src/content/registry/npm.test.js
+++ b/src/content/registry/npm.test.js
@@ -1,7 +1,9 @@
 import { describe, expect, it } from '@jest/globals';
-import { parseCommand } from './npm';
+import { parseCommands } from './npm';
 import { cli } from './tests-utils';
 
+const parseCommand = (command) => parseCommands.flatMap((parser) => parser(command));
+
 const packageResult = (p) => ({
   type: 'npm',
   version: undefined,
diff --git a/src/content/registry/shared.js b/src/content/registry/shared.js
index 27847de..73432e8 100644
--- a/src/content/registry/shared.js
+++ b/src/content/registry/shared.js
@@ -10,7 +10,7 @@ const finishAllWords = (argsAndPackagesWords) => {
  * @param {string} registryName `type` in result element
  * @param {(line: string) => RegExpMatchArray} getBaseCommandMatch get the base command match for a line (examples: `pip install`, `yarn add`)
  * @param {(word: string, argsAndPackagesWords: string[]) => number} handleArgument add a length to the counter to move it to the next argument
- * @param {(word: string, baseCommand?: string, packagesLength?: number) => { packageName: string, packageVersion?: string, packagePart: string } | null } parsePackageWord parse a word to its package name and its whole part (includes the version)
+ * @param {(word: string, packagesLength?: number) => { packageName: string, packageVersion?: string, packagePart: string } | null } parsePackageWord parse a word to its package name and its whole part (includes the version)
  */
 export const createParseCommand =
   (registryName, getBaseCommandMatch, handleArgument, parsePackageWord) =>
@@ -50,7 +50,7 @@ export const createParseCommand =
           continue;
         }
 
-        const packageMatch = parsePackageWord(word, baseCommandMatch[0], packages.length);
+        const packageMatch = parsePackageWord(word, packages.length);
         if (!packageMatch) {
           counterIndex += word.length + 1;
           continue;
diff --git a/src/content/stackoverflow/finder.js b/src/content/stackoverflow/finder.js
index 3e52df2..08a77bb 100644
--- a/src/content/stackoverflow/finder.js
+++ b/src/content/stackoverflow/finder.js
@@ -20,7 +20,7 @@ const urlParsers = {
   ...python.urlParsers,
 };
 
-const codeBlockParsers = [npm.parseCommand, python.parseCommand, go.parseCommand];
+const codeBlockParsers = [...npm.parseCommands, python.parseCommand, go.parseCommand];
 
 export const findRanges = (body) => {
   const links = Array.from(body.querySelectorAll(`${POST_SELECTOR} a`))

From f3151bff1ab01fa74376a2c448a409a2ef45f562 Mon Sep 17 00:00:00 2001
From: Baruch Odem 
Date: Thu, 8 Jun 2023 14:00:02 +0300
Subject: [PATCH 10/13] npx --package=my-package

---
 src/content/registry/npm.js | 31 +++++++++++++++++++------------
 1 file changed, 19 insertions(+), 12 deletions(-)

diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js
index 94c77d9..c9769ce 100644
--- a/src/content/registry/npm.js
+++ b/src/content/registry/npm.js
@@ -7,6 +7,7 @@ const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_
 const packageLabel = /@[a-z0-9_-]+/;
 const fullPackage = new RegExp(String.raw`^${packageName.source}(${packageVersion.source}|${packageLabel.source})?$`);
 
+// npm
 const parsePackageString = (str) => {
   const match = str.match(fullPackage);
   if (!match) return null;
@@ -17,6 +18,14 @@ const parsePackageString = (str) => {
   };
 };
 
+const parseNpmCommand = createParseCommand(
+  'npm',
+  (line) => line.match(npmInstall),
+  (word) => word.length + 1,
+  parsePackageString
+);
+
+// npx
 const parseOnlyFirstPackage = (str, foundPackages) => {
   if (foundPackages > 0) {
     return null;
@@ -25,22 +34,20 @@ const parseOnlyFirstPackage = (str, foundPackages) => {
   return parsePackageString(str);
 };
 
-const parseNpmCommand = createParseCommand(
-  'npm',
-  (line) => line.match(npmInstall),
-  (word) => word.length + 1,
-  parsePackageString
-);
+const handleNpxArgumentForPackage = (arg, argsAndPackagesWords) => {
+  const packageArg = '--package=';
+  if (arg.startsWith(packageArg)) {
+    argsAndPackagesWords.unshift(arg.slice(packageArg.length));
+    return packageArg.length;
+  }
+  return arg.length + 1;
+};
 
-const parseNpxCommand = createParseCommand(
-  'npm',
-  (line) => line.match(npxInstall),
-  (word) => word.length + 1,
-  parseOnlyFirstPackage
-);
+const parseNpxCommand = createParseCommand('npm', (line) => line.match(npxInstall), handleNpxArgumentForPackage, parseOnlyFirstPackage);
 
 export const parseCommands = [parseNpmCommand, parseNpxCommand];
 
+// URL
 const urlParser = ({ pathname }) => {
   if (!pathname.startsWith('/package/')) return;
   const [name, version] = pathname.replace('/package/', '').split('/v/');

From f23c72b59d88ec600fb72be53e78e9577876bfa7 Mon Sep 17 00:00:00 2001
From: Baruch Odem 
Date: Thu, 8 Jun 2023 14:23:07 +0300
Subject: [PATCH 11/13] support npm exec

---
 src/content/registry/npm.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js
index c9769ce..5c79327 100644
--- a/src/content/registry/npm.js
+++ b/src/content/registry/npm.js
@@ -2,6 +2,7 @@ import { createParseCommand } from './shared';
 
 const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update)) /;
 const npxInstall = /(npx)\b/;
+const npmExec = /npm exec/;
 const packageName = /(?[a-z0-9_@][a-z0-9_./-]*)/;
 const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_-]+)?)/;
 const packageLabel = /@[a-z0-9_-]+/;
@@ -43,7 +44,12 @@ const handleNpxArgumentForPackage = (arg, argsAndPackagesWords) => {
   return arg.length + 1;
 };
 
-const parseNpxCommand = createParseCommand('npm', (line) => line.match(npxInstall), handleNpxArgumentForPackage, parseOnlyFirstPackage);
+const parseNpxCommand = createParseCommand(
+  'npm',
+  (line) => line.match(npxInstall) || line.match(npmExec),
+  handleNpxArgumentForPackage,
+  parseOnlyFirstPackage
+);
 
 export const parseCommands = [parseNpmCommand, parseNpxCommand];
 

From c556c3ff7976a86ef35ef5d5e4d8e6f7d8db5d7e Mon Sep 17 00:00:00 2001
From: Baruch Odem 
Date: Thu, 8 Jun 2023 15:19:51 +0300
Subject: [PATCH 12/13] support create-*

---
 src/content/registry/npm.js              | 30 ++++++++++++++++-
 src/content/registry/npm.test.js         | 42 +++++++++++++++++++++++-
 src/content/registry/shared.js           |  1 +
 src/content/stackoverflow/finder.test.js |  8 +----
 4 files changed, 72 insertions(+), 9 deletions(-)

diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js
index 5c79327..2365e2f 100644
--- a/src/content/registry/npm.js
+++ b/src/content/registry/npm.js
@@ -3,6 +3,7 @@ import { createParseCommand } from './shared';
 const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update)) /;
 const npxInstall = /(npx)\b/;
 const npmExec = /npm exec/;
+const npmCreate = /((npm|yarn) (init|create))\b/;
 const packageName = /(?[a-z0-9_@][a-z0-9_./-]*)/;
 const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_-]+)?)/;
 const packageLabel = /@[a-z0-9_-]+/;
@@ -51,7 +52,34 @@ const parseNpxCommand = createParseCommand(
   parseOnlyFirstPackage
 );
 
-export const parseCommands = [parseNpmCommand, parseNpxCommand];
+// create-*, @scope/create
+const parseCreatePackageString = (str, foundPackages) => {
+  if (foundPackages > 0) {
+    return null;
+  }
+
+  const match = str.match(packageName);
+  if (!match) return null;
+
+  const nameWithCreate = match.groups.package_name.startsWith('@')
+    ? match.groups.package_name + '/create'
+    : 'create-' + match.groups.package_name;
+
+  return {
+    packageName: nameWithCreate,
+    packageVersion: match.groups.package_version,
+    packagePart: str,
+  };
+};
+
+const parseCreateCommand = createParseCommand(
+  'npm',
+  (line) => line.match(npmCreate),
+  (word) => word.length + 1,
+  parseCreatePackageString
+);
+
+export const parseCommands = [parseNpmCommand, parseNpxCommand, parseCreateCommand];
 
 // URL
 const urlParser = ({ pathname }) => {
diff --git a/src/content/registry/npm.test.js b/src/content/registry/npm.test.js
index 9471191..067e0ad 100644
--- a/src/content/registry/npm.test.js
+++ b/src/content/registry/npm.test.js
@@ -19,11 +19,31 @@ describe('npm', () => {
       'npm install',
       'npm install -g',
       '`npm install node-sass`', // this is not a valid command because of the `
-      'npm init react-app my-app', // not yet supported. #37
     ])('should return empty array if no packages found', (command) => {
       expect(parseCommand(command)).toStrictEqual([]);
     });
 
+    it.each(['npm init  argument', 'npm create  argument', 'yarn create  argument'])(
+      `'%s' find create-* package`,
+      (template) => {
+        const startIndex = template.indexOf('');
+        const endIndex = startIndex + 'react-app'.length;
+        const command = template.replace('', 'react-app');
+
+        const packagePosition = parseCommand(command);
+
+        expect(packagePosition).toStrictEqual([packageResult({ name: 'create-react-app', startIndex, endIndex })]);
+      }
+    );
+
+    it.each(['npm init @scope', 'npm create @scope'])(`'%s' find @scope/create package`, (command) => {
+      const startIndex = command.indexOf('@scope');
+      const endIndex = startIndex + '@scope'.length;
+      const packagePosition = parseCommand(command);
+
+      expect(packagePosition).toStrictEqual([packageResult({ name: '@scope/create', startIndex, endIndex })]);
+    });
+
     it('should return the right position for recurrent package name', () => {
       const { command, positions } = cli`npm install ${'n'}`;
       const expectedPackages = positions.map(({ index, value }) => packageResult({ name: value, startIndex: index }));
@@ -75,5 +95,25 @@ describe('npm', () => {
 
       expect(packagePosition).toStrictEqual(expectedPackages);
     });
+
+    it('should find create-* in multiple lines', () => {
+      const { command, positions } = cli`
+      npx ${'create-react-app'} my-app
+      npm init ${'react-app'} my-app
+      yarn create ${'react-app'} my-app
+      `;
+
+      const expectedPackages = positions.map(({ index, value }) =>
+        packageResult({
+          name: 'create-react-app',
+          startIndex: index,
+          endIndex: index + value.length,
+        })
+      );
+
+      const packagePosition = parseCommand(command);
+
+      expect(packagePosition).toStrictEqual(expectedPackages);
+    });
   });
 });
diff --git a/src/content/registry/shared.js b/src/content/registry/shared.js
index 73432e8..9f733fd 100644
--- a/src/content/registry/shared.js
+++ b/src/content/registry/shared.js
@@ -50,6 +50,7 @@ export const createParseCommand =
           continue;
         }
 
+        // TODO: pass argsAndPackagesWords instead of packages.length, and empty the array inside the function if needed
         const packageMatch = parsePackageWord(word, packages.length);
         if (!packageMatch) {
           counterIndex += word.length + 1;
diff --git a/src/content/stackoverflow/finder.test.js b/src/content/stackoverflow/finder.test.js
index 3ff313b..1b5784f 100644
--- a/src/content/stackoverflow/finder.test.js
+++ b/src/content/stackoverflow/finder.test.js
@@ -80,12 +80,7 @@ describe(findRanges.name, () => {
 
     const npxVariants = ['npx ', 'npx -p  command', 'npx --package= command'];
 
-    const yarnVariants = [
-      'yarn add ',
-      'yarn add -D ',
-      'yarn global add ',
-      'yarn create  argument',
-    ];
+    const yarnVariants = ['yarn add ', 'yarn add -D ', 'yarn global add '];
 
     const pipVariants = [
       'pip install ',
@@ -343,7 +338,6 @@ describe(findRanges.name, () => {
     it.each([
       'npm install git://github.com/user-c/dep-2#node0.8.0',
       'npm init react-app my-app', // create-react-app
-      'npm exec create-react-app',
     ])(`Future support '%s`, (command) => {
       const { body } = createCodeBlock(command);
 

From 6e94c01e23e267a4f5ea0345629bead901d051e6 Mon Sep 17 00:00:00 2001
From: Baruch Odem 
Date: Fri, 9 Jun 2023 14:46:16 +0300
Subject: [PATCH 13/13] multiline npx command

---
 src/content/registry/npm.js                   |  14 +-
 src/content/registry/npm.test.js              |   1 +
 src/content/registry/shared.js                |   7 +-
 src/content/stackoverflow/finder.test.js      |   5 +-
 .../real-examples/real-examples-results.yaml  | 188 ++++++++++++++++++
 5 files changed, 198 insertions(+), 17 deletions(-)

diff --git a/src/content/registry/npm.js b/src/content/registry/npm.js
index 2365e2f..f53a15f 100644
--- a/src/content/registry/npm.js
+++ b/src/content/registry/npm.js
@@ -3,7 +3,7 @@ import { createParseCommand } from './shared';
 const npmInstall = /((npm|yarn)( -g)?( global)? (install|i|add|update)) /;
 const npxInstall = /(npx)\b/;
 const npmExec = /npm exec/;
-const npmCreate = /((npm|yarn) (init|create))\b/;
+const npmCreate = /((npm|yarn) (init|create)) /;
 const packageName = /(?[a-z0-9_@][a-z0-9_./-]*)/;
 const packageVersion = /@(?[~^]?\d+(\.(\d|X|x)+){0,2}(-[a-z0-9_-]+)?)/;
 const packageLabel = /@[a-z0-9_-]+/;
@@ -28,10 +28,8 @@ const parseNpmCommand = createParseCommand(
 );
 
 // npx
-const parseOnlyFirstPackage = (str, foundPackages) => {
-  if (foundPackages > 0) {
-    return null;
-  }
+const parseOnlyFirstPackage = (str, argsAndPackagesWords) => {
+  argsAndPackagesWords.splice(0, argsAndPackagesWords.length);
 
   return parsePackageString(str);
 };
@@ -53,10 +51,8 @@ const parseNpxCommand = createParseCommand(
 );
 
 // create-*, @scope/create
-const parseCreatePackageString = (str, foundPackages) => {
-  if (foundPackages > 0) {
-    return null;
-  }
+const parseCreatePackageString = (str, argsAndPackagesWords) => {
+  argsAndPackagesWords.splice(0, argsAndPackagesWords.length);
 
   const match = str.match(packageName);
   if (!match) return null;
diff --git a/src/content/registry/npm.test.js b/src/content/registry/npm.test.js
index 067e0ad..050ab6a 100644
--- a/src/content/registry/npm.test.js
+++ b/src/content/registry/npm.test.js
@@ -19,6 +19,7 @@ describe('npm', () => {
       'npm install',
       'npm install -g',
       '`npm install node-sass`', // this is not a valid command because of the `
+      'npm create-react-app my-app',
     ])('should return empty array if no packages found', (command) => {
       expect(parseCommand(command)).toStrictEqual([]);
     });
diff --git a/src/content/registry/shared.js b/src/content/registry/shared.js
index 9f733fd..fe72248 100644
--- a/src/content/registry/shared.js
+++ b/src/content/registry/shared.js
@@ -9,8 +9,8 @@ const finishAllWords = (argsAndPackagesWords) => {
 /**
  * @param {string} registryName `type` in result element
  * @param {(line: string) => RegExpMatchArray} getBaseCommandMatch get the base command match for a line (examples: `pip install`, `yarn add`)
- * @param {(word: string, argsAndPackagesWords: string[]) => number} handleArgument add a length to the counter to move it to the next argument
- * @param {(word: string, packagesLength?: number) => { packageName: string, packageVersion?: string, packagePart: string } | null } parsePackageWord parse a word to its package name and its whole part (includes the version)
+ * @param {(word: string, argsAndPackagesWords?: string[]) => number} handleArgument add a length to the counter to move it to the next argument
+ * @param {(word: string, argsAndPackagesWords?: string[]) => { packageName: string, packageVersion?: string, packagePart: string } | null } parsePackageWord parse a word to its package name and its whole part (includes the version)
  */
 export const createParseCommand =
   (registryName, getBaseCommandMatch, handleArgument, parsePackageWord) =>
@@ -50,8 +50,7 @@ export const createParseCommand =
           continue;
         }
 
-        // TODO: pass argsAndPackagesWords instead of packages.length, and empty the array inside the function if needed
-        const packageMatch = parsePackageWord(word, packages.length);
+        const packageMatch = parsePackageWord(word, argsAndPackagesWords);
         if (!packageMatch) {
           counterIndex += word.length + 1;
           continue;
diff --git a/src/content/stackoverflow/finder.test.js b/src/content/stackoverflow/finder.test.js
index 1b5784f..39e5d12 100644
--- a/src/content/stackoverflow/finder.test.js
+++ b/src/content/stackoverflow/finder.test.js
@@ -335,10 +335,7 @@ describe(findRanges.name, () => {
     });
 
     // issue #37, #38
-    it.each([
-      'npm install git://github.com/user-c/dep-2#node0.8.0',
-      'npm init react-app my-app', // create-react-app
-    ])(`Future support '%s`, (command) => {
+    it.each(['npm install git://github.com/user-c/dep-2#node0.8.0'])(`Future support '%s`, (command) => {
       const { body } = createCodeBlock(command);
 
       const foundElements = findRanges(body);
diff --git a/tests/real-examples/real-examples-results.yaml b/tests/real-examples/real-examples-results.yaml
index 88cef76..c1bfd34 100644
--- a/tests/real-examples/real-examples-results.yaml
+++ b/tests/real-examples/real-examples-results.yaml
@@ -1016,6 +1016,26 @@ https://stackoverflow.com/questions/59188624:
     - range: npx create-react-app my-app
       type: npm
       name: create-react-app
+    - range: npm init react-app my-app
+      type: npm
+      name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
+    - range: |-
+        npx create-react-app my-app
+        npm init react-app my-app
+        yarn create react-app my-app
+        
+      type: npm
+      name: create-react-app
+    - range: |-
+        npx create-react-app my-app
+        npm init react-app my-app
+        yarn create react-app my-app
+        
+      type: npm
+      name: create-react-app
     - range: |-
         npx create-react-app my-app
         npm init react-app my-app
@@ -1029,6 +1049,15 @@ https://stackoverflow.com/questions/59188624:
     - range: npx create-react-app my-app
       type: npm
       name: create-react-app
+    - range: npm init react-app my-app
+      type: npm
+      name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
     - range: |-
         npm install create-react-app@latest
         
@@ -1044,6 +1073,9 @@ https://stackoverflow.com/questions/59188624:
         
       type: npm
       name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
     - range: npx create-react-app@latest
       type: npm
       name: create-react-app
@@ -1053,6 +1085,12 @@ https://stackoverflow.com/questions/59188624:
     - range: npx create-react-app@latest ./client
       type: npm
       name: create-react-app
+    - range: npm init react-app my-app
+      type: npm
+      name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
     - range: npm install -g create-react-app
       type: npm
       name: create-react-app
@@ -1085,9 +1123,18 @@ https://stackoverflow.com/questions/59188624:
     - range: npm update -g create-react-app
       type: npm
       name: create-react-app
+    - range: yarn create react-app
+      type: npm
+      name: create-react-app
+    - range: npm init react-app
+      type: npm
+      name: create-react-app
     - range: npx create-react-app
       type: npm
       name: create-react-app
+    - range: yarn create react-app
+      type: npm
+      name: create-react-app
     - range: npm install -g create-react-app
       type: npm
       name: create-react-app
@@ -1117,12 +1164,21 @@ https://stackoverflow.com/questions/59188624:
         
       type: npm
       name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
     - range: npx create-react-app my-app
       type: npm
       name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
     - range: npx create-react-app my-app
       type: npm
       name: create-react-app
+    - range: yarn create react-app my-app
+      type: npm
+      name: create-react-app
     - range: npx create-react-app my-app
       type: npm
       name: create-react-app
@@ -1236,6 +1292,138 @@ https://stackoverflow.com/questions/50605219:
         
       type: npm
       name: http-server
+    - range: >-
+        Start a HTTP Server      : npx http-server
+
+        Lint code                : npx eslint ./src
+                                 # Run uglifyjs command in the package uglify-js
+        Minify JS                : npx -p uglify-js uglifyjs -o app.min.js
+        app.js common.js
+
+        Minify CSS               : npx clean-css-cli -o style.min.css
+        css/bootstrap.css style.css
+
+        Minify HTML              : npx html-minifier index-2.html -o index.html
+        --remove-comments --collapse-whitespace
+
+        Scan for open ports      : npx evilscan 192.168.1.10 --port=10-9999
+
+        Cast video to Chromecast : npx castnow
+        http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4
+
+        
+      type: npm
+      name: eslint
+    - range: >-
+        Start a HTTP Server      : npx http-server
+
+        Lint code                : npx eslint ./src
+                                 # Run uglifyjs command in the package uglify-js
+        Minify JS                : npx -p uglify-js uglifyjs -o app.min.js
+        app.js common.js
+
+        Minify CSS               : npx clean-css-cli -o style.min.css
+        css/bootstrap.css style.css
+
+        Minify HTML              : npx html-minifier index-2.html -o index.html
+        --remove-comments --collapse-whitespace
+
+        Scan for open ports      : npx evilscan 192.168.1.10 --port=10-9999
+
+        Cast video to Chromecast : npx castnow
+        http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4
+
+        
+      type: npm
+      name: uglify-js
+    - range: >-
+        Start a HTTP Server      : npx http-server
+
+        Lint code                : npx eslint ./src
+                                 # Run uglifyjs command in the package uglify-js
+        Minify JS                : npx -p uglify-js uglifyjs -o app.min.js
+        app.js common.js
+
+        Minify CSS               : npx clean-css-cli -o style.min.css
+        css/bootstrap.css style.css
+
+        Minify HTML              : npx html-minifier index-2.html -o index.html
+        --remove-comments --collapse-whitespace
+
+        Scan for open ports      : npx evilscan 192.168.1.10 --port=10-9999
+
+        Cast video to Chromecast : npx castnow
+        http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4
+
+        
+      type: npm
+      name: clean-css-cli
+    - range: >-
+        Start a HTTP Server      : npx http-server
+
+        Lint code                : npx eslint ./src
+                                 # Run uglifyjs command in the package uglify-js
+        Minify JS                : npx -p uglify-js uglifyjs -o app.min.js
+        app.js common.js
+
+        Minify CSS               : npx clean-css-cli -o style.min.css
+        css/bootstrap.css style.css
+
+        Minify HTML              : npx html-minifier index-2.html -o index.html
+        --remove-comments --collapse-whitespace
+
+        Scan for open ports      : npx evilscan 192.168.1.10 --port=10-9999
+
+        Cast video to Chromecast : npx castnow
+        http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4
+
+        
+      type: npm
+      name: html-minifier
+    - range: >-
+        Start a HTTP Server      : npx http-server
+
+        Lint code                : npx eslint ./src
+                                 # Run uglifyjs command in the package uglify-js
+        Minify JS                : npx -p uglify-js uglifyjs -o app.min.js
+        app.js common.js
+
+        Minify CSS               : npx clean-css-cli -o style.min.css
+        css/bootstrap.css style.css
+
+        Minify HTML              : npx html-minifier index-2.html -o index.html
+        --remove-comments --collapse-whitespace
+
+        Scan for open ports      : npx evilscan 192.168.1.10 --port=10-9999
+
+        Cast video to Chromecast : npx castnow
+        http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4
+
+        
+      type: npm
+      name: evilscan
+    - range: >-
+        Start a HTTP Server      : npx http-server
+
+        Lint code                : npx eslint ./src
+                                 # Run uglifyjs command in the package uglify-js
+        Minify JS                : npx -p uglify-js uglifyjs -o app.min.js
+        app.js common.js
+
+        Minify CSS               : npx clean-css-cli -o style.min.css
+        css/bootstrap.css style.css
+
+        Minify HTML              : npx html-minifier index-2.html -o index.html
+        --remove-comments --collapse-whitespace
+
+        Scan for open ports      : npx evilscan 192.168.1.10 --port=10-9999
+
+        Cast video to Chromecast : npx castnow
+        http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4
+
+        
+      type: npm
+      name: castnow
     - range: |-
         npm install -g npx