@tapjs/processinfo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +97 -0
- package/lib/argv-to-node-options.cjs +2 -0
- package/lib/child_process.cjs +63 -0
- package/lib/cjs.cjs +16 -0
- package/lib/esm.mjs +44 -0
- package/lib/get-exclude.cjs +21 -0
- package/lib/get-process-info.cjs +45 -0
- package/lib/index.cjs +163 -0
- package/lib/json-file.cjs +19 -0
- package/lib/node-options-env.cjs +100 -0
- package/lib/node-options-to-argv.cjs +52 -0
- package/lib/process-info-node.cjs +76 -0
- package/lib/register-coverage.cjs +74 -0
- package/lib/register-env.cjs +21 -0
- package/lib/register-process-end.cjs +29 -0
- package/lib/spawn-opts.cjs +16 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
The ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Isaac Z. Schlueter and Contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
|
15
|
+
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @tapjs/processinfo
|
|
2
|
+
|
|
3
|
+
A Node.js loader to track processes and which JavaScript files they load.
|
|
4
|
+
|
|
5
|
+
After the process has run, all wrapped process info is dumped to
|
|
6
|
+
`.tap/processinfo`.
|
|
7
|
+
|
|
8
|
+
The exported object can also be used to spawn processes, clear the
|
|
9
|
+
processinfo data, or load the processinfo data.
|
|
10
|
+
|
|
11
|
+
## USAGE
|
|
12
|
+
|
|
13
|
+
Run the top level process with a `--loader` or `--require` argument to
|
|
14
|
+
track all Node.js child processes.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
# wrap both CommonJS and ESM
|
|
18
|
+
node --loader=@tapjs/processinfo/esm file.js
|
|
19
|
+
|
|
20
|
+
# wrap only CommonJS
|
|
21
|
+
node --require=@tapjs/processinfo/cjs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
To spawn a wrapped process from JavaScript, you can run:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const ProcessInfo = require('@tapjs/processinfo')
|
|
28
|
+
// any of these will work
|
|
29
|
+
const childProcess = ProcessInfo.spawn(cmd, args, options)
|
|
30
|
+
const childProcess = ProcessInfo.exec(cmd, options)
|
|
31
|
+
const childProcess = ProcessInfo.spawnSync(cmd, args, options)
|
|
32
|
+
const childProcess = ProcessInfo.execSync(cmd, options)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The `cmd` and `args` parameters are identical to the methods from the
|
|
36
|
+
Node.js `child_process` module. The `options` parameter is also identical,
|
|
37
|
+
but may also include an `externalID` field, which if set to a string, will
|
|
38
|
+
be used as the processinfo `externalID`.
|
|
39
|
+
|
|
40
|
+
If you just use the normal `spawn`/`exec` methods from the Node.js
|
|
41
|
+
`child_process` module, then the relevant environment variables will still
|
|
42
|
+
be tracked, unless explicitly set to `''` or some other value.
|
|
43
|
+
|
|
44
|
+
### Interacting with Process Info
|
|
45
|
+
|
|
46
|
+
To load the process info data, use the exported `ProcessInfo` class.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const ProcessInfo = require('@tapjs/processinfo')
|
|
50
|
+
|
|
51
|
+
const processInfo = new ProcessInfo()
|
|
52
|
+
// returns
|
|
53
|
+
// {
|
|
54
|
+
// roots: Set([ProcessInfo.Node, ...]) for each root process group
|
|
55
|
+
// files: Map({ filename => Set([ProcessInfo.Node, ...]) }),
|
|
56
|
+
// externalIDs: Map({ externalID => ProcessInfo.Node }),
|
|
57
|
+
// uuids: Map({ uuid => ProcessInfo.Node }),
|
|
58
|
+
// }
|
|
59
|
+
// A ProcessInfo.Node looks like:
|
|
60
|
+
// {
|
|
61
|
+
// date: iso date string,
|
|
62
|
+
// argv,
|
|
63
|
+
// execArgv,
|
|
64
|
+
// cwd,
|
|
65
|
+
// pid,
|
|
66
|
+
// ppid,
|
|
67
|
+
// uuid,
|
|
68
|
+
// externalID,
|
|
69
|
+
// parent: <ProcessInfo.Node or null for root node>,
|
|
70
|
+
// root: <ProcessInfo.Node>,
|
|
71
|
+
// children: [ProcessInfo.Node, ...],
|
|
72
|
+
// files: [ filename, ... ],
|
|
73
|
+
// code: unix exit code,
|
|
74
|
+
// signal: terminating signal or null,
|
|
75
|
+
// runtime: high resolution run time in ms,
|
|
76
|
+
// }
|
|
77
|
+
const data = await processInfo.load()
|
|
78
|
+
// say we wanted to find all the files loaded by the process 'foo'
|
|
79
|
+
const proc = data.externalIDs.get('foo')
|
|
80
|
+
console.error(`Files loaded by process named 'foo':`, proc.files)
|
|
81
|
+
|
|
82
|
+
// now let's find all any other named processes that loaded them
|
|
83
|
+
for (const f of proc.files) {
|
|
84
|
+
for (const otherProc of data.files.get(f)) {
|
|
85
|
+
// walk up the tree looking for a named process
|
|
86
|
+
for (let parent = otherProc; parent; parent = parent.parent) {
|
|
87
|
+
if (parent.externalID && parent !== proc) {
|
|
88
|
+
console.error(`Also loaded by process ${parent.externalID}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Note: unless there has been a previous wrapped process run, nothing will be
|
|
96
|
+
present in the data. That is, `data.root` will be null, and all the maps
|
|
97
|
+
will be empty.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const {
|
|
2
|
+
spawn,
|
|
3
|
+
spawnSync,
|
|
4
|
+
exec,
|
|
5
|
+
execSync,
|
|
6
|
+
execFile,
|
|
7
|
+
execFileSync,
|
|
8
|
+
} = require('child_process')
|
|
9
|
+
const { getExclude } = require('./get-exclude.cjs')
|
|
10
|
+
const { spawnOpts } = require('./spawn-opts.cjs')
|
|
11
|
+
const k = '_TAPJS_PROCESSINFO_EXCLUDE_'
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
spawn(cmd, args, options) {
|
|
15
|
+
return spawn(cmd, args, spawnOpts(options, getExclude(k)))
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
spawnSync(cmd, args, options) {
|
|
19
|
+
return spawnSync(cmd, args, spawnOpts(options, getExclude(k)))
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
exec(cmd, options, callback) {
|
|
23
|
+
if (typeof options === 'function') {
|
|
24
|
+
callback = options
|
|
25
|
+
options = {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return exec(cmd, spawnOpts(options, getExclude(k)), callback)
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
execSync(cmd, options) {
|
|
32
|
+
return execSync(cmd, spawnOpts(options, getExclude(k)))
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
execFile(cmd, ...execFileArgs) {
|
|
36
|
+
let args = []
|
|
37
|
+
let options = {}
|
|
38
|
+
let callback = undefined
|
|
39
|
+
for (const arg of execFileArgs) {
|
|
40
|
+
if (Array.isArray(arg)) {
|
|
41
|
+
args = arg
|
|
42
|
+
} else if (arg && typeof arg === 'object') {
|
|
43
|
+
options = arg
|
|
44
|
+
} else if (typeof arg === 'function') {
|
|
45
|
+
callback = arg
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return execFile(cmd, args, spawnOpts(options, getExclude(k)), callback)
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
execFileSync(cmd, ...execFileArgs) {
|
|
52
|
+
let args = []
|
|
53
|
+
let options = {}
|
|
54
|
+
for (const arg of execFileArgs) {
|
|
55
|
+
if (Array.isArray(arg)) {
|
|
56
|
+
args = arg
|
|
57
|
+
} else if (arg && typeof arg === 'object') {
|
|
58
|
+
options = arg
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return execFileSync(cmd, args, spawnOpts(options, getExclude(k)))
|
|
62
|
+
},
|
|
63
|
+
}
|
package/lib/cjs.cjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// commonjs style loader hook
|
|
2
|
+
const { getProcessInfo } = require('./get-process-info.cjs')
|
|
3
|
+
const { getExclude } = require('./get-exclude.cjs')
|
|
4
|
+
|
|
5
|
+
// have to make this a module-local, otherwise it reloads the info
|
|
6
|
+
const processInfo = getProcessInfo()
|
|
7
|
+
const exclude = getExclude('_TAPJS_PROCESSINFO_EXCLUDE_')
|
|
8
|
+
|
|
9
|
+
const { addHook } = require('pirates')
|
|
10
|
+
|
|
11
|
+
const matcher = filename => !exclude.test(filename)
|
|
12
|
+
const hook = (code, filename) => {
|
|
13
|
+
processInfo.files.push(filename)
|
|
14
|
+
return code
|
|
15
|
+
}
|
|
16
|
+
addHook(hook, { exts: ['.js', '.cjs'], matcher })
|
package/lib/esm.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// usage: node '--loader=esm.mjs?other-loader&otherotherloader' foo.mjs
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import { getProcessInfo } from './get-process-info.cjs'
|
|
4
|
+
import { getExclude } from './get-exclude.cjs'
|
|
5
|
+
import './cjs.cjs'
|
|
6
|
+
|
|
7
|
+
const processInfo = getProcessInfo()
|
|
8
|
+
const exclude = getExclude('_TAPJS_PROCESSINFO_EXCLUDE_')
|
|
9
|
+
|
|
10
|
+
const others = Promise.all(
|
|
11
|
+
[...new URL(import.meta.url).searchParams.keys()].map(s =>
|
|
12
|
+
import(s).catch(() => ({}))
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
const hasLoad = (async () =>
|
|
16
|
+
(await others).filter(loader => typeof loader.load === 'function'))()
|
|
17
|
+
const hasResolve = (async () =>
|
|
18
|
+
(await others).filter(loader => typeof loader.resolve === 'function'))()
|
|
19
|
+
|
|
20
|
+
const myLoad = defaultFn => async (url, context) => {
|
|
21
|
+
if (/^file:/.test(url)) {
|
|
22
|
+
const filename = fileURLToPath(url)
|
|
23
|
+
if (!exclude.test(filename)) {
|
|
24
|
+
processInfo.files.push(filename)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return defaultFn(url, context, defaultFn)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const load = async (url, context, defaultFn) =>
|
|
31
|
+
runAll(await hasLoad, 'load', url, context, myLoad(defaultFn))
|
|
32
|
+
|
|
33
|
+
export const resolve = async (url, context, defaultFn) =>
|
|
34
|
+
runAll(await hasResolve, 'resolve', url, context, defaultFn)
|
|
35
|
+
|
|
36
|
+
const runAll = async (set, method, url, context, defaultFn, i = 0) => {
|
|
37
|
+
if (i >= set.length) {
|
|
38
|
+
return defaultFn(url, context, defaultFn)
|
|
39
|
+
} else {
|
|
40
|
+
return set[i][method](url, context, (url, context, _) =>
|
|
41
|
+
runAll(set, method, url, context, defaultFn, i + 1)
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const defaultExclude =
|
|
2
|
+
/(^|[\\/])(node_modules|\.tap|tap-testdir-.*?|tap-snapshots|tests?|[^\/]+\.test\.[cm]?js|__tests?__)([\\/]|$)/i
|
|
3
|
+
|
|
4
|
+
const parseExclude = src => {
|
|
5
|
+
const parsed = src.match(/^\/(.*)\/([a-z]*)$/)
|
|
6
|
+
if (parsed) {
|
|
7
|
+
try {
|
|
8
|
+
return new RegExp(parsed[1], parsed[2])
|
|
9
|
+
} catch (e) {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getExclude = k => {
|
|
14
|
+
const src = process.env[k]
|
|
15
|
+
const exclude = (src && parseExclude(src)) || defaultExclude
|
|
16
|
+
process.env[k] = String(exclude)
|
|
17
|
+
return exclude
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
exports.defaultExclude = defaultExclude
|
|
21
|
+
exports.getExclude = getExclude
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { v4: uuid } = require('uuid')
|
|
2
|
+
|
|
3
|
+
const envKey = k => `_TAPJS_PROCESSINFO_${k.toUpperCase()}_`
|
|
4
|
+
const getEnv = k => process.env[envKey(k)]
|
|
5
|
+
const setEnv = (k, v) => (process.env[envKey(k)] = v)
|
|
6
|
+
const delEnv = k => delete process.env[envKey(k)]
|
|
7
|
+
|
|
8
|
+
let processInfo = null
|
|
9
|
+
const getProcessInfo = () => {
|
|
10
|
+
if (!processInfo) {
|
|
11
|
+
require('./register-env.cjs')
|
|
12
|
+
require('./register-coverage.cjs')
|
|
13
|
+
require('./register-process-end.cjs')
|
|
14
|
+
processInfo = {
|
|
15
|
+
hrstart: process.hrtime(),
|
|
16
|
+
date: new Date().toISOString(),
|
|
17
|
+
argv: process.argv,
|
|
18
|
+
execArgv: process.execArgv,
|
|
19
|
+
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
pid: process.pid,
|
|
22
|
+
ppid: process.ppid,
|
|
23
|
+
parent: getEnv('parent') || null,
|
|
24
|
+
uuid: uuid(),
|
|
25
|
+
files: [],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!processInfo.parent) {
|
|
29
|
+
processInfo.root = processInfo.uuid
|
|
30
|
+
setEnv('root', processInfo.uuid)
|
|
31
|
+
} else {
|
|
32
|
+
processInfo.root = getEnv('root')
|
|
33
|
+
}
|
|
34
|
+
// this is the parent of any further child processes
|
|
35
|
+
setEnv('parent', processInfo.uuid)
|
|
36
|
+
const externalID = getEnv('external_id')
|
|
37
|
+
if (externalID) {
|
|
38
|
+
processInfo.externalID = externalID
|
|
39
|
+
// externalID only applies to ONE process, not all its children.
|
|
40
|
+
delEnv('external_id')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return processInfo
|
|
44
|
+
}
|
|
45
|
+
module.exports = { getProcessInfo }
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const {
|
|
2
|
+
spawn,
|
|
3
|
+
spawnSync,
|
|
4
|
+
exec,
|
|
5
|
+
execSync,
|
|
6
|
+
execFile,
|
|
7
|
+
execFileSync,
|
|
8
|
+
} = require('./child_process.cjs')
|
|
9
|
+
|
|
10
|
+
const { resolve, basename } = require('path')
|
|
11
|
+
|
|
12
|
+
const { ProcessInfoNode } = require('./process-info-node.cjs')
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
writeFileSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
rmdirSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
} = require('fs')
|
|
21
|
+
const { writeFile, readdir, rm, rmdir, mkdir } = require('fs/promises')
|
|
22
|
+
|
|
23
|
+
const { safeJSONSync, safeJSON } = require('./json-file.cjs')
|
|
24
|
+
|
|
25
|
+
class ProcessInfo {
|
|
26
|
+
constructor({
|
|
27
|
+
dir = resolve(process.cwd(), '.tap/processinfo'),
|
|
28
|
+
exclude = /(^|\\|\/)node_modules(\\|\/|$)/,
|
|
29
|
+
} = {}) {
|
|
30
|
+
this.dir = dir
|
|
31
|
+
this.exclude = exclude
|
|
32
|
+
this.roots = new Set()
|
|
33
|
+
this.files = new Map()
|
|
34
|
+
this.uuids = new Map()
|
|
35
|
+
this.pendingRoot = new Map()
|
|
36
|
+
this.pendingParent = new Map()
|
|
37
|
+
this.externalIDs = new Map()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
clear() {
|
|
41
|
+
this.roots.clear()
|
|
42
|
+
this.files.clear()
|
|
43
|
+
this.uuids.clear()
|
|
44
|
+
this.externalIDs.clear()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async save() {
|
|
48
|
+
await mkdir(this.dir, { recursive: true })
|
|
49
|
+
const writes = []
|
|
50
|
+
for (const [uuid, info] of this.uuids.entries()) {
|
|
51
|
+
const f = `${this.dir}/${uuid}.json`
|
|
52
|
+
writes.push(writeFile(f, JSON.stringify(info) + '\n', 'utf8'))
|
|
53
|
+
}
|
|
54
|
+
await Promise.all(writes)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async saveSync() {
|
|
58
|
+
mkdirSync(this.dir, { recursive: true })
|
|
59
|
+
for (const [uuid, info] of this.uuids.entries()) {
|
|
60
|
+
const f = `${this.dir}/${uuid}.json`
|
|
61
|
+
writeFileSync(f, JSON.stringify(info) + '\n', 'utf8')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async erase() {
|
|
66
|
+
this.clear()
|
|
67
|
+
/* istanbul ignore next - node version compat */
|
|
68
|
+
try {
|
|
69
|
+
await rm(this.dir, { recursive: true })
|
|
70
|
+
} catch (e) {
|
|
71
|
+
await rmdir(this.dir, { recursive: true })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
eraseSync() {
|
|
76
|
+
this.clear()
|
|
77
|
+
/* istanbul ignore next - node version compat */
|
|
78
|
+
try {
|
|
79
|
+
rmSync(this.dir, { recursive: true })
|
|
80
|
+
} catch (e) {
|
|
81
|
+
rmdirSync(this.dir, { recursive: true })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async load() {
|
|
86
|
+
const promises = []
|
|
87
|
+
for (const entry of await readdir(this.dir).catch(() => [])) {
|
|
88
|
+
const uuid = basename(entry, '.json')
|
|
89
|
+
if (this.uuids.has(uuid)) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
const f = resolve(this.dir, entry)
|
|
93
|
+
promises.push(
|
|
94
|
+
safeJSON(f).then(data => {
|
|
95
|
+
if (!data.uuid || data.uuid !== uuid) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
new ProcessInfoNode(data).link(this)
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
await Promise.all(promises)
|
|
103
|
+
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
loadSync() {
|
|
108
|
+
let entries
|
|
109
|
+
try {
|
|
110
|
+
entries = readdirSync(this.dir)
|
|
111
|
+
} catch (_) {
|
|
112
|
+
entries = []
|
|
113
|
+
}
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const uuid = basename(entry, '.json')
|
|
116
|
+
if (this.uuids.has(uuid)) {
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const f = resolve(this.dir, entry)
|
|
120
|
+
const data = safeJSONSync(f)
|
|
121
|
+
if (!data.uuid || data.uuid !== uuid) {
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
new ProcessInfoNode(data).link(this)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static get Node() {
|
|
131
|
+
return ProcessInfoNode
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static get ProcessInfo() {
|
|
135
|
+
return ProcessInfo
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static get spawn() {
|
|
139
|
+
return spawn
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static get spawnSync() {
|
|
143
|
+
return spawnSync
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static get exec() {
|
|
147
|
+
return exec
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static get execSync() {
|
|
151
|
+
return execSync
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static get execFile() {
|
|
155
|
+
return execFile
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static get execFileSync() {
|
|
159
|
+
return execFileSync
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = ProcessInfo
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// read the file and json decode it, if anything fails, return {}
|
|
2
|
+
|
|
3
|
+
const { readFile } = require('fs/promises')
|
|
4
|
+
const { readFileSync } = require('fs')
|
|
5
|
+
|
|
6
|
+
const safeJSONSync = f => {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(readFileSync(f, 'utf8'))
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return {}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const safeJSON = f =>
|
|
15
|
+
readFile(f, 'utf8')
|
|
16
|
+
.then(d => JSON.parse(d))
|
|
17
|
+
.catch(() => ({}))
|
|
18
|
+
|
|
19
|
+
module.exports = { safeJSON, safeJSONSync }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const cjsLoader = require.resolve('./cjs.cjs')
|
|
2
|
+
const esmLoader = require.resolve('./esm.mjs')
|
|
3
|
+
|
|
4
|
+
// esm --loader is actually last-wins, so only counts as having
|
|
5
|
+
// it if it's the last loader in line. To get around that, we pass
|
|
6
|
+
// ALL loaders to our loader as ?a&b&c&...
|
|
7
|
+
//
|
|
8
|
+
// cjs --require stacks in order, so as long as it's there, it counts.
|
|
9
|
+
|
|
10
|
+
const hasCJSLoader = args => hasLoader(args, ['-r', '--require'], cjsLoader)
|
|
11
|
+
|
|
12
|
+
const hasESMLoader = args =>
|
|
13
|
+
hasLoader(args, ['--experimental-loader', '--loader'], esmLoader, true)
|
|
14
|
+
|
|
15
|
+
const addCJS = args =>
|
|
16
|
+
!hasCJSLoader(args) ? args.concat('--require=' + cjsLoader) : args
|
|
17
|
+
const addESM = args => squashLoaders(args)
|
|
18
|
+
|
|
19
|
+
const cjsOnly = args => (hasESMLoader(args) ? false : hasCJSLoader(args))
|
|
20
|
+
|
|
21
|
+
const { nodeOptionsToArgv } = require('./node-options-to-argv.cjs')
|
|
22
|
+
const { argvToNodeOptions } = require('./argv-to-node-options.cjs')
|
|
23
|
+
const { fileURLToPath, pathToFileURL } = require('url')
|
|
24
|
+
|
|
25
|
+
const { resolve } = require('path')
|
|
26
|
+
const resolveLoader = (path, isURI) => {
|
|
27
|
+
path = decode(path, isURI)
|
|
28
|
+
try {
|
|
29
|
+
return require.resolve(/^\.?\.[\\/]/.test(path) ? resolve(path) : path)
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return path
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const decode = (path, isURI) =>
|
|
36
|
+
!isURI
|
|
37
|
+
? path
|
|
38
|
+
: /^file:/.test(path)
|
|
39
|
+
? fileURLToPath(path)
|
|
40
|
+
: decodeURIComponent(path)
|
|
41
|
+
|
|
42
|
+
const squashLoaders = args => {
|
|
43
|
+
const loaders = []
|
|
44
|
+
const re = /^--(?:experimental-)?loader(?:=(.*))?$/
|
|
45
|
+
const squashed = []
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
const arg = args[i]
|
|
48
|
+
const parsed = arg.match(re)
|
|
49
|
+
if (!parsed) {
|
|
50
|
+
squashed.push(arg)
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
const val = parsed[1] || args[++i]
|
|
54
|
+
if (!val) {
|
|
55
|
+
// --loader with no value anywhere, just put it back how it was
|
|
56
|
+
// and let node barf on it
|
|
57
|
+
squashed.push(arg, args[i - 1])
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
const resolved = resolveLoader(val, true)
|
|
61
|
+
if (resolved === esmLoader) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
loaders.push(encodeURIComponent(val.replace(/\\/g, '/')))
|
|
65
|
+
}
|
|
66
|
+
const q = loaders.length ? '?' : ''
|
|
67
|
+
squashed.push(`--loader=${pathToFileURL(esmLoader)}${q}${loaders.join('&')}`)
|
|
68
|
+
return squashed
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const hasLoader = (args, keys, value, isURI = false) => {
|
|
72
|
+
value = decode(value, isURI)
|
|
73
|
+
for (let i = 0; i < args.length; i++) {
|
|
74
|
+
const arg = args[i]
|
|
75
|
+
// -r <value>
|
|
76
|
+
if (
|
|
77
|
+
keys.includes(arg) &&
|
|
78
|
+
i < args.length - 1 &&
|
|
79
|
+
resolveLoader(args[i + 1], isURI) === value
|
|
80
|
+
) {
|
|
81
|
+
return true
|
|
82
|
+
} else if (arg.startsWith('--') && arg.includes('=')) {
|
|
83
|
+
// --require=<value>
|
|
84
|
+
const [k, ...rest] = arg.split('=')
|
|
85
|
+
if (keys.includes(k) && resolveLoader(rest.join('='), isURI) === value) {
|
|
86
|
+
return true
|
|
87
|
+
} else {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const nodeOptionsEnv = (env, args) => {
|
|
96
|
+
const no = nodeOptionsToArgv(env.NODE_OPTIONS)
|
|
97
|
+
return argvToNodeOptions(cjsOnly(args.concat(no)) ? addCJS(no) : addESM(no))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { nodeOptionsEnv }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
exports.nodeOptionsToArgv = no => {
|
|
2
|
+
if (!no) return []
|
|
3
|
+
const argv = []
|
|
4
|
+
let escaping = false
|
|
5
|
+
let inquote = false
|
|
6
|
+
let tokStart = 0
|
|
7
|
+
let tok = ''
|
|
8
|
+
for (let i = 0; i < no.length; i++) {
|
|
9
|
+
const c = no.charAt(i)
|
|
10
|
+
switch (c) {
|
|
11
|
+
case '"':
|
|
12
|
+
if (escaping) {
|
|
13
|
+
tok += no.slice(tokStart, i - 1) + '"'
|
|
14
|
+
tokStart = i + 1
|
|
15
|
+
escaping = false
|
|
16
|
+
} else if (inquote) {
|
|
17
|
+
tok += no.slice(tokStart, i)
|
|
18
|
+
tokStart = i + 1
|
|
19
|
+
inquote = false
|
|
20
|
+
} else {
|
|
21
|
+
inquote = true
|
|
22
|
+
tok += no.slice(tokStart, i)
|
|
23
|
+
tokStart = i + 1
|
|
24
|
+
}
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
case '\\':
|
|
28
|
+
if (inquote) {
|
|
29
|
+
escaping = true
|
|
30
|
+
}
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
case ' ':
|
|
34
|
+
if (inquote) continue
|
|
35
|
+
tok += no.slice(tokStart, i)
|
|
36
|
+
tokStart = i + 1
|
|
37
|
+
argv.push(tok)
|
|
38
|
+
tok = ''
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
default:
|
|
42
|
+
escaping = false
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (inquote) {
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
49
|
+
tok += no.slice(tokStart)
|
|
50
|
+
argv.push(tok)
|
|
51
|
+
return argv
|
|
52
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
class ProcessInfoNode {
|
|
2
|
+
constructor(data) {
|
|
3
|
+
this.parent = null
|
|
4
|
+
this.children = null
|
|
5
|
+
this.files = null
|
|
6
|
+
Object.assign(this, data)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
toJSON() {
|
|
10
|
+
return {
|
|
11
|
+
...this,
|
|
12
|
+
root: this.root && this.root.uuid,
|
|
13
|
+
parent: this.parent && this.parent.uuid,
|
|
14
|
+
children: this.children && [...this.children].map(c => c.uuid),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
link(db) {
|
|
19
|
+
db.uuids.set(this.uuid, this)
|
|
20
|
+
this.parent = db.uuids.get(this.parent) || this.parent || null
|
|
21
|
+
this.root = db.uuids.get(this.root) || this.root
|
|
22
|
+
|
|
23
|
+
if (this.parent === null) {
|
|
24
|
+
this.root = this
|
|
25
|
+
if (db.pendingRoot.has(this.uuid)) {
|
|
26
|
+
for (const n of db.pendingRoot.get(this.uuid)) {
|
|
27
|
+
n.root = this
|
|
28
|
+
}
|
|
29
|
+
db.pendingRoot.delete(this.uuid)
|
|
30
|
+
}
|
|
31
|
+
db.roots.add(this)
|
|
32
|
+
} else if (typeof this.root === 'string') {
|
|
33
|
+
if (db.pendingRoot.has(this.root)) {
|
|
34
|
+
db.pendingRoot.get(this.root).add(this)
|
|
35
|
+
} else {
|
|
36
|
+
db.pendingRoot.set(this.root, new Set([this]))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof this.parent === 'string') {
|
|
41
|
+
if (db.pendingParent.has(this.parent)) {
|
|
42
|
+
db.pendingParent.get(this.parent).add(this)
|
|
43
|
+
} else {
|
|
44
|
+
db.pendingParent.set(this.parent, new Set([this]))
|
|
45
|
+
}
|
|
46
|
+
} else if (this.parent !== null) {
|
|
47
|
+
if (!this.parent.children) {
|
|
48
|
+
this.parent.children = new Set([this])
|
|
49
|
+
} else {
|
|
50
|
+
this.parent.children.add(this)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (db.pendingParent.has(this.uuid)) {
|
|
55
|
+
this.children = db.pendingParent.get(this.uuid)
|
|
56
|
+
for (const n of this.children) {
|
|
57
|
+
n.parent = this
|
|
58
|
+
}
|
|
59
|
+
db.pendingParent.delete(this.uuid)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const f of this.files) {
|
|
63
|
+
if (!db.files.has(f)) {
|
|
64
|
+
db.files.set(f, new Set([this]))
|
|
65
|
+
} else {
|
|
66
|
+
db.files.get(f).add(this)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.externalID) {
|
|
71
|
+
db.externalIDs.set(this.externalID, this)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { ProcessInfoNode }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// start tracking coverage, unless disabled explicltly
|
|
2
|
+
// export so that we know to collect at the end of the process
|
|
3
|
+
const enabled = process.env._TAPJS_PROCESSINFO_COVERAGE_ !== '0'
|
|
4
|
+
if (enabled) {
|
|
5
|
+
process.env._TAPJS_PROCESSINFO_COVERAGE_ = '1'
|
|
6
|
+
process.setSourceMapsEnabled(true)
|
|
7
|
+
|
|
8
|
+
// NB: coverage exclusion is in addition to processinfo
|
|
9
|
+
// exclusion. Only show coverage for a file we care
|
|
10
|
+
// about at least somewhat, but coverage is a subset.
|
|
11
|
+
const { getExclude } = require('./get-exclude.cjs')
|
|
12
|
+
const exclude = getExclude('_TAPJS_PROCESSINFO_COV_EXCLUDE_')
|
|
13
|
+
const inspector = require('inspector')
|
|
14
|
+
const session = new inspector.Session()
|
|
15
|
+
module.exports.session = session
|
|
16
|
+
session.connect()
|
|
17
|
+
session.post('Profiler.enable')
|
|
18
|
+
session.post('Runtime.enable')
|
|
19
|
+
session.post('Profiler.startPreciseCoverage', {
|
|
20
|
+
callCount: true,
|
|
21
|
+
detailed: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const { fileURLToPath } = require('url')
|
|
25
|
+
const { findSourceMap } = require('module')
|
|
26
|
+
|
|
27
|
+
const lineLengths = f =>
|
|
28
|
+
readFileSync(f, 'utf8')
|
|
29
|
+
.split(/\n|\u2028|\u2029/)
|
|
30
|
+
.map(l => l.length)
|
|
31
|
+
|
|
32
|
+
const { mkdirSync, writeFileSync, readFileSync } = require('fs')
|
|
33
|
+
|
|
34
|
+
const coverageOnProcessEnd = (cwd, processInfo) => {
|
|
35
|
+
const f = `${cwd}/.tap/coverage/${processInfo.uuid}.json`
|
|
36
|
+
mkdirSync(`${cwd}/.tap/coverage`, { recursive: true })
|
|
37
|
+
|
|
38
|
+
session.post('Profiler.takePreciseCoverage', (er, cov) => {
|
|
39
|
+
session.post('Profiler.stopPreciseCoverage')
|
|
40
|
+
|
|
41
|
+
/* istanbul ignore next - something very strange and bad happened */
|
|
42
|
+
if (er) {
|
|
43
|
+
throw er
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sourceMapCache = (cov['source-map-cache'] = {})
|
|
47
|
+
cov.result = cov.result.filter(obj => {
|
|
48
|
+
if (!/^file:/.test(obj.url)) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
const f = fileURLToPath(obj.url)
|
|
52
|
+
if (!processInfo.files.includes(f) || exclude.test(f)) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
// see if it has a source map
|
|
56
|
+
const s = findSourceMap(f)
|
|
57
|
+
if (s) {
|
|
58
|
+
const { payload } = s
|
|
59
|
+
sourceMapCache[obj.url] = Object.assign(Object.create(null), {
|
|
60
|
+
lineLengths: lineLengths(f),
|
|
61
|
+
data: payload,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
return true
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
writeFileSync(f, JSON.stringify(cov, 0, 2) + '\n', 'utf8')
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { coverageOnProcessEnd }
|
|
72
|
+
} else {
|
|
73
|
+
module.exports = { coverageOnProcessEnd: () => {} }
|
|
74
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const processOnSpawn = require('process-on-spawn')
|
|
2
|
+
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
|
|
3
|
+
|
|
4
|
+
processOnSpawn.addListener(obj => {
|
|
5
|
+
const env = obj.env || {}
|
|
6
|
+
obj.env = Object.assign(env, getEnvs(env))
|
|
7
|
+
return obj
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const envRE = /^_TAPJS_PROCESSINFO_/
|
|
11
|
+
|
|
12
|
+
const getEnvs = env => {
|
|
13
|
+
// load it here so that it isn't cached before the loader attaches
|
|
14
|
+
// in self-test scenario.
|
|
15
|
+
const { nodeOptionsEnv } = require('./node-options-env.cjs')
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(process.env)
|
|
18
|
+
.filter(([k, v]) => !hasOwn(env, k) && envRE.test(k))
|
|
19
|
+
.concat([['NODE_OPTIONS', nodeOptionsEnv(env, process.execArgv)]])
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const onExit = require('signal-exit')
|
|
2
|
+
const { getProcessInfo } = require('./get-process-info.cjs')
|
|
3
|
+
|
|
4
|
+
const { mkdirSync, writeFileSync } = require('fs')
|
|
5
|
+
const cwd = process.cwd()
|
|
6
|
+
const globals = new Set(Object.keys(global))
|
|
7
|
+
const { coverageOnProcessEnd } = require('./register-coverage.cjs')
|
|
8
|
+
|
|
9
|
+
onExit(
|
|
10
|
+
(code, signal) => {
|
|
11
|
+
const processInfo = getProcessInfo()
|
|
12
|
+
processInfo.code = code
|
|
13
|
+
processInfo.signal = signal
|
|
14
|
+
const runtime = process.hrtime(processInfo.hrstart)
|
|
15
|
+
delete processInfo.hrstart
|
|
16
|
+
processInfo.files = [...new Set(processInfo.files)]
|
|
17
|
+
processInfo.runtime = runtime[0] * 1e3 + runtime[1] / 1e6
|
|
18
|
+
const globalsAdded = Object.keys(global).filter(k => !globals.has(k))
|
|
19
|
+
if (globalsAdded.length) {
|
|
20
|
+
processInfo.globalsAdded = globalsAdded
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const f = `${cwd}/.tap/processinfo/${processInfo.uuid}.json`
|
|
24
|
+
mkdirSync(`${cwd}/.tap/processinfo`, { recursive: true })
|
|
25
|
+
writeFileSync(f, JSON.stringify(processInfo) + '\n', 'utf8')
|
|
26
|
+
coverageOnProcessEnd(cwd, processInfo)
|
|
27
|
+
},
|
|
28
|
+
{ alwaysLast: true }
|
|
29
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { nodeOptionsEnv } = require('./node-options-env.cjs')
|
|
2
|
+
|
|
3
|
+
const spawnOpts = (options = {}, exclude) => {
|
|
4
|
+
const { externalID } = options
|
|
5
|
+
const env = { ...(options.env || process.env) }
|
|
6
|
+
env.NODE_OPTIONS = nodeOptionsEnv(env, [])
|
|
7
|
+
if (externalID) {
|
|
8
|
+
env._TAPJS_PROCESSINFO_EXTERNAL_ID_ = externalID
|
|
9
|
+
}
|
|
10
|
+
if (exclude) {
|
|
11
|
+
env._TAPJS_PROCESSINFO_EXCLUDE_ = String(exclude)
|
|
12
|
+
}
|
|
13
|
+
return { ...options, env }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { spawnOpts }
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tapjs/processinfo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "lib/index.cjs",
|
|
5
|
+
"files": [
|
|
6
|
+
"lib"
|
|
7
|
+
],
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.cjs",
|
|
10
|
+
"./esm": "./lib/esm.mjs",
|
|
11
|
+
"./cjs": "./lib/cjs.cjs"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"format": "prettier --write . --loglevel warn",
|
|
15
|
+
"test": "tap",
|
|
16
|
+
"snap": "tap",
|
|
17
|
+
"preversion": "npm test",
|
|
18
|
+
"postversion": "npm publish",
|
|
19
|
+
"prepublishOnly": "git push origin --follow-tags"
|
|
20
|
+
},
|
|
21
|
+
"tap": {
|
|
22
|
+
"coverage-map": "map.cjs"
|
|
23
|
+
},
|
|
24
|
+
"prettier": {
|
|
25
|
+
"semi": false,
|
|
26
|
+
"printWidth": 80,
|
|
27
|
+
"tabWidth": 2,
|
|
28
|
+
"useTabs": false,
|
|
29
|
+
"singleQuote": true,
|
|
30
|
+
"jsxSingleQuote": false,
|
|
31
|
+
"bracketSameLine": true,
|
|
32
|
+
"arrowParens": "avoid",
|
|
33
|
+
"endOfLine": "lf"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"pirates": "^4.0.5",
|
|
37
|
+
"process-on-spawn": "^1.0.0",
|
|
38
|
+
"tap": "^16.3.0",
|
|
39
|
+
"uuid": "^8.3.2"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=16"
|
|
43
|
+
},
|
|
44
|
+
"license": "ISC",
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@npmcli/promise-spawn": "^2.0.1",
|
|
47
|
+
"eslint-config-prettier": "^8.5.0",
|
|
48
|
+
"prettier": "^2.6.2",
|
|
49
|
+
"diff": "^5.0.0"
|
|
50
|
+
},
|
|
51
|
+
"repository": "https://github.com/tapjs/processinfo"
|
|
52
|
+
}
|