cloudflare/cloudflare-typescript

Public

mirrored fromhttps://github.com/cloudflare/cloudflare-typescriptAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2d51afdcabc76566fefef0ec43ae4a87283ae44b

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/postprocess-files.cjs

165lines · modecode

1const fs = require('fs');
2const path = require('path');
3const { parse } = require('@typescript-eslint/parser');
4
5const pkgImportPath = process.env['PKG_IMPORT_PATH'] ?? 'cloudflare/'
6
7const distDir =
8 process.env['DIST_PATH'] ?
9 path.resolve(process.env['DIST_PATH'])
10 : path.resolve(__dirname, '..', 'dist');
11const distSrcDir = path.join(distDir, 'src');
12
13/**
14 * Quick and dirty AST traversal
15 */
16function traverse(node, visitor) {
17 if (!node || typeof node.type !== 'string') return;
18 visitor.node?.(node);
19 visitor[node.type]?.(node);
20 for (const key in node) {
21 const value = node[key];
22 if (Array.isArray(value)) {
23 for (const elem of value) traverse(elem, visitor);
24 } else if (value instanceof Object) {
25 traverse(value, visitor);
26 }
27 }
28}
29
30/**
31 * Helper method for replacing arbitrary ranges of text in input code.
32 *
33 * The `replacer` is a function that will be called with a mini-api. For example:
34 *
35 * replaceRanges('foobar', ({ replace }) => replace([0, 3], 'baz')) // 'bazbar'
36 *
37 * The replaced ranges must not be overlapping.
38 */
39function replaceRanges(code, replacer) {
40 const replacements = [];
41 replacer({ replace: (range, replacement) => replacements.push({ range, replacement }) });
42
43 if (!replacements.length) return code;
44 replacements.sort((a, b) => a.range[0] - b.range[0]);
45 const overlapIndex = replacements.findIndex(
46 (r, index) => index > 0 && replacements[index - 1].range[1] > r.range[0],
47 );
48 if (overlapIndex >= 0) {
49 throw new Error(
50 `replacements overlap: ${JSON.stringify(replacements[overlapIndex - 1])} and ${JSON.stringify(
51 replacements[overlapIndex],
52 )}`,
53 );
54 }
55
56 const parts = [];
57 let end = 0;
58 for (const {
59 range: [from, to],
60 replacement,
61 } of replacements) {
62 if (from > end) parts.push(code.substring(end, from));
63 parts.push(replacement);
64 end = to;
65 }
66 if (end < code.length) parts.push(code.substring(end));
67 return parts.join('');
68}
69
70/**
71 * Like calling .map(), where the iteratee is called on the path in every import or export from statement.
72 * @returns the transformed code
73 */
74function mapModulePaths(code, iteratee) {
75 const ast = parse(code, { range: true });
76 return replaceRanges(code, ({ replace }) =>
77 traverse(ast, {
78 node(node) {
79 switch (node.type) {
80 case 'ImportDeclaration':
81 case 'ExportNamedDeclaration':
82 case 'ExportAllDeclaration':
83 case 'ImportExpression':
84 if (node.source) {
85 const { range, value } = node.source;
86 const transformed = iteratee(value);
87 if (transformed !== value) {
88 replace(range, JSON.stringify(transformed));
89 }
90 }
91 }
92 },
93 }),
94 );
95}
96
97async function* walk(dir) {
98 for await (const d of await fs.promises.opendir(dir)) {
99 const entry = path.join(dir, d.name);
100 if (d.isDirectory()) yield* walk(entry);
101 else if (d.isFile()) yield entry;
102 }
103}
104
105async function postprocess() {
106 for await (const file of walk(path.resolve(__dirname, '..', 'dist'))) {
107 if (!/\.([cm]?js|(\.d)?[cm]?ts)$/.test(file)) continue;
108
109 const code = await fs.promises.readFile(file, 'utf8');
110
111 let transformed = mapModulePaths(code, (importPath) => {
112 if (file.startsWith(distSrcDir)) {
113 if (importPath.startsWith(pkgImportPath)) {
114 // convert self-references in dist/src to relative paths
115 let relativePath = path.relative(
116 path.dirname(file),
117 path.join(distSrcDir, importPath.substring(pkgImportPath.length)),
118 );
119 if (!relativePath.startsWith('.')) relativePath = `./${relativePath}`;
120 return relativePath;
121 }
122 return importPath;
123 }
124 if (importPath.startsWith('.')) {
125 // add explicit file extensions to relative imports
126 const { dir, name } = path.parse(importPath);
127 const ext = /\.mjs$/.test(file) ? '.mjs' : '.js';
128 return `${dir}/${name}${ext}`;
129 }
130 return importPath;
131 });
132
133 if (file.startsWith(distSrcDir) && !file.endsWith('_shims/index.d.ts')) {
134 // strip out `unknown extends Foo ? never :` shim guards in dist/src
135 // to prevent errors from appearing in Go To Source
136 transformed = transformed.replace(
137 new RegExp('unknown extends (typeof )?\\S+ \\? \\S+ :\\s*'.replace(/\s+/, '\\s+'), 'gm'),
138 // replace with same number of characters to avoid breaking source maps
139 (match) => ' '.repeat(match.length),
140 );
141 }
142
143 if (file.endsWith('.d.ts')) {
144 // work around bad tsc behavior
145 // if we have `import { type Readable } from 'cloudflare/_shims/index'`,
146 // tsc sometimes replaces `Readable` with `import("stream").Readable` inline
147 // in the output .d.ts
148 transformed = transformed.replace(/import\("stream"\).Readable/g, 'Readable');
149 }
150
151 // strip out lib="dom" and types="node" references; these are needed at build time,
152 // but would pollute the user's TS environment
153 transformed = transformed.replace(
154 /^ *\/\/\/ *<reference +(lib="dom"|types="node").*?\n/gm,
155 // replace with same number of characters to avoid breaking source maps
156 (match) => ' '.repeat(match.length - 1) + '\n',
157 );
158
159 if (transformed !== code) {
160 await fs.promises.writeFile(file, transformed, 'utf8');
161 console.error(`wrote ${path.relative(process.cwd(), file)}`);
162 }
163 }
164}
165postprocess();
166