├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ ├── release.yml │ └── update.yml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── deno.json ├── mod.ts ├── package-lock.json ├── package.json ├── scripts ├── hooks │ └── pre-commit ├── pretest.ts ├── ts-version.ts └── update.sh ├── src ├── _transformations │ ├── shim.ts │ ├── specifiers.test.ts │ ├── specifiers.ts │ ├── vendor.test.ts │ └── vendor.ts ├── cli.ts ├── config.ts ├── context.ts ├── deno2node.ts ├── deps.deno.ts ├── deps.node.ts ├── emit.ts ├── help.ts ├── init.ts ├── mod.ts ├── shim.node.ts └── util │ └── regexp.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*.*] 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [*] 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [.github/CODEOWNERS] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/ woj.pawlik@gmail.com 2 | /package*.json woj.pawlik@gmail.com 3 | /scripts/ woj.pawlik@gmail.com 4 | /src/deps.deno.ts woj.pawlik@gmail.com 5 | /README.md @KnorpelSenf 6 | /src/help.ts @KnorpelSenf -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'main' 8 | - 'v[0-9]+' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - run: npm install --ignore-scripts 16 | - run: echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH" 17 | - run: deno test --no-run 18 | - run: deno lint 19 | 20 | - run: src/cli.ts 21 | - run: lib/cli.js --outDir nodejs/ 22 | - run: git diff --no-index lib/ nodejs/ 23 | 24 | - run: deno test --allow-read=. src/ 25 | - run: node --test lib/ 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | id-token: write 11 | 12 | jobs: 13 | npm: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - run: npm ci --ignore-scripts 18 | - run: npm run prepare 19 | - name: Publish to npm 20 | run: | 21 | npm config set //registry.npmjs.org/:_authToken '${NPM_TOKEN}' 22 | [[ "$GITHUB_REF_NAME" =~ - ]] && npm config set tag=next 23 | npm pkg set version="${GITHUB_REF_NAME/v/}" 24 | npm publish --ignore-scripts 25 | env: 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | NPM_CONFIG_PROVENANCE: true 28 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: Update 2 | 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | # 30 min after deno-bin 7 | - cron: '30 12 * * SAT' 8 | 9 | jobs: 10 | update: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | # to trigger release workflow 16 | ssh-key: ${{ secrets.SSH_DEPLOY_KEY }} 17 | - name: git config 18 | run: | 19 | git config user.name 'GitHub Actions' 20 | git config user.email '41898282+github-actions[bot]@users.noreply.github.com' 21 | git config user.signingKey "$SSH_KEY" 22 | git config gpg.format ssh 23 | git config core.hooksPath scripts/hooks/ 24 | env: 25 | SSH_KEY: ${{ secrets.SSH_DEPLOY_KEY }} 26 | - run: scripts/update.sh 27 | env: 28 | npm_config_sign_git_commit: true 29 | npm_config_sign_git_tag: true 30 | - run: git push --follow-tags 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /deno2node-*.tgz 2 | /node_modules/ 3 | /src/vendor/ 4 | /lib/ 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": false, 5 | "deno.path": "node_modules/.bin/deno", 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": "explicit" 8 | }, 9 | "editor.defaultFormatter": "denoland.vscode-deno", 10 | "editor.formatOnSave": true, 11 | "files.readonlyInclude": { 12 | "lib/**": true, 13 | "node_modules/**": true, 14 | "package-lock.json": true, 15 | "src/deps.deno.ts": true 16 | }, 17 | "terminal.integrated.env.linux": { 18 | "NPM_CONFIG_PREID": "beta", 19 | "DENO_FUTURE": "1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "prepare", 7 | "group": "build", 8 | "label": "npm: prepare" 9 | }, 10 | { 11 | "type": "npm", 12 | "script": "test", 13 | "group": "test", 14 | "label": "npm: test" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wojciech Pawlik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno2node 2 | 3 | `tsc` replacement for transpiling [Deno] libraries to run on [Node.js]. 4 | 5 | > [!Note] 6 | > You don't need this if you maintain a npm package Deno can already 7 | > run: https://deno.land/manual/node 8 | 9 | ![Because Deno's tooling is way simpler than 10 | Node's](https://pbs.twimg.com/media/FBba11IXMAQB7pX?format=jpg) 11 | 12 | ## Quick Setup 13 | 14 | Run `npx deno2node --init` in an empty directory. 15 | 16 | > [!Note] 17 | > If you don't already have a `package.json`, you may find [`dnt`] 18 | > easier to use. 19 | 20 | ## CLI Usage From Node.js 21 | 22 | ```sh 23 | npm install -ED deno2node 24 | npm pkg set scripts.prepare=deno2node 25 | ``` 26 | 27 | > [!Warning] 28 | > New features or TypeScript upgrades may change output or diagnostics 29 | > across minor versions of `deno2node`. Use `--save-prefix='~'` or 30 | > `--save-exact` (`-E`) to avoid unexpected failures. 31 | 32 | Create a [`tsconfig.json`] to specify `"compilerOptions"` and `"files"` to 33 | `"include"`, if you don't have one already. 34 | 35 | You can now invoke `deno2node` by running `npm run prepare`. 36 | 37 | It will alse be executed automatically when you run `npm install`. 38 | 39 | ## CLI Usage From Deno 40 | 41 | `deno2node` is actually a Deno project that compiles itself to run on Node.js. 42 | (This is a great way to test the tool, too.) 43 | 44 | ```sh 45 | deno run --no-prompt --allow-read=. --allow-write=. \ 46 | https://deno.land/x/deno2node/src/cli.ts 47 | ``` 48 | 49 | ## How It Works 50 | 51 | There are three main steps to this. 52 | 53 | 1. Transform the code base in-memory, by rewriting all import statements. 54 | 2. Typecheck the code. 55 | 3. Emit `.js` and `.d.ts` files. These files can directly be run by Node or 56 | published on npm. 57 | 58 | `deno2node` uses [`ts-morph`] under the hood, which in turn builds on top of the 59 | TypeScript compiler `tsc`. Hence, you get the same behaviour as if you had 60 | developed your code directly for Node. 61 | 62 | `deno2node` can perform more powerful transpilation steps that make it flexible 63 | enough for most needs. 64 | 65 | ### Shimming 66 | 67 | Some things are global in Deno, but not in Node.js. 68 | 69 | To rectify this, create a file that exports shims for the globals you need: 70 | 71 | ```ts 72 | // @filename: src/shim.node.ts 73 | export { webcrypto as crypto } from "crypto"; 74 | export { Deno } from "@deno/shim-deno"; 75 | export { alert, confirm, prompt } from "@deno/shim-prompts"; 76 | ``` 77 | 78 | > [!Note] 79 | > `node:` APIs are well-supported on both runtimes. 80 | 81 | Then, register your shims in [`tsconfig.json`], so `deno2node` can import them 82 | where needed: 83 | 84 | ```jsonc 85 | // @filename: tsconfig.json 86 | { 87 | "deno2node": { 88 | "shim": "src/shim.node.ts" // path to shim file, relative to tsconfig 89 | } 90 | } 91 | ``` 92 | 93 | ### Runtime-specific code 94 | 95 | In same cases you may want to have two different implementations, depending on 96 | whether you're running on Deno or on Node. When shimming is not enough, you can 97 | provide a Node-specific `.node.ts` and a Deno-specific 98 | `.deno.ts` version of any file. They need to reside next to each other 99 | in the same directory. 100 | 101 | `deno2node` will ignore the Deno version and rewrite imports to use the Node.js 102 | version instead. Thus, the Deno-specific file will not be part of the build 103 | output. 104 | 105 | For example, provide `greet.deno.ts` for Deno: 106 | 107 | ```ts 108 | // @filename: src/greet.deno.ts 109 | export function greet() { 110 | console.log("Hello Deno!"); 111 | // access Deno-specific APIs here 112 | } 113 | ``` 114 | 115 | Now, provide `greet.node.ts` for Node: 116 | 117 | ```ts 118 | // @filename: src/greet.node.ts 119 | export function greet() { 120 | console.log("Hello Node!"); 121 | // access Node-specific APIs here 122 | } 123 | ``` 124 | 125 | Finally, use it in `foo.ts`: 126 | 127 | ```ts 128 | import { greet } from "./platform.deno.ts"; 129 | 130 | // Prints "Hello Deno!" on Deno, 131 | // and "Hello Node!" on Node: 132 | greet(); 133 | ``` 134 | 135 | This technique has many uses. `deno2node` itself uses it to import from 136 | https://deno.land/x. The Telegram bot framework [`grammY`] uses it to abstract 137 | away platform-specific APIs. 138 | 139 | ### Vendoring 140 | 141 | To import a module which has no npm equivalent, first set up `vendorDir`. 142 | 143 | ```jsonc 144 | // @filename: tsconfig.json 145 | { 146 | "deno2node": { 147 | "vendorDir": "src/vendor/" // path within `rootDir` 148 | } 149 | } 150 | ``` 151 | 152 | Then, populate it: `deno vendor src/deps.vendor.ts --output src/vendor/`. 153 | 154 | Vendoring is still experimental, so be welcome to open an issue if you encounter 155 | a problem with it! 156 | 157 | Also, consider recommending [`pnpm`] to users of your library. It might be able 158 | to deduplicate vendored files across packages. 159 | 160 | ## API 161 | 162 | Confer the automatically generated [API Reference] if you want to use 163 | `deno2node` from code. 164 | 165 | ## Testing 166 | 167 | Register tests via `"node:test"`, and build them alongside the source. 168 | 169 | Then, test with `deno test src/` _and_ `node --test lib/`. 170 | 171 | ## Contributing 172 | 173 | `npm it` to install dependencies, build, and test the project. 174 | 175 | `git config core.hooksPath scripts/hooks/` to build before each commit. 176 | 177 | [deno]: https://deno.land/ 178 | [node.js]: https://nodejs.org/ 179 | [`dnt`]: https://github.com/denoland/dnt 180 | [`grammy`]: https://github.com/grammyjs/grammY 181 | [`pnpm`]: https://github.com/pnpm/pnpm#background 182 | [`ts-morph`]: https://github.com/dsherret/ts-morph 183 | [`tsconfig.json`]: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html 184 | [api reference]: https://doc.deno.land/https/deno.land/x/deno2node/src/mod.ts 185 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "unstable": ["byonm"], 4 | "fmt": { "proseWrap": "preserve" }, 5 | "exclude": ["lib/", "package-lock.json"] 6 | } 7 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/mod.ts"; 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deno2node", 3 | "version": "1.16.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "deno2node", 9 | "version": "1.16.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "ts-morph": "^26.0.0" 13 | }, 14 | "bin": { 15 | "deno2node": "lib/cli.js" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^18.15.11", 19 | "deno": "~2.3.3", 20 | "fast-check": "^3.10.0" 21 | }, 22 | "engines": { 23 | "node": ">=14.13.1" 24 | } 25 | }, 26 | "node_modules/@deno/darwin-arm64": { 27 | "version": "2.3.3", 28 | "resolved": "https://registry.npmjs.org/@deno/darwin-arm64/-/darwin-arm64-2.3.3.tgz", 29 | "integrity": "sha512-nvbEP3fLINcs4IAvlBrDv6H2WxRxyeGne/704kP62weUFFh/XVHW/QLAdcvkBnWcW6as6BhAX0870wiMgtw6wQ==", 30 | "cpu": [ 31 | "arm64" 32 | ], 33 | "dev": true, 34 | "license": "MIT", 35 | "optional": true, 36 | "os": [ 37 | "darwin" 38 | ] 39 | }, 40 | "node_modules/@deno/darwin-x64": { 41 | "version": "2.3.3", 42 | "resolved": "https://registry.npmjs.org/@deno/darwin-x64/-/darwin-x64-2.3.3.tgz", 43 | "integrity": "sha512-g7kC/l2RWar/p+9y5E3spgB3lPHYMGmNqYYFH5jULw5k6bVzsyn+4ID14RRI0BlyYtg/N3F2TBuFWTYmgVN9uA==", 44 | "cpu": [ 45 | "x64" 46 | ], 47 | "dev": true, 48 | "license": "MIT", 49 | "optional": true, 50 | "os": [ 51 | "darwin" 52 | ] 53 | }, 54 | "node_modules/@deno/linux-arm64-glibc": { 55 | "version": "2.3.3", 56 | "resolved": "https://registry.npmjs.org/@deno/linux-arm64-glibc/-/linux-arm64-glibc-2.3.3.tgz", 57 | "integrity": "sha512-GJwc6x5J6ZSaCr1Xv8mYLoMfoPdFzfweJIBDqRVjdEowFCkRbaaS0rhgBPDataQ6IfCN2nt6jO5lyDjg/byTwg==", 58 | "cpu": [ 59 | "arm64" 60 | ], 61 | "dev": true, 62 | "license": "MIT", 63 | "optional": true, 64 | "os": [ 65 | "linux" 66 | ] 67 | }, 68 | "node_modules/@deno/linux-x64-glibc": { 69 | "version": "2.3.3", 70 | "resolved": "https://registry.npmjs.org/@deno/linux-x64-glibc/-/linux-x64-glibc-2.3.3.tgz", 71 | "integrity": "sha512-+w+XtukKjF7O3GiZKyaoOxFadu8HWfwijaQK1qQgUsB82QCmfmmDssRAX/MYiOeveM2izj/QlIzCOwb+O2Ovaw==", 72 | "cpu": [ 73 | "x64" 74 | ], 75 | "dev": true, 76 | "license": "MIT", 77 | "optional": true, 78 | "os": [ 79 | "linux" 80 | ] 81 | }, 82 | "node_modules/@deno/win32-arm64": { 83 | "version": "2.3.3", 84 | "resolved": "https://registry.npmjs.org/@deno/win32-arm64/-/win32-arm64-2.3.3.tgz", 85 | "integrity": "sha512-vEA4HY1sjUeKXXoRpbyKJXyuF69jqz+Z4bDYxLGTUqXa8mYpK/QJpjYD0UtCewWMMfH6Q5IqpKEm2Fw6hOLuQw==", 86 | "cpu": [ 87 | "arm64" 88 | ], 89 | "dev": true, 90 | "license": "MIT", 91 | "optional": true, 92 | "os": [ 93 | "win32" 94 | ] 95 | }, 96 | "node_modules/@deno/win32-x64": { 97 | "version": "2.3.3", 98 | "resolved": "https://registry.npmjs.org/@deno/win32-x64/-/win32-x64-2.3.3.tgz", 99 | "integrity": "sha512-0YeiukjSTLuiXMghry/liRQEbp20l1nDmgjbG7Mf98Rw2Zz68xZ9IZMXMzWS3J5xfTYexEjQCWPJJk3NRWyufQ==", 100 | "cpu": [ 101 | "x64" 102 | ], 103 | "dev": true, 104 | "license": "MIT", 105 | "optional": true, 106 | "os": [ 107 | "win32" 108 | ] 109 | }, 110 | "node_modules/@nodelib/fs.scandir": { 111 | "version": "2.1.5", 112 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 113 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 114 | "license": "MIT", 115 | "dependencies": { 116 | "@nodelib/fs.stat": "2.0.5", 117 | "run-parallel": "^1.1.9" 118 | }, 119 | "engines": { 120 | "node": ">= 8" 121 | } 122 | }, 123 | "node_modules/@nodelib/fs.stat": { 124 | "version": "2.0.5", 125 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 126 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 127 | "license": "MIT", 128 | "engines": { 129 | "node": ">= 8" 130 | } 131 | }, 132 | "node_modules/@nodelib/fs.walk": { 133 | "version": "1.2.8", 134 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 135 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 136 | "license": "MIT", 137 | "dependencies": { 138 | "@nodelib/fs.scandir": "2.1.5", 139 | "fastq": "^1.6.0" 140 | }, 141 | "engines": { 142 | "node": ">= 8" 143 | } 144 | }, 145 | "node_modules/@ts-morph/common": { 146 | "version": "0.27.0", 147 | "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", 148 | "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", 149 | "license": "MIT", 150 | "dependencies": { 151 | "fast-glob": "^3.3.3", 152 | "minimatch": "^10.0.1", 153 | "path-browserify": "^1.0.1" 154 | } 155 | }, 156 | "node_modules/@types/node": { 157 | "version": "18.15.11", 158 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", 159 | "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", 160 | "dev": true 161 | }, 162 | "node_modules/balanced-match": { 163 | "version": "1.0.2", 164 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 165 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 166 | "license": "MIT" 167 | }, 168 | "node_modules/brace-expansion": { 169 | "version": "2.0.1", 170 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 171 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 172 | "license": "MIT", 173 | "dependencies": { 174 | "balanced-match": "^1.0.0" 175 | } 176 | }, 177 | "node_modules/braces": { 178 | "version": "3.0.3", 179 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 180 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 181 | "license": "MIT", 182 | "dependencies": { 183 | "fill-range": "^7.1.1" 184 | }, 185 | "engines": { 186 | "node": ">=8" 187 | } 188 | }, 189 | "node_modules/code-block-writer": { 190 | "version": "13.0.3", 191 | "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", 192 | "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", 193 | "license": "MIT" 194 | }, 195 | "node_modules/deno": { 196 | "version": "2.3.3", 197 | "resolved": "https://registry.npmjs.org/deno/-/deno-2.3.3.tgz", 198 | "integrity": "sha512-98lcMdDYMFCx3OjI2HKNCxRqNh5iEgJXESg0BahrOddwfjEVyqrYz63lAy/fysph+ADbdVhKvuBxHdlTRhOHtw==", 199 | "dev": true, 200 | "hasInstallScript": true, 201 | "license": "MIT", 202 | "bin": { 203 | "deno": "bin.cjs" 204 | }, 205 | "optionalDependencies": { 206 | "@deno/darwin-arm64": "2.3.3", 207 | "@deno/darwin-x64": "2.3.3", 208 | "@deno/linux-arm64-glibc": "2.3.3", 209 | "@deno/linux-x64-glibc": "2.3.3", 210 | "@deno/win32-arm64": "2.3.3", 211 | "@deno/win32-x64": "2.3.3" 212 | } 213 | }, 214 | "node_modules/fast-check": { 215 | "version": "3.10.0", 216 | "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.10.0.tgz", 217 | "integrity": "sha512-I2FldZwnCbcY6iL+H0rp9m4D+O3PotuFu9FasWjMCzUedYHMP89/37JbSt6/n7Yq/IZmJDW0B2h30sPYdzrfzw==", 218 | "dev": true, 219 | "funding": [ 220 | { 221 | "type": "individual", 222 | "url": "https://github.com/sponsors/dubzzz" 223 | }, 224 | { 225 | "type": "opencollective", 226 | "url": "https://opencollective.com/fast-check" 227 | } 228 | ], 229 | "dependencies": { 230 | "pure-rand": "^6.0.0" 231 | }, 232 | "engines": { 233 | "node": ">=8.0.0" 234 | } 235 | }, 236 | "node_modules/fast-glob": { 237 | "version": "3.3.3", 238 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", 239 | "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", 240 | "license": "MIT", 241 | "dependencies": { 242 | "@nodelib/fs.stat": "^2.0.2", 243 | "@nodelib/fs.walk": "^1.2.3", 244 | "glob-parent": "^5.1.2", 245 | "merge2": "^1.3.0", 246 | "micromatch": "^4.0.8" 247 | }, 248 | "engines": { 249 | "node": ">=8.6.0" 250 | } 251 | }, 252 | "node_modules/fastq": { 253 | "version": "1.19.1", 254 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", 255 | "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", 256 | "license": "ISC", 257 | "dependencies": { 258 | "reusify": "^1.0.4" 259 | } 260 | }, 261 | "node_modules/fill-range": { 262 | "version": "7.1.1", 263 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 264 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 265 | "license": "MIT", 266 | "dependencies": { 267 | "to-regex-range": "^5.0.1" 268 | }, 269 | "engines": { 270 | "node": ">=8" 271 | } 272 | }, 273 | "node_modules/glob-parent": { 274 | "version": "5.1.2", 275 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 276 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 277 | "license": "ISC", 278 | "dependencies": { 279 | "is-glob": "^4.0.1" 280 | }, 281 | "engines": { 282 | "node": ">= 6" 283 | } 284 | }, 285 | "node_modules/is-extglob": { 286 | "version": "2.1.1", 287 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 288 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 289 | "license": "MIT", 290 | "engines": { 291 | "node": ">=0.10.0" 292 | } 293 | }, 294 | "node_modules/is-glob": { 295 | "version": "4.0.3", 296 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 297 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 298 | "license": "MIT", 299 | "dependencies": { 300 | "is-extglob": "^2.1.1" 301 | }, 302 | "engines": { 303 | "node": ">=0.10.0" 304 | } 305 | }, 306 | "node_modules/is-number": { 307 | "version": "7.0.0", 308 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 309 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 310 | "license": "MIT", 311 | "engines": { 312 | "node": ">=0.12.0" 313 | } 314 | }, 315 | "node_modules/merge2": { 316 | "version": "1.4.1", 317 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 318 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 319 | "license": "MIT", 320 | "engines": { 321 | "node": ">= 8" 322 | } 323 | }, 324 | "node_modules/micromatch": { 325 | "version": "4.0.8", 326 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 327 | "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 328 | "license": "MIT", 329 | "dependencies": { 330 | "braces": "^3.0.3", 331 | "picomatch": "^2.3.1" 332 | }, 333 | "engines": { 334 | "node": ">=8.6" 335 | } 336 | }, 337 | "node_modules/minimatch": { 338 | "version": "10.0.1", 339 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", 340 | "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", 341 | "license": "ISC", 342 | "dependencies": { 343 | "brace-expansion": "^2.0.1" 344 | }, 345 | "engines": { 346 | "node": "20 || >=22" 347 | }, 348 | "funding": { 349 | "url": "https://github.com/sponsors/isaacs" 350 | } 351 | }, 352 | "node_modules/path-browserify": { 353 | "version": "1.0.1", 354 | "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", 355 | "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", 356 | "license": "MIT" 357 | }, 358 | "node_modules/picomatch": { 359 | "version": "2.3.1", 360 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 361 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 362 | "license": "MIT", 363 | "engines": { 364 | "node": ">=8.6" 365 | }, 366 | "funding": { 367 | "url": "https://github.com/sponsors/jonschlinkert" 368 | } 369 | }, 370 | "node_modules/pure-rand": { 371 | "version": "6.0.1", 372 | "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", 373 | "integrity": "sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==", 374 | "dev": true, 375 | "funding": [ 376 | { 377 | "type": "individual", 378 | "url": "https://github.com/sponsors/dubzzz" 379 | }, 380 | { 381 | "type": "opencollective", 382 | "url": "https://opencollective.com/fast-check" 383 | } 384 | ] 385 | }, 386 | "node_modules/queue-microtask": { 387 | "version": "1.2.3", 388 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 389 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 390 | "funding": [ 391 | { 392 | "type": "github", 393 | "url": "https://github.com/sponsors/feross" 394 | }, 395 | { 396 | "type": "patreon", 397 | "url": "https://www.patreon.com/feross" 398 | }, 399 | { 400 | "type": "consulting", 401 | "url": "https://feross.org/support" 402 | } 403 | ], 404 | "license": "MIT" 405 | }, 406 | "node_modules/reusify": { 407 | "version": "1.1.0", 408 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", 409 | "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", 410 | "license": "MIT", 411 | "engines": { 412 | "iojs": ">=1.0.0", 413 | "node": ">=0.10.0" 414 | } 415 | }, 416 | "node_modules/run-parallel": { 417 | "version": "1.2.0", 418 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 419 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 420 | "funding": [ 421 | { 422 | "type": "github", 423 | "url": "https://github.com/sponsors/feross" 424 | }, 425 | { 426 | "type": "patreon", 427 | "url": "https://www.patreon.com/feross" 428 | }, 429 | { 430 | "type": "consulting", 431 | "url": "https://feross.org/support" 432 | } 433 | ], 434 | "license": "MIT", 435 | "dependencies": { 436 | "queue-microtask": "^1.2.2" 437 | } 438 | }, 439 | "node_modules/to-regex-range": { 440 | "version": "5.0.1", 441 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 442 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 443 | "license": "MIT", 444 | "dependencies": { 445 | "is-number": "^7.0.0" 446 | }, 447 | "engines": { 448 | "node": ">=8.0" 449 | } 450 | }, 451 | "node_modules/ts-morph": { 452 | "version": "26.0.0", 453 | "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", 454 | "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", 455 | "license": "MIT", 456 | "dependencies": { 457 | "@ts-morph/common": "~0.27.0", 458 | "code-block-writer": "^13.0.3" 459 | } 460 | } 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deno2node", 3 | "version": "1.16.0", 4 | "description": "`tsc` replacement for transpiling Deno libraries to run on Node.js.", 5 | "type": "module", 6 | "bin": { 7 | "deno2node": "lib/cli.js" 8 | }, 9 | "main": "./lib/mod.js", 10 | "exports": "./lib/mod.js", 11 | "typings": "./lib/mod.d.ts", 12 | "directories": { 13 | "lib": "lib" 14 | }, 15 | "files": [ 16 | "lib/", 17 | "tsconfig.json", 18 | "!lib/**/*.test.*" 19 | ], 20 | "scripts": { 21 | "fmt": "deno fmt", 22 | "lint": "deno lint", 23 | "prepare": "src/cli.ts", 24 | "postprepare": "node --test lib/", 25 | "test": "deno test --allow-read=.", 26 | "dependencies": "scripts/pretest.ts", 27 | "clean": "git clean -fXde !node_modules/" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/wojpawlik/deno2node.git" 32 | }, 33 | "homepage": "https://t.me/fromdeno", 34 | "keywords": [ 35 | "typescript", 36 | "transpile", 37 | "ts-morph", 38 | "dnt", 39 | "deno" 40 | ], 41 | "author": "Wojciech Pawlik ", 42 | "license": "MIT", 43 | "engines": { 44 | "node": ">=14.13.1" 45 | }, 46 | "dependencies": { 47 | "ts-morph": "^26.0.0" 48 | }, 49 | "devDependencies": { 50 | "@types/node": "^18.15.11", 51 | "deno": "~2.3.3", 52 | "fast-check": "^3.10.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | git diff --quiet --cached src/ || src/cli.ts --noEmit 3 | -------------------------------------------------------------------------------- /scripts/pretest.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node_modules/.bin/deno run --allow-read --allow-write="src/deps.deno.ts" 2 | const { version } = JSON.parse( 3 | await Deno.readTextFile("node_modules/ts-morph/package.json"), 4 | ); 5 | const deps = await Deno.readTextFile("src/deps.deno.ts"); 6 | await Deno.writeTextFile( 7 | "src/deps.deno.ts", 8 | deps.replace(/(?<=ts-morph@)[\d.]+/, version), 9 | ); 10 | -------------------------------------------------------------------------------- /scripts/ts-version.ts: -------------------------------------------------------------------------------- 1 | #!node_modules/.bin/deno run 2 | import { ts } from "../src/deps.deno.ts"; 3 | 4 | const minor = (version: string) => version.split(".", 2).join("."); 5 | const deno_s = minor(Deno.version.typescript); 6 | const ts_morph_s = minor(ts.version); 7 | 8 | if (deno_s !== ts_morph_s) { 9 | console.error( 10 | "Deno's TypeScript version (%s) doesn't match ts-morph's (%s).", 11 | deno_s, 12 | ts_morph_s, 13 | ); 14 | Deno.exit(1); 15 | } 16 | 17 | console.info(ts_morph_s); 18 | -------------------------------------------------------------------------------- /scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export NPM_CONFIG_COMMIT_HOOKS=false 5 | 6 | git diff --quiet || { 7 | echo 'Stash or stage all changes.' 8 | exit 2 9 | } 10 | 11 | NPM_CONFIG_PACKAGE_LOCK_ONLY=1 \ 12 | npm query --expect-results '#ts-morph:outdated(major)' &>/dev/null || { 13 | echo 'ts-morph already up to date.' 14 | exit 0 15 | } 16 | 17 | npm install-test ts-morph@latest 18 | npm install-test --save-dev --save-prefix='~' deno@latest 19 | 20 | tsVersion="$(scripts/ts-version.ts)" || exit 0 21 | npm run prepare 22 | lib/cli.js --noEmit 23 | 24 | git add src/deps.deno.ts 25 | npm version "${1:-minor}" --force --message "Upgrade to TypeScript $tsVersion" 26 | -------------------------------------------------------------------------------- /src/_transformations/shim.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../context.ts"; 2 | import type { SourceFile } from "../deps.deno.ts"; 3 | 4 | export const shimFile = (shimFile: SourceFile) => { 5 | const shims = Array.from(shimFile.getExportedDeclarations().keys()); 6 | return (sourceFile: SourceFile) => { 7 | if (sourceFile === shimFile) return; 8 | const locals = new Set( 9 | sourceFile.getLocals().map((l) => l.getEscapedName()), 10 | ); 11 | const index = sourceFile.getStatementsWithComments().length; 12 | const moduleSpecifier = "./" + 13 | sourceFile.getRelativePathTo(shimFile).replace( 14 | /tsx?$/, 15 | "js", 16 | ); 17 | sourceFile.insertImportDeclaration(index, { 18 | // do not shim declared locals 19 | namedImports: shims.filter((s) => !locals.has(s)), 20 | moduleSpecifier, 21 | }); 22 | }; 23 | }; 24 | 25 | const isNodeSpecific = (sourceFile: SourceFile) => 26 | sourceFile.getBaseNameWithoutExtension().toLowerCase().endsWith(".node"); 27 | 28 | export function shimEverything(ctx: Context) { 29 | if (!ctx.config.shim) return; 30 | console.time("Shimming"); 31 | const shim = shimFile( 32 | ctx.project.addSourceFileAtPath(ctx.resolve(ctx.config.shim)), 33 | ); 34 | for (const sourceFile of ctx.project.getSourceFiles()) { 35 | if (!isNodeSpecific(sourceFile)) { 36 | shim(sourceFile); 37 | } 38 | } 39 | console.timeEnd("Shimming"); 40 | } 41 | -------------------------------------------------------------------------------- /src/_transformations/specifiers.test.ts: -------------------------------------------------------------------------------- 1 | import fc from "fast-check"; 2 | import test from "node:test"; 3 | import { transpileSpecifier } from "./specifiers.ts"; 4 | 5 | const join = (array: string[]) => array.join(""); 6 | const path = fc.option(fc.webPath(), { nil: "" }); 7 | const relativePrefix = fc.constantFrom("./", "../"); 8 | const extension = fc.constantFrom("js", "ts", "jsx", "tsx"); 9 | const version = fc.webSegment().map((s) => s ? `@${s}` : ""); 10 | const query = fc.webQueryParameters().map((s) => s ? `?${s}` : ""); 11 | const name = fc.stringOf(fc.char().filter((c) => /[\w.-]/.test(c))); 12 | const relativePath = fc.tuple(relativePrefix, fc.webPath()).map(join); 13 | 14 | const scopedPackage = fc.tuple( 15 | name.map((s) => s ? `@${s}/` : ""), 16 | name.filter(Boolean), 17 | ).map(join); 18 | 19 | const service = fc.constantFrom( 20 | "npm:", 21 | "https://esm.sh/", 22 | "https://cdn.skypack.dev/", 23 | ); 24 | 25 | test(function localSpecifiers() { 26 | fc.assert( 27 | fc.property( 28 | relativePath, 29 | extension, 30 | (path, ext) => 31 | transpileSpecifier(`${path}.deno.${ext}`) === `${path}.node.js`, 32 | ), 33 | ); 34 | }); 35 | 36 | test(function remoteSpecifiers() { 37 | fc.assert( 38 | fc.property(service, scopedPackage, version, path, query, (...segments) => { 39 | const [, scopedPackage, , path] = segments; 40 | return transpileSpecifier(join(segments)) === scopedPackage + path; 41 | }), 42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /src/_transformations/specifiers.ts: -------------------------------------------------------------------------------- 1 | import * as re from "../util/regexp.ts"; 2 | 3 | export const name = /(?:@[\w.-]+\/)?[\w.-]+/; 4 | const version = /[^/?]+/; 5 | const path = /\/[^?]*/; 6 | 7 | const patterns = [ 8 | re.tag()`^npm:(${name})(?:@${version})?(${path})?`, 9 | re.tag()`^https://esm\.sh/(${name})(?:@${version})?(${path})?`, 10 | re.tag()`^https://cdn\.skypack\.dev/(${name})(?:@${version})?(${path})?`, 11 | re.tag()`^https://deno\.land/std(?:@${version})?/node/([\w/]+)\.ts$`, 12 | re.tag()`^https://nest\.land/std/node/${version}/([\w/]+)\.ts$`, 13 | ]; 14 | 15 | const transpileHttpsImport = (specifier: string) => { 16 | for (const pattern of patterns) { 17 | const match = pattern.exec(specifier); 18 | if (match === null) continue; 19 | const [, pkg, path = ""] = match; 20 | return pkg + path; 21 | } 22 | return specifier; 23 | }; 24 | 25 | const transpileRelativeImport = (specifier: string) => 26 | specifier 27 | .replace(/\.[jt]sx?$/i, ".js") 28 | .replace(/\.deno\.js$/i, ".node.js"); 29 | 30 | const isRelative = (specifier: string) => /^\.\.?\//.test(specifier); 31 | 32 | export const transpileSpecifier = (specifier: string) => { 33 | if (isRelative(specifier)) return transpileRelativeImport(specifier); 34 | return transpileHttpsImport(specifier); 35 | }; 36 | -------------------------------------------------------------------------------- /src/_transformations/vendor.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "node:assert/strict"; 2 | import test from "node:test"; 3 | import { Project, ts } from "../deps.deno.ts"; 4 | import { vendorSpecifiers } from "./vendor.ts"; 5 | 6 | test(function vendoring() { 7 | const project = new Project({ 8 | tsConfigFilePath: "tsconfig.json", 9 | skipAddingFilesFromTsConfig: true, 10 | }); 11 | const vendorDir = project.createDirectory("src/vendor"); 12 | const file = project.createSourceFile( 13 | "src/deps.deno.ts", 14 | 'export * from "https://deno.land/x/ts_morph/mod.ts"', 15 | { overwrite: true }, 16 | ); 17 | const exportDeclaration = 18 | file.getChildrenOfKind(ts.SyntaxKind.ExportDeclaration)[0]; 19 | 20 | vendorSpecifiers(vendorDir.getPath())(file); 21 | const specifierValue = exportDeclaration.getModuleSpecifierValue()!; 22 | assert.match(specifierValue, /^.\/vendor\//); 23 | assert.match(specifierValue, /\.js$/); 24 | 25 | // test idempotence 26 | vendorSpecifiers(vendorDir.getPath())(file); 27 | assert.equal(exportDeclaration.getModuleSpecifierValue(), specifierValue); 28 | }); 29 | -------------------------------------------------------------------------------- /src/_transformations/vendor.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../context.ts"; 2 | import { Node, SourceFile } from "../deps.deno.ts"; 3 | 4 | const https = /^https:\//; 5 | 6 | /** 7 | * Rewrites specifiers in `sourceFile` to point into the specified `vendorDir`. 8 | * @param vendorDir - absolute path 9 | */ 10 | export const vendorSpecifiers = 11 | (vendorDir: string) => (sourceFile: SourceFile) => { 12 | for (const statement of sourceFile.getStatements()) { 13 | if ( 14 | Node.isImportDeclaration(statement) || 15 | Node.isExportDeclaration(statement) 16 | ) { 17 | const oldSpecifierValue = statement.getModuleSpecifierValue(); 18 | if (oldSpecifierValue === undefined) continue; 19 | if (!https.test(oldSpecifierValue)) continue; 20 | const newSpecifierValue = "./" + sourceFile.getRelativePathTo( 21 | oldSpecifierValue.replace(https, vendorDir), 22 | ).replace(/tsx?$/, "js"); 23 | statement.setModuleSpecifier(newSpecifierValue); 24 | } 25 | } 26 | }; 27 | 28 | export function vendorEverything(ctx: Context) { 29 | if (!ctx.config.vendorDir) return; 30 | console.time("Vendoring"); 31 | ctx.project.getSourceFiles().forEach(vendorSpecifiers( 32 | ctx.resolve(ctx.config.vendorDir), 33 | )); 34 | console.timeEnd("Vendoring"); 35 | } 36 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --no-npm --no-prompt --allow-read=. --allow-write=. 2 | import { ts } from "./deps.deno.ts"; 3 | import { getHelpText } from "./help.ts"; 4 | import { getVersion, initializeProject } from "./init.ts"; 5 | import { Context, deno2node, emit } from "./mod.ts"; 6 | 7 | const { options, fileNames, errors } = ts.parseCommandLine(Deno.args); 8 | const tsConfigFilePath = options.project ?? fileNames[0] ?? "tsconfig.json"; 9 | 10 | if (errors.length) { 11 | for (const error of errors) { 12 | console.error(error.messageText); 13 | } 14 | Deno.exit(2); 15 | } 16 | 17 | if (options.help) { 18 | console.log(getHelpText(await getVersion())); 19 | Deno.exit(0); 20 | } 21 | 22 | if (options.version) { 23 | console.log("deno2node", await getVersion()); 24 | console.log("typescript", ts.version); 25 | Deno.exit(0); 26 | } 27 | 28 | if (options.init) { 29 | await initializeProject(); 30 | Deno.exit(0); 31 | } 32 | 33 | console.time("Loading tsconfig"); 34 | const ctx = new Context({ tsConfigFilePath, compilerOptions: options }); 35 | console.timeEnd("Loading tsconfig"); 36 | 37 | await deno2node(ctx); 38 | console.time("Emitting"); 39 | const diagnostics = await emit(ctx.project); 40 | console.timeEnd("Emitting"); 41 | if (diagnostics.length !== 0) { 42 | console.info(ctx.project.formatDiagnosticsWithColorAndContext(diagnostics)); 43 | console.info("TypeScript", ts.version); 44 | console.info(`Found ${diagnostics.length} errors.`); 45 | Deno.exit(1); 46 | } 47 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | readonly shim?: string; 3 | readonly vendorDir?: string; 4 | } 5 | 6 | // deno-lint-ignore no-explicit-any 7 | export function parse({ shim, vendorDir, ...rest }: any): Config { 8 | if (typeof shim !== "string" && typeof shim !== "undefined") { 9 | throw new TypeError( 10 | "deno2node option 'shim' requires a value of type string.", 11 | ); 12 | } 13 | 14 | if (typeof vendorDir !== "string" && typeof vendorDir !== "undefined") { 15 | throw new TypeError( 16 | "deno2node option 'vendorDir' requires a value of type string.", 17 | ); 18 | } 19 | 20 | const unknown = Object.keys(rest); 21 | if (unknown.length) { 22 | throw new TypeError(`Unknown deno2node option '${unknown[0]}'.`); 23 | } 24 | 25 | return { shim, vendorDir }; 26 | } 27 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { type Config, parse } from "./config.ts"; 3 | import { Project, ts } from "./deps.deno.ts"; 4 | 5 | const compilerOptions: ts.CompilerOptions = { 6 | // footguns 7 | removeComments: false, 8 | // Deno defaults 9 | strict: true, 10 | useDefineForClassFields: true, 11 | }; 12 | 13 | export interface Options { 14 | readonly tsConfigFilePath?: string; 15 | readonly compilerOptions?: ts.CompilerOptions; 16 | readonly skipAddingFilesFromTsConfig?: boolean; 17 | } 18 | 19 | export class Context { 20 | public baseDir: string; 21 | public config: Config; 22 | readonly project: Project; 23 | 24 | /** 25 | * Synchronously loads `tsconfig.json` and `"files"`. 26 | */ 27 | constructor(options: Options) { 28 | const { tsConfigFilePath } = options; 29 | this.project = new Project({ 30 | compilerOptions, 31 | ...options, 32 | }); 33 | const fs = this.project.getFileSystem(); 34 | if (tsConfigFilePath === undefined) { 35 | this.baseDir = fs.getCurrentDirectory(); 36 | this.config = {}; 37 | return; 38 | } 39 | const result = ts.readConfigFile(tsConfigFilePath, fs.readFileSync); 40 | this.baseDir = path.resolve(tsConfigFilePath, "../"); 41 | this.config = parse(result.config.deno2node ?? {}); 42 | } 43 | 44 | resolve(...pathSegments: string[]) { 45 | return path.join(this.baseDir, ...pathSegments); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/deno2node.ts: -------------------------------------------------------------------------------- 1 | // based on https://github.com/dsherret/code-block-writer/blob/99454249cd13bd89befa45ac815b37b3e02896f5/scripts/build_npm.ts 2 | 3 | import { Context } from "./context.ts"; 4 | import { Node, SourceFile } from "./deps.deno.ts"; 5 | import { transpileSpecifier } from "./_transformations/specifiers.ts"; 6 | import { vendorEverything } from "./_transformations/vendor.ts"; 7 | import { shimEverything } from "./_transformations/shim.ts"; 8 | 9 | function transpileImportSpecifiers(sourceFile: SourceFile) { 10 | for (const statement of sourceFile.getStatements()) { 11 | if ( 12 | Node.isImportDeclaration(statement) || 13 | Node.isExportDeclaration(statement) 14 | ) { 15 | const modSpecifierValue = statement.getModuleSpecifierValue(); 16 | if (modSpecifierValue !== undefined) { 17 | statement.setModuleSpecifier(transpileSpecifier(modSpecifierValue)); 18 | } 19 | } 20 | } 21 | } 22 | 23 | const isDenoSpecific = (sourceFile: SourceFile) => 24 | sourceFile.getBaseNameWithoutExtension().toLowerCase().endsWith(".deno"); 25 | 26 | /** 27 | * Attempts to transform arbitrary `ctx.project` into a valid Node.js project: 28 | * 29 | * 1. Changes import specifiers to be Node-friendly: 30 | * - changes extension in relative specifiers to `.js`, 31 | * - replaces some `https://` imports with bare specifiers. 32 | * 33 | * 2. Changes `*.deno.js` imports specifiers to `*.node.js` 34 | * (`import './deps.deno.ts'` -> `import './deps.node.js'`). 35 | * This can be used for re-exporting dependencies 36 | * and other runtime-specific code. 37 | * 38 | * 3. Rewrites remaining https: imports to point 39 | * into `vendorDir`, if specified: 40 | * ```json 41 | * // @filename: tsconfig.json 42 | * { 43 | * "deno2node": { 44 | * "vendorDir": "src/.deno2node/vendor/" 45 | * } 46 | * } 47 | * ``` 48 | * 49 | * 4. Imports Node.js shims for Deno globals 50 | * from [shim file], if specified: 51 | * ```json 52 | * // @filename: tsconfig.json 53 | * { 54 | * "deno2node": { 55 | * "shim": "src/shim.node.ts" 56 | * } 57 | * } 58 | * ``` 59 | * 60 | * [shim file]: https://github.com/wojpawlik/deno2node/blob/main/src/shim.node.ts 61 | */ 62 | export async function deno2node(ctx: Context): Promise { 63 | console.time("Basic transformations"); 64 | for (const sourceFile of ctx.project.getSourceFiles()) { 65 | if (isDenoSpecific(sourceFile)) { 66 | ctx.project.removeSourceFile(sourceFile); 67 | continue; 68 | } 69 | transpileImportSpecifiers(sourceFile); 70 | } 71 | console.timeEnd("Basic transformations"); 72 | await vendorEverything(ctx); 73 | shimEverything(ctx); 74 | } 75 | -------------------------------------------------------------------------------- /src/deps.deno.ts: -------------------------------------------------------------------------------- 1 | // Deno-only, see https://doc.deno.land/https/deno.land/x/deno2node/src/mod.ts#deno2node 2 | // Auto-updated in `dependencies` script 3 | export * from "jsr:@ts-morph/ts-morph@26.0.0"; 4 | -------------------------------------------------------------------------------- /src/deps.node.ts: -------------------------------------------------------------------------------- 1 | // Node-only, see https://doc.deno.land/https/deno.land/x/deno2node/src/mod.ts#deno2node 2 | export * from "ts-morph"; 3 | -------------------------------------------------------------------------------- /src/emit.ts: -------------------------------------------------------------------------------- 1 | import type { Diagnostic, MemoryEmitResultFile, Project } from "./deps.deno.ts"; 2 | 3 | const anyShebang = /^#![^\n]*\n/; 4 | const denoShebang = /^#!\/usr\/bin\/env -S deno run\b[^\n]*\n/; 5 | const nodeShebang = "#!/usr/bin/env node\n"; 6 | 7 | function transpileShebang(file: MemoryEmitResultFile) { 8 | file.text = file.filePath.endsWith(".js") 9 | ? file.text.replace(denoShebang, nodeShebang) 10 | : file.text.replace(anyShebang, "\n"); 11 | } 12 | 13 | async function markExecutableIfNeeded(file: MemoryEmitResultFile) { 14 | if (Deno.build.os === "windows") return; 15 | if (!file.text.startsWith(nodeShebang)) return; 16 | await Deno.chmod(file.filePath, 0o755); 17 | } 18 | 19 | /** 20 | * Emits project to the filesystem. 21 | * Returns diagnostics. 22 | * 23 | * Replaces Deno shebang with Node.js shebang in JS outputs. 24 | * Removes shebangs from non-JS outputs. 25 | * Then `chmod +x`'s outputs with Node.js shebang. 26 | */ 27 | export async function emit(project: Project): Promise { 28 | const result = project.emitToMemory(); 29 | const files = result.getFiles(); 30 | files.forEach(transpileShebang); 31 | await result.saveFiles(); 32 | await Promise.all(files.map(markExecutableIfNeeded)); 33 | const preEmitDiagnostics = project.getPreEmitDiagnostics(); 34 | if (preEmitDiagnostics.length !== 0) return preEmitDiagnostics; 35 | return result.getDiagnostics(); 36 | } 37 | -------------------------------------------------------------------------------- /src/help.ts: -------------------------------------------------------------------------------- 1 | export function getHelpText(version: string) { 2 | return `\ 3 | deno2node: Compile Deno code for Node - Version ${version} 4 | 5 | ${bold("COMMON COMMANDS")} 6 | 7 | ${blue("deno2node")} 8 | Compiles the current project (tsconfig.json in the working directory.) 9 | 10 | ${blue("deno2node --project ")} 11 | Compiles the current project with the specified tsconfig.json file. 12 | 13 | ${blue("deno2node --init")} 14 | Creates a tsconfig.json with the recommended settings in the working directory. 15 | 16 | ${bold("COMMAND LINE FLAGS")} 17 | 18 | ${blue(" --help, -h ")}Print this message. 19 | 20 | ${blue(" --version, -v ")}Print the CLI's version. 21 | 22 | ${blue(" --init ")}Initializes a deno2node project \ 23 | and creates a tsconfig.json file. 24 | 25 | You can learn about the compiler options at https://aka.ms/tsc 26 | `; 27 | } 28 | 29 | export const useColors = Deno.stdout.isTerminal() && !Deno.noColor; 30 | 31 | function bold(text: string) { 32 | return format(text, 1); 33 | } 34 | function blue(text: string) { 35 | return format(text, 34); 36 | } 37 | function format(text: string, ansi: number) { 38 | return useColors ? `\u001b[${ansi}m${text}\u001b[0m` : text; 39 | } 40 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --allow-read --allow-write='.' 2 | import * as fs from "node:fs/promises"; 3 | 4 | const shimFile = "// See https://github.com/fromdeno/deno2node#shimming"; 5 | const gitignore = "/lib/\n/node_modules/\n/src/vendor/"; 6 | const denoJson = '{ "exclude": ["lib/"] }'; 7 | 8 | async function download(url: URL, target: string) { 9 | const response = await fetch(url); 10 | await fs.writeFile(target, await response.text(), { flag: "wx" }); 11 | } 12 | 13 | export async function getVersion(): Promise { 14 | const packageUrl = new URL("../package.json", import.meta.url); 15 | const response = await fetch(packageUrl); 16 | const { version } = await response.json(); 17 | return version; 18 | } 19 | 20 | async function createPackageJson() { 21 | const pkg = { 22 | "type": "module", 23 | "version": "0.0.0", 24 | "exports": "./lib/mod.js", 25 | "typings": "./lib/mod.d.ts", 26 | "files": [ 27 | "lib/", 28 | "!lib/**/*.test.*", 29 | "!*/vendor/**/*.ts*", 30 | ], 31 | "scripts": { 32 | "fmt": "deno fmt", 33 | "lint": "deno lint", 34 | "test": "deno test", 35 | "prepare": "deno2node", 36 | "postprepare": "node --test lib/", 37 | "clean": "git clean -fXde !node_modules/", 38 | }, 39 | "devDependencies": { 40 | "deno2node": `~${await getVersion()}`, 41 | }, 42 | }; 43 | await fs.writeFile("package.json", JSON.stringify(pkg, null, 2), { 44 | flag: "wx", 45 | }); 46 | } 47 | 48 | export async function initializeProject() { 49 | const tsconfigUrl = new URL("../tsconfig.json", import.meta.url); 50 | await fs.mkdir("src/"); 51 | await Promise.all([ 52 | createPackageJson(), 53 | download(tsconfigUrl, "tsconfig.json"), 54 | fs.writeFile("deno.json", denoJson, { flag: "wx" }), 55 | fs.writeFile(".gitignore", gitignore, { flag: "wx" }), 56 | fs.writeFile("src/shim.node.ts", shimFile, { flag: "wx" }), 57 | ]); 58 | } 59 | 60 | // @ts-ignore not available on Node.js: https://github.com/nodejs/modules/issues/274 61 | if (import.meta.main) { 62 | await initializeProject(); 63 | } 64 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | export { Context } from "./context.ts"; 2 | export { deno2node } from "./deno2node.ts"; 3 | export { ts } from "./deps.deno.ts"; 4 | export { emit } from "./emit.ts"; 5 | -------------------------------------------------------------------------------- /src/shim.node.ts: -------------------------------------------------------------------------------- 1 | // Node-only, see https://github.com/fromdeno/deno2node#shimming 2 | import { chmod, readFile } from "node:fs/promises"; 3 | import process from "node:process"; 4 | import { isatty } from "node:tty"; 5 | 6 | const os = process.platform === "win32" ? "windows" : process.platform; 7 | 8 | export const Deno = { 9 | // please keep sorted 10 | args: process.argv.slice(2), 11 | build: { os }, 12 | chmod, 13 | exit: process.exit, 14 | get noColor() { 15 | return Boolean(process.env.NO_COLOR); 16 | }, 17 | stdout: { 18 | isTerminal: () => isatty(process.stdout.fd), 19 | }, 20 | }; 21 | 22 | export async function fetch(fileUrl: URL) { 23 | const data = await readFile(fileUrl, { encoding: "utf-8" }); 24 | return { 25 | json: () => JSON.parse(data), 26 | text: () => data, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/util/regexp.ts: -------------------------------------------------------------------------------- 1 | export type RegExpSource = Pick; 2 | 3 | const _source = (arg: string | RegExpSource) => 4 | typeof arg === "string" 5 | ? arg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // escape string 6 | : arg.source; 7 | 8 | export const tag = (flags = "") => 9 | ( 10 | literals: TemplateStringsArray, 11 | ...substitutions: ReadonlyArray 12 | ) => { 13 | const subpatterns = substitutions.map((sub) => `(?:${_source(sub)})`); 14 | return new RegExp(String.raw(literals, ...subpatterns), flags); 15 | }; 16 | 17 | export const union = ( 18 | strings: ReadonlyArray, 19 | ): RegExpSource => ({ 20 | source: strings.map(_source).join("|"), 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/"], 3 | "deno2node": { 4 | "shim": "src/shim.node.ts" 5 | }, 6 | "compilerOptions": { 7 | "allowSyntheticDefaultImports": true, 8 | "declaration": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "outDir": "lib/", 13 | "skipLibCheck": true, // speed 14 | "strict": true, 15 | "target": "ES2020" 16 | } 17 | } 18 | --------------------------------------------------------------------------------