-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpostprocess-files.cjs
165 lines (150 loc) · 5.36 KB
/
postprocess-files.cjs
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
const fs = require('fs');
const path = require('path');
const { parse } = require('@typescript-eslint/parser');
const pkgImportPath = process.env['PKG_IMPORT_PATH'] ?? 'openlayer/';
const distDir =
process.env['DIST_PATH'] ?
path.resolve(process.env['DIST_PATH'])
: path.resolve(__dirname, '..', '..', 'dist');
const distSrcDir = path.join(distDir, 'src');
/**
* Quick and dirty AST traversal
*/
function traverse(node, visitor) {
if (!node || typeof node.type !== 'string') return;
visitor.node?.(node);
visitor[node.type]?.(node);
for (const key in node) {
const value = node[key];
if (Array.isArray(value)) {
for (const elem of value) traverse(elem, visitor);
} else if (value instanceof Object) {
traverse(value, visitor);
}
}
}
/**
* Helper method for replacing arbitrary ranges of text in input code.
*
* The `replacer` is a function that will be called with a mini-api. For example:
*
* replaceRanges('foobar', ({ replace }) => replace([0, 3], 'baz')) // 'bazbar'
*
* The replaced ranges must not be overlapping.
*/
function replaceRanges(code, replacer) {
const replacements = [];
replacer({ replace: (range, replacement) => replacements.push({ range, replacement }) });
if (!replacements.length) return code;
replacements.sort((a, b) => a.range[0] - b.range[0]);
const overlapIndex = replacements.findIndex(
(r, index) => index > 0 && replacements[index - 1].range[1] > r.range[0],
);
if (overlapIndex >= 0) {
throw new Error(
`replacements overlap: ${JSON.stringify(replacements[overlapIndex - 1])} and ${JSON.stringify(
replacements[overlapIndex],
)}`,
);
}
const parts = [];
let end = 0;
for (const {
range: [from, to],
replacement,
} of replacements) {
if (from > end) parts.push(code.substring(end, from));
parts.push(replacement);
end = to;
}
if (end < code.length) parts.push(code.substring(end));
return parts.join('');
}
/**
* Like calling .map(), where the iteratee is called on the path in every import or export from statement.
* @returns the transformed code
*/
function mapModulePaths(code, iteratee) {
const ast = parse(code, { range: true });
return replaceRanges(code, ({ replace }) =>
traverse(ast, {
node(node) {
switch (node.type) {
case 'ImportDeclaration':
case 'ExportNamedDeclaration':
case 'ExportAllDeclaration':
case 'ImportExpression':
if (node.source) {
const { range, value } = node.source;
const transformed = iteratee(value);
if (transformed !== value) {
replace(range, JSON.stringify(transformed));
}
}
}
},
}),
);
}
async function* walk(dir) {
for await (const d of await fs.promises.opendir(dir)) {
const entry = path.join(dir, d.name);
if (d.isDirectory()) yield* walk(entry);
else if (d.isFile()) yield entry;
}
}
async function postprocess() {
for await (const file of walk(path.resolve(__dirname, '..', '..', 'dist'))) {
if (!/\.([cm]?js|(\.d)?[cm]?ts)$/.test(file)) continue;
const code = await fs.promises.readFile(file, 'utf8');
let transformed = mapModulePaths(code, (importPath) => {
if (file.startsWith(distSrcDir)) {
if (importPath.startsWith(pkgImportPath)) {
// convert self-references in dist/src to relative paths
let relativePath = path.relative(
path.dirname(file),
path.join(distSrcDir, importPath.substring(pkgImportPath.length)),
);
if (!relativePath.startsWith('.')) relativePath = `./${relativePath}`;
return relativePath;
}
return importPath;
}
if (importPath.startsWith('.')) {
// add explicit file extensions to relative imports
const { dir, name } = path.parse(importPath);
const ext = /\.mjs$/.test(file) ? '.mjs' : '.js';
return `${dir}/${name}${ext}`;
}
return importPath;
});
if (file.startsWith(distSrcDir) && !file.endsWith('_shims/index.d.ts')) {
// strip out `unknown extends Foo ? never :` shim guards in dist/src
// to prevent errors from appearing in Go To Source
transformed = transformed.replace(
new RegExp('unknown extends (typeof )?\\S+ \\? \\S+ :\\s*'.replace(/\s+/, '\\s+'), 'gm'),
// replace with same number of characters to avoid breaking source maps
(match) => ' '.repeat(match.length),
);
}
if (file.endsWith('.d.ts')) {
// work around bad tsc behavior
// if we have `import { type Readable } from 'openlayer/_shims/index'`,
// tsc sometimes replaces `Readable` with `import("stream").Readable` inline
// in the output .d.ts
transformed = transformed.replace(/import\("stream"\).Readable/g, 'Readable');
}
// strip out lib="dom" and types="node" references; these are needed at build time,
// but would pollute the user's TS environment
transformed = transformed.replace(
/^ *\/\/\/ *<reference +(lib="dom"|types="node").*?\n/gm,
// replace with same number of characters to avoid breaking source maps
(match) => ' '.repeat(match.length - 1) + '\n',
);
if (transformed !== code) {
await fs.promises.writeFile(file, transformed, 'utf8');
console.error(`wrote ${path.relative(process.cwd(), file)}`);
}
}
}
postprocess();