-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathts-pnp-paths.js
206 lines (194 loc) · 6.76 KB
/
ts-pnp-paths.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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#!/usr/bin/env node
const path = require("path");
const fs = require("fs");
const { promisify } = require("util");
const { createHash } = require("crypto");
const automatedConfigMarker =
"//This file is auto-generated by ts-pnp-path and should not be edited directly." +
require("os").EOL;
//Read args in a no-dependencies-needed way. = or space between arg values.
const argsObject = process.argv
.join(" ")
.split("-")
.reduce((acc, arg) => {
const [argKey, argValue] = arg.split(/[ =]/);
acc[argKey.replace(/^-?/, "").toLowerCase()] = argValue || true;
return acc;
}, {});
const {
configPath = argsObject.c || "./tsconfig.pnp.json",
depth = argsObject.d,
info = argsObject.i || false,
force = argsObject.f || false,
silent = argsObject.s || false,
rootDir = argsObject.r || process.cwd()
} = argsObject;
function log(...params) {
!silent && console.log(...params);
}
function logInfo(...params) {
info && log(...params);
}
//main async processing
(async () => {
//Start load of existing config.
const existingConfigLoad = promisify(fs.readFile)(
path.resolve(rootDir, configPath),
"utf8"
)
.then(fileContents =>
fileContents.startsWith(automatedConfigMarker)
? JSON.parse(fileContents.substring(automatedConfigMarker.length))
: Promise.reject(
new Error(
`The file on path "${path.resolve(
rootDir,
configPath
)}" seems to not be generated by ts-pnp-paths.
Use another --configPath or remove this file manually to continue."`
)
)
)
.catch(err => (err.code === "ENOENT" ? null : Promise.reject(err)));
//Check for .pnp.js under root. Quit out and clean up if it's not there.
const pnpPath = path.resolve(rootDir, ".pnp.js");
if (!fs.existsSync(pnpPath)) {
logInfo(
`No .pnp.js exists under "${rootDir}", cleaning up "${configPath}."`
);
await existingConfigLoad
.then(
file =>
file !== null &&
promisify(fs.unlink)(path.resolve(rootDir, configPath))
)
.catch(err => log(`Could not remove "${configPath}"`, err));
return;
}
//Hash .yarn.lock and compare with potential previous generated .tsconfig. Quit out if yarn.lock is the same.
const [existingConfig, yarnLockChecksum] = await Promise.all([
existingConfigLoad,
promisify(fs.readFile)(path.resolve(rootDir, "./yarn.lock"), "utf8").then(
fileContents =>
//actually not async but keeps down the var bloat
createHash("sha1")
.update(fileContents, "utf8")
.digest("hex")
)
]);
if (
!force &&
existingConfig != null &&
existingConfig.generatedForDependencyDepth === depth &&
existingConfig.generatedForYarnLockChecksum === yarnLockChecksum
) {
logInfo(
`Previous checksum for yarn.lock matches current, will not regenerate "${configPath}". Use --force to bypass.`
);
return;
}
//list dependency tree by calling yarn cli with supplied depth argument
//this will result in a potentially really big string and JSON, could be changed to buffer
const deps = await new Promise((resolve, reject) => {
let outResult = "";
let errResult = "";
const yarnList = require("child_process").spawn("yarn", [
"list",
"--json",
...(depth != null ? [`--depth=${depth}`] : [])
]);
yarnList.stdout.on("data", data => {
outResult += data.toString();
});
yarnList.stderr.on("data", data => {
errResult += data.toString();
});
yarnList.on("close", code =>
code === 0 ? resolve(JSON.parse(outResult)) : reject(errResult)
);
});
//Pnp file is needed for package location
const pnp = require(pnpPath);
//Traverse dependency tree and use .pnp.js to find package location.
//TODO maybe: Breadth-first traversal would skip out early on more iterations, but perf implications are dwarfed by yarn cli call.
const pathMap = (function reduceDepTreeToMap(
nodes,
targetMap = new Map(),
depth = 0
) {
return nodes == null
? targetMap
: nodes.reduce((acc, { name, children }) => {
const indexOfVerSeparator = name.lastIndexOf("@");
const depName = name.substring(0, indexOfVerSeparator);
const depVersion = name.substring(indexOfVerSeparator + 1);
//Get previous path if accumulated.
const [prevDepth, prevVersion] = acc.get(depName) || [];
if (
//package has been located previously and
prevDepth != null &&
//package has been located at a shallower dependency depth ...
(prevDepth < depth ||
//or, package has been located on the same dependency depth with >= version...
(prevDepth === depth && prevVersion >= depVersion))
) {
//...,then use that one instead.
return acc;
}
//Get package information from pnp
const packageInformation = pnp.getPackageInformation({
name: depName,
reference: depVersion
});
if (packageInformation == null) {
logInfo(
`Could not get package information of ${name}, will not add path for package.`
);
return acc;
}
acc.set(depName, [
depth,
depVersion,
packageInformation.packageLocation
]);
return reduceDepTreeToMap(children, targetMap, depth + 1);
}, targetMap);
})(deps.data.trees);
//Generate finalized paths for tsconfig where @types/ packages are merged to package name.
const paths = Array.from(pathMap.entries()).reduce(
(acc, [packageName, [_depth, _version, path]]) => {
//If this is a types package, stripping the "@types/" prefix is needed
const pathKey =
packageName.indexOf("@types/") == 0
? packageName.substring(7)
: packageName;
const prevPackagePaths = acc[pathKey];
//combine @types/[PACKAGENAME] with [PACKAGENAME]
acc[pathKey] =
prevPackagePaths != null ? [...prevPackagePaths, path] : [path];
return acc;
},
{}
);
//Finalize new tsconfig content
const tsConfigPnp = JSON.stringify(
{
//Save info about this execution for next time
generatedForYarnLockChecksum: yarnLockChecksum,
generatedForDependencyDepth: depth,
compilerOptions: {
//Base url needs to be set to something for paths to work.
baseUrl: "",
paths
}
},
null,
2
);
//Write file
await promisify(fs.writeFile)(
path.resolve(rootDir, configPath),
automatedConfigMarker + tsConfigPnp
);
log(`Generated "${configPath}" with pnp path mappings for Typescript`);
})().catch(err => console.error(`Could not generate "${configPath}"`, err));