openai-node
165 строк · 5.3 Кб
1const fs = require('fs');2const path = require('path');3const { parse } = require('@typescript-eslint/parser');4
5const pkgImportPath = process.env['PKG_IMPORT_PATH'] ?? 'openai/'6
7const distDir =8process.env['DIST_PATH'] ?9path.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) {17if (!node || typeof node.type !== 'string') return;18visitor.node?.(node);19visitor[node.type]?.(node);20for (const key in node) {21const value = node[key];22if (Array.isArray(value)) {23for (const elem of value) traverse(elem, visitor);24} else if (value instanceof Object) {25traverse(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) {40const replacements = [];41replacer({ replace: (range, replacement) => replacements.push({ range, replacement }) });42
43if (!replacements.length) return code;44replacements.sort((a, b) => a.range[0] - b.range[0]);45const overlapIndex = replacements.findIndex(46(r, index) => index > 0 && replacements[index - 1].range[1] > r.range[0],47);48if (overlapIndex >= 0) {49throw new Error(50`replacements overlap: ${JSON.stringify(replacements[overlapIndex - 1])} and ${JSON.stringify(51replacements[overlapIndex],52)}`,53);54}55
56const parts = [];57let end = 0;58for (const {59range: [from, to],60replacement,61} of replacements) {62if (from > end) parts.push(code.substring(end, from));63parts.push(replacement);64end = to;65}66if (end < code.length) parts.push(code.substring(end));67return 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) {75const ast = parse(code, { range: true });76return replaceRanges(code, ({ replace }) =>77traverse(ast, {78node(node) {79switch (node.type) {80case 'ImportDeclaration':81case 'ExportNamedDeclaration':82case 'ExportAllDeclaration':83case 'ImportExpression':84if (node.source) {85const { range, value } = node.source;86const transformed = iteratee(value);87if (transformed !== value) {88replace(range, JSON.stringify(transformed));89}90}91}92},93}),94);95}
96
97async function* walk(dir) {98for await (const d of await fs.promises.opendir(dir)) {99const entry = path.join(dir, d.name);100if (d.isDirectory()) yield* walk(entry);101else if (d.isFile()) yield entry;102}103}
104
105async function postprocess() {106for await (const file of walk(path.resolve(__dirname, '..', 'dist'))) {107if (!/\.([cm]?js|(\.d)?[cm]?ts)$/.test(file)) continue;108
109const code = await fs.promises.readFile(file, 'utf8');110
111let transformed = mapModulePaths(code, (importPath) => {112if (file.startsWith(distSrcDir)) {113if (importPath.startsWith(pkgImportPath)) {114// convert self-references in dist/src to relative paths115let relativePath = path.relative(116path.dirname(file),117path.join(distSrcDir, importPath.substring(pkgImportPath.length)),118);119if (!relativePath.startsWith('.')) relativePath = `./${relativePath}`;120return relativePath;121}122return importPath;123}124if (importPath.startsWith('.')) {125// add explicit file extensions to relative imports126const { dir, name } = path.parse(importPath);127const ext = /\.mjs$/.test(file) ? '.mjs' : '.js';128return `${dir}/${name}${ext}`;129}130return importPath;131});132
133if (file.startsWith(distSrcDir) && !file.endsWith('_shims/index.d.ts')) {134// strip out `unknown extends Foo ? never :` shim guards in dist/src135// to prevent errors from appearing in Go To Source136transformed = transformed.replace(137new RegExp('unknown extends (typeof )?\\S+ \\? \\S+ :\\s*'.replace(/\s+/, '\\s+'), 'gm'),138// replace with same number of characters to avoid breaking source maps139(match) => ' '.repeat(match.length),140);141}142
143if (file.endsWith('.d.ts')) {144// work around bad tsc behavior145// if we have `import { type Readable } from 'openai/_shims/index'`,146// tsc sometimes replaces `Readable` with `import("stream").Readable` inline147// in the output .d.ts148transformed = 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 environment153transformed = transformed.replace(154/^ *\/\/\/ *<reference +(lib="dom"|types="node").*?\n/gm,155// replace with same number of characters to avoid breaking source maps156(match) => ' '.repeat(match.length - 1) + '\n',157);158
159if (transformed !== code) {160await fs.promises.writeFile(file, transformed, 'utf8');161console.error(`wrote ${path.relative(process.cwd(), file)}`);162}163}164}
165postprocess();166