@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 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,2 @@
1
+ exports.argvToNodeOptions = argv =>
2
+ argv.map(o => `"${o.replace(/"/g, '\\"')}"`).join(' ')
@@ -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
+ }