Skip to content

Commit 77d9df0

Browse files
committed
add init
0 parents  commit 77d9df0

File tree

12 files changed

+5106
-0
lines changed

12 files changed

+5106
-0
lines changed

.gitignore

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
8+
# Runtime data
9+
pids
10+
*.pid
11+
*.seed
12+
*.pid.lock
13+
14+
# Directory for instrumented libs generated by jscoverage/JSCover
15+
lib-cov
16+
17+
# Coverage directory used by tools like istanbul
18+
coverage
19+
20+
# nyc test coverage
21+
.nyc_output
22+
23+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
24+
.grunt
25+
26+
# Bower dependency directory (https://bower.io/)
27+
bower_components
28+
29+
# node-waf configuration
30+
.lock-wscript
31+
32+
# Compiled binary addons (https://nodejs.org/api/addons.html)
33+
build/Release
34+
35+
# Dependency directories
36+
node_modules/
37+
jspm_packages/
38+
39+
# TypeScript v1 declaration files
40+
typings/
41+
42+
# Optional npm cache directory
43+
.npm
44+
45+
# Optional eslint cache
46+
.eslintcache
47+
48+
# Optional REPL history
49+
.node_repl_history
50+
51+
# Output of 'npm pack'
52+
*.tgz
53+
54+
# Yarn Integrity file
55+
.yarn-integrity
56+
57+
# dotenv environment variables file
58+
.env
59+
.env.test
60+
61+
# parcel-bundler cache (https://parceljs.org/)
62+
.cache
63+
64+
# next.js build output
65+
.next
66+
67+
# nuxt.js build output
68+
.nuxt
69+
70+
# vuepress build output
71+
.vuepress/dist
72+
73+
# Serverless directories
74+
.serverless/
75+
76+
# FuseBox cache
77+
.fusebox/
78+
79+
# DynamoDB Local files
80+
.dynamodb/
81+
82+
# General
83+
.DS_Store
84+
.AppleDouble
85+
.LSOverride
86+
87+
# Icon must end with two \r
88+
Icon
89+
90+
91+
# Thumbnails
92+
._*
93+
94+
# Files that might appear in the root of a volume
95+
.DocumentRevisions-V100
96+
.fseventsd
97+
.Spotlight-V100
98+
.TemporaryItems
99+
.Trashes
100+
.VolumeIcon.icns
101+
.com.apple.timemachine.donotpresent
102+
103+
# Directories potentially created on remote AFP share
104+
.AppleDB
105+
.AppleDesktop
106+
Network Trash Folder
107+
Temporary Items
108+
.apdisk

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Parse NPM Script
2+
3+
Parse a given `npm` script command from `package.json` & return information.
4+
5+
- What the `script` lifecycle looks like
6+
- How the command **really** resolves
7+
- & What the final command run is
8+
9+
Useful for parsing dependencies actually used and figuring out wtf is happening in a complex `package.json`.
10+
11+
## Usage
12+
13+
```js
14+
const path = require('path')
15+
const util = require('util')
16+
const parse = require('./lib')
17+
18+
/* path to your package.json file */
19+
const packagePath = path.join(__dirname, 'tests/fixtures/one.json')
20+
21+
async function runParser() {
22+
const parsed = await parse(packagePath, 'npm run build')
23+
console.log(util.inspect(parsed, {
24+
showHidden: false,
25+
depth: null
26+
}))
27+
}
28+
29+
runParser()
30+
31+
/* Parsed contents
32+
{
33+
command: 'npm run build',
34+
steps: [{
35+
name: 'prebuild',
36+
raw: 'echo a && npm run foo',
37+
parsed: ['echo a', 'echo foo']
38+
},
39+
{
40+
name: 'build',
41+
raw: 'echo b && npm run cleanup',
42+
parsed: ['echo b', 'echo cleanup']
43+
},
44+
{
45+
name: 'postbuild',
46+
raw: 'echo c',
47+
parsed: 'echo c'
48+
}
49+
],
50+
raw: ['echo a', 'echo foo', 'echo b', 'echo cleanup', 'echo c'],
51+
combined: 'echo a && echo foo && echo b && echo cleanup && echo c'
52+
}
53+
*/
54+
```
55+
56+
## Example:
57+
58+
Parsing a `package.json`
59+
60+
```json
61+
{
62+
"name": "parse-npm-script",
63+
"scripts": {
64+
"foo": "echo foo",
65+
"cleanup": "echo cleanup",
66+
"prebuild": "echo a && npm run foo",
67+
"build": "echo b && npm run cleanup",
68+
"postbuild": "echo c"
69+
},
70+
"author": "David Wells",
71+
"license": "MIT"
72+
}
73+
```
74+
75+
Will result in this output:
76+
77+
```js
78+
{
79+
command: 'npm run build',
80+
steps: [{
81+
name: 'prebuild',
82+
raw: 'echo a && npm run foo',
83+
parsed: ['echo a', 'echo foo']
84+
},
85+
{
86+
name: 'build',
87+
raw: 'echo b && npm run cleanup',
88+
parsed: ['echo b', 'echo cleanup']
89+
},
90+
{
91+
name: 'postbuild',
92+
raw: 'echo c',
93+
parsed: 'echo c'
94+
}
95+
],
96+
raw: ['echo a', 'echo foo', 'echo b', 'echo cleanup', 'echo c'],
97+
combined: 'echo a && echo foo && echo b && echo cleanup && echo c'
98+
}
99+
```

lib/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const parse = require('./parser')
2+
const parseJson = require('./parse-json')
3+
4+
/**
5+
* Parse npm package script command
6+
* @param {object|string} pkgOrObject - path to package.json or resolved pkg object
7+
* @param {string} command - npm script command to resolve & parse
8+
* @return {Promise} - resolved command data
9+
*/
10+
async function parseNpmScript(pkgOrObject, command) {
11+
if (typeof pkgOrObject === 'object') {
12+
return parse(pkgOrObject, command)
13+
}
14+
const pkg = await parseJson(pkgOrObject)
15+
return parse(pkg, command)
16+
}
17+
18+
module.exports = parseNpmScript

lib/parse-json.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const fs = require('fs')
2+
const { promisify } = require('util')
3+
const readFileAsync = promisify(fs.readFile)
4+
5+
async function parseJson(packagePath, script) {
6+
const pkgString = await readFileAsync(packagePath, 'utf-8')
7+
const pkg = JSON.parse(pkgString)
8+
return pkg
9+
}
10+
11+
module.exports = parseJson

lib/parser-utils.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
2+
const __DEV__ = false
3+
const noOp = () => {}
4+
let LOGGER = noOp
5+
if (__DEV__) {
6+
LOGGER = (padLevel, ...args) => {
7+
console.log(`${' '.repeat(padLevel)}${args.join(' ')}`)
8+
}
9+
}
10+
11+
const COMPLEX_BASH_COMMANDS = [ // complex command, do not parse further
12+
'bash', 'sh', 'ssh', '.', 'source',
13+
'su', 'sudo',
14+
'cd', // TODO: try parse simple cd, to unwrap further
15+
'if',
16+
'eval',
17+
'cross-env'
18+
]
19+
20+
const SIMPLE_BASH_COMMAND = [
21+
'node', 'npx', 'git',
22+
'rm', 'mkdir',
23+
'echo', 'cat',
24+
'exit', 'kill'
25+
]
26+
27+
const PACKAGE_INSTALLER = ['npm', 'yarn']
28+
29+
const REGEXP_ESCAPE = /\\/g
30+
const REGEXP_QUOTE = /[" ]/g
31+
const wrapJoinBashArgs = (args) => args.map((arg) => {
32+
return `"${arg.replace(REGEXP_ESCAPE, '\\\\').replace(REGEXP_QUOTE, '\\$&')}"`
33+
}).join(' ')
34+
35+
const warpBashSubShell = (command) => `(
36+
${indentLine(command, ' ')}
37+
)`
38+
39+
function parseCommand(pkg, scriptString, level, log = LOGGER) {
40+
log(level, '[parseCommand]', `input: <${scriptString}>`)
41+
scriptString = scriptString.trim()
42+
43+
const [leadingCommand, secondCommand, ...additionalCommands] = scriptString.split(' ')
44+
45+
// Check for complex bash
46+
if (COMPLEX_BASH_COMMANDS.includes(leadingCommand) || leadingCommand.startsWith('./')) {
47+
log(level, '✓ directly executable complex command, return')
48+
return scriptString
49+
} else {
50+
log(level, `? not directly executable complex command: ${leadingCommand}`)
51+
}
52+
53+
// Check for combo commands
54+
if (scriptString.includes(' && ')) {
55+
log(level, '✓ combo command, split')
56+
57+
const subCommandList = scriptString.split(' && ')
58+
return subCommandList.map((command) => {
59+
return parseCommand(pkg, command, level + 1, log) || command
60+
})
61+
/*
62+
return warpBashSubShell(subCommandList.map((command) => {
63+
return parseCommand(pkg, command, level + 1, log) || command
64+
}).join('\n'))
65+
*/
66+
} else {
67+
log(level, `? not combo command, I guess`)
68+
}
69+
70+
if (SIMPLE_BASH_COMMAND.includes(leadingCommand)) {
71+
log(level, '✓ directly executable simple command, return')
72+
73+
return scriptString
74+
} else {
75+
log(level, `? not directly executable simple command: ${leadingCommand}`)
76+
}
77+
78+
// TODO: consider allow package dependency command
79+
80+
if (PACKAGE_INSTALLER.includes(leadingCommand)) {
81+
if (secondCommand === 'run') {
82+
log(level, '✓ package script, parse')
83+
const [scriptName, ...extraArgs] = additionalCommands
84+
extraArgs[0] === '--' && extraArgs.shift()
85+
return parsePackageScript(pkg, scriptName, extraArgs.join(' '), level + 1, log)
86+
}
87+
if (secondCommand === 'test' || secondCommand === 't') {
88+
log(level, '✓ package test script, parse')
89+
const [...extraArgs] = additionalCommands
90+
extraArgs[0] === '--' && extraArgs.shift()
91+
return parsePackageScript(pkg, 'test', extraArgs.join(' '), level + 1, log)
92+
}
93+
if (secondCommand === 'start') {
94+
log(level, '✓ package test script, parse')
95+
const [...extraArgs] = additionalCommands
96+
extraArgs[0] === '--' && extraArgs.shift()
97+
return parsePackageScript(pkg, 'start', extraArgs.join(' '), level + 1, log)
98+
}
99+
} else {
100+
log(level, '? unknown npm/yarn script')
101+
}
102+
103+
log(level, '? unknown script, bail')
104+
return scriptString
105+
}
106+
107+
function parsePackageScript(pkg, scriptName, extraArgsString = '', level, log = LOGGER) {
108+
log(level, '[parsePackageScript]', `script name: <${scriptName}>, extra: ${extraArgsString}`)
109+
110+
const scriptString = pkg.scripts[scriptName]
111+
if (!scriptString) {
112+
throw new Error(`[parsePackageScript] missing script with name: ${scriptName}`)
113+
}
114+
115+
const otherScriptString = [scriptString, extraArgsString].filter(Boolean).join(' ')
116+
const command = parseCommand(pkg, otherScriptString, level + 1, log)
117+
if (command) {
118+
return command
119+
}
120+
121+
log(level, '? unexpected script, bail to npm run')
122+
123+
return [`${otherScriptString}`, extraArgsString].filter(Boolean).join(' -- ')
124+
// return [`npm run ${scriptName}`, extraArgsString].filter(Boolean).join(' -- ')
125+
}
126+
127+
const REGEXP_INDENT_LINE = /\n/g
128+
function indentLine(string, indentString = ' ', indentStringStart = indentString) {
129+
return `${indentStringStart}${string.replace(REGEXP_INDENT_LINE, `\n${indentString}`)}`
130+
}
131+
132+
module.exports = {
133+
wrapJoinBashArgs,
134+
warpBashSubShell,
135+
parseCommand,
136+
parsePackageScript
137+
}

0 commit comments

Comments
 (0)