-
Notifications
You must be signed in to change notification settings - Fork 67
/
generateConfig.js
141 lines (127 loc) · 4.89 KB
/
generateConfig.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const through = require('through2')
const fromEntries = require('fromentries')
const jsonStringify = require('json-stable-stringify')
const acornGlobals = require('acorn-globals')
const { inspectSource, utils: { mergeConfig, mapToObj } } = require('sesify-tofu')
const { inspectEnvironment, environmentTypes, environmentTypeStrings } = require('./inspectEnvironment')
const defaultEnvironment = environmentTypes.frozen
const rootSlug = '<root>'
module.exports = { rootSlug, createConfigSpy }
// createConfigSpy creates a pass-through object stream for the Browserify pipeline.
// it analyses modules for global namespace usages, and generates a config for LavaMoat.
// it calls `onResult` with the config when the stream ends.
function createConfigSpy ({ onResult }) {
const packageToEnvironments = {}
const packageToGlobals = {}
const packageToModules = {}
const moduleIdToPackageName = {}
const configSpy = createSpy(inspectModule, onBuildEnd)
return configSpy
function inspectModule (moduleData) {
const packageName = moduleData.package
moduleIdToPackageName[moduleData.id] = packageName
// initialize mapping from package to module
const packageModules = packageToModules[packageName] = packageToModules[packageName] || {}
packageModules[moduleData.id] = moduleData
// skip for project files (files not from deps)
const isDependency = packageName === rootSlug
if (isDependency) return
// get eval environment
const ast = acornGlobals.parse(moduleData.source)
inspectForEnvironment(ast, packageName)
// get global usage
inspectForGlobals(moduleData, packageName)
}
function inspectForEnvironment (ast, packageName) {
const result = inspectEnvironment(ast, packageName)
// initialize results for package
const environments = packageToEnvironments[packageName] = packageToEnvironments[packageName] || []
environments.push(result)
}
function inspectForGlobals (moduleData, packageName) {
const { source, file } = moduleData
const foundGlobals = inspectSource(source, {
// browserify commonjs scope
ignoredRefs: ['require', 'module', 'exports', 'arguments'],
// browser global refs + browserify global
globalRefs: ['globalThis', 'self', 'window', 'global'],
})
// add globalUsage info
moduleData.globalUsage = mapToObj(foundGlobals)
// skip if no results
if (!foundGlobals.size) return
const packageGlobals = packageToGlobals[packageName]
if (packageGlobals) {
// merge maps
packageToGlobals[packageName] = mergeConfig(packageGlobals, foundGlobals)
} else {
// new map
packageToGlobals[packageName] = foundGlobals
}
}
function generateConfig () {
const resources = {}
const config = { resources }
Object.entries(packageToModules).forEach(([packageName, packageModules]) => {
let globals, packages, environment
// get dependencies
const packageDeps = aggregateDeps({ packageModules, moduleIdToPackageName })
if (packageDeps.length) {
packages = fromEntries(packageDeps.map(dep => [dep, true]))
}
// get globals
if (packageToGlobals[packageName]) {
globals = mapToObj(packageToGlobals[packageName])
// prefer "true" over "read" for clearer difference between
// read/write syntax highlighting
Object.keys(globals).forEach(key => {
if (globals[key] === 'read') globals[key] = true
})
}
// get environment
const environments = packageToEnvironments[packageName]
if (environments) {
const bestEnvironment = environments.sort()[environments.length-1]
const isDefault = bestEnvironment === defaultEnvironment
environment = isDefault ? undefined : environmentTypeStrings[bestEnvironment]
}
// skip package config if there are no settings needed
if (!packages && !globals && !environment) return
// set config for package
resources[packageName] = { packages, globals, environment }
})
return jsonStringify(config, { space: 2 })
}
function onBuildEnd () {
// generate the final config
const config = generateConfig()
// report result
onResult(config)
}
}
function aggregateDeps ({ packageModules, moduleIdToPackageName }) {
const deps = new Set()
Object.values(packageModules).forEach((module) => {
const newDeps = Object.values(module.deps)
.filter(Boolean)
.map(id => moduleIdToPackageName[id])
newDeps.forEach(dep => deps.add(dep))
// ensure the package is not listed as its own dependency
deps.delete(module.package)
})
const depsArray = Array.from(deps.values())
return depsArray
}
function createSpy (onData, onEnd) {
return through.obj((data, _, cb) => {
// give data to observer fn
onData(data)
// pass the data through normally
cb(null, data)
}, (cb) => {
// call flush observer
onEnd()
// end normally
cb()
})
}