Skip to content

Commit

Permalink
feat(client): support importing node or web shims manually (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-bot committed Sep 20, 2023
1 parent 01973fe commit c1237fe
Show file tree
Hide file tree
Showing 77 changed files with 1,275 additions and 991 deletions.
3 changes: 3 additions & 0 deletions .prettierignore
Expand Up @@ -2,3 +2,6 @@ CHANGELOG.md
/ecosystem-tests
/node_modules
/deno

# don't format tsc output, will break source maps
/dist
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -341,5 +341,9 @@ The following runtimes are supported:
- Bun 1.0 or later.
- Cloudflare Workers.
- Vercel Edge Runtime.
- Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time).
- Nitro v2.6 or greater.

Note that React Native is not supported at this time.

If you are interested in other runtime environments, please open or upvote an issue on GitHub.
15 changes: 4 additions & 11 deletions build
Expand Up @@ -12,7 +12,7 @@ rm -rf dist; mkdir dist
# Copy src to dist/src and build from dist/src into dist, so that
# the source map for index.js.map will refer to ./src/index.ts etc
cp -rp src README.md dist
rm dist/src/_shims/*-deno.*
rm dist/src/_shims/*-deno.ts dist/src/_shims/auto/*-deno.ts
for file in LICENSE CHANGELOG.md; do
if [ -e "${file}" ]; then cp "${file}" dist; fi
done
Expand All @@ -27,8 +27,8 @@ node scripts/make-dist-package-json.cjs > dist/package.json
# build to .js/.mjs/.d.ts files
npm exec tsc-multi
# copy over handwritten .js/.mjs/.d.ts files
cp src/_shims/*.{d.ts,js,mjs} dist/_shims
npm exec tsc-alias -- -p tsconfig.build.json
cp src/_shims/*.{d.ts,js,mjs,md} dist/_shims
cp src/_shims/auto/*.{d.ts,js,mjs} dist/_shims/auto
# we need to add exports = module.exports = Anthropic TypeScript to index.js;
# No way to get that from index.ts because it would cause compile errors
# when building .mjs
Expand All @@ -40,14 +40,7 @@ node scripts/fix-index-exports.cjs
cp dist/index.d.ts dist/index.d.mts
cp tsconfig.dist-src.json dist/src/tsconfig.json

# strip out lib="dom" and types="node" references; these are needed at build time,
# but would pollute the user's TS environment
find dist -type f -exec node scripts/remove-triple-slash-references.js {} +
# strip out `unknown extends RequestInit ? never :` from dist/src/_shims;
# these cause problems when viewing the .ts source files in go to definition
find dist/src/_shims -type f -exec node scripts/replace-shim-guards.js {} +

npm exec prettier -- --loglevel=warn --write .
node scripts/postprocess-files.cjs

# make sure that nothing crashes when we require the output CJS or
# import the output ESM
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Expand Up @@ -4,7 +4,7 @@ module.exports = {
testEnvironment: 'node',
moduleNameMapper: {
'^@anthropic-ai/sdk$': '<rootDir>/src/index.ts',
'^@anthropic-ai/sdk/_shims/(.*)$': '<rootDir>/src/_shims/$1-node',
'^@anthropic-ai/sdk/_shims/auto/(.*)$': '<rootDir>/src/_shims/auto/$1-node',
'^@anthropic-ai/sdk/(.*)$': '<rootDir>/src/$1',
},
modulePathIgnorePatterns: ['<rootDir>/ecosystem-tests/', '<rootDir>/dist/', '<rootDir>/deno_tests/'],
Expand Down
94 changes: 33 additions & 61 deletions package.json
Expand Up @@ -9,76 +9,49 @@
"repository": "github:anthropics/anthropic-sdk-typescript",
"license": "MIT",
"private": false,
"sideEffects": [
"./_shims/index.js",
"./_shims/index.mjs",
"./shims/node.js",
"./shims/node.mjs",
"./shims/web.js",
"./shims/web.mjs"
],
"exports": {
"./_shims/node-readable": {
"./_shims/auto/*": {
"deno": {
"types": "./dist/_shims/node-readable.d.ts",
"require": "./dist/_shims/node-readable.js",
"default": "./dist/_shims/node-readable.mjs"
"types": "./dist/_shims/auto/*.d.ts",
"require": "./dist/_shims/auto/*.js",
"default": "./dist/_shims/auto/*.mjs"
},
"bun": {
"types": "./dist/_shims/node-readable-node.d.ts",
"require": "./dist/_shims/node-readable-node.js",
"default": "./dist/_shims/node-readable-node.mjs"
"types": "./dist/_shims/auto/*.d.ts",
"require": "./dist/_shims/auto/*-bun.js",
"default": "./dist/_shims/auto/*-bun.mjs"
},
"browser": {
"types": "./dist/_shims/node-readable.d.ts",
"require": "./dist/_shims/node-readable.js",
"default": "./dist/_shims/node-readable.mjs"
"types": "./dist/_shims/auto/*.d.ts",
"require": "./dist/_shims/auto/*.js",
"default": "./dist/_shims/auto/*.mjs"
},
"worker": {
"types": "./dist/_shims/node-readable.d.ts",
"require": "./dist/_shims/node-readable.js",
"default": "./dist/_shims/node-readable.mjs"
"types": "./dist/_shims/auto/*.d.ts",
"require": "./dist/_shims/auto/*.js",
"default": "./dist/_shims/auto/*.mjs"
},
"workerd": {
"types": "./dist/_shims/node-readable.d.ts",
"require": "./dist/_shims/node-readable.js",
"default": "./dist/_shims/node-readable.mjs"
"types": "./dist/_shims/auto/*.d.ts",
"require": "./dist/_shims/auto/*.js",
"default": "./dist/_shims/auto/*.mjs"
},
"node": {
"types": "./dist/_shims/node-readable-node.d.ts",
"require": "./dist/_shims/node-readable-node.js",
"default": "./dist/_shims/node-readable-node.mjs"
"types": "./dist/_shims/auto/*-node.d.ts",
"require": "./dist/_shims/auto/*-node.js",
"default": "./dist/_shims/auto/*-node.mjs"
},
"types": "./dist/_shims/node-readable.d.ts",
"require": "./dist/_shims/node-readable.js",
"default": "./dist/_shims/node-readable.mjs"
},
"./_shims/*": {
"deno": {
"types": "./dist/_shims/*.d.ts",
"require": "./dist/_shims/*.js",
"default": "./dist/_shims/*.mjs"
},
"bun": {
"types": "./dist/_shims/*.d.ts",
"require": "./dist/_shims/*.js",
"default": "./dist/_shims/*.mjs"
},
"browser": {
"types": "./dist/_shims/*.d.ts",
"require": "./dist/_shims/*.js",
"default": "./dist/_shims/*.mjs"
},
"worker": {
"types": "./dist/_shims/*.d.ts",
"require": "./dist/_shims/*.js",
"default": "./dist/_shims/*.mjs"
},
"workerd": {
"types": "./dist/_shims/*.d.ts",
"require": "./dist/_shims/*.js",
"default": "./dist/_shims/*.mjs"
},
"node": {
"types": "./dist/_shims/*-node.d.ts",
"require": "./dist/_shims/*-node.js",
"default": "./dist/_shims/*-node.mjs"
},
"types": "./dist/_shims/*.d.ts",
"require": "./dist/_shims/*.js",
"default": "./dist/_shims/*.mjs"
"types": "./dist/_shims/auto/*.d.ts",
"require": "./dist/_shims/auto/*.js",
"default": "./dist/_shims/auto/*.mjs"
},
".": {
"require": {
Expand Down Expand Up @@ -123,18 +96,17 @@
},
"devDependencies": {
"@types/jest": "^29.4.0",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"@anthropic-ai/sdk": "link:.",
"eslint": "^8.22.0",
"eslint": "^8.49.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^29.4.0",
"prettier": "rattrayalex/prettier#postfix-ternaries",
"ts-jest": "^29.1.0",
"ts-morph": "^19.0.0",
"ts-node": "^10.5.0",
"tsc-alias": "^1.8.6",
"tsc-multi": "^1.1.0",
"tsconfig-paths": "^4.0.0",
"typescript": "^4.8.2"
Expand Down
8 changes: 6 additions & 2 deletions release-please-config.json
Expand Up @@ -57,7 +57,11 @@
"hidden": true
}
],
"reviewers": ["RobertCraigie"],
"reviewers": [
"RobertCraigie"
],
"release-type": "node",
"extra-files": ["src/version.ts"]
"extra-files": [
"src/version.ts"
]
}
160 changes: 160 additions & 0 deletions scripts/postprocess-files.cjs
@@ -0,0 +1,160 @@
const fs = require('fs');
const path = require('path');
const { parse } = require('@typescript-eslint/parser');

const distDir = 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('@anthropic-ai/sdk/')) {
// convert self-references in dist/src to relative paths
let relativePath = path.relative(
path.dirname(file),
path.join(distSrcDir, importPath.substring('openai/'.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 '@anthropic-ai/sdk/_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();
11 changes: 0 additions & 11 deletions scripts/remove-triple-slash-references.js

This file was deleted.

0 comments on commit c1237fe

Please sign in to comment.