├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── canary.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── bob-esbuild.config.ts ├── examples ├── basic │ ├── bin │ │ └── index.js │ ├── package.json │ ├── src │ │ ├── aliased │ │ │ └── deep │ │ │ │ └── module.ts │ │ ├── deep │ │ │ └── other.ts │ │ ├── index.ts │ │ ├── innerShared.ts │ │ ├── other.ts │ │ └── testJson.json │ └── tsconfig.json ├── nextjs-custom-server │ ├── next-env.d.ts │ ├── package.json │ ├── src │ │ ├── pages │ │ │ └── index.tsx │ │ └── server.ts │ └── tsconfig.json ├── other │ └── package.json └── shared │ ├── package.json │ ├── src │ ├── deep │ │ └── two-deep │ │ │ └── other.ts │ └── index.ts │ └── tsconfig.json ├── package.json ├── packages ├── bob-cli │ ├── .gitignore │ ├── CHANGELOG.md │ ├── bin │ │ ├── run.cmd │ │ └── run.mjs │ ├── package.json │ ├── self-build.ts │ ├── src │ │ ├── commands │ │ │ ├── build.ts │ │ │ ├── index.ts │ │ │ ├── tsc.ts │ │ │ └── watch.ts │ │ └── index.ts │ └── tsconfig.json ├── bob-ts │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ ├── bob-ts-watch.mjs │ │ └── bob-ts.mjs │ ├── package.json │ ├── self-build.ts │ └── src │ │ ├── bin │ │ ├── build.ts │ │ └── watch.ts │ │ ├── build.ts │ │ ├── clean.ts │ │ ├── defaults.ts │ │ ├── deps.ts │ │ ├── index.ts │ │ ├── packageJson.ts │ │ ├── rollupConfig.ts │ │ ├── watch.ts │ │ └── watchDeps.ts ├── bob-tsm │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── bob-tsm.mjs │ ├── build.ts │ ├── package.json │ ├── playground │ │ ├── index.ts │ │ └── other │ │ │ └── index.ts │ ├── src │ │ ├── bin.ts │ │ ├── config.ts │ │ ├── deps │ │ │ ├── chokidar.ts │ │ │ ├── commander.ts │ │ │ ├── treeKill.ts │ │ │ └── typescriptPaths.ts │ │ ├── index.ts │ │ ├── loader.ts │ │ ├── register.ts │ │ ├── require.ts │ │ └── utils.ts │ ├── test │ │ ├── cts.cts │ │ ├── index │ │ │ └── index.ts │ │ ├── mts.mts │ │ ├── test.cjs │ │ └── ts.ts │ └── tsconfig.json ├── bob-watch │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── bob-watch.mjs │ ├── build.ts │ ├── package.json │ └── src │ │ ├── bin.ts │ │ ├── deps.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts ├── bob │ ├── CHANGELOG.md │ ├── build-types.ts │ ├── package.json │ ├── self-build.ts │ ├── self-watch.ts │ ├── src │ │ ├── build.ts │ │ ├── config │ │ │ ├── copyToDist.ts │ │ │ ├── cosmiconfig.ts │ │ │ ├── index.ts │ │ │ ├── packageBuildConfig.ts │ │ │ ├── packageJson.ts │ │ │ ├── rewrite-exports.ts │ │ │ ├── rollup.ts │ │ │ ├── rollupBin.ts │ │ │ └── tsconfig.ts │ │ ├── deps.ts │ │ ├── index.ts │ │ ├── log │ │ │ ├── debug.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── label.ts │ │ │ └── warn.ts │ │ ├── rollup │ │ │ ├── build.ts │ │ │ ├── index.ts │ │ │ └── watch.ts │ │ ├── tsc │ │ │ ├── build.ts │ │ │ ├── hash.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── utils │ │ │ ├── getDefault.ts │ │ │ ├── importFromString.ts │ │ │ ├── index.ts │ │ │ ├── object.ts │ │ │ └── retry.ts │ │ └── watch.ts │ └── tsconfig.json └── esbuild-plugin │ ├── CHANGELOG.md │ ├── package.json │ ├── self-build.ts │ ├── src │ ├── bundle.ts │ ├── index.ts │ └── options.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── scripts └── canary-release.js └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": ["shared", "basic", "other", "nextjs-custom-server"], 10 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 11 | "onlyUpdatePeerDependentsWhenOutOfRange": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/canary.yaml: -------------------------------------------------------------------------------- 1 | name: Canary Release 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish-canary: 10 | name: Publish Canary 11 | runs-on: ubuntu-latest 12 | if: github.event.pull_request.head.repo.full_name == github.repository 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@master 16 | with: 17 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 18 | fetch-depth: 0 19 | 20 | - name: Setup Node.js 20.5.1 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 20.5.1 24 | 25 | - uses: oven-sh/setup-bun@v1 26 | 27 | - name: Setup NPM credentials 28 | run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 29 | env: 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | 32 | - name: Cache pnpm modules 33 | uses: actions/cache@v3 34 | env: 35 | cache-name: cache-pnpm-modules 36 | with: 37 | path: ~/.pnpm-store 38 | key: ${{ runner.os }}-v2-${{ hashFiles('./pnpm-lock.yaml') }} 39 | restore-keys: | 40 | ${{ runner.os }}-v2 41 | - name: install pnpm 42 | run: npm i pnpm@latest -g 43 | 44 | - name: Install Dependencies 45 | run: pnpm i 46 | 47 | - name: Release Canary 48 | id: canary 49 | uses: 'kamilkisiela/release-canary@master' 50 | with: 51 | npm-token: ${{ secrets.NPM_TOKEN }} 52 | npm-script: 'pnpm release:canary' 53 | changesets: true 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'examples/**' 7 | - '.vscode/**' 8 | - '.husky/**' 9 | branches: 10 | - main 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@master 18 | with: 19 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 20 | fetch-depth: 0 21 | 22 | - name: Setup Node.js 20.5.1 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: 20.5.1 26 | 27 | - uses: oven-sh/setup-bun@v1 28 | 29 | - name: Cache pnpm modules 30 | uses: actions/cache@v3 31 | env: 32 | cache-name: cache-pnpm-modules 33 | with: 34 | path: ~/.pnpm-store 35 | key: ${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }} 36 | restore-keys: | 37 | ${{ runner.os }}- 38 | - name: install pnpm 39 | run: npm i pnpm@^8.4.0 -g 40 | 41 | - name: Install Dependencies 42 | run: pnpm i 43 | 44 | - name: Create Release Pull Request or Publish to npm 45 | id: changesets 46 | uses: changesets/action@v1 47 | with: 48 | publish: pnpm ci:release 49 | version: pnpm ci:version 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Install & prepare 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | InstallPrepare: 6 | name: Install & prepare 7 | runs-on: ${{matrix.os}} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macOS-latest] 11 | node_version: [18, 20.5.1] 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Use Node ${{matrix.node_version}} 20 | uses: actions/setup-node@master 21 | with: 22 | node-version: ${{ matrix.node_version }} 23 | 24 | - uses: oven-sh/setup-bun@v1 25 | 26 | - name: Cache pnpm modules 27 | uses: actions/cache@v3 28 | env: 29 | cache-name: cache-pnpm-modules 30 | with: 31 | path: ~/.pnpm-store 32 | key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('./pnpm-lock.yaml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-${{ matrix.node_version }}- 35 | - name: install pnpm 36 | run: npm i pnpm@latest -g 37 | 38 | - name: Install Dependencies & prepare 39 | run: pnpm i 40 | 41 | - name: Test 42 | run: pnpm test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | tmp 4 | *.log 5 | /lib-types 6 | .temp 7 | *.tsbuildinfo 8 | .next 9 | dist 10 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | workspace-concurrency=Infinity 2 | stream=true 3 | prefer-workspace-packages=true 4 | strict-peer-dependencies=false 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | lib 3 | .next 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "printWidth": 130, 4 | "singleQuote": true, 5 | "arrowParens": "avoid", 6 | "endOfLine": "lf" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bob-esbuild 2 | 3 | ## bob-tsm 4 | 5 | [![npm](https://img.shields.io/npm/v/bob-tsm)](https://npm.im/bob-tsm) 6 | 7 | Check the package [bob-tsm](/packages/bob-tsm/README.md) inspired on https://github.com/lukeed/tsm with extra support for watch mode and with extra fixes to follow the new [TypeScript 4.5 extensions](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#new-file-extensions) `.mts`=>`ESM` and `.cts`=>`CommonJS`. 8 | 9 | ## bob-ts 10 | 11 | [![npm](https://img.shields.io/npm/v/bob-ts)](https://npm.im/bob-ts) 12 | 13 | Check the new library [bob-ts](/packages/bob-ts/README.md) made to accelerate and simplify the TypeScript development with watcher and first class support for JavaScript testing frameworks. 14 | 15 | ## Blog Post 16 | 17 | Check my latest blog post [What does it take to support Node.js ESM?](https://the-guild.dev/blog/support-nodejs-esm) 18 | 19 | ## Notes 20 | 21 | Is recommended to be used with [pnpm](https://pnpm.io/), but it's not a requirement and it should to work with any package manager. 22 | 23 | This library is primarily focused into monorepo projects, and all the instructions are going to be focused on that usage. 24 | 25 | ## Install 26 | 27 | If you are using it in a monorepo project, you have to install it in your root like this: 28 | 29 | ```sh 30 | pnpm add bob-esbuild bob-esbuild-cli 31 | pnpm add -D esbuild 32 | ``` 33 | 34 | Then, in every package you want to build with `bob-esbuild`, you can simply install `bob-esbuild-cli`: 35 | 36 | ```sh 37 | pnpm add bob-esbuild-cli 38 | ``` 39 | 40 | ## Usage 41 | 42 | In your root, you have to make a file called `bob-esbuild.config.ts`, and it's body should be like this: 43 | 44 | ```ts 45 | export const config: import('bob-esbuild').BobConfig = { 46 | tsc: { 47 | dirs: ['packages/*'], 48 | }, 49 | verbose: true, 50 | }; 51 | ``` 52 | 53 | You can use all the typescript auto-completion and type-safety to inspect all the possible options. 54 | 55 | But a required configuration is to specify the `tsc.dirs` with the pattern of directories you want to add the types to. 56 | 57 | In every package you want to build, you have to specify in every package.json: 58 | 59 | ```json 60 | { 61 | "prepack": "bob-esbuild build" 62 | } 63 | ``` 64 | 65 | Or, if you want to build it alongside in the monorepo setup: 66 | 67 | ```json 68 | { 69 | "prepare": "bob-esbuild build" 70 | } 71 | ``` 72 | 73 | Then in your root monorepo a recommended build script would be something like this: 74 | 75 | ```json 76 | { 77 | "scripts": { 78 | "build": "bob-esbuild tsc && pnpm prepack -r" 79 | } 80 | } 81 | ``` 82 | 83 | And it will pre-build the types, and call the "prepack" script in every package in your monorepo. 84 | 85 | ## ESM Support 86 | 87 | Check my latest blog post [What does it take to support Node.js ESM?](https://the-guild.dev/blog/support-nodejs-esm) 88 | 89 | This library is focused on giving first-class support for Node.js ESM, and for that reason, it always builds a `.js` file for CommonJS, alongside a `.mjs` file for ESM. 90 | 91 | In every package you build for, you have to specify the package.json fields like this: 92 | 93 | > You can also change the "dist" folder to any name, you only have to be consistent and change it in your root configuration in the field "distDir" 94 | 95 | > This also enables to import every module separately 96 | 97 | ```json 98 | { 99 | "main": "dist/index.js", 100 | "module": "dist/index.mjs", 101 | "types": "dist/index.d.ts", 102 | "exports": { 103 | ".": { 104 | "require": "./dist/index.js", 105 | "import": "./dist/index.mjs" 106 | }, 107 | "./*": { 108 | "require": "./dist/*.js", 109 | "import": "./dist/*.mjs" 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | ## tsconfig 116 | 117 | `bob-esbuild` enforces specifying a couple of options in your root tsconfig, and it will error out if you are not doing it: 118 | 119 | ```json 120 | { 121 | "compilerOptions": { 122 | "outDir": "", 123 | "rootDir": "." 124 | } 125 | } 126 | ``` 127 | 128 | And it's recommended to use `tsconfig` paths & baseUrl, check [the official docs](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) about it. 129 | -------------------------------------------------------------------------------- /bob-esbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | export const config: import('bob-esbuild').BobConfig = { 4 | tsc: { 5 | dirs: ['examples/*', 'packages/*'], 6 | }, 7 | distDir: 'lib', 8 | verbose: true, 9 | outputOptions: { 10 | sourcemap: false, 11 | }, 12 | esbuildPluginOptions: { 13 | target: 'node13.2', 14 | }, 15 | keepDynamicImport: true, 16 | useTsconfigPaths: true, 17 | packageConfigs: { 18 | 'bob-ts': { 19 | external: ['./watchDeps.js', './deps.js', '../deps.js'], 20 | clean: true, 21 | globbyOptions: { 22 | ignore: [resolve('./src/deps.ts'), resolve('./src/watchDeps.ts')], 23 | }, 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /examples/basic/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | console.log('OK!'); 5 | 6 | require('../lib/bin/index.js'); 7 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "author": "PabloSzx ", 7 | "exports": { 8 | ".": { 9 | "require": "./lib/index.js", 10 | "import": "./lib/index.mjs" 11 | }, 12 | "./*": { 13 | "require": "./lib/*.js", 14 | "import": "./lib/*.mjs" 15 | }, 16 | "./other": "./lib/other.js", 17 | "./other_other": { 18 | "require": "./lib/other.js", 19 | "import": "./lib/other.mjs" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "main": "lib/index.js", 24 | "module": "lib/index.mjs", 25 | "types": "lib/index.d.ts", 26 | "bin": { 27 | "test_cli": "bin/index.js" 28 | }, 29 | "files": [ 30 | "lib" 31 | ], 32 | "scripts": { 33 | "dev": "bob-esbuild watch --onlyCJS", 34 | "prepare": "bob-esbuild build && bob-ts --target=node13.2 --paths -i src -f interop && bob-tsm lib/index.js && bob-tsm dist/index.js", 35 | "test": "bob-tsm --cjs --paths src/index.ts && bob-tsm --paths src/index.ts" 36 | }, 37 | "dependencies": { 38 | "bob-esbuild": "workspace:^4.0.3", 39 | "bob-esbuild-cli": "workspace:^4.0.0", 40 | "bob-ts": "workspace:^4.1.1", 41 | "bob-tsm": "workspace:^1.1.2", 42 | "shared": "workspace:^1.0.0" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^20.10.6", 46 | "esbuild": "^0.19.11", 47 | "typescript": "^5.3.3" 48 | }, 49 | "publishConfig": { 50 | "directory": "lib", 51 | "linkDirectory": false 52 | }, 53 | "buildConfig": { 54 | "bin": { 55 | "test_cli": { 56 | "input": "src/other.ts" 57 | } 58 | } 59 | }, 60 | "dependenciesMeta": { 61 | "bob-esbuild-cli": { 62 | "injected": true 63 | }, 64 | "bob-esbuild": { 65 | "injected": true 66 | }, 67 | "bob-tsm": { 68 | "injected": true 69 | }, 70 | "bob-ts": { 71 | "injected": true 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/basic/src/aliased/deep/module.ts: -------------------------------------------------------------------------------- 1 | export const Hello = 'World'; 2 | -------------------------------------------------------------------------------- /examples/basic/src/deep/other.ts: -------------------------------------------------------------------------------- 1 | export const C = 3; 2 | -------------------------------------------------------------------------------- /examples/basic/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Foo } from 'shared'; 2 | 3 | import { Bar } from 'shared/deep/two-deep/other'; 4 | import { C } from './innerShared'; 5 | import { createRequire } from 'module'; 6 | 7 | const require = createRequire(typeof __filename !== 'undefined' ? __filename : import.meta.url); 8 | 9 | /** 10 | * XD 11 | */ 12 | export const A = C * 1; 13 | 14 | console.log('node', process.version); 15 | 16 | import('shared/package.json').then(({ default: { name, version } }) => console.log('esm', { name, version })); 17 | Promise.resolve(require('shared/package.json')).then(({ name, version }) => console.log('cjs', { name, version })); 18 | 19 | console.log(Foo); 20 | 21 | console.log(Bar); 22 | 23 | export { Foo, Bar }; 24 | 25 | export { B } from './other'; 26 | 27 | import { Hello } from 'aliased-deep'; 28 | 29 | console.log(Hello); 30 | -------------------------------------------------------------------------------- /examples/basic/src/innerShared.ts: -------------------------------------------------------------------------------- 1 | import json from './testJson.json'; 2 | 3 | export const C = 123; 4 | 5 | export { json }; 6 | -------------------------------------------------------------------------------- /examples/basic/src/other.ts: -------------------------------------------------------------------------------- 1 | import { C } from './innerShared'; 2 | 3 | import { C as C2 } from '~examples/basic/src/deep/other'; 4 | 5 | export const B = C * 2 * C2; 6 | -------------------------------------------------------------------------------- /examples/basic/src/testJson.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": 123, 3 | "other": "hello" 4 | } 5 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "noEmit": true, 6 | "resolveJsonModule": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/nextjs-custom-server/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/nextjs-custom-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-custom-server", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "next build", 8 | "dev": "bob-tsm --watch src node_modules --node-env=dev src/server.ts", 9 | "start": "cross-env NODE_ENV=production bob-tsm src/server.ts" 10 | }, 11 | "dependencies": { 12 | "bob-tsm": "workspace:^1.1.2", 13 | "next": "^14.0.4", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20.10.6", 19 | "@types/react": "^18.2.46", 20 | "cross-env": "^7.0.3", 21 | "esbuild": "^0.19.11", 22 | "typescript": "^5.3.3" 23 | }, 24 | "dependenciesMeta": { 25 | "bob-tsm": { 26 | "injected": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/nextjs-custom-server/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export default function Hello() { 2 | return

Hello World!

; 3 | } 4 | -------------------------------------------------------------------------------- /examples/nextjs-custom-server/src/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import next from 'next'; 3 | import { parse } from 'url'; 4 | 5 | const dev = process.env.NODE_ENV !== 'production'; 6 | const app = next({ dev }); 7 | const handle = app.getRequestHandler(); 8 | 9 | await app.prepare(); 10 | 11 | createServer((req, res) => { 12 | const parsedUrl = parse(req.url!, true); 13 | 14 | handle(req, res, parsedUrl); 15 | }).listen(3000, () => { 16 | console.log('> Ready on http://localhost:3000'); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/nextjs-custom-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/other/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "other", 3 | "private": true, 4 | "scripts": { 5 | "test": "test_cli --help" 6 | }, 7 | "dependencies": { 8 | "basic": "workspace:1.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "author": "PabloSzx ", 7 | "exports": { 8 | ".": { 9 | "require": "./lib/index.js", 10 | "import": "./lib/index.mjs" 11 | }, 12 | "./*": { 13 | "require": "./lib/*.js", 14 | "import": "./lib/*.mjs" 15 | }, 16 | "./package.json": "./package.json" 17 | }, 18 | "main": "lib/index.js", 19 | "module": "lib/index.mjs", 20 | "types": "lib/index.d.ts", 21 | "files": [ 22 | "lib" 23 | ], 24 | "scripts": { 25 | "dev": "bob-esbuild watch", 26 | "prepare": "bob-esbuild build" 27 | }, 28 | "dependencies": { 29 | "bob-esbuild": "workspace:^4.0.3", 30 | "bob-esbuild-cli": "workspace:^4.0.0" 31 | }, 32 | "devDependencies": { 33 | "esbuild": "^0.19.11", 34 | "typescript": "^5.3.3" 35 | }, 36 | "dependenciesMeta": { 37 | "bob-esbuild-cli": { 38 | "injected": true 39 | }, 40 | "bob-esbuild": { 41 | "injected": true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/shared/src/deep/two-deep/other.ts: -------------------------------------------------------------------------------- 1 | export const Bar = 'Bar'; 2 | -------------------------------------------------------------------------------- /examples/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export const Foo = 123; 2 | -------------------------------------------------------------------------------- /examples/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psz-bob", 3 | "version": "1.0.0", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/PabloSzx/bob-esbuild.git" 8 | }, 9 | "license": "MIT", 10 | "author": "PabloSzx ", 11 | "scripts": { 12 | "changeset": "changeset", 13 | "ci:release": "pnpm pretty:all && pnpm -r publish --access public --no-git-checks", 14 | "ci:version": "pnpm pretty:all && changeset version && pnpm i --no-frozen-lockfile --lockfile-only --ignore-scripts", 15 | "clean": "pnpm dlx rimraf \"**/{node_modules,dist,lib,lib-types}\" pnpm-lock.yaml && pnpm i", 16 | "dev": "pnpm dev -r --no-sort", 17 | "prepare": "husky install && pnpm types", 18 | "pretty:all": "prettier -w \"**/*.{ts,tsx,js,cjs,mjs}\"", 19 | "release:canary": "(node scripts/canary-release.js && pnpm -r publish --access public --no-git-checks --tag alpha) || echo Skipping Canary...", 20 | "test": "pnpm test -r", 21 | "types": "pnpm --filter bob-esbuild build:types" 22 | }, 23 | "devDependencies": { 24 | "@changesets/apply-release-plan": "^7.0.0", 25 | "@changesets/assemble-release-plan": "^6.0.0", 26 | "@changesets/cli": "^2.27.1", 27 | "@changesets/config": "^3.0.0", 28 | "@changesets/read": "^0.6.0", 29 | "@manypkg/get-packages": "^2.2.0", 30 | "@types/node": "^20.10.6", 31 | "changesets-github-release": "^0.1.0", 32 | "esbuild": "^0.19.11", 33 | "husky": "^8.0.3", 34 | "prettier": "^3.1.1", 35 | "pretty-quick": "^3.1.3", 36 | "rimraf": "^5.0.5", 37 | "semver": "^7.5.4", 38 | "tsx": "^4.7.0", 39 | "typescript": "^5.3.3" 40 | }, 41 | "engines": { 42 | "pnpm": ">=8.4.0" 43 | }, 44 | "pnpm": { 45 | "peerDependencyRules": { 46 | "ignoreMissing": [ 47 | "@babel/core" 48 | ] 49 | }, 50 | "overrides": { 51 | "ramda": "0.27.2" 52 | } 53 | }, 54 | "lint-staged": { 55 | "*.{js,css,md}": "prettier --write" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/bob-cli/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/bob-cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # bob-esbuild-cli 2 | 3 | ## 4.0.0 4 | 5 | ### Major Changes 6 | 7 | - 9fb97f8: Require Node.js >=14.13.1 8 | 9 | ### Patch Changes 10 | 11 | - 3b414e1: Make bob-esbuild optional peer dependency 12 | - Updated dependencies [9fb97f8] 13 | - bob-esbuild@4.0.0 14 | 15 | ## 3.0.2 16 | 17 | ### Patch Changes 18 | 19 | - 4889dc1: Bump 20 | - Updated dependencies [4889dc1] 21 | - bob-esbuild@3.2.5 22 | 23 | ## 3.0.1 24 | 25 | ### Patch Changes 26 | 27 | - abff05c: Add skipValidate to watch command 28 | - Updated dependencies [2939cca] 29 | - bob-esbuild@3.1.1 30 | 31 | ## 3.0.0 32 | 33 | ### Major Changes 34 | 35 | - c783b22: Only copy typescript definitions on currently building project 36 | 37 | ### Patch Changes 38 | 39 | - afd2edb: add skipValidate option 40 | - Updated dependencies [afd2edb] 41 | - Updated dependencies [c783b22] 42 | - Updated dependencies [0ecbd1c] 43 | - Updated dependencies [a695127] 44 | - Updated dependencies [b8a13ba] 45 | - bob-esbuild@3.0.0 46 | 47 | ## 2.0.0 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [7678c9a] 52 | - bob-esbuild@2.0.0 53 | 54 | ## 1.0.1 55 | 56 | ### Patch Changes 57 | 58 | - 195ba2f: fix watch command 59 | 60 | ## 1.0.0 61 | 62 | ### Major Changes 63 | 64 | - b8df666: set bob-esbuild as peer dependency 65 | 66 | ### Patch Changes 67 | 68 | - Updated dependencies [b8df666] 69 | - Updated dependencies [46b9292] 70 | - Updated dependencies [b989382] 71 | - bob-esbuild@1.0.0 72 | 73 | ## 0.2.4 74 | 75 | ### Patch Changes 76 | 77 | - 582b21f: fix bundle target 78 | - Updated dependencies [582b21f] 79 | - bob-esbuild@0.2.4 80 | 81 | ## 0.2.3 82 | 83 | ### Patch Changes 84 | 85 | - 8e3f251: sourcemap disabled by default 86 | - Updated dependencies [8e3f251] 87 | - bob-esbuild@0.2.3 88 | 89 | ## 0.2.2 90 | 91 | ### Patch Changes 92 | 93 | - 5a92561: fix bin build 94 | - Updated dependencies [5a92561] 95 | - bob-esbuild@0.2.2 96 | 97 | ## 0.2.1 98 | 99 | ### Patch Changes 100 | 101 | - 91772d6: rollback globby to v11 102 | - Updated dependencies [8f52656] 103 | - Updated dependencies [91772d6] 104 | - bob-esbuild@0.2.1 105 | 106 | ## 0.2.0 107 | 108 | ### Minor Changes 109 | 110 | - 1d3375e: change ocliff to commander 111 | - 1d3375e: update deps 112 | 113 | ### Patch Changes 114 | 115 | - Updated dependencies [1d3375e] 116 | - bob-esbuild@0.2.0 117 | 118 | ## 0.1.27 119 | 120 | ### Patch Changes 121 | 122 | - Updated dependencies [dd3dd9d] 123 | - bob-esbuild@0.1.27 124 | 125 | ## 0.1.26 126 | 127 | ### Patch Changes 128 | 129 | - Updated dependencies [7afb215] 130 | - Updated dependencies [a0aaffa] 131 | - bob-esbuild@0.1.26 132 | 133 | ## 0.1.25 134 | 135 | ### Patch Changes 136 | 137 | - Updated dependencies [b96c19b] 138 | - bob-esbuild@0.1.25 139 | 140 | ## 0.1.24 141 | 142 | ### Patch Changes 143 | 144 | - Updated dependencies [3272150] 145 | - bob-esbuild@0.1.24 146 | 147 | ## 0.1.23 148 | 149 | ### Patch Changes 150 | 151 | - 1556971: allow skipTsc in watch mode 152 | - b4ee29a: allow only build cjs/esm 153 | - Updated dependencies [1556971] 154 | - Updated dependencies [0aabb50] 155 | - Updated dependencies [b4ee29a] 156 | - Updated dependencies [d3806f2] 157 | - bob-esbuild@0.1.23 158 | 159 | ## 0.1.22 160 | 161 | ### Patch Changes 162 | 163 | - fe24982: allow skip tsc build && default clean false on watch 164 | - Updated dependencies [fe24982] 165 | - bob-esbuild@0.1.22 166 | 167 | ## 0.1.21 168 | 169 | ### Patch Changes 170 | 171 | - c9d38ba: fix package.json rewrite w/ workspace protocol & sync packages 172 | - Updated dependencies [c9d38ba] 173 | - bob-esbuild@0.1.21 174 | 175 | ## 0.1.20 176 | 177 | ### Patch Changes 178 | 179 | - Updated dependencies [312fa02] 180 | - bob-esbuild@0.1.20 181 | 182 | ## 0.1.19 183 | 184 | ### Patch Changes 185 | 186 | - Updated dependencies [a47e85c] 187 | - Updated dependencies [63bcf77] 188 | - bob-esbuild@0.1.19 189 | 190 | ## 0.1.18 191 | 192 | ### Patch Changes 193 | 194 | - 26f57cb: pnpm with publishConfig.directory 195 | - Updated dependencies [26f57cb] 196 | - bob-esbuild@0.1.18 197 | 198 | ## 0.1.17 199 | 200 | ### Patch Changes 201 | 202 | - faf71da: sync 203 | - Updated dependencies [f0c7788] 204 | - Updated dependencies [0092814] 205 | - Updated dependencies [25ae121] 206 | - Updated dependencies [ed7a61c] 207 | - Updated dependencies [faf71da] 208 | - bob-esbuild@0.1.17 209 | 210 | ## 0.1.16 211 | 212 | ### Patch Changes 213 | 214 | - 4ba5263: rewrite package.json for publish & allow custom dir 215 | - Updated dependencies [4ba5263] 216 | - bob-esbuild@0.1.14 217 | 218 | ## 0.1.15 219 | 220 | ### Patch Changes 221 | 222 | - 4c678c8: improve build 223 | - Updated dependencies [4c678c8] 224 | - bob-esbuild@0.1.12 225 | -------------------------------------------------------------------------------- /packages/bob-cli/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run.mjs" %* 4 | -------------------------------------------------------------------------------- /packages/bob-cli/bin/run.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import '../lib/index.mjs'; 4 | -------------------------------------------------------------------------------- /packages/bob-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bob-esbuild-cli", 3 | "version": "4.0.0", 4 | "homepage": "https://github.com/PabloSzx/bob-esbuild", 5 | "bugs": "https://github.com/PabloSzx/bob-esbuild/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/PabloSzx/bob-esbuild", 9 | "directory": "packages/bob-cli" 10 | }, 11 | "license": "MIT", 12 | "author": "PabloSzx ", 13 | "exports": { 14 | ".": { 15 | "require": "./lib/index.js", 16 | "import": "./lib/index.mjs" 17 | }, 18 | "./*": { 19 | "require": "./lib/*.js", 20 | "import": "./lib/*.mjs" 21 | } 22 | }, 23 | "main": "lib/index.js", 24 | "module": "lib/index.mjs", 25 | "types": "lib/index.d.ts", 26 | "bin": { 27 | "bob-esbuild": "./bin/run.mjs" 28 | }, 29 | "files": [ 30 | "/bin", 31 | "/lib" 32 | ], 33 | "scripts": { 34 | "dev": "node bin/run.mjs watch", 35 | "prepare": "bun self-build.ts", 36 | "postpublish": "gh-release" 37 | }, 38 | "dependencies": { 39 | "commander": "^11.1.0" 40 | }, 41 | "devDependencies": { 42 | "bob-esbuild": "workspace:^4.0.3", 43 | "changesets-github-release": "^0.1.0", 44 | "typescript": "^5.3.3" 45 | }, 46 | "peerDependencies": { 47 | "bob-esbuild": "workspace:^4.0.0" 48 | }, 49 | "peerDependenciesMeta": { 50 | "bob-esbuild": { 51 | "optional": true 52 | } 53 | }, 54 | "engines": { 55 | "node": ">=14.13.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/bob-cli/self-build.ts: -------------------------------------------------------------------------------- 1 | import { startBuild } from '../bob/src/build'; 2 | 3 | startBuild().catch(err => { 4 | console.error(err); 5 | process.exit(1); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/bob-cli/src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import { startBuild } from 'bob-esbuild/build'; 2 | 3 | import { Command } from 'commander'; 4 | 5 | export const BuildCommand = new Command('build') 6 | .description('Build using rollup+esbuild, all these flags override the bob-esbuild.config') 7 | .option('--cwd ', 'Change target current directory') 8 | .option('-i --input ', "Input pattern files, if not specified, it reads '**/*.ts'") 9 | .option('--bundle', 'Enable bundling every entry point (With no support for code-splitting)') 10 | .option('--clean', "Clean the output files before writing the new build, by default it's set as 'true' by the global config") 11 | .option('--skipTsc', 'Skip TSC build') 12 | .option('--onlyCJS', 'Only build for CJS') 13 | .option('--onlyESM', 'Only build for ESM') 14 | .option('--skipValidate', 'Skip package.json validation') 15 | .action(async ({ cwd, inputFiles, bundle, clean, onlyCJS, onlyESM, skipTsc, skipValidate }) => { 16 | await startBuild({ 17 | rollup: { 18 | cwd, 19 | inputFiles, 20 | bundle, 21 | clean, 22 | onlyCJS, 23 | onlyESM, 24 | skipValidate, 25 | }, 26 | tsc: skipTsc 27 | ? false 28 | : { 29 | cwd, 30 | }, 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/bob-cli/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build'; 2 | export * from './tsc'; 3 | export * from './watch'; 4 | -------------------------------------------------------------------------------- /packages/bob-cli/src/commands/tsc.ts: -------------------------------------------------------------------------------- 1 | import { buildTsc } from 'bob-esbuild'; 2 | 3 | import { Command } from 'commander'; 4 | 5 | export const TSCCommand = new Command('tsc') 6 | .description('Run tsc and then copy the types') 7 | .option('-t --target <...dirs>') 8 | .action(async ({ target }) => { 9 | await buildTsc({ 10 | dirs: target, 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/bob-cli/src/commands/watch.ts: -------------------------------------------------------------------------------- 1 | import { startWatch } from 'bob-esbuild/watch'; 2 | 3 | import { Command } from 'commander'; 4 | 5 | export const WatchCommand = new Command('watch').description( 6 | 'Watch using rollup+esbuild, all these flags override the bob-esbuild.config' 7 | ); 8 | 9 | WatchCommand.option('--cwd ', 'Change target current directory') 10 | .option('-i --input ', "Input pattern files, if not specified, it reads '**/*.ts'") 11 | .option('--bundle', 'Enable bundling every entry point (With no support for code-splitting)') 12 | .option( 13 | '--clean', 14 | "DEFAULT=false. Clean the output files before writing the new build, by default it's set as 'true' by the global config", 15 | false 16 | ) 17 | .option('--skipTsc', 'Skip TSC build') 18 | .option('--onlyCJS', 'Only build for CJS') 19 | .option('--onlyESM', 'Only build for ESM') 20 | .option('--onSuccess ', 'Execute script after successful JS build') 21 | .option('--skipValidate', 'Skip package.json validation') 22 | .action(async ({ cwd, input: inputFiles, bundle, clean, onSuccess, onlyCJS, onlyESM, skipTsc, skipValidate }) => { 23 | const { watcher } = await startWatch({ 24 | rollup: { 25 | config: { 26 | cwd, 27 | inputFiles, 28 | bundle, 29 | clean, 30 | onlyCJS, 31 | onlyESM, 32 | skipValidate, 33 | }, 34 | onSuccessCommand: onSuccess, 35 | }, 36 | tsc: skipTsc ? false : {}, 37 | }); 38 | 39 | return new Promise(resolve => { 40 | watcher.on('close', () => { 41 | resolve(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/bob-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { program } from 'commander'; 2 | 3 | import { BuildCommand, TSCCommand, WatchCommand } from './commands'; 4 | 5 | program.addCommand(BuildCommand).addCommand(TSCCommand).addCommand(WatchCommand); 6 | 7 | program 8 | .parseAsync(process.argv) 9 | .then(() => { 10 | process.exit(0); 11 | }) 12 | .catch(err => { 13 | console.error(err); 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/bob-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/bob-ts/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # bob-ts 2 | 3 | ## 4.1.1 4 | 5 | ### Patch Changes 6 | 7 | - 89af531: Return rollup input, output and result on `buildCode` 8 | 9 | ## 4.1.0 10 | 11 | ### Minor Changes 12 | 13 | - b389104: New "globbyOptions" available for `buildCode` function to override options for globby file scan 14 | 15 | ## 4.0.0 16 | 17 | ### Major Changes 18 | 19 | - 9fb97f8: Require Node.js >=14.13.1 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [9fb97f8] 24 | - bob-esbuild-plugin@4.0.0 25 | 26 | ## 3.1.2 27 | 28 | ### Patch Changes 29 | 30 | - ee099eb: Set "typescript" and "@types/node" as optional peer dependency 31 | - Updated dependencies [ee099eb] 32 | - bob-esbuild-plugin@3.1.5 33 | 34 | ## 3.1.1 35 | 36 | ### Patch Changes 37 | 38 | - 4889dc1: Bump 39 | - Updated dependencies [4889dc1] 40 | - bob-esbuild-plugin@3.1.4 41 | 42 | ## 3.1.0 43 | 44 | ### Minor Changes 45 | 46 | - eedd60d: New options for programmatic usage: 47 | 48 | - "plugins": Allow specifying custom rollup plugins 49 | - "inputOptions": Customize specific input options 50 | 51 | ## 3.0.2 52 | 53 | ### Patch Changes 54 | 55 | - 5d2289b: Update bundled globby version to v13.1.1 56 | 57 | ## 3.0.1 58 | 59 | ### Patch Changes 60 | 61 | - bd701b3: Allow customize "keepDynamicImport" 62 | 63 | ## 3.0.0 64 | 65 | ### Major Changes 66 | 67 | - a958cf1: Set "typescript" as peer dependency 68 | 69 | ### Patch Changes 70 | 71 | - ebf0d1c: Bump recommended esbuild version 72 | - Updated dependencies [ebf0d1c] 73 | - bob-esbuild-plugin@3.1.3 74 | 75 | ## 2.0.1 76 | 77 | ### Patch Changes 78 | 79 | - 54baca4: Fix esbuild peer dependency range 80 | - Updated dependencies [54baca4] 81 | - bob-esbuild-plugin@3.1.2 82 | 83 | ## 2.0.0 84 | 85 | ### Major Changes 86 | 87 | - 5da1598: Improve external module resolution 88 | 89 | ### Minor Changes 90 | 91 | - a4a848d: Support JSON imports out-of-the-box 92 | 93 | ### Patch Changes 94 | 95 | - 75c77c6: Update & Require esbuild>=13.14 96 | - Updated dependencies [75c77c6] 97 | - bob-esbuild-plugin@3.1.1 98 | 99 | ## 1.2.2 100 | 101 | ### Patch Changes 102 | 103 | - 6824b73: Improve released package 104 | - Updated dependencies [078402e] 105 | - bob-esbuild-plugin@3.1.0 106 | 107 | ## 1.2.1 108 | 109 | ### Patch Changes 110 | 111 | - 215cbac: Add new "external" programmatic API option 112 | 113 | ## 1.2.0 114 | 115 | ### Minor Changes 116 | 117 | - 74a843e: Add new "--paths" options for tsconfig paths resolution 118 | 119 | ## 1.1.6 120 | 121 | ### Patch Changes 122 | 123 | - 287037b: Allow disable sourcemap from cli 124 | 125 | ## 1.1.5 126 | 127 | ### Patch Changes 128 | 129 | - b5eac61: Dynamic rollup import 130 | 131 | closes #159 132 | 133 | ## 1.1.4 134 | 135 | ### Patch Changes 136 | 137 | - a720d68: swallow clean fs errors 138 | - Updated dependencies [19b48c5] 139 | - bob-esbuild-plugin@2.1.0 140 | 141 | ## 1.1.3 142 | 143 | ### Patch Changes 144 | 145 | - 2fcd68a: improve programmatic usage options 146 | 147 | ## 1.1.2 148 | 149 | ### Patch Changes 150 | 151 | - 057823d: new "--ignore" option to specify specific paths to be ignored on watch mode 152 | 153 | ## 1.1.1 154 | 155 | ### Patch Changes 156 | 157 | - Updated dependencies [7678c9a] 158 | - bob-esbuild-plugin@2.0.0 159 | 160 | ## 1.1.0 161 | 162 | ### Minor Changes 163 | 164 | - 8ace193: Allow multiple concurrent commands on "bob-ts-watch" & change watching build delay from 1000 to 500 ms 165 | 166 | ## 1.0.6 167 | 168 | ### Patch Changes 169 | 170 | - a5923a9: fix & improve watch console 171 | 172 | ## 1.0.5 173 | 174 | ### Patch Changes 175 | 176 | - bb39a4b: fix crlf bin 177 | 178 | ## 1.0.4 179 | 180 | ### Patch Changes 181 | 182 | - 930ed29: Recommend c8 instead of nyc for mocha 183 | - 5affb52: Fix issue with esbuild target node12 with ESM dynamic imports 184 | 185 | ## 1.0.3 186 | 187 | ### Patch Changes 188 | 189 | - 0d2db1b: no clean output folder on watch mode by default 190 | 191 | ## 1.0.2 192 | 193 | ### Patch Changes 194 | 195 | - 4dcbeb9: allow customize runtime target with default current Node.js version 196 | 197 | ## 1.0.1 198 | 199 | ### Patch Changes 200 | 201 | - d408915: improve README & add LICENSE 202 | 203 | ## 1.0.0 204 | 205 | ### Major Changes 206 | 207 | - 9be540f: Release 🎉 208 | -------------------------------------------------------------------------------- /packages/bob-ts/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Pablo Sáez 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/bob-ts/README.md: -------------------------------------------------------------------------------- 1 | # bob-ts 2 | 3 | [![npm](https://img.shields.io/npm/v/bob-ts)](https://npm.im/bob-ts) 4 | 5 | Transpile your **TypeScript** projects quickly using **esbuild** + **rollup**, made to accelerate and simplify the TypeScript development with file watcher and first-class support for JavaScript testing frameworks. 6 | 7 | > This library doesn't handle type definitions, but you can add them simply by doing `tsc --declaration --emitDeclarationOnly` 8 | 9 | ## Install 10 | 11 | ```sh 12 | pnpm add -D bob-ts esbuild 13 | ``` 14 | 15 | ```sh 16 | yarn add -D bob-ts esbuild 17 | ``` 18 | 19 | ```sh 20 | npm install -D bob-ts esbuild 21 | ``` 22 | 23 | ## Usage 24 | 25 | By default `bob-ts` is ESM first, but you can change it to be either CJS with: `-f cjs` or `-f interop` to transpile for both **CommonJS** & **ESM**. 26 | 27 | If you **don't** have `"type": "module"` in your `package.json`, `ESM` will be outputted with the extension `.mjs` and `CJS` as `.js` 28 | 29 | If you **do** have `"type": "module"` in your `package.json`, `ESM` will be outputted with the extension `.js` and `CJS` as `.cjs`. 30 | 31 | ### Build 32 | 33 | ``` 34 | Usage: bob-ts [options] 35 | 36 | Options: 37 | -d, --dir Custom output dir (default: "dist") 38 | -i, --input Input patterns (default: ".") 39 | -f, --format Format, it can be 'cjs', 'esm' or 'interop' (default: "esm") 40 | --cwd Custom target directory (default: "X") 41 | --no-clean Don't clean output dir (default: true) 42 | -t, --target Javascript runtime target (default: "_YOUR_CURRENT_NODE_VERSION_") 43 | --no-sourcemap Disable sourcemap generation 44 | --paths Resolve tsconfig paths (default: false) 45 | -h, --help display help for command 46 | ``` 47 | 48 | > This will transpile all your `src` folder, and its structure will be kept **as is** in the `dist` folder 49 | 50 | ```json 51 | { 52 | "scripts": { 53 | "prepare": "bob-ts -i src" 54 | } 55 | } 56 | ``` 57 | 58 | ### Development / Watch Mode 59 | 60 | ``` 61 | Usage: bob-ts-watch [options] 62 | 63 | Options: 64 | -d, --dir Custom output dir (default: "dist") 65 | -i, --input Input patterns (default: ".") 66 | -f, --format Output format (choices: "cjs", "esm", "interop", default: "esm") 67 | --clean Clean output dir (default: false) 68 | --cwd Custom target directory (default: "___") 69 | -c, --command Execute scripts after successful JS build, You can specify more than a single command 70 | to be executed concurrently 71 | -t, --target Javascript runtime target (default: "_YOUR_CURRENT_NODE_VERSION_") 72 | --ignore Patterns of files to ignore watching 73 | --no-sourcemap Disable sourcemap generation 74 | --paths Resolve tsconfig paths (default: false) 75 | -h, --help display help for command 76 | ``` 77 | 78 | ```json 79 | { 80 | "scripts": { 81 | "dev": "bob-ts-watch -i src -c \"node dist/index.mjs\"" 82 | } 83 | } 84 | ``` 85 | 86 | ## Usage with Testing 87 | 88 | The main reason for the default input being `"."` is because this library has first-class support for being used for JavaScript test frameworks like [Mocha](https://mochajs.org/) and [Node-Tap](https://node-tap.org/). 89 | 90 | You can have all the tests files inside a specific test directory, for example, `test`, and use the transpiled version of your code with all the required source maps. 91 | 92 | In the end, a better and faster alternative than `ts-node/register`, and the solution for **ESM support** with TypeScript, where `ts-node` struggles a lot. 93 | 94 | > Example structure 95 | 96 | ``` 97 | src/ 98 | index.ts 99 | test/ 100 | main.test.ts 101 | package.json 102 | ``` 103 | 104 | ### Mocha 105 | 106 | ```sh 107 | pnpm add -D mocha c8 108 | ``` 109 | 110 | ```sh 111 | yarn add -D mocha c8 112 | ``` 113 | 114 | ```sh 115 | npm install -D mocha c8 116 | ``` 117 | 118 | #### ESM 119 | 120 | ```json 121 | { 122 | "scripts": { 123 | "dev": "bob-ts-watch -c \"node/dist/src/index.mjs\"", 124 | "start": "bob-ts && node/dist/src/index.mjs", 125 | "test": "bob-ts && c8 mocha dist/test", 126 | "test:watch": "bob-ts-watch -c \"c8 mocha dist/test\"" 127 | }, 128 | "mocha": { 129 | "enable-source-maps": true 130 | } 131 | } 132 | ``` 133 | 134 | #### CJS 135 | 136 | ```json 137 | { 138 | "scripts": { 139 | "dev": "bob-ts-watch -f cjs -c \"node dist/src/index.js\"", 140 | "start": "bob-ts -f cjs && node dist/src/index.js", 141 | "test": "bob-ts -f cjs && c8 mocha dist/test", 142 | "test:watch": "bob-ts-watch -f cjs -c \"c8 mocha dist/test\"" 143 | }, 144 | "mocha": { 145 | "enable-source-maps": true 146 | } 147 | } 148 | ``` 149 | 150 | ### Node Tap 151 | 152 | ```sh 153 | pnpm add -D tap @istanbuljs/esm-loader-hook 154 | ``` 155 | 156 | ```sh 157 | yarn add -D tap @istanbuljs/esm-loader-hook 158 | ``` 159 | 160 | ```sh 161 | npm install -D tap @istanbuljs/esm-loader-hook 162 | ``` 163 | 164 | #### ESM 165 | 166 | > Assuming that your tests are inside a `test` directory 167 | 168 | ```json 169 | { 170 | "scripts": { 171 | "dev": "bob-ts-watch -c \"node/dist/src/index.mjs\"", 172 | "start": "bob-ts && node/dist/src/index.mjs", 173 | "test": "bob-ts && tap dist/test", 174 | "test:watch": "bob-ts-watch -c \"tap dist/test\"" 175 | }, 176 | "tap": { 177 | "node-arg": ["--no-warnings", "--experimental-loader", "@istanbuljs/esm-loader-hook"] 178 | } 179 | } 180 | ``` 181 | 182 | #### CJS 183 | 184 | ```json 185 | { 186 | "scripts": { 187 | "dev": "bob-ts-watch -f cjs -c \"node/dist/src/index.js\"", 188 | "start": "bob-ts -f cjs && node/dist/src/index.js", 189 | "test": "bob-ts -f cjs && tap dist/test", 190 | "test:watch": "bob-ts-watch -f cjs -c \"tap dist/test\"" 191 | } 192 | } 193 | ``` 194 | 195 | ## LICENSE 196 | 197 | MIT 198 | -------------------------------------------------------------------------------- /packages/bob-ts/bin/bob-ts-watch.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import '../lib/bin/watch.mjs'; 4 | -------------------------------------------------------------------------------- /packages/bob-ts/bin/bob-ts.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import '../lib/bin/build.mjs'; 4 | -------------------------------------------------------------------------------- /packages/bob-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bob-ts", 3 | "version": "4.1.1", 4 | "homepage": "https://github.com/PabloSzx/bob-esbuild", 5 | "bugs": "https://github.com/PabloSzx/bob-esbuild/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/PabloSzx/bob-esbuild", 9 | "directory": "packages/bob-ts" 10 | }, 11 | "license": "MIT", 12 | "author": "PabloSzx ", 13 | "exports": { 14 | ".": { 15 | "require": "./lib/index.js", 16 | "import": "./lib/index.mjs" 17 | }, 18 | "./*": { 19 | "require": "./lib/*.js", 20 | "import": "./lib/*.mjs" 21 | } 22 | }, 23 | "main": "lib/index.js", 24 | "module": "lib/index.mjs", 25 | "types": "lib/index.d.ts", 26 | "bin": { 27 | "bob-ts": "./bin/bob-ts.mjs", 28 | "bob-ts-watch": "./bin/bob-ts-watch.mjs" 29 | }, 30 | "files": [ 31 | "/bin", 32 | "/lib" 33 | ], 34 | "scripts": { 35 | "prepare": "bun self-build.ts", 36 | "postpublish": "gh-release" 37 | }, 38 | "dependencies": { 39 | "bob-esbuild-plugin": "workspace:^4.0.0", 40 | "rollup": "^4.9.2" 41 | }, 42 | "devDependencies": { 43 | "@rollup/plugin-json": "^6.1.0", 44 | "@types/node": "^20.10.6", 45 | "bob-esbuild-cli": "workspace:^4.0.0", 46 | "changesets-github-release": "^0.1.0", 47 | "commander": "^11.1.0", 48 | "esbuild": "^0.19.11", 49 | "execa": "^6.1.0", 50 | "globby": "^14.0.0", 51 | "rollup-plugin-delete": "^2.0.0", 52 | "rollup-plugin-node-externals": "^6.1.2", 53 | "rollup-plugin-tsconfig-paths": "^1.5.2", 54 | "tree-kill": "^1.2.2", 55 | "typescript": "^5.3.3" 56 | }, 57 | "peerDependencies": { 58 | "@types/node": "*", 59 | "esbuild": ">=0.14.39", 60 | "typescript": "*" 61 | }, 62 | "peerDependenciesMeta": { 63 | "@types/node": { 64 | "optional": true 65 | }, 66 | "typescript": { 67 | "optional": true 68 | } 69 | }, 70 | "engines": { 71 | "node": ">=14.13.1" 72 | }, 73 | "publishConfig": { 74 | "access": "public", 75 | "directory": "lib", 76 | "linkDirectory": false 77 | }, 78 | "typesVersions": { 79 | "*": { 80 | "lib/index.d.ts": [ 81 | "lib/index.d.ts" 82 | ], 83 | "*": [ 84 | "lib/*", 85 | "lib/*/index.d.ts" 86 | ] 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/bob-ts/self-build.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | import { promises } from 'fs'; 3 | import { resolve } from 'path'; 4 | import { startBuild } from '../bob/src/build'; 5 | import { writePackageJson } from '../bob/src/config/packageJson'; 6 | import { buildRollup } from '../bob/src/rollup/build'; 7 | import pkg from './package.json'; 8 | 9 | async function main() { 10 | await promises.rm('lib', { 11 | recursive: true, 12 | force: true, 13 | }); 14 | await buildRollup({ 15 | inputFiles: ['./src/bin'], 16 | outputOptions: { 17 | banner: '#!/usr/bin/env node\n', 18 | }, 19 | clean: true, 20 | onlyESM: true, 21 | }); 22 | await startBuild({ 23 | rollup: { 24 | clean: false, 25 | globbyOptions: { 26 | ignore: [ 27 | resolve('./src/bin/build.ts'), 28 | resolve('./src/bin/watch.ts'), 29 | resolve('./src/deps.ts'), 30 | resolve('./src/watchDeps.ts'), 31 | ].map(v => v.replace(/\\/g, '/')), 32 | }, 33 | }, 34 | }); 35 | 36 | await build({ 37 | bundle: true, 38 | format: 'cjs', 39 | target: 'node12.20', 40 | entryPoints: ['src/deps.ts', 'src/watchDeps.ts'], 41 | outdir: 'lib', 42 | platform: 'node', 43 | minify: true, 44 | external: ['rollup', 'fsevents', 'typescript'], 45 | }); 46 | 47 | await Promise.all([ 48 | writePackageJson({ 49 | packageJson: { 50 | ...pkg, 51 | bin: { 52 | 'bob-ts': './bin/build.mjs', 53 | 'bob-ts-watch': './bin/watch.mjs', 54 | }, 55 | }, 56 | distDir: 'lib', 57 | }), 58 | promises.copyFile('LICENSE', 'lib/LICENSE'), 59 | promises.copyFile('README.md', 'lib/README.md'), 60 | ]); 61 | } 62 | 63 | main().catch(err => { 64 | console.error(err); 65 | process.exit(1); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/bob-ts/src/bin/build.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { program } from '../deps.js'; 3 | import { resolve } from 'path'; 4 | import { getDefaultNodeTargetVersion } from '../defaults'; 5 | 6 | program 7 | .option('-d, --dir ', 'Custom output dir', 'dist') 8 | .option('-i, --input ', 'Input patterns', '.') 9 | .option('-f, --format ', "Format, it can be 'cjs', 'esm' or 'interop'", 'esm') 10 | .option('--cwd ', 'Custom target directory', process.cwd()) 11 | .option('--no-clean', "Don't clean output dir (default: true)", true) 12 | .option('-t, --target ', 'Javascript runtime target', getDefaultNodeTargetVersion()) 13 | .option('--no-sourcemap', 'Disable sourcemap generation') 14 | .option('--paths', 'Resolve tsconfig paths', false); 15 | 16 | program 17 | .parseAsync() 18 | .then(async () => { 19 | const { dir, input, format, clean, cwd, target, sourcemap, paths } = program.opts<{ 20 | dir: string; 21 | input: string[]; 22 | format: 'cjs' | 'esm' | 'interop'; 23 | clean: boolean; 24 | cwd: string; 25 | target: string; 26 | sourcemap?: boolean; 27 | paths: boolean; 28 | }>(); 29 | 30 | process.chdir(resolve(cwd)); 31 | 32 | assert(['cjs', 'esm', 'interop'].includes(format), "Format has to be 'cjs', 'esm' or 'interop'"); 33 | 34 | const { buildCode } = await import('../build'); 35 | await buildCode({ 36 | entryPoints: Array.isArray(input) ? input : [input], 37 | format, 38 | outDir: dir, 39 | clean, 40 | target, 41 | sourcemap, 42 | paths, 43 | }); 44 | }) 45 | .catch(err => { 46 | console.error(err); 47 | process.exit(1); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/bob-ts/src/bin/watch.ts: -------------------------------------------------------------------------------- 1 | import { Option, program } from '../deps.js'; 2 | import { resolve } from 'path'; 3 | import { getDefaultNodeTargetVersion } from '../defaults'; 4 | 5 | program 6 | .option('-d, --dir ', 'Custom output dir', 'dist') 7 | .option('-i, --input ', 'Input patterns', '.') 8 | .addOption(new Option('-f, --format ', 'Output format').default('esm').choices(['cjs', 'esm', 'interop'])) 9 | .option('--clean', 'Clean output dir', false) 10 | .option('--cwd ', 'Custom target directory', process.cwd()) 11 | .option( 12 | '-c, --command ', 13 | 'Execute scripts after successful JS build, You can specify more than a single command to be executed concurrently' 14 | ) 15 | .option('-t, --target ', 'Javascript runtime target', getDefaultNodeTargetVersion()) 16 | .option('--ignore ', 'Patterns of files to ignore watching') 17 | .option('--no-sourcemap', 'Disable sourcemap generation') 18 | .option('--paths', 'Resolve tsconfig paths', false); 19 | 20 | program 21 | .parseAsync() 22 | .then(async () => { 23 | const { dir, input, format, clean, command, cwd, target, ignore, sourcemap, paths } = program.opts<{ 24 | dir: string; 25 | input: string[]; 26 | format: 'cjs' | 'esm' | 'interop'; 27 | clean: boolean; 28 | command?: string[]; 29 | cwd: string; 30 | target: string; 31 | ignore: string[]; 32 | sourcemap?: boolean; 33 | paths: boolean; 34 | }>(); 35 | 36 | process.chdir(resolve(cwd)); 37 | 38 | const [{ getRollupConfig }, { watchRollup }] = await Promise.all([import('../rollupConfig'), import('../watch')]); 39 | 40 | const { inputOptions, outputOptions } = await getRollupConfig({ 41 | entryPoints: Array.isArray(input) ? input : [input], 42 | format, 43 | outDir: dir, 44 | clean, 45 | target, 46 | sourcemap, 47 | paths, 48 | }); 49 | 50 | const { watcher } = await watchRollup({ 51 | input: inputOptions, 52 | output: outputOptions, 53 | onSuccessCommands: command, 54 | ignoreWatch: ignore, 55 | }); 56 | 57 | return new Promise(resolve => { 58 | watcher.on('close', () => { 59 | resolve(); 60 | }); 61 | }); 62 | }) 63 | .catch(err => { 64 | console.error(err); 65 | process.exit(1); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/bob-ts/src/build.ts: -------------------------------------------------------------------------------- 1 | import type { RollupConfig } from './rollupConfig'; 2 | import { getRollupConfig } from './rollupConfig'; 3 | 4 | export async function buildCode(config: RollupConfig) { 5 | const { rollup } = await import('rollup'); 6 | const { inputOptions, outputOptions, input } = await getRollupConfig(config); 7 | 8 | const build = await rollup(inputOptions); 9 | 10 | const result = await Promise.all( 11 | outputOptions.map(output => { 12 | return build.write(output); 13 | }) 14 | ); 15 | 16 | return { 17 | result, 18 | inputOptions, 19 | outputOptions, 20 | input, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/bob-ts/src/clean.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | export async function cleanEmptyFoldersRecursively(folder: string) { 5 | try { 6 | const isDir = (await promises.stat(folder)).isDirectory(); 7 | if (!isDir) return; 8 | 9 | let files = await promises.readdir(folder); 10 | 11 | if (files.length > 0) { 12 | await Promise.all( 13 | files.map(file => { 14 | const fullPath = join(folder, file); 15 | return cleanEmptyFoldersRecursively(fullPath); 16 | }) 17 | ); 18 | 19 | // re-evaluate files; after deleting subfolder 20 | // we may have parent folder empty now 21 | files = await promises.readdir(folder); 22 | } 23 | 24 | if (files.length == 0) { 25 | await promises.rmdir(folder); 26 | return; 27 | } 28 | } catch (err) {} 29 | } 30 | -------------------------------------------------------------------------------- /packages/bob-ts/src/defaults.ts: -------------------------------------------------------------------------------- 1 | export function getDefaultNodeTargetVersion(): `node${string}` { 2 | return `node${process.versions.node}`; 3 | } 4 | -------------------------------------------------------------------------------- /packages/bob-ts/src/deps.ts: -------------------------------------------------------------------------------- 1 | export { default as rollupJson } from '@rollup/plugin-json'; 2 | export { Option, program } from 'commander'; 3 | export { globby } from 'globby'; 4 | export { default as del } from 'rollup-plugin-delete'; 5 | export { default as externals } from 'rollup-plugin-node-externals'; 6 | export { default as tsconfigPaths } from 'rollup-plugin-tsconfig-paths'; 7 | -------------------------------------------------------------------------------- /packages/bob-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build'; 2 | export * from './packageJson'; 3 | export * from './rollupConfig'; 4 | export * from './watch'; 5 | -------------------------------------------------------------------------------- /packages/bob-ts/src/packageJson.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs'; 2 | import { resolve } from 'path'; 3 | 4 | export const getPackageJson = async () => { 5 | const packageJsonString = await promises.readFile(resolve('./package.json'), { encoding: 'utf-8' }); 6 | 7 | return JSON.parse(packageJsonString) as { 8 | type?: string; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/bob-ts/src/rollupConfig.ts: -------------------------------------------------------------------------------- 1 | import { bobEsbuildPlugin, EsbuildPluginOptions } from 'bob-esbuild-plugin'; 2 | import { existsSync } from 'fs'; 3 | import { resolve } from 'path'; 4 | import type { ExternalOption, InputOptions, OutputOptions } from 'rollup'; 5 | import type { CompilerOptions } from 'typescript'; 6 | import { cleanEmptyFoldersRecursively } from './clean'; 7 | import { del, globby, rollupJson, tsconfigPaths, externals } from './deps.js'; 8 | import { getPackageJson } from './packageJson'; 9 | 10 | export interface TsConfigPayload { 11 | compilerOptions: CompilerOptions; 12 | fileNames: string[]; 13 | } 14 | 15 | export interface GlobbyOptions { 16 | /** 17 | * Return the absolute path for entries. 18 | * 19 | * @default false 20 | */ 21 | absolute?: boolean; 22 | /** 23 | * If set to `true`, then patterns without slashes will be matched against 24 | * the basename of the path if it contains slashes. 25 | * 26 | * @default false 27 | */ 28 | baseNameMatch?: boolean; 29 | /** 30 | * Enables Bash-like brace expansion. 31 | * 32 | * @default true 33 | */ 34 | braceExpansion?: boolean; 35 | /** 36 | * Enables a case-sensitive mode for matching files. 37 | * 38 | * @default true 39 | */ 40 | caseSensitiveMatch?: boolean; 41 | /** 42 | * Specifies the maximum number of concurrent requests from a reader to read 43 | * directories. 44 | * 45 | * @default os.cpus().length 46 | */ 47 | concurrency?: number; 48 | /** 49 | * Specifies the maximum depth of a read directory relative to the start 50 | * directory. 51 | * 52 | * @default Infinity 53 | */ 54 | deep?: number; 55 | /** 56 | * Allow patterns to match entries that begin with a period (`.`). 57 | * 58 | * @default false 59 | */ 60 | dot?: boolean; 61 | /** 62 | * Enables Bash-like `extglob` functionality. 63 | * 64 | * @default true 65 | */ 66 | extglob?: boolean; 67 | /** 68 | * Indicates whether to traverse descendants of symbolic link directories. 69 | * 70 | * @default true 71 | */ 72 | followSymbolicLinks?: boolean; 73 | /** 74 | * Custom implementation of methods for working with the file system. 75 | * 76 | * @default fs.* 77 | */ 78 | fs?: any; 79 | /** 80 | * Enables recursively repeats a pattern containing `**`. 81 | * If `false`, `**` behaves exactly like `*`. 82 | * 83 | * @default true 84 | */ 85 | globstar?: boolean; 86 | /** 87 | * An array of glob patterns to exclude matches. 88 | * This is an alternative way to use negative patterns. 89 | * 90 | * @default [] 91 | */ 92 | ignore?: string[]; 93 | /** 94 | * Mark the directory path with the final slash. 95 | * 96 | * @default false 97 | */ 98 | markDirectories?: boolean; 99 | /** 100 | * Returns objects (instead of strings) describing entries. 101 | * 102 | * @default false 103 | */ 104 | objectMode?: boolean; 105 | /** 106 | * Return only directories. 107 | * 108 | * @default false 109 | */ 110 | onlyDirectories?: boolean; 111 | /** 112 | * Return only files. 113 | * 114 | * @default true 115 | */ 116 | onlyFiles?: boolean; 117 | /** 118 | * Enables an object mode (`objectMode`) with an additional `stats` field. 119 | * 120 | * @default false 121 | */ 122 | stats?: boolean; 123 | /** 124 | * By default this package suppress only `ENOENT` errors. 125 | * Set to `true` to suppress any error. 126 | * 127 | * @default false 128 | */ 129 | suppressErrors?: boolean; 130 | /** 131 | * Throw an error when symbolic link is broken if `true` or safely 132 | * return `lstat` call if `false`. 133 | * 134 | * @default false 135 | */ 136 | throwErrorOnBrokenSymbolicLink?: boolean; 137 | /** 138 | * Ensures that the returned entries are unique. 139 | * 140 | * @default true 141 | */ 142 | unique?: boolean; 143 | /** 144 | If set to `true`, `globby` will automatically glob directories for you. If you define an `Array` it will only glob files that matches the patterns inside the `Array`. You can also define an `Object` with `files` and `extensions` like in the example below. 145 | 146 | Note that if you set this option to `false`, you won't get back matched directories unless you set `onlyFiles: false`. 147 | 148 | @default true 149 | 150 | @example 151 | ``` 152 | import {globby} from 'globby'; 153 | 154 | const paths = await globby('images', { 155 | expandDirectories: { 156 | files: ['cat', 'unicorn', '*.jpg'], 157 | extensions: ['png'] 158 | } 159 | }); 160 | 161 | console.log(paths); 162 | //=> ['cat.png', 'unicorn.png', 'cow.jpg', 'rainbow.jpg'] 163 | ``` 164 | */ 165 | readonly expandDirectories?: boolean | readonly string[] | { files?: readonly string[]; extensions?: readonly string[] }; 166 | 167 | /** 168 | Respect ignore patterns in `.gitignore` files that apply to the globbed files. 169 | 170 | @default false 171 | */ 172 | readonly gitignore?: boolean; 173 | 174 | /** 175 | Glob patterns to look for ignore files, which are then used to ignore globbed files. 176 | 177 | This is a more generic form of the `gitignore` option, allowing you to find ignore files with a [compatible syntax](http://git-scm.com/docs/gitignore). For instance, this works with Babel's `.babelignore`, Prettier's `.prettierignore`, or ESLint's `.eslintignore` files. 178 | 179 | @default undefined 180 | */ 181 | readonly ignoreFiles?: string | readonly string[]; 182 | 183 | /** 184 | The current working directory in which to search. 185 | 186 | @default process.cwd() 187 | */ 188 | readonly cwd?: URL | string; 189 | } 190 | 191 | export interface RollupConfig { 192 | entryPoints: string[]; 193 | globbyOptions?: GlobbyOptions; 194 | format: 'cjs' | 'esm' | 'interop'; 195 | outDir: string; 196 | clean: boolean; 197 | target: string; 198 | esbuild?: EsbuildPluginOptions; 199 | sourcemap?: OutputOptions['sourcemap'] & EsbuildPluginOptions['sourceMap']; 200 | rollup?: Partial; 201 | paths?: 202 | | boolean 203 | | { 204 | tsConfigPath: string | string[] | TsConfigPayload | TsConfigPayload[]; 205 | logLevel: 'none' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; 206 | colors: boolean; 207 | strict: boolean; 208 | respectCoreModule: boolean; 209 | }; 210 | external?: ExternalOption; 211 | /** 212 | * If dynamic imports `await import("foo")` should be kept as `import` 213 | * and NOT be transpiled as `await Promise.resolve(require("foo"))` 214 | * 215 | * This is specially useful when is needed to import an `ES Module` from `CommonJS`, 216 | * for example, when an external package has `"type": "module"`. 217 | * 218 | * If an array of strings is specified, the dynamic imports are only kept 219 | * for those specified modules 220 | * 221 | * @default true 222 | */ 223 | keepDynamicImport?: boolean | string[] | ((moduleName: string) => boolean); 224 | plugins?: InputOptions['plugins']; 225 | inputOptions?: Omit; 226 | } 227 | 228 | export const getRollupConfig = async ({ 229 | entryPoints, 230 | globbyOptions, 231 | format, 232 | outDir, 233 | clean, 234 | target, 235 | esbuild, 236 | sourcemap = true, 237 | rollup, 238 | paths, 239 | external, 240 | keepDynamicImport = true, 241 | inputOptions: customInputOptions, 242 | plugins: customPlugins, 243 | }: RollupConfig) => { 244 | const dir = resolve(outDir); 245 | 246 | const input = ( 247 | await globby( 248 | entryPoints.map(v => v.replace(/\\/g, '/')), 249 | { 250 | absolute: true, 251 | ignore: ['**/node_modules'], 252 | ...globbyOptions, 253 | } 254 | ) 255 | ).filter(file => !!file.match(/\.(js|cjs|mjs|ts|tsx|cts|mts|ctsx|mtsx)$/)); 256 | 257 | const customPluginsAwaited = (await customPlugins) || []; 258 | 259 | const plugins: InputOptions['plugins'] = [ 260 | ...Array.from(Array.isArray(customPluginsAwaited) ? customPluginsAwaited : [customPluginsAwaited]), 261 | externals({ 262 | packagePath: resolve(process.cwd(), 'package.json'), 263 | deps: true, 264 | }), 265 | bobEsbuildPlugin({ 266 | target, 267 | sourceMap: sourcemap, 268 | ...esbuild, 269 | }), 270 | rollupJson({ 271 | preferConst: true, 272 | }), 273 | clean && 274 | del({ 275 | targets: [`${dir}/**/*.js`, `${dir}/**/*.mjs`, `${dir}/**/*.cjs`, `${dir}/**/*.map`], 276 | }), 277 | clean && 278 | (() => { 279 | let deleted = false; 280 | 281 | return { 282 | name: 'Clean Empty Directories', 283 | async buildEnd() { 284 | if (deleted) return; 285 | deleted = true; 286 | if (existsSync(dir)) await cleanEmptyFoldersRecursively(dir); 287 | }, 288 | }; 289 | })(), 290 | paths && tsconfigPaths(typeof paths === 'boolean' ? undefined : paths), 291 | ]; 292 | 293 | if (keepDynamicImport) { 294 | if (Array.isArray(keepDynamicImport)) { 295 | plugins.unshift({ 296 | name: 'keep-dynamic-import', 297 | renderDynamicImport({ targetModuleId }) { 298 | if (!targetModuleId || !keepDynamicImport.includes(targetModuleId)) return null; 299 | 300 | return { 301 | left: 'import(', 302 | right: ')', 303 | }; 304 | }, 305 | }); 306 | } else if (typeof keepDynamicImport === 'function') { 307 | plugins.unshift({ 308 | name: 'keep-dynamic-import', 309 | renderDynamicImport({ targetModuleId }) { 310 | if (!targetModuleId || !keepDynamicImport(targetModuleId)) return null; 311 | 312 | return { 313 | left: 'import(', 314 | right: ')', 315 | }; 316 | }, 317 | }); 318 | } else { 319 | plugins.unshift({ 320 | name: 'keep-dynamic-import', 321 | renderDynamicImport() { 322 | return { 323 | left: 'import(', 324 | right: ')', 325 | }; 326 | }, 327 | }); 328 | } 329 | } 330 | 331 | const inputOptions: InputOptions = { 332 | ...customInputOptions, 333 | input, 334 | plugins, 335 | external, 336 | }; 337 | 338 | const isTypeModule = (await getPackageJson()).type === 'module'; 339 | 340 | const cjsEntryFileNames = isTypeModule ? '[name].cjs' : undefined; 341 | const esmEntryFileNames = isTypeModule ? undefined : '[name].mjs'; 342 | 343 | const outputOptions: OutputOptions[] = 344 | format === 'interop' 345 | ? [ 346 | { 347 | format: 'cjs', 348 | dir, 349 | entryFileNames: cjsEntryFileNames, 350 | preserveModules: true, 351 | exports: 'auto', 352 | sourcemap, 353 | ...rollup, 354 | }, 355 | { 356 | format: 'esm', 357 | dir, 358 | entryFileNames: esmEntryFileNames, 359 | preserveModules: true, 360 | exports: 'auto', 361 | sourcemap, 362 | ...rollup, 363 | }, 364 | ] 365 | : [ 366 | { 367 | format, 368 | dir, 369 | entryFileNames: format === 'esm' ? esmEntryFileNames : cjsEntryFileNames, 370 | preserveModules: true, 371 | exports: 'auto', 372 | sourcemap, 373 | ...rollup, 374 | }, 375 | ]; 376 | 377 | return { 378 | inputOptions, 379 | outputOptions, 380 | input, 381 | }; 382 | }; 383 | -------------------------------------------------------------------------------- /packages/bob-ts/src/watch.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'child_process'; 2 | import type { InputOptions, OutputOptions } from 'rollup'; 3 | import { command, treeKill } from './watchDeps.js'; 4 | 5 | export interface WatchRollupOptions { 6 | input: InputOptions; 7 | output: OutputOptions[]; 8 | onSuccessCommands?: string[]; 9 | onSuccessCallback?: (builds: number) => void; 10 | onStartCallback?: (builds: number) => void; 11 | ignoreWatch?: string | string[]; 12 | } 13 | 14 | const cwd = process.cwd(); 15 | 16 | export async function watchRollup(options: WatchRollupOptions) { 17 | const { watch: rollupWatch } = await import('rollup'); 18 | const { input: inputOptions, output: outputOptions, ignoreWatch } = options; 19 | 20 | const watcher = rollupWatch({ 21 | ...inputOptions, 22 | output: outputOptions, 23 | 24 | watch: { 25 | skipWrite: true, 26 | buildDelay: 500, 27 | chokidar: { 28 | ignoreInitial: false, 29 | }, 30 | exclude: ignoreWatch, 31 | }, 32 | }); 33 | 34 | const onSuccessProcesses: ChildProcess[] = []; 35 | 36 | function cleanUp() { 37 | try { 38 | for (const onSuccessProcess of onSuccessProcesses) { 39 | if (onSuccessProcess?.pid) killPromise(onSuccessProcess.pid); 40 | } 41 | 42 | watcher.close(); 43 | } catch (err) { 44 | } finally { 45 | process.exit(0); 46 | } 47 | } 48 | 49 | process.on('SIGINT', cleanUp); 50 | process.on('SIGHUP', cleanUp); 51 | process.on('SIGQUIT', cleanUp); 52 | process.on('SIGTERM', cleanUp); 53 | process.on('uncaughtException', cleanUp); 54 | process.on('exit', cleanUp); 55 | 56 | const pendingKillPromises = new Set>(); 57 | 58 | function killPromise(pid: number) { 59 | const pendingKillPromise = new Promise(resolve => { 60 | treeKill(pid, () => { 61 | resolve(); 62 | pendingKillPromises.delete(pendingKillPromise); 63 | }); 64 | }); 65 | 66 | pendingKillPromises.add(pendingKillPromise); 67 | } 68 | 69 | let buildsDone = 0; 70 | 71 | let lastStart = Date.now(); 72 | 73 | watcher.on('event', event => { 74 | switch (event.code) { 75 | case 'BUNDLE_START': { 76 | { 77 | while (onSuccessProcesses.length) { 78 | const onSuccessProcess = onSuccessProcesses.shift(); 79 | if (onSuccessProcess?.pid != null) { 80 | killPromise(onSuccessProcess.pid); 81 | } 82 | } 83 | } 84 | 85 | options.onStartCallback?.(buildsDone); 86 | 87 | lastStart = Date.now(); 88 | break; 89 | } 90 | case 'BUNDLE_END': { 91 | const { result } = event; 92 | 93 | (async () => { 94 | try { 95 | await Promise.all( 96 | outputOptions.map(output => { 97 | return result.write(output); 98 | }) 99 | ); 100 | 101 | console.log(`[${new Date().toLocaleString()}] Build success for ${cwd} in ${Date.now() - lastStart}ms`); 102 | 103 | options.onSuccessCallback?.(buildsDone); 104 | 105 | pendingKillPromises.size && (await Promise.all(pendingKillPromises)); 106 | 107 | if (options.onSuccessCommands) { 108 | for (const onSuccessCommand of options.onSuccessCommands) { 109 | console.log(`$ ${onSuccessCommand}`); 110 | onSuccessProcesses.push( 111 | command(onSuccessCommand, { 112 | stdio: 'inherit', 113 | shell: true, 114 | }) 115 | ); 116 | } 117 | } 118 | } catch (err) { 119 | console.error(err); 120 | } finally { 121 | result.close().catch(console.error); 122 | 123 | ++buildsDone; 124 | } 125 | })(); 126 | 127 | break; 128 | } 129 | case 'ERROR': { 130 | console.error(event.error.message ? `\n[${new Date().toLocaleString()}] [ERROR]: ${event.error.message}\n` : event.error); 131 | break; 132 | } 133 | case 'START': { 134 | break; 135 | } 136 | } 137 | }); 138 | 139 | return { watcher, cleanUp }; 140 | } 141 | -------------------------------------------------------------------------------- /packages/bob-ts/src/watchDeps.ts: -------------------------------------------------------------------------------- 1 | export { default as treeKill } from 'tree-kill'; 2 | export { execaCommand as command } from 'execa'; 3 | -------------------------------------------------------------------------------- /packages/bob-tsm/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # bob-tsm 2 | 3 | ## 1.1.2 4 | 5 | ### Patch Changes 6 | 7 | - f496750: Properly hide Experimental loaders message on Node>=v16.17.0 8 | 9 | ## 1.1.1 10 | 11 | ### Patch Changes 12 | 13 | - be4c165: Fix compatibility with Node.js v18 14 | 15 | ## 1.1.0 16 | 17 | ### Minor Changes 18 | 19 | - 085921c: New "--restart-on-fail" option to restart node execution when Node process exits with a non-zero exit code 20 | 21 | ## 1.0.0 22 | 23 | ### Major Changes 24 | 25 | - 9fb97f8: Require Node.js >=14.13.1 26 | 27 | ## 0.4.8 28 | 29 | ### Patch Changes 30 | 31 | - cbe654d: New option "--no-sourcemap" to disable sourcemaps 32 | 33 | ## 0.4.7 34 | 35 | ### Patch Changes 36 | 37 | - 4889dc1: Bump 38 | 39 | ## 0.4.6 40 | 41 | ### Patch Changes 42 | 43 | - b349c7e: Add "--keep-esm-loader" option, it enables to keep the ESM Loader for forks (It can break certain environments like Next.js custom server) 44 | 45 | ## 0.4.5 46 | 47 | ### Patch Changes 48 | 49 | - c09357a: Set "typescript" as optional peer dependency 50 | 51 | ## 0.4.4 52 | 53 | ### Patch Changes 54 | 55 | - 23dbed1: ESM: Try resolve adding script extensions (.ts, .tsx, .jsx, ...) 56 | 57 | ## 0.4.3 58 | 59 | ### Patch Changes 60 | 61 | - 3407fd1: Async check if file exists on ESM 62 | - 4367476: Define `__dirname` & `__filename` for ESM 63 | 64 | ## 0.4.2 65 | 66 | ### Patch Changes 67 | 68 | - ebf0d1c: Bump recommended esbuild version 69 | 70 | ## 0.4.1 71 | 72 | ### Patch Changes 73 | 74 | - a138e87: Set "typescript>=4.1.2" as peer dependency 75 | 76 | ## 0.4.0 77 | 78 | ### Minor Changes 79 | 80 | - ec841fc: New "--paths" CLI option to enable [tsconfig paths](https://www.typescriptlang.org/tsconfig#paths) mapping resolution. The `tsconfig.json` location by default is where `bob-tsm` is called, but it can be customized using the `TS_NODE_PROJECT` environment variable. This mapping is used as a fallback over the default Node.js + Default TypeScript path resolution. 81 | 82 | ## 0.3.9 83 | 84 | ### Patch Changes 85 | 86 | - 5526c66: Fix: require shouldn't mutate global options 87 | 88 | ## 0.3.8 89 | 90 | ### Patch Changes 91 | 92 | - 54baca4: Fix esbuild peer dependency range 93 | 94 | ## 0.3.7 95 | 96 | ### Patch Changes 97 | 98 | - 75c77c6: Update & Require esbuild>=13.14 99 | 100 | ## 0.3.6 101 | 102 | ### Patch Changes 103 | 104 | - af75da4: Respect import maps 105 | 106 | ## 0.3.5 107 | 108 | ### Patch Changes 109 | 110 | - 451d9f8: Prevent ".d.ts" imports 111 | 112 | ## 0.3.4 113 | 114 | ### Patch Changes 115 | 116 | - 6ccfc7b: Improved released package 117 | 118 | ## 0.3.3 119 | 120 | ### Patch Changes 121 | 122 | - 518f6ee: Add support for "/index" default resolve 123 | 124 | ## 0.3.2 125 | 126 | ### Patch Changes 127 | 128 | - 0ea9ec7: Fix node spawn log on non-watch mode 129 | 130 | ## 0.3.1 131 | 132 | ### Patch Changes 133 | 134 | - b8a7fd7: Silence warning: `"--experimental-loader is an experimental feature"` 135 | 136 | ## 0.3.0 137 | 138 | ### Minor Changes 139 | 140 | - 05e11ab: - New option `--node-env` / `--node_env` to automatically add the specified option as `NODE_ENV` environment variable, `"prod"` is an alias for "production" and `"dev"` is an alias for "development". For example: `bob-tsm --node-env=dev --watch=src src/index.ts` or `bob-tsm --node-env=prod src/index.ts` 141 | - Automatically add `--enable-source-maps` flag 142 | - Fix `--tsmconfig` and `--quiet` usage 143 | - Watch mode prints what file paths have changed and caused the re-execution, you can use `--quiet` or `-q` to silence them. 144 | 145 | ## 0.2.4 146 | 147 | ### Patch Changes 148 | 149 | - 061905b: Remove "--loader" from `process.execArgv` to fix applications that rely on forked Node.js sub-processes like Next.js Custom Server, you can opt-out specifying "KEEP_LOADER_ARGV" environment variable. 150 | 151 | ## 0.2.3 152 | 153 | ### Patch Changes 154 | 155 | - 79e89ce: Update to use new consolidated hooks with backwards compatibility https://github.com/nodejs/node/pull/37468 156 | 157 | ## 0.2.2 158 | 159 | ### Patch Changes 160 | 161 | - 1993fa1: Fix chokidar as dep 162 | 163 | ## 0.2.1 164 | 165 | ### Patch Changes 166 | 167 | - bf740bd: Fix and test ESM/CJS Interop 168 | 169 | ## 0.2.0 170 | 171 | ### Minor Changes 172 | 173 | - ffecc6a: add "--cjs" option to use CommonJS instead of ESM for ".ts" files 174 | 175 | ## 0.1.2 176 | 177 | ### Patch Changes 178 | 179 | - 25c1bcb: Fix windows 180 | 181 | ## 0.1.1 182 | 183 | ### Patch Changes 184 | 185 | - 817e5a9: Fix windows usage 186 | 187 | ## 0.1.0 188 | 189 | ### Minor Changes 190 | 191 | - 1450803: `bob-tsm` release 🎉, inspired on https://github.com/lukeed/tsm with extra watch mode & following TypeScript 4.5 `.cts` & `mts` extensions behavior 192 | -------------------------------------------------------------------------------- /packages/bob-tsm/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Pablo Sáez , Luke Edwards (lukeed.com) 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/bob-tsm/README.md: -------------------------------------------------------------------------------- 1 | # bob-tsm 2 | 3 | [![npm](https://img.shields.io/npm/v/bob-tsm)](https://npm.im/bob-tsm) 4 | 5 | Package inspired on https://github.com/lukeed/tsm with extra features and support for watch mode and with extra fixes to follow the new [TypeScript 4.5 extensions](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#new-file-extensions) `.mts`=>`ESM` and `.cts`=>`CommonJS`. 6 | 7 | ## Install 8 | 9 | ```sh 10 | pnpm add -D bob-tsm esbuild 11 | ``` 12 | 13 | ```sh 14 | yarn add -D bob-tsm esbuild 15 | ``` 16 | 17 | ```sh 18 | npm install -D bob-tsm esbuild 19 | ``` 20 | 21 | ## Usage 22 | 23 | All the arguments that are not part of `bob-tsm` are passed directly to the `node` executable. 24 | 25 | ``` 26 | Usage: bob-tsm [options] [node arguments...] 27 | 28 | Options: 29 | -V, --version output the version number 30 | --tsmconfig Configuration file path (default: "tsm.js") 31 | --watch Enable & specify watch mode 32 | --ignore Ignore watch patterns 33 | --node-env,--node_env Automatically add the specified option as NODE_ENV environment variable, "prod" is an alias 34 | for "production" and "dev" is an alias for "development" (choices: "production", "prod", 35 | "development", "dev", "test") 36 | -q, --quiet 37 | --cjs Use CommonJS instead of ESM for ".ts" files. You still can use ".mts" to force ESM in 38 | specific typescript files. 39 | --paths Use tsconfig paths resolver. It only works as a fallback of the default path resolving and 40 | you can use the environment variable TS_NODE_PROJECT to customize the tsconfig.json to use. 41 | -h, --help display help for command 42 | ``` 43 | -------------------------------------------------------------------------------- /packages/bob-tsm/bin/bob-tsm.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | import '../lib/bin.mjs'; 5 | -------------------------------------------------------------------------------- /packages/bob-tsm/build.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | import { promises } from 'fs'; 3 | import { resolve } from 'path'; 4 | import { buildCode } from '../bob-ts/src/build'; 5 | import { writePackageJson } from '../bob/src/config/packageJson'; 6 | import { buildTsc } from '../bob/src/tsc/build'; 7 | import pkg from './package.json'; 8 | 9 | async function main() { 10 | await promises.rm('lib', { 11 | recursive: true, 12 | force: true, 13 | }); 14 | 15 | const tscPromise = Promise.allSettled([buildTsc()]).then(v => v[0]); 16 | 17 | await promises.mkdir('lib', { 18 | recursive: true, 19 | }); 20 | 21 | await Promise.all([ 22 | buildCode({ 23 | entryPoints: ['./src/require.ts'], 24 | clean: false, 25 | format: 'cjs', 26 | outDir: 'lib', 27 | target: 'node12.20', 28 | sourcemap: false, 29 | external: ['./deps/typescriptPaths.js'], 30 | esbuild: { 31 | minify: false, 32 | }, 33 | }), 34 | buildCode({ 35 | entryPoints: ['./src/register.ts'], 36 | clean: false, 37 | format: 'cjs', 38 | outDir: 'lib', 39 | target: 'node18', 40 | sourcemap: false, 41 | esbuild: { 42 | minify: false, 43 | }, 44 | }), 45 | buildCode({ 46 | entryPoints: ['./src/loader.ts'], 47 | clean: false, 48 | format: 'esm', 49 | outDir: 'lib', 50 | target: 'node12.20', 51 | sourcemap: false, 52 | external: ['./deps/typescriptPaths.js'], 53 | esbuild: { 54 | minify: false, 55 | }, 56 | }), 57 | buildCode({ 58 | entryPoints: ['./src/bin.ts'], 59 | clean: false, 60 | format: 'esm', 61 | outDir: 'lib', 62 | target: 'node12.20', 63 | esbuild: { 64 | define: { 65 | VERSION: JSON.stringify(pkg.version), 66 | }, 67 | minify: false, 68 | }, 69 | sourcemap: false, 70 | external: ['./deps/commander.js', './deps/treeKill.js', './deps/chokidar.js', './deps/typescriptPaths.js'], 71 | rollup: { 72 | banner: '#!/usr/bin/env node\n', 73 | }, 74 | }), 75 | promises.readdir('./src/deps').then(relativeDeps => { 76 | const entryPoints = relativeDeps.map(v => resolve('./src/deps', v)); 77 | 78 | return build({ 79 | bundle: true, 80 | format: 'cjs', 81 | target: 'node12.20', 82 | entryPoints, 83 | outdir: 'lib/deps', 84 | platform: 'node', 85 | minify: true, 86 | external: ['fsevents', 'typescript'], 87 | }); 88 | }), 89 | writePackageJson({ 90 | packageJson: { 91 | ...pkg, 92 | bin: { 93 | 'bob-tsm': './bin.mjs', 94 | }, 95 | }, 96 | distDir: 'lib', 97 | }), 98 | promises.copyFile('LICENSE', 'lib/LICENSE'), 99 | promises.copyFile('README.md', 'lib/README.md'), 100 | ]); 101 | await buildCode({ 102 | entryPoints: ['./src/config.ts', './src/utils.ts'], 103 | clean: false, 104 | format: 'interop', 105 | outDir: 'lib', 106 | target: 'node12.20', 107 | sourcemap: false, 108 | esbuild: { 109 | minify: false, 110 | }, 111 | }); 112 | 113 | await tscPromise.then(v => { 114 | if (v.status === 'rejected') throw v.reason; 115 | }); 116 | } 117 | 118 | main().catch(err => { 119 | console.error(err); 120 | process.exit(1); 121 | }); 122 | -------------------------------------------------------------------------------- /packages/bob-tsm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bob-tsm", 3 | "version": "1.1.2", 4 | "homepage": "https://github.com/PabloSzx/bob-esbuild", 5 | "bugs": "https://github.com/PabloSzx/bob-esbuild/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/PabloSzx/bob-esbuild", 9 | "directory": "packages/bob-tsm" 10 | }, 11 | "license": "MIT", 12 | "author": "PabloSzx ", 13 | "exports": { 14 | ".": { 15 | "import": "./lib/loader.mjs", 16 | "require": "./lib/require.js", 17 | "types": "./lib/index.d.ts" 18 | }, 19 | "./*": { 20 | "import": "./lib/*.mjs", 21 | "require": "./lib/*.js", 22 | "types": "./lib/*.d.ts" 23 | }, 24 | "./package.json": "./package.json" 25 | }, 26 | "types": "lib/index.d.ts", 27 | "bin": { 28 | "bob-tsm": "./bin/bob-tsm.mjs" 29 | }, 30 | "files": [ 31 | "lib", 32 | "config", 33 | "bin" 34 | ], 35 | "scripts": { 36 | "dev": "node ../bob-watch/bin/bob-watch.mjs --watch=src -c \"pnpm i\"", 37 | "playground": "node ../bob-watch/bin/bob-watch.mjs --watch src playground -c \"pnpm prepare && node bin/bob-tsm.mjs playground/index.ts\"", 38 | "prepare": "bun build.ts", 39 | "postpublish": "gh-release", 40 | "test": "bun build.ts && c8 bun ./test/test.cjs" 41 | }, 42 | "devDependencies": { 43 | "@types/semver": "^7.5.6", 44 | "bob-esbuild-plugin": "workspace:^4.0.0", 45 | "c8": "^8.0.1", 46 | "chokidar": "^3.5.3", 47 | "commander": "^11.1.0", 48 | "esbuild": "^0.19.11", 49 | "execa": "^6.1.0", 50 | "tree-kill": "^1.2.2", 51 | "typescript": "^5.3.3", 52 | "typescript-paths": "^1.5.1" 53 | }, 54 | "peerDependencies": { 55 | "esbuild": ">=0.14.39", 56 | "typescript": ">=4.7.4" 57 | }, 58 | "peerDependenciesMeta": { 59 | "typescript": { 60 | "optional": true 61 | } 62 | }, 63 | "optionalDependencies": { 64 | "fsevents": "~2.3.3" 65 | }, 66 | "engines": { 67 | "node": ">=14.13.1" 68 | }, 69 | "publishConfig": { 70 | "access": "public", 71 | "directory": "lib", 72 | "linkDirectory": false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/bob-tsm/playground/index.ts: -------------------------------------------------------------------------------- 1 | import { hello } from './other'; 2 | 3 | console.log(hello); 4 | -------------------------------------------------------------------------------- /packages/bob-tsm/playground/other/index.ts: -------------------------------------------------------------------------------- 1 | export const hello: string = 'Hello World'; 2 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/bin.ts: -------------------------------------------------------------------------------- 1 | // note: injected @ build 2 | declare const VERSION: string; 3 | 4 | import { ChildProcess, spawn, SpawnOptions } from 'child_process'; 5 | import commanderPkg from './deps/commander.js'; 6 | import { existsSync } from 'fs'; 7 | import { dirname, join } from 'path'; 8 | import { fileURLToPath, pathToFileURL } from 'url'; 9 | import { debouncePromise, getDefault } from './utils'; 10 | 11 | const { program, Option } = getDefault(commanderPkg); 12 | 13 | program 14 | .version(VERSION) 15 | .option('--tsmconfig ', 'Configuration file path', 'tsm.js') 16 | .option('--watch ', 'Enable & specify watch mode') 17 | .option('--ignore ', 'Ignore watch patterns') 18 | .option('--no-sourcemap', "Don't generate/enable source maps") 19 | .option('--restart-on-fail', 'Restart node on execution error') 20 | .option('--keep-esm-loader', 'Keep ESM Loader for forks (It can break certain environments like Next.js custom server)') 21 | .addOption( 22 | new Option( 23 | '--node-env,--node_env ', 24 | 'Automatically add the specified option as NODE_ENV environment variable, "prod" is an alias for "production" and "dev" is an alias for "development"' 25 | ).choices(['production', 'prod', 'development', 'dev', 'test']) 26 | ) 27 | .option('-q, --quiet') 28 | .option( 29 | '--cjs', 30 | 'Use CommonJS instead of ESM for ".ts" files. You still can use ".mts" to force ESM in specific typescript files.' 31 | ) 32 | .option( 33 | '--paths', 34 | 'Use tsconfig paths resolver. It only works as a fallback of the default path resolving and you can use the environment variable TS_NODE_PROJECT to customize the tsconfig.json to use.' 35 | ) 36 | .allowUnknownOption() 37 | .argument('[node arguments...]'); 38 | 39 | program 40 | .parseAsync(process.argv) 41 | .then(async ({ args }) => { 42 | const options = program.opts<{ 43 | watch?: string[]; 44 | ignore?: string[]; 45 | cjs?: boolean; 46 | tsmconfig?: string; 47 | node_env: 'production' | 'prod' | 'development' | 'dev' | 'test'; 48 | quiet?: boolean; 49 | paths?: boolean; 50 | keepEsmLoader?: boolean; 51 | sourcemap?: boolean; 52 | restartOnFail?: boolean; 53 | }>(); 54 | 55 | const { watch, ignore, cjs, node_env, quiet, tsmconfig, paths, keepEsmLoader, sourcemap, restartOnFail } = options; 56 | 57 | const binDirname = dirname(fileURLToPath(import.meta.url)); 58 | 59 | const spawnArgs = [ 60 | '--require=' + join(binDirname, 'require.js'), 61 | '--loader=' + pathToFileURL(join(binDirname, 'loader.mjs')).href, 62 | ]; 63 | 64 | if (sourcemap) { 65 | spawnArgs.push('--enable-source-maps'); 66 | } 67 | 68 | if (tsmconfig && existsSync(tsmconfig)) { 69 | spawnArgs.push('--tsmconfig', tsmconfig); 70 | } 71 | 72 | if (quiet) { 73 | spawnArgs.push('--quiet'); 74 | } 75 | 76 | spawnArgs.push(...args); 77 | 78 | let execNodeLog = `$ node ${['--require=bob-tsm', '--loader=bob-tsm', ...spawnArgs.slice(2)].join(' ')}`; 79 | 80 | let spawnEnv: NodeJS.ProcessEnv | undefined; 81 | 82 | if (cjs) { 83 | Object.assign((spawnEnv ||= { ...process.env }), { FORCE_CJS: '1' }); 84 | } 85 | 86 | if (!sourcemap) { 87 | Object.assign((spawnEnv ||= { ...process.env }), { DISABLE_SOURCEMAP: '1' }); 88 | } 89 | 90 | if (node_env) { 91 | const NODE_ENV = node_env === 'prod' ? 'production' : node_env === 'dev' ? 'development' : node_env; 92 | Object.assign((spawnEnv ||= { ...process.env }), { 93 | NODE_ENV, 94 | }); 95 | 96 | execNodeLog = execNodeLog.replace('$ node', `$ NODE_ENV=${NODE_ENV} node`); 97 | } 98 | 99 | if (paths) { 100 | Object.assign((spawnEnv ||= { ...process.env }), { 101 | TSCONFIG_PATHS: '1', 102 | }); 103 | } 104 | 105 | if (keepEsmLoader) { 106 | Object.assign((spawnEnv ||= { ...process.env }), { 107 | KEEP_LOADER_ARGV: '1', 108 | }); 109 | } 110 | 111 | const spawnOptions: SpawnOptions = { 112 | stdio: 'inherit', 113 | env: spawnEnv, 114 | }; 115 | const spawnNode = () => { 116 | if (!quiet) console.log(execNodeLog); 117 | 118 | return spawn('node', spawnArgs, spawnOptions); 119 | }; 120 | 121 | if (watch) { 122 | const [chokidar, treeKill] = await Promise.all([ 123 | import('./deps/chokidar.js').then(v => getDefault(v.default)), 124 | import('./deps/treeKill.js').then(v => getDefault(v.default)), 125 | ]); 126 | 127 | const watcher = chokidar.watch(watch, { 128 | ignored: ignore, 129 | ignoreInitial: true, 130 | }); 131 | 132 | watcher.on('error', console.error); 133 | 134 | const nodeProcesses: ChildProcess[] = []; 135 | 136 | function killPromise(pid: number) { 137 | const pendingKillPromise = new Promise(resolve => { 138 | treeKill(pid, () => { 139 | resolve(); 140 | pendingKillPromises.delete(pendingKillPromise); 141 | }); 142 | }); 143 | 144 | pendingKillPromises.add(pendingKillPromise); 145 | } 146 | 147 | function cleanUp() { 148 | try { 149 | for (const nodeProcess of nodeProcesses) { 150 | if (nodeProcess?.pid) killPromise(nodeProcess.pid); 151 | } 152 | 153 | watcher.close(); 154 | } catch (err) { 155 | } finally { 156 | process.exit(0); 157 | } 158 | } 159 | 160 | process.on('SIGINT', cleanUp); 161 | process.on('SIGHUP', cleanUp); 162 | process.on('SIGQUIT', cleanUp); 163 | process.on('SIGTERM', cleanUp); 164 | process.on('uncaughtException', cleanUp); 165 | process.on('exit', cleanUp); 166 | 167 | const pendingKillPromises = new Set>(); 168 | 169 | async function execNode() { 170 | while (nodeProcesses.length) { 171 | const onSuccessProcess = nodeProcesses.shift(); 172 | if (onSuccessProcess?.pid != null) { 173 | killPromise(onSuccessProcess.pid); 174 | } 175 | } 176 | 177 | pendingKillPromises.size && (await Promise.all(pendingKillPromises)); 178 | 179 | const nodeProcess = spawnNode(); 180 | 181 | if (restartOnFail) { 182 | nodeProcess.once('exit', function onExit(code) { 183 | if (typeof code === 'number' && code !== 0) { 184 | debouncedExec(); 185 | } 186 | }); 187 | } 188 | 189 | nodeProcesses.push(nodeProcess); 190 | } 191 | 192 | const debouncedExec = debouncePromise( 193 | () => { 194 | return execNode(); 195 | }, 196 | 500, 197 | console.error 198 | ); 199 | 200 | watcher.on('change', path => { 201 | if (!quiet) console.log(`[bob-tsm] ${path} changed.`); 202 | debouncedExec(); 203 | }); 204 | 205 | execNode(); 206 | } else { 207 | function execNode() { 208 | spawnNode().once( 209 | 'exit', 210 | restartOnFail 211 | ? code => { 212 | if (typeof code === 'number' && code !== 0) { 213 | setTimeout(execNode, 500); 214 | } else { 215 | process.exit(code!); 216 | } 217 | } 218 | : process.exit 219 | ); 220 | } 221 | 222 | execNode(); 223 | } 224 | }) 225 | .catch(err => { 226 | console.error(err); 227 | process.exit(1); 228 | }); 229 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/config.ts: -------------------------------------------------------------------------------- 1 | import type { Loader, TransformOptions } from 'esbuild'; 2 | 3 | export type Extension = `.${string}`; 4 | export type Options = TransformOptions; 5 | 6 | export type Config = { 7 | [extn: Extension]: Options; 8 | }; 9 | 10 | export type ConfigFile = 11 | | { common?: Options; config?: Config; loaders?: never; [extn: Extension]: never } 12 | | { common?: Options; loaders?: Loaders; config?: never; [extn: Extension]: never } 13 | | { common?: Options; config?: never; loaders?: never; [extn: Extension]: Options }; 14 | 15 | export type Loaders = { 16 | [extn: Extension]: Loader; 17 | }; 18 | 19 | /** 20 | * TypeScript helper for writing `tsm.js` contents. 21 | */ 22 | export function define(contents: ConfigFile): ConfigFile { 23 | return contents; 24 | } 25 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/deps/chokidar.ts: -------------------------------------------------------------------------------- 1 | export * as default from 'chokidar'; 2 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/deps/commander.ts: -------------------------------------------------------------------------------- 1 | export * as default from 'commander'; 2 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/deps/treeKill.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'tree-kill'; 2 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/deps/typescriptPaths.ts: -------------------------------------------------------------------------------- 1 | export { createHandler } from 'typescript-paths'; 2 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/index.ts: -------------------------------------------------------------------------------- 1 | export declare const tsconfigPathsHandler: 2 | | ((request: string, importer: string) => string | undefined) 3 | | Promise<(request: string, importer: string) => string | undefined> 4 | | undefined; 5 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/loader.ts: -------------------------------------------------------------------------------- 1 | // CREDITS TO lukeed https://github.com/lukeed/tsm 2 | 3 | import { promises } from 'fs'; 4 | import { dirname, extname } from 'path'; 5 | import { fileURLToPath, pathToFileURL, URL } from 'url'; 6 | import type { Config, Extension, Options } from './config'; 7 | import { defaults, fileExists, finalize, nodeMajor, nodeMinor } from './utils'; 8 | 9 | if (!process.env.KEEP_LOADER_ARGV) { 10 | const loaderArgIndex = process.execArgv.findIndex(v => v.startsWith('--loader')); 11 | 12 | if (loaderArgIndex !== -1) process.execArgv.splice(loaderArgIndex, 1); 13 | } 14 | 15 | export const tsconfigPathsHandler = process.env.TSCONFIG_PATHS 16 | ? import('./deps/typescriptPaths.js').then(({ createHandler }) => createHandler()) 17 | : undefined; 18 | 19 | const HAS_UPDATED_HOOKS = nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 12); 20 | 21 | let config: Config; 22 | let esbuild: typeof import('esbuild'); 23 | 24 | const env = defaults('esm'); 25 | const setup = env.file && import('file:///' + env.file); 26 | 27 | type Promisable = Promise | T; 28 | type Source = string | SharedArrayBuffer | Uint8Array; 29 | type Format = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'; 30 | 31 | type Resolve = ( 32 | specifier: string, 33 | context: { 34 | conditions: string[]; 35 | parentURL?: string; 36 | }, 37 | fallback: Resolve 38 | ) => Promisable<{ url: string; format?: Format | null; shortCircuit: boolean }>; 39 | 40 | type Inspect = (url: string, context: object, fallback: Inspect) => Promisable<{ format: Format }>; 41 | 42 | type Transform = ( 43 | source: Source, 44 | context: Record<'url' | 'format', string>, 45 | fallback: Transform 46 | ) => Promisable<{ source: Source }>; 47 | 48 | type Load = ( 49 | url: string, 50 | context: { format: Format | null | undefined }, 51 | defaultLoad: Load 52 | ) => Promise<{ format: Format; source: Source; shortCircuit: boolean }>; 53 | 54 | async function getConfig(): Promise { 55 | let mod = await setup; 56 | mod = (mod && mod.default) || mod; 57 | return finalize(env, mod); 58 | } 59 | 60 | const EXTN = /\.\w+(?=\?|$)/; 61 | const isTS = /\.[mc]?tsx?(?=\?|$)/; 62 | const isJS = /\.([mc])?js$/; 63 | async function toOptions(uri: string): Promise { 64 | config = config || (await getConfig()); 65 | let [extn] = EXTN.exec(uri) || []; 66 | return config[extn as `.${string}`]; 67 | } 68 | 69 | async function check(fileurl: string): Promise { 70 | if (await fileExists(fileURLToPath(fileurl))) return fileurl; 71 | } 72 | 73 | const root = new URL('file:///' + process.cwd() + '/'); 74 | const rootPath = fileURLToPath(root); 75 | 76 | let scriptExtensions: `.${string}`[]; 77 | 78 | export const resolve: Resolve = async function (specifier, context, defaultResolve) { 79 | try { 80 | return await defaultResolve(specifier, context, defaultResolve); 81 | } catch (err: any) { 82 | if ('code' in err && err.code === 'ERR_MODULE_NOT_FOUND') { 83 | if (tsconfigPathsHandler) { 84 | try { 85 | const handlerTsconfigPaths = await tsconfigPathsHandler; 86 | 87 | const tsResolvedUrl = handlerTsconfigPaths?.( 88 | specifier, 89 | context.parentURL ? fileURLToPath(context.parentURL) : rootPath 90 | ); 91 | 92 | if (tsResolvedUrl) { 93 | return { 94 | url: pathToFileURL(tsResolvedUrl).href, 95 | shortCircuit: true, 96 | }; 97 | } 98 | } catch (err) {} 99 | } 100 | 101 | if (extname(specifier) === '') { 102 | config ||= await getConfig(); 103 | 104 | scriptExtensions ||= (Object.keys(config) as Array).filter(v => v !== '.json'); 105 | 106 | for (const ext of scriptExtensions) { 107 | try { 108 | return await defaultResolve(specifier + ext, context, defaultResolve); 109 | } catch (err) {} 110 | } 111 | } 112 | } 113 | } 114 | 115 | // ignore "prefix:", non-relative identifiers, and respect import maps 116 | if (/^\w+\:?/.test(specifier)) { 117 | return defaultResolve(specifier, context, defaultResolve); 118 | } 119 | 120 | let match: RegExpExecArray | null; 121 | let idx: number, ext: Extension, path: string | void; 122 | let output = new URL(specifier, context.parentURL || root); 123 | 124 | // source ident includes extension 125 | if ((match = EXTN.exec(output.href))) { 126 | ext = match[0] as Extension; 127 | if (!context.parentURL || isTS.test(ext)) { 128 | return { url: output.href, shortCircuit: true }; 129 | } 130 | // source ident exists 131 | path = await check(output.href); 132 | if (path) return { url: path, shortCircuit: true }; 133 | // parent importer is a ts file 134 | // source ident is js & NOT exists 135 | if (isJS.test(ext) && isTS.test(context.parentURL)) { 136 | // reconstruct ".js" -> ".ts" source file 137 | path = output.href.substring(0, (idx = match.index)); 138 | if ((path = await check(path + ext.replace('js', 'ts')))) { 139 | idx += ext.length; 140 | if (idx > output.href.length) { 141 | path += output.href.substring(idx); 142 | } 143 | return { url: path, shortCircuit: true }; 144 | } 145 | // return original, let it error 146 | return defaultResolve(specifier, context, defaultResolve); 147 | } 148 | } 149 | 150 | config ||= await getConfig(); 151 | 152 | scriptExtensions ||= (Object.keys(config) as Array).filter(v => v !== '.json'); 153 | 154 | for (ext of scriptExtensions) { 155 | path = await check(output.href + ext); 156 | if (path) return { url: path, shortCircuit: true }; 157 | } 158 | 159 | // Check if + "/index.{ts,tsx,mts,cts}" exists 160 | const trailingOutputHref = output.href.endsWith('/') ? output.href : output.href + '/'; 161 | for (ext of scriptExtensions) { 162 | path = await check(trailingOutputHref + 'index' + ext); 163 | if (path) return { url: path, shortCircuit: true }; 164 | } 165 | 166 | return defaultResolve(specifier, context, defaultResolve); 167 | }; 168 | 169 | export const getFormat: Inspect | undefined = HAS_UPDATED_HOOKS 170 | ? undefined 171 | : async function (uri, context, fallback) { 172 | let options = await toOptions(uri); 173 | if (options == null) return fallback(uri, context, fallback); 174 | 175 | if (uri.endsWith('.d.ts')) return { format: 'module' }; 176 | 177 | return { format: options.format === 'cjs' ? 'commonjs' : 'module' }; 178 | }; 179 | 180 | function getDirnames(url: string) { 181 | const filename = fileURLToPath(url); 182 | 183 | return { __dirname: JSON.stringify(dirname(filename)), __filename: JSON.stringify(filename) }; 184 | } 185 | 186 | export const load: Load = async function (url, context, defaultLoad) { 187 | let options = await toOptions(url); 188 | 189 | if (options == null) return defaultLoad(url, context, defaultLoad); 190 | 191 | if (url.endsWith('.d.ts')) return { format: 'module', source: '', shortCircuit: true }; 192 | 193 | const format = options.format === 'cjs' ? 'commonjs' : 'module'; 194 | 195 | const rawSource = await promises.readFile(new URL(url)); 196 | 197 | esbuild = esbuild || (await import('esbuild')); 198 | 199 | const isModule = format === 'module'; 200 | 201 | const { code: source } = await esbuild.transform(rawSource.toString(), { 202 | ...options, 203 | define: isModule ? { ...getDirnames(url), ...options.define } : options.define, 204 | sourcefile: url, 205 | format: isModule ? 'esm' : 'cjs', 206 | }); 207 | 208 | return { 209 | format, 210 | source, 211 | shortCircuit: true, 212 | }; 213 | }; 214 | 215 | export const transformSource: Transform | undefined = HAS_UPDATED_HOOKS 216 | ? undefined 217 | : async function (source, context, xform) { 218 | let options = await toOptions(context.url); 219 | if (options == null) return xform(source, context, xform); 220 | 221 | if (context.url.endsWith('.d.ts')) return { source: '' }; 222 | 223 | const isModule = context.format === 'module'; 224 | 225 | // TODO: decode SAB/U8 correctly 226 | esbuild = esbuild || (await import('esbuild')); 227 | let result = await esbuild.transform(source.toString(), { 228 | ...options, 229 | define: isModule ? { ...getDirnames(context.url), ...options.define } : options.define, 230 | sourcefile: context.url, 231 | format: isModule ? 'esm' : 'cjs', 232 | }); 233 | 234 | return { source: result.code }; 235 | }; 236 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/register.ts: -------------------------------------------------------------------------------- 1 | import { register } from 'node:module'; 2 | import { pathToFileURL } from 'node:url'; 3 | 4 | register('./loader.mjs', pathToFileURL(__filename)); 5 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/require.ts: -------------------------------------------------------------------------------- 1 | // CREDITS TO lukeed https://github.com/lukeed/tsm 2 | 3 | import type { TransformOptions } from 'esbuild'; 4 | import { readFileSync } from 'fs'; 5 | import { extname } from 'path'; 6 | import type { Config, Extension, Options } from './config'; 7 | import { defaults, finalize, nodeMajor, nodeMinor } from './utils'; 8 | 9 | export const tsconfigPathsHandler = process.env.TSCONFIG_PATHS 10 | ? (require('./deps/typescriptPaths.js') as typeof import('./deps/typescriptPaths.js')).createHandler() 11 | : undefined; 12 | 13 | type Module = NodeJS.Module & { 14 | _compile?(source: string, filename: string): typeof loader; 15 | }; 16 | 17 | const loadJS = require.extensions['.js']; 18 | 19 | let esbuild: typeof import('esbuild'); 20 | let env = defaults('cjs'); 21 | let uconf = env.file && require(env.file); 22 | let config: Config = finalize(env, uconf); 23 | 24 | declare const $$req: NodeJS.Require; 25 | declare const TSCONFIG_PATHS: string; 26 | 27 | const tsrequire = ( 28 | 'var $$req=require("module").createRequire(__filename);require=(' + 29 | function () { 30 | let { existsSync } = $$req('fs') as typeof import('fs'); 31 | let $url = $$req('url') as typeof import('url'); 32 | 33 | const PATHS_ROOT_REQUIRE = TSCONFIG_PATHS; 34 | 35 | return new Proxy(require, { 36 | // NOTE: only here if source is TS 37 | apply(req, ctx, args: [id: string]) { 38 | let [ident] = args; 39 | 40 | if (!ident) return req.apply(ctx || $$req, args); 41 | 42 | try { 43 | // ignore "prefix:" and non-relative identifiers 44 | if (/^\w+\:?/.test(ident)) return $$req(ident); 45 | 46 | // exit early if no extension provided 47 | let match = /\.([mc])?js(?=\?|$)/.exec(ident); 48 | if (match == null) return $$req(ident); 49 | 50 | let base = $url.pathToFileURL(__filename); 51 | let file = $url.fileURLToPath(new $url.URL(ident, base)); 52 | if (existsSync(file)) return $$req(ident); 53 | 54 | // ?js -> ?ts file 55 | file = file.replace(new RegExp(match[0] + '$'), match[0]!.replace('js', 'ts')); 56 | 57 | // return the new "[mc]ts" file, or let error 58 | return existsSync(file) ? $$req(file) : $$req(ident); 59 | } catch (err: any) { 60 | if (PATHS_ROOT_REQUIRE && 'code' in err && err.code === 'MODULE_NOT_FOUND') { 61 | const { tsconfigPathsHandler } = $$req(PATHS_ROOT_REQUIRE) as { 62 | tsconfigPathsHandler: (request: string, importer: string) => string | undefined; 63 | }; 64 | 65 | const tsconfigResolvedPath = tsconfigPathsHandler?.(ident, __filename); 66 | 67 | if (tsconfigResolvedPath) return $$req(tsconfigResolvedPath); 68 | } 69 | 70 | throw err; 71 | } 72 | }, 73 | }); 74 | } + 75 | ')();' 76 | ).replace('TSCONFIG_PATHS', tsconfigPathsHandler ? JSON.stringify(__filename) : '""'); 77 | 78 | function transform(source: string, options: Options): string { 79 | esbuild = esbuild || require('esbuild'); 80 | return esbuild.transformSync(source, options).code; 81 | } 82 | 83 | function loader(Module: Module, sourcefile: string) { 84 | let extn = extname(sourcefile) as Extension; 85 | let options = config[extn] || {}; 86 | let pitch = Module._compile!.bind(Module); 87 | 88 | let banner = options.banner; 89 | 90 | if (/\.[mc]?tsx?$/.test(extn)) { 91 | banner = tsrequire + (options.banner || ''); 92 | } 93 | 94 | const transformOptions: TransformOptions = { 95 | ...options, 96 | banner, 97 | sourcefile, 98 | }; 99 | 100 | if (config[extn] != null) { 101 | Module._compile = source => { 102 | let result = transform(source, transformOptions); 103 | return pitch(result, sourcefile); 104 | }; 105 | } 106 | 107 | try { 108 | return loadJS(Module, sourcefile); 109 | } catch (err) { 110 | let ec = err && (err as any).code; 111 | if (ec !== 'ERR_REQUIRE_ESM') throw err; 112 | 113 | let input = readFileSync(sourcefile, 'utf8'); 114 | let result = transform(input, { ...transformOptions, format: 'cjs' }); 115 | return pitch(result, sourcefile); 116 | } 117 | } 118 | 119 | for (let extn in config) { 120 | if (extn === '.json') continue; 121 | 122 | require.extensions[extn] = loader; 123 | } 124 | 125 | if (config['.js'] == null) { 126 | require.extensions['.js'] = loader; 127 | } 128 | 129 | if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 17)) { 130 | const prevEmitWarning = process.emitWarning; 131 | 132 | process.emitWarning = ((...args: Parameters) => { 133 | if (typeof args[0] === 'string' && args[0].startsWith('--experimental-loader is an experimental feature')) return; 134 | 135 | prevEmitWarning(...args); 136 | }) as typeof prevEmitWarning; 137 | } else { 138 | const prevConsoleError = console.error; 139 | 140 | console.error = (...args) => { 141 | if (typeof args[0] === 'string' && args[0].includes('Custom ESM Loaders is an experimental feature')) return; 142 | prevConsoleError(...args); 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /packages/bob-tsm/src/utils.ts: -------------------------------------------------------------------------------- 1 | // CREDITS TO lukeed https://github.com/lukeed/tsm 2 | 3 | import type { Format } from 'esbuild'; 4 | import { existsSync, PathLike, promises } from 'fs'; 5 | import { resolve } from 'path'; 6 | import type { Config, ConfigFile, Extension, Options } from './config'; 7 | 8 | export interface Defaults { 9 | file: string | false; 10 | isESM: boolean; 11 | options: Options; 12 | } 13 | 14 | export function fileExists(url: PathLike) { 15 | return promises.access(url).then( 16 | () => true, 17 | () => false 18 | ); 19 | } 20 | 21 | export const defaults = function (format: Format): Defaults { 22 | let { FORCE_COLOR, NO_COLOR, NODE_DISABLE_COLORS, TERM } = process.env; 23 | 24 | let argv = process.argv.slice(2); 25 | 26 | let flags = new Set(argv); 27 | let isQuiet = flags.has('-q') || flags.has('--quiet'); 28 | 29 | // @see lukeed/kleur 30 | let enabled = 31 | !NODE_DISABLE_COLORS && 32 | NO_COLOR == null && 33 | TERM !== 'dumb' && 34 | ((FORCE_COLOR != null && FORCE_COLOR !== '0') || process.stdout.isTTY); 35 | 36 | let idx = flags.has('--tsmconfig') ? argv.indexOf('--tsmconfig') : -1; 37 | let file = resolve('.', (!!~idx && argv[++idx]) || 'tsm.js'); 38 | 39 | if (process.env.FORCE_CJS) format = 'cjs'; 40 | 41 | return { 42 | file: existsSync(file) && file, 43 | isESM: format === 'esm', 44 | options: { 45 | format, 46 | charset: 'utf8', 47 | sourcemap: process.env.DISABLE_SOURCEMAP ? false : 'inline', 48 | target: 'node' + process.versions.node, 49 | logLevel: isQuiet ? 'silent' : 'warning', 50 | color: enabled, 51 | }, 52 | }; 53 | }; 54 | 55 | export const finalize = function (env: Defaults, custom?: ConfigFile): Config { 56 | let base = env.options; 57 | if (custom && custom.common) { 58 | Object.assign(base, custom.common!); 59 | delete custom.common; // loop below 60 | } 61 | 62 | let config: Config = { 63 | '.ts': { ...base, loader: 'ts' }, 64 | '.mts': { ...base, format: 'esm', loader: 'ts' }, 65 | '.jsx': { ...base, loader: 'jsx' }, 66 | '.tsx': { ...base, loader: 'tsx' }, 67 | '.cts': { ...base, format: 'cjs', loader: 'ts' }, 68 | '.json': { ...base, loader: 'json' }, 69 | }; 70 | 71 | if (!env.isESM) { 72 | config['.mjs'] = { ...base, format: 'esm', loader: 'js' }; 73 | } 74 | 75 | let extn: Extension; 76 | if (custom && custom.loaders) { 77 | for (extn in custom.loaders) 78 | config[extn] = { 79 | ...base, 80 | loader: custom.loaders[extn], 81 | }; 82 | } else if (custom) { 83 | let conf = (custom.config || custom) as Config; 84 | for (extn in conf) config[extn] = { ...base, ...conf[extn] }; 85 | } 86 | 87 | return config; 88 | }; 89 | 90 | export function debouncePromise( 91 | fn: (...args: T) => Promise, 92 | delay: number, 93 | onError: (err: unknown) => void 94 | ) { 95 | let timeout: ReturnType | undefined; 96 | 97 | let promiseInFly: Promise | undefined; 98 | 99 | let callbackPending: (() => void) | undefined; 100 | 101 | return function debounced(...args: Parameters) { 102 | if (promiseInFly) { 103 | callbackPending = () => { 104 | debounced(...args); 105 | callbackPending = undefined; 106 | }; 107 | } else { 108 | if (timeout != null) clearTimeout(timeout); 109 | 110 | timeout = setTimeout(() => { 111 | timeout = undefined; 112 | promiseInFly = fn(...args) 113 | .catch(onError) 114 | .finally(() => { 115 | promiseInFly = undefined; 116 | if (callbackPending) callbackPending(); 117 | }); 118 | }, delay); 119 | } 120 | }; 121 | } 122 | 123 | export function getDefault(module: T & { default?: T }): T { 124 | return module.default || module; 125 | } 126 | 127 | export const [nodeMajor, nodeMinor, nodePatch] = process.version 128 | .slice(1) 129 | .split('.') 130 | .map(v => parseInt(v, 10)) as [major: number, minor: number, patch: number]; 131 | -------------------------------------------------------------------------------- /packages/bob-tsm/test/cts.cts: -------------------------------------------------------------------------------- 1 | import 'fs'; 2 | 3 | console.log('CTS', typeof require === 'undefined' ? 'ESM' : 'CJS'); 4 | -------------------------------------------------------------------------------- /packages/bob-tsm/test/index/index.ts: -------------------------------------------------------------------------------- 1 | export const Hello: string = 'Hello World!'; 2 | 3 | console.log(Hello); 4 | -------------------------------------------------------------------------------- /packages/bob-tsm/test/mts.mts: -------------------------------------------------------------------------------- 1 | import 'fs'; 2 | 3 | console.log('MTS', typeof require === 'undefined' ? 'ESM' : 'CJS'); 4 | -------------------------------------------------------------------------------- /packages/bob-tsm/test/test.cjs: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { execa } = require('execa'); 3 | const { strictEqual } = require('assert'); 4 | 5 | async function test() { 6 | const tsFilePath = join(__dirname, 'ts.ts'); 7 | 8 | const binFilePath = join(__dirname, '../lib/bin.mjs'); 9 | 10 | { 11 | const { stdout } = await execa('node', [binFilePath, tsFilePath]); 12 | 13 | const lines = stdout.split(/\n|\r\n/g); 14 | 15 | strictEqual(lines.length, 5); 16 | strictEqual(lines[0], '$ node --require=bob-tsm --loader=bob-tsm --enable-source-maps ' + tsFilePath); 17 | strictEqual(lines[1], 'Hello World!'); 18 | strictEqual(lines[2], 'CTS CJS'); 19 | strictEqual(lines[3], 'TS ESM'); 20 | strictEqual(lines[4], 'MTS ESM'); 21 | } 22 | 23 | { 24 | const { stdout } = await execa('node', [binFilePath, '--cjs', tsFilePath]); 25 | 26 | const lines = stdout.split(/\n|\r\n/g); 27 | 28 | strictEqual(lines.length, 5); 29 | strictEqual(lines[0], '$ node --require=bob-tsm --loader=bob-tsm --enable-source-maps ' + tsFilePath); 30 | strictEqual(lines[1], 'Hello World!'); 31 | strictEqual(lines[2], 'CTS CJS'); 32 | strictEqual(lines[3], 'TS CJS'); 33 | strictEqual(lines[4], 'MTS ESM'); 34 | } 35 | } 36 | 37 | test() 38 | .then(() => { 39 | process.exit(0); 40 | }) 41 | .catch(err => { 42 | console.error(err); 43 | process.exit(1); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/bob-tsm/test/ts.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | 3 | import './index'; 4 | import './index/'; 5 | 6 | import 'fs'; 7 | import('./mts').catch(err => { 8 | console.error(err); 9 | process.exit(1); 10 | }); 11 | import './cts'; 12 | 13 | console.log('TS', typeof require === 'undefined' ? 'ESM' : 'CJS'); 14 | -------------------------------------------------------------------------------- /packages/bob-tsm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "incremental": false 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": ["lib"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/bob-watch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # bob-watch 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - c99d3d4: Fix commander dependency 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - 07473c9: Improved released package 14 | 15 | ## 0.1.0 16 | 17 | ### Minor Changes 18 | 19 | - 2587fa2: Release bob-watch 20 | -------------------------------------------------------------------------------- /packages/bob-watch/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Pablo Sáez 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/bob-watch/README.md: -------------------------------------------------------------------------------- 1 | # bob-watch 2 | 3 | [![npm](https://img.shields.io/npm/v/bob-watch)](https://npm.im/bob-watch) 4 | 5 | Execute commands on start and after every file change in specified directories/files patterns, powered by [Chokidar](https://github.com/paulmillr/chokidar). 6 | 7 | The previously specified commands processes are killed before the commands are re-executed, specially useful for APIs. 8 | 9 | ## Install 10 | 11 | ```sh 12 | pnpm add -D bob-watch 13 | ``` 14 | 15 | ```sh 16 | yarn add -D bob-watch 17 | ``` 18 | 19 | ```sh 20 | npm install -D bob-watch 21 | ``` 22 | 23 | ## Usage 24 | 25 | ``` 26 | Usage: bob-watch [options] 27 | 28 | Options: 29 | -V, --version output the version number 30 | -c, --command Commands to be executed on start and on every change 31 | -w, --watch Patterns of directories or files to be watched 32 | -i, --ignore Ignore watch patterns 33 | --quiet Prevent non-error logs (default: false) 34 | -h, --help display help for command 35 | ``` 36 | 37 | ### Example 38 | 39 | ```json 40 | { 41 | "scripts": { 42 | "dev": "bob-watch -w src -c \"bob-tsm src/index.ts\"" 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /packages/bob-watch/bin/bob-watch.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | import '../lib/bin.mjs'; 5 | -------------------------------------------------------------------------------- /packages/bob-watch/build.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | import { promises } from 'fs'; 3 | import { buildCode } from '../bob-ts/src/build'; 4 | import { writePackageJson } from '../bob/src/config/packageJson'; 5 | import { buildTsc } from '../bob/src/index'; 6 | import pkg from './package.json'; 7 | 8 | async function main() { 9 | await promises.rm('lib', { 10 | force: true, 11 | recursive: true, 12 | }); 13 | await promises.mkdir('lib'); 14 | await Promise.all([ 15 | buildCode({ 16 | entryPoints: ['./src/index.ts'], 17 | clean: false, 18 | format: 'interop', 19 | outDir: 'lib', 20 | target: 'node12.20', 21 | esbuild: { 22 | define: { 23 | VERSION: JSON.stringify(pkg.version), 24 | }, 25 | minify: false, 26 | }, 27 | sourcemap: false, 28 | external: ['./deps.js'], 29 | }), 30 | buildCode({ 31 | entryPoints: ['./src/bin.ts'], 32 | clean: false, 33 | format: 'esm', 34 | outDir: 'lib', 35 | target: 'node12.20', 36 | esbuild: { 37 | define: { 38 | VERSION: JSON.stringify(pkg.version), 39 | }, 40 | minify: false, 41 | }, 42 | sourcemap: false, 43 | external: ['./deps.js'], 44 | rollup: { 45 | banner: '#!/usr/bin/env node\n', 46 | }, 47 | }), 48 | build({ 49 | bundle: true, 50 | format: 'cjs', 51 | target: 'node12.20', 52 | entryPoints: ['src/deps.ts'], 53 | outdir: 'lib', 54 | platform: 'node', 55 | minify: true, 56 | external: ['fsevents'], 57 | }), 58 | buildTsc(), 59 | writePackageJson({ 60 | distDir: 'lib', 61 | packageJson: { 62 | ...pkg, 63 | bin: { 64 | 'bob-watch': './bin.mjs', 65 | }, 66 | }, 67 | }), 68 | promises.copyFile('README.md', 'lib/README.md'), 69 | promises.copyFile('LICENSE', 'lib/LICENSE'), 70 | ]); 71 | } 72 | 73 | main().catch(err => { 74 | console.error(err); 75 | process.exit(1); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/bob-watch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bob-watch", 3 | "version": "0.1.2", 4 | "homepage": "https://github.com/PabloSzx/bob-esbuild", 5 | "bugs": "https://github.com/PabloSzx/bob-esbuild/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/PabloSzx/bob-esbuild", 9 | "directory": "packages/bob-watch" 10 | }, 11 | "license": "MIT", 12 | "author": "PabloSzx ", 13 | "sideEffects": false, 14 | "exports": { 15 | ".": { 16 | "require": "./lib/index.js", 17 | "import": "./lib/index.mjs", 18 | "types": "./lib/index.d.ts" 19 | }, 20 | "./*": { 21 | "require": "./lib/*.js", 22 | "import": "./lib/*.mjs", 23 | "types": "./lib/*.d.ts" 24 | }, 25 | "./package.json": "./package.json" 26 | }, 27 | "main": "lib/index.js", 28 | "module": "lib/index.mjs", 29 | "types": "lib/index.d.ts", 30 | "bin": { 31 | "bob-watch": "./bin/bob-watch.mjs" 32 | }, 33 | "files": [ 34 | "/lib", 35 | "/bin" 36 | ], 37 | "scripts": { 38 | "prepare": "bob-tsm build.ts", 39 | "postpublish": "gh-release" 40 | }, 41 | "devDependencies": { 42 | "bob-esbuild": "workspace:^4.0.3", 43 | "bob-esbuild-cli": "workspace:^4.0.0", 44 | "bob-tsm": "workspace:^1.1.0", 45 | "chokidar": "^3.5.3", 46 | "commander": "^11.1.0", 47 | "execa": "^6.1.0", 48 | "tree-kill": "^1.2.2" 49 | }, 50 | "optionalDependencies": { 51 | "fsevents": "~2.3.3" 52 | }, 53 | "publishConfig": { 54 | "access": "public", 55 | "directory": "lib", 56 | "linkDirectory": false 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/bob-watch/src/bin.ts: -------------------------------------------------------------------------------- 1 | import { program, StartWatcher } from './index'; 2 | 3 | declare const VERSION: string; 4 | 5 | program 6 | .version(VERSION) 7 | .requiredOption('-c, --command ', 'Commands to be executed on start and on every change') 8 | .requiredOption('-w, --watch ', 'Patterns of directories or files to be watched') 9 | .option('-i, --ignore ', 'Ignore watch patterns') 10 | .option('--quiet', 'Prevent non-error logs', false); 11 | 12 | const { watch, command, ignore, quiet } = program.parse(process.argv).opts<{ 13 | watch: string[]; 14 | command: string[]; 15 | ignore?: string[]; 16 | quiet?: boolean; 17 | }>(); 18 | 19 | StartWatcher({ 20 | paths: watch, 21 | commands: command, 22 | ignored: ignore, 23 | quiet, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/bob-watch/src/deps.ts: -------------------------------------------------------------------------------- 1 | export { watch } from 'chokidar'; 2 | 3 | export { default as treeKill } from 'tree-kill'; 4 | 5 | export { execaCommand as command } from 'execa'; 6 | 7 | export { program } from 'commander'; 8 | -------------------------------------------------------------------------------- /packages/bob-watch/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'child_process'; 2 | import type { FSWatcher } from 'chokidar'; 3 | import * as deps from './deps.js'; 4 | import type { WatchOptions } from './types'; 5 | import { debouncePromise } from './utils'; 6 | 7 | const { treeKill, command, watch } = deps; 8 | 9 | export const { program } = deps; 10 | 11 | export function StartWatcher({ 12 | paths, 13 | ignored, 14 | onError = console.error, 15 | commands, 16 | callbacks, 17 | chokidarOptions, 18 | quiet, 19 | }: { 20 | paths: string[]; 21 | ignored?: string[]; 22 | commands?: string[]; 23 | callbacks?: Array<() => void>; 24 | quiet?: boolean; 25 | /** 26 | * @default console.error 27 | */ 28 | onError?: (err: unknown) => void; 29 | chokidarOptions?: WatchOptions; 30 | }) { 31 | const watcher: FSWatcher = watch(paths, { 32 | ignored, 33 | ignoreInitial: true, 34 | ...chokidarOptions, 35 | }); 36 | 37 | watcher.on('error', onError); 38 | 39 | const commandProcesses: ChildProcess[] = []; 40 | 41 | const pendingKillPromises = new Set>(); 42 | 43 | function killPromise(pid: number) { 44 | const pendingKillPromise = new Promise(resolve => { 45 | treeKill(pid, () => { 46 | resolve(); 47 | pendingKillPromises.delete(pendingKillPromise); 48 | }); 49 | }); 50 | 51 | pendingKillPromises.add(pendingKillPromise); 52 | } 53 | 54 | function cleanUp() { 55 | try { 56 | for (const cmdProcess of commandProcesses) { 57 | if (cmdProcess?.pid) killPromise(cmdProcess.pid); 58 | } 59 | 60 | watcher.close(); 61 | } catch (err) { 62 | } finally { 63 | process.exit(0); 64 | } 65 | } 66 | 67 | process.on('SIGINT', cleanUp); 68 | process.on('SIGHUP', cleanUp); 69 | process.on('SIGQUIT', cleanUp); 70 | process.on('SIGTERM', cleanUp); 71 | process.on('uncaughtException', cleanUp); 72 | process.on('exit', cleanUp); 73 | 74 | async function exec() { 75 | while (commandProcesses.length) { 76 | const cmdProcess = commandProcesses.shift(); 77 | if (cmdProcess?.pid != null) { 78 | killPromise(cmdProcess.pid); 79 | } 80 | } 81 | 82 | pendingKillPromises.size && (await Promise.all(pendingKillPromises)); 83 | 84 | await Promise.allSettled([ 85 | ...(callbacks?.map(cb => cb()) || []), 86 | ...(commands?.map(async cmd => { 87 | if (!quiet) console.log(`$ ${cmd}`); 88 | commandProcesses.push( 89 | command(cmd, { 90 | stdio: 'inherit', 91 | shell: true, 92 | }) 93 | ); 94 | }) || []), 95 | ]); 96 | } 97 | 98 | const debouncedExec = debouncePromise( 99 | () => { 100 | return exec(); 101 | }, 102 | 500, 103 | console.error 104 | ); 105 | 106 | watcher.on('change', path => { 107 | if (!quiet) console.log(`${path} changed.`); 108 | debouncedExec(); 109 | }); 110 | 111 | exec(); 112 | 113 | return { 114 | watcher, 115 | pendingKillPromises, 116 | commandProcesses, 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /packages/bob-watch/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface WatchOptions { 2 | /** 3 | * Indicates whether the process should continue to run as long as files are being watched. If 4 | * set to `false` when using `fsevents` to watch, no more events will be emitted after `ready`, 5 | * even if the process continues to run. 6 | */ 7 | persistent?: boolean; 8 | 9 | /** 10 | * ([anymatch](https://github.com/micromatch/anymatch)-compatible definition) Defines files/paths to 11 | * be ignored. The whole relative or absolute path is tested, not just filename. If a function 12 | * with two arguments is provided, it gets called twice per path - once with a single argument 13 | * (the path), second time with two arguments (the path and the 14 | * [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object of that path). 15 | */ 16 | ignored?: any; 17 | 18 | /** 19 | * If set to `false` then `add`/`addDir` events are also emitted for matching paths while 20 | * instantiating the watching as chokidar discovers these file paths (before the `ready` event). 21 | */ 22 | ignoreInitial?: boolean; 23 | 24 | /** 25 | * When `false`, only the symlinks themselves will be watched for changes instead of following 26 | * the link references and bubbling events through the link's path. 27 | */ 28 | followSymlinks?: boolean; 29 | 30 | /** 31 | * The base directory from which watch `paths` are to be derived. Paths emitted with events will 32 | * be relative to this. 33 | */ 34 | cwd?: string; 35 | 36 | /** 37 | * If set to true then the strings passed to .watch() and .add() are treated as literal path 38 | * names, even if they look like globs. Default: false. 39 | */ 40 | disableGlobbing?: boolean; 41 | 42 | /** 43 | * Whether to use fs.watchFile (backed by polling), or fs.watch. If polling leads to high CPU 44 | * utilization, consider setting this to `false`. It is typically necessary to **set this to 45 | * `true` to successfully watch files over a network**, and it may be necessary to successfully 46 | * watch files in other non-standard situations. Setting to `true` explicitly on OS X overrides 47 | * the `useFsEvents` default. 48 | */ 49 | usePolling?: boolean; 50 | 51 | /** 52 | * Whether to use the `fsevents` watching interface if available. When set to `true` explicitly 53 | * and `fsevents` is available this supercedes the `usePolling` setting. When set to `false` on 54 | * OS X, `usePolling: true` becomes the default. 55 | */ 56 | useFsEvents?: boolean; 57 | 58 | /** 59 | * If relying upon the [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object that 60 | * may get passed with `add`, `addDir`, and `change` events, set this to `true` to ensure it is 61 | * provided even in cases where it wasn't already available from the underlying watch events. 62 | */ 63 | alwaysStat?: boolean; 64 | 65 | /** 66 | * If set, limits how many levels of subdirectories will be traversed. 67 | */ 68 | depth?: number; 69 | 70 | /** 71 | * Interval of file system polling. 72 | */ 73 | interval?: number; 74 | 75 | /** 76 | * Interval of file system polling for binary files. ([see list of binary extensions](https://gi 77 | * thub.com/sindresorhus/binary-extensions/blob/master/binary-extensions.json)) 78 | */ 79 | binaryInterval?: number; 80 | 81 | /** 82 | * Indicates whether to watch files that don't have read permissions if possible. If watching 83 | * fails due to `EPERM` or `EACCES` with this set to `true`, the errors will be suppressed 84 | * silently. 85 | */ 86 | ignorePermissionErrors?: boolean; 87 | 88 | /** 89 | * `true` if `useFsEvents` and `usePolling` are `false`). Automatically filters out artifacts 90 | * that occur when using editors that use "atomic writes" instead of writing directly to the 91 | * source file. If a file is re-added within 100 ms of being deleted, Chokidar emits a `change` 92 | * event rather than `unlink` then `add`. If the default of 100 ms does not work well for you, 93 | * you can override it by setting `atomic` to a custom value, in milliseconds. 94 | */ 95 | atomic?: boolean | number; 96 | 97 | /** 98 | * can be set to an object in order to adjust timing params: 99 | */ 100 | awaitWriteFinish?: AwaitWriteFinishOptions | boolean; 101 | } 102 | 103 | export interface AwaitWriteFinishOptions { 104 | /** 105 | * Amount of time in milliseconds for a file size to remain constant before emitting its event. 106 | */ 107 | stabilityThreshold?: number; 108 | 109 | /** 110 | * File size polling interval. 111 | */ 112 | pollInterval?: number; 113 | } 114 | -------------------------------------------------------------------------------- /packages/bob-watch/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function debouncePromise( 2 | fn: (...args: T) => Promise, 3 | delay: number, 4 | onError: (err: unknown) => void 5 | ) { 6 | let timeout: ReturnType | undefined; 7 | 8 | let promiseInFly: Promise | undefined; 9 | 10 | let callbackPending: (() => void) | undefined; 11 | 12 | return function debounced(...args: Parameters) { 13 | if (promiseInFly) { 14 | callbackPending = () => { 15 | debounced(...args); 16 | callbackPending = undefined; 17 | }; 18 | } else { 19 | if (timeout != null) clearTimeout(timeout); 20 | 21 | timeout = setTimeout(() => { 22 | timeout = undefined; 23 | promiseInFly = fn(...args) 24 | .catch(onError) 25 | .finally(() => { 26 | promiseInFly = undefined; 27 | if (callbackPending) callbackPending(); 28 | }); 29 | }, delay); 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/bob/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # bob-esbuild 2 | 3 | ## 4.0.3 4 | 5 | ### Patch Changes 6 | 7 | - 40bae3b: Put exports.types first as required by typescript 8 | 9 | ## 4.0.2 10 | 11 | ### Patch Changes 12 | 13 | - 765968e: Don't add "types" field if it doesn't already exists on package.json 14 | 15 | ## 4.0.1 16 | 17 | ### Patch Changes 18 | 19 | - 22bb51b: Re-export writePackageJson and rewritePackageJson from index 20 | - caf3cac: exports["."] is not required for rewrite package json 21 | 22 | ## 4.0.0 23 | 24 | ### Major Changes 25 | 26 | - 9fb97f8: Require Node.js >=14.13.1 27 | 28 | ### Patch Changes 29 | 30 | - Updated dependencies [9fb97f8] 31 | - bob-esbuild-plugin@4.0.0 32 | 33 | ## 3.2.6 34 | 35 | ### Patch Changes 36 | 37 | - ee099eb: Update dependencies 38 | - Updated dependencies [ee099eb] 39 | - bob-esbuild-plugin@3.1.5 40 | 41 | ## 3.2.5 42 | 43 | ### Patch Changes 44 | 45 | - 4889dc1: Bump 46 | - Updated dependencies [4889dc1] 47 | - bob-esbuild-plugin@3.1.4 48 | 49 | ## 3.2.4 50 | 51 | ### Patch Changes 52 | 53 | - edbb808: fix package.json rewrite & "type":"module" usage 54 | - 5d2289b: Update bundled globby version to v13.1.1 55 | 56 | ## 3.2.3 57 | 58 | ### Patch Changes 59 | 60 | - 0696aae: allow to rewrite package.json before writeFile with config "rewritePackage" & improve PackageJSON type 61 | - ab336b9: Keep "peerDependenciesMeta" on package.json rewrite 62 | 63 | ## 3.2.2 64 | 65 | ### Patch Changes 66 | 67 | - bd58512: Use typescript peer dependency 68 | - ebf0d1c: Bump recommended esbuild version 69 | - Updated dependencies [ebf0d1c] 70 | - bob-esbuild-plugin@3.1.3 71 | 72 | ## 3.2.1 73 | 74 | ### Patch Changes 75 | 76 | - 54baca4: Fix esbuild peer dependency range 77 | - Updated dependencies [54baca4] 78 | - bob-esbuild-plugin@3.1.2 79 | 80 | ## 3.2.0 81 | 82 | ### Minor Changes 83 | 84 | - a4a848d: Support JSON imports out-of-the-box 85 | 86 | ### Patch Changes 87 | 88 | - 75c77c6: Update & Require esbuild>=13.14 89 | - Updated dependencies [75c77c6] 90 | - bob-esbuild-plugin@3.1.1 91 | 92 | ## 3.1.1 93 | 94 | ### Patch Changes 95 | 96 | - 2939cca: watch mode always logs errors 97 | 98 | ## 3.1.0 99 | 100 | ### Minor Changes 101 | 102 | - 1c3c506: Bundle dependencies 103 | 104 | ### Patch Changes 105 | 106 | - 17d1680: new outputOptions config 107 | - b89ee69: Add external and globby options 108 | - Updated dependencies [078402e] 109 | - bob-esbuild-plugin@3.1.0 110 | 111 | ## 3.0.2 112 | 113 | ### Patch Changes 114 | 115 | - f6106d8: Support rewrite exports types 116 | 117 | ## 3.0.1 118 | 119 | ### Patch Changes 120 | 121 | - 99f5fe6: Improve plugin promise resolution 122 | 123 | ## 3.0.0 124 | 125 | ### Major Changes 126 | 127 | - c783b22: Only copy typescript definitions on currently building project 128 | 129 | ### Patch Changes 130 | 131 | - afd2edb: add skipValidate option 132 | - 0ecbd1c: Fix packageConfigs defaults 133 | - a695127: Fix circular import 134 | - b8a13ba: New "useTsconfigPaths" global config that resolves root tsconfig "paths" 135 | 136 | Closes #156 137 | 138 | ## 2.2.0 139 | 140 | ### Minor Changes 141 | 142 | - f667154: support "type": "module", flexible package.json validation, improved per-package config, manual package.json rewrite, remove sucrase dep 143 | 144 | ### Patch Changes 145 | 146 | - f27ff9a: Fix bin output ESM or CJS based on extension 147 | - Updated dependencies [816d97e] 148 | - bob-esbuild-plugin@2.2.0 149 | 150 | ## 2.1.1 151 | 152 | ### Patch Changes 153 | 154 | - b5eac61: Dynamic rollup import 155 | 156 | closes #159 157 | 158 | ## 2.1.0 159 | 160 | ### Minor Changes 161 | 162 | - 19b48c5: Improve tsconfig resolution using [tsconfck](https://github.com/dominikg/tsconfck) 163 | 164 | ### Patch Changes 165 | 166 | - Updated dependencies [19b48c5] 167 | - bob-esbuild-plugin@2.1.0 168 | 169 | ## 2.0.1 170 | 171 | ### Patch Changes 172 | 173 | - 9e061f9: allow function in keepDynamicImport config 174 | 175 | ## 2.0.0 176 | 177 | ### Major Changes 178 | 179 | - 7678c9a: esbuild as peer dependency 180 | 181 | ### Patch Changes 182 | 183 | - Updated dependencies [7678c9a] 184 | - bob-esbuild-plugin@2.0.0 185 | 186 | ## 1.3.0 187 | 188 | ### Minor Changes 189 | 190 | - 698d845: Improve package.json exports rewrite 191 | 192 | ## 1.2.3 193 | 194 | ### Patch Changes 195 | 196 | - 20d5f3a: core package.json fields first in rewrite 197 | - 59350b2: allow copy to dist from parent dirs 198 | 199 | ## 1.2.2 200 | 201 | ### Patch Changes 202 | 203 | - 753cb22: add "package.json" to exports 204 | 205 | ## 1.2.1 206 | 207 | ### Patch Changes 208 | 209 | - 2a11cf2: gen package.json exports after name/main 210 | 211 | ## 1.2.0 212 | 213 | ### Minor Changes 214 | 215 | - 01ee72c: improve generate package.json logic 216 | - 6ecf937: add "singleBuild" config for non-monorepo usage 217 | 218 | ## 1.1.0 219 | 220 | ### Minor Changes 221 | 222 | - b43bb3a: add "keepDynamicImport" config to allow importing ESM from CommonJS 223 | 224 | ## 1.0.0 225 | 226 | ### Major Changes 227 | 228 | - b8df666: set bob-esbuild as peer dependency 229 | 230 | ### Minor Changes 231 | 232 | - b989382: allow no tsc target directories 233 | 234 | ### Patch Changes 235 | 236 | - 46b9292: keep custom package.json exports with dist translated 237 | - Updated dependencies [b8df666] 238 | - bob-esbuild-plugin@1.0.0 239 | 240 | ## 0.2.4 241 | 242 | ### Patch Changes 243 | 244 | - 582b21f: fix bundle target 245 | - Updated dependencies [582b21f] 246 | - bob-esbuild-plugin@0.2.4 247 | 248 | ## 0.2.3 249 | 250 | ### Patch Changes 251 | 252 | - 8e3f251: sourcemap disabled by default 253 | - Updated dependencies [8e3f251] 254 | - bob-esbuild-plugin@0.2.3 255 | 256 | ## 0.2.2 257 | 258 | ### Patch Changes 259 | 260 | - 5a92561: fix bin build 261 | 262 | ## 0.2.1 263 | 264 | ### Patch Changes 265 | 266 | - 8f52656: rmSync -> unlinkSync 267 | - 91772d6: rollback globby to v11 268 | 269 | ## 0.2.0 270 | 271 | ### Minor Changes 272 | 273 | - 1d3375e: update deps 274 | 275 | ### Patch Changes 276 | 277 | - Updated dependencies [7dadf1b] 278 | - Updated dependencies [1d3375e] 279 | - bob-esbuild-plugin@0.2.0 280 | 281 | ## 0.1.27 282 | 283 | ### Patch Changes 284 | 285 | - dd3dd9d: fix concurrent tsc hash write log errors 286 | 287 | ## 0.1.26 288 | 289 | ### Patch Changes 290 | 291 | - 7afb215: remove hash on tsc error 292 | - a0aaffa: add support for "buildConfig.input" 293 | 294 | ## 0.1.25 295 | 296 | ### Patch Changes 297 | 298 | - b96c19b: (tsc) support custom tsconfig path 299 | 300 | ## 0.1.24 301 | 302 | ### Patch Changes 303 | 304 | - 3272150: add warn logging 305 | 306 | ## 0.1.23 307 | 308 | ### Patch Changes 309 | 310 | - 1556971: allow skipTsc in watch mode 311 | - 0aabb50: add skipAutoTSCBuild config 312 | - b4ee29a: allow only build cjs/esm 313 | - d3806f2: rewrite package.json exports only if specified 314 | 315 | ## 0.1.22 316 | 317 | ### Patch Changes 318 | 319 | - fe24982: allow skip tsc build && default clean false on watch 320 | 321 | ## 0.1.21 322 | 323 | ### Patch Changes 324 | 325 | - c9d38ba: fix package.json rewrite w/ workspace protocol & sync packages 326 | - Updated dependencies [c9d38ba] 327 | - bob-esbuild-plugin@0.1.21 328 | 329 | ## 0.1.20 330 | 331 | ### Patch Changes 332 | 333 | - 312fa02: skip package.json rewrite in subdir while validating files property 334 | 335 | ## 0.1.19 336 | 337 | ### Patch Changes 338 | 339 | - a47e85c: fix gen package json with empty dir folder 340 | - 63bcf77: allow package without main module 341 | 342 | ## 0.1.18 343 | 344 | ### Patch Changes 345 | 346 | - 26f57cb: pnpm with publishConfig.directory 347 | - Updated dependencies [26f57cb] 348 | - bob-esbuild-plugin@0.1.18 349 | 350 | ## 0.1.17 351 | 352 | ### Patch Changes 353 | 354 | - f0c7788: allow not use esbuild plugin 355 | - 0092814: add buildOptions.bin support 356 | - 25ae121: allow tsc hash customization 357 | - ed7a61c: rename rollupoptions & add support for package.json buildConfig with copy dist 358 | - faf71da: sync 359 | - Updated dependencies [0092814] 360 | - Updated dependencies [faf71da] 361 | - bob-esbuild-plugin@0.1.17 362 | 363 | ## 0.1.14 364 | 365 | ### Patch Changes 366 | 367 | - 4ba5263: rewrite package.json for publish & allow custom dir 368 | 369 | ## 0.1.13 370 | 371 | ### Patch Changes 372 | 373 | - e28bf30: include tsx files by default 374 | 375 | ## 0.1.12 376 | 377 | ### Patch Changes 378 | 379 | - 4c678c8: improve build 380 | - Updated dependencies [4c678c8] 381 | - bob-esbuild-plugin@0.1.1 382 | -------------------------------------------------------------------------------- /packages/bob/build-types.ts: -------------------------------------------------------------------------------- 1 | import { buildTsc } from './src/tsc/build'; 2 | 3 | buildTsc().catch(err => { 4 | console.error(err); 5 | process.exit(1); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/bob/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bob-esbuild", 3 | "version": "4.0.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/PabloSzx/bob-esbuild", 7 | "directory": "packages/bob" 8 | }, 9 | "license": "MIT", 10 | "author": "PabloSzx ", 11 | "exports": { 12 | ".": { 13 | "require": "./lib/index.js", 14 | "import": "./lib/index.mjs" 15 | }, 16 | "./*": { 17 | "require": "./lib/*.js", 18 | "import": "./lib/*.mjs" 19 | } 20 | }, 21 | "main": "lib/index.js", 22 | "module": "lib/index.mjs", 23 | "types": "lib/index.d.ts", 24 | "files": [ 25 | "lib" 26 | ], 27 | "scripts": { 28 | "build:types": "bun build-types.ts", 29 | "dev": "bun self-watch.ts", 30 | "prepare": "bun self-build.ts", 31 | "postpublish": "gh-release" 32 | }, 33 | "dependencies": { 34 | "@pnpm/types": "^9.4.2", 35 | "bob-esbuild-plugin": "workspace:4.0.0", 36 | "rollup": "^4.9.2" 37 | }, 38 | "devDependencies": { 39 | "@pnpm/exportable-manifest": "^5.0.11", 40 | "@rollup/plugin-json": "^6.1.0", 41 | "@types/folder-hash": "^4.0.4", 42 | "@types/fs-extra": "^11.0.4", 43 | "@types/lodash.get": "^4.4.9", 44 | "@types/node": "^20.10.6", 45 | "builtin-modules": "^3.3.0", 46 | "chalk": "^5.3.0", 47 | "changesets-github-release": "^0.1.0", 48 | "cosmiconfig": "^7.0.1", 49 | "date-fns": "^3.0.6", 50 | "esbuild": "^0.19.11", 51 | "execa": "^6.1.0", 52 | "fast-glob": "^3.3.2", 53 | "folder-hash": "^4.0.4", 54 | "fs-extra": "^11.2.0", 55 | "globby": "^14.0.0", 56 | "lodash.get": "^4.4.2", 57 | "rollup-plugin-delete": "^2.0.0", 58 | "rollup-plugin-node-externals": "^6.1.2", 59 | "rollup-plugin-tsconfig-paths": "^1.5.2", 60 | "tree-kill": "^1.2.2", 61 | "tsconfck": "^3.0.0", 62 | "typescript": "^5.3.3" 63 | }, 64 | "peerDependencies": { 65 | "esbuild": ">=0.14.39", 66 | "typescript": "*" 67 | }, 68 | "peerDependenciesMeta": { 69 | "esbuild": { 70 | "optional": true 71 | }, 72 | "typescript": { 73 | "optional": true 74 | } 75 | }, 76 | "publishConfig": { 77 | "access": "public", 78 | "directory": "lib", 79 | "linkDirectory": false 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/bob/self-build.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | import { resolve } from 'path'; 3 | import { startBuild } from './src/build'; 4 | 5 | async function main() { 6 | await startBuild({ 7 | rollup: { 8 | external: ['../deps.js', './deps.js'], 9 | clean: true, 10 | globbyOptions: { 11 | ignore: [resolve('./src/deps.ts')].map(v => v.replace(/\\/g, '/')), 12 | }, 13 | }, 14 | }); 15 | 16 | await build({ 17 | bundle: true, 18 | format: 'cjs', 19 | target: 'node12.20', 20 | entryPoints: ['src/deps.ts'], 21 | outdir: 'lib', 22 | platform: 'node', 23 | minify: true, 24 | external: ['rollup', 'fsevents', 'typescript'], 25 | }); 26 | } 27 | 28 | main().catch(err => { 29 | console.error(err); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/bob/self-watch.ts: -------------------------------------------------------------------------------- 1 | import { startWatch } from './src/watch'; 2 | 3 | startWatch().catch(err => { 4 | console.error(err); 5 | process.exit(1); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/bob/src/build.ts: -------------------------------------------------------------------------------- 1 | import { globalConfig } from './config/cosmiconfig'; 2 | import { buildRollup } from './rollup/build'; 3 | import { buildTsc } from './tsc/build'; 4 | 5 | import type { ConfigOptions } from './config/rollup'; 6 | import type { TSCOptions } from './tsc/types'; 7 | 8 | export interface BuildOptions { 9 | rollup?: ConfigOptions; 10 | tsc?: TSCOptions | false; 11 | } 12 | 13 | export async function startBuild(options: BuildOptions = {}) { 14 | const skipTsc = options.tsc !== false ? (await globalConfig).config.skipAutoTSCBuild : true; 15 | 16 | await Promise.all([buildRollup(options.rollup), skipTsc ? null : buildTsc({ ...options.tsc })]); 17 | } 18 | -------------------------------------------------------------------------------- /packages/bob/src/config/copyToDist.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'path'; 2 | import type { Plugin } from 'rollup'; 3 | import { copyFile, globby, mkdirp, pathExists } from '../deps.js'; 4 | 5 | export interface CopyFilesOptions { 6 | distDir: string; 7 | files: string[]; 8 | cwd?: string; 9 | } 10 | 11 | async function copyFilesToDist({ cwd = process.cwd(), distDir, files }: CopyFilesOptions) { 12 | const allFiles = await globby(files, { cwd }); 13 | 14 | await Promise.all( 15 | allFiles.map(async file => { 16 | const sourcePath = join(cwd, file); 17 | 18 | if (await pathExists(sourcePath)) { 19 | const destPath = join(cwd, distDir, file.replace(/\.\.\//g, '')); 20 | await mkdirp(dirname(destPath)); 21 | 22 | await copyFile(sourcePath, destPath); 23 | } 24 | }) 25 | ); 26 | } 27 | 28 | export const copyToDist = (options: CopyFilesOptions): Plugin => { 29 | const pending: Promise>[] = []; 30 | 31 | return { 32 | name: 'CopyToDist', 33 | buildStart() { 34 | pending.push(Promise.allSettled([copyFilesToDist(options)]).then(v => v[0])); 35 | }, 36 | async buildEnd() { 37 | await Promise.all( 38 | pending.map(v => 39 | v.then(v => { 40 | if (v.status === 'rejected') throw v.reason; 41 | }) 42 | ) 43 | ); 44 | }, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/bob/src/config/cosmiconfig.ts: -------------------------------------------------------------------------------- 1 | import type { EsbuildPluginOptions } from 'bob-esbuild-plugin'; 2 | import { transform } from 'esbuild'; 3 | import fs from 'fs'; 4 | import { dirname } from 'path'; 5 | import type { InputOptions, OutputOptions, Plugin } from 'rollup'; 6 | import { cosmiconfig } from '../deps.js'; 7 | import { error } from '../log/error'; 8 | import type { TSCOptions } from '../tsc/types'; 9 | import { importFromString } from '../utils/importFromString'; 10 | import type { PackageJSON } from './packageJson'; 11 | import type { ConfigOptions } from './rollup'; 12 | 13 | export type PickRequired = T & Required>; 14 | 15 | export interface ExternalsOptions { 16 | /** 17 | * Path/to/your/package.json file (or array of paths). 18 | * Defaults to all package.json files found in parent directories recursively. 19 | * Won't got outside of a git repository. 20 | */ 21 | packagePath?: string | string[]; 22 | /** Mark node built-in modules like `path`, `fs`... as external. Defaults to `true`. */ 23 | builtins?: boolean; 24 | /** Mark dependencies as external. Defaults to `false`. */ 25 | deps?: boolean; 26 | /** Mark devDependencies as external. Defaults to `true`. */ 27 | devDeps?: boolean; 28 | /** Mark peerDependencies as external. Defaults to `true`. */ 29 | peerDeps?: boolean; 30 | /** Mark optionalDependencies as external. Defaults to `true`. */ 31 | optDeps?: boolean; 32 | /** Force these deps in the list of externals, regardless of other settings. Defaults to `[]` */ 33 | include?: string | RegExp | (string | RegExp)[]; 34 | /** Exclude these deps from the list of externals, regardless of other settings. Defaults to `[]` */ 35 | exclude?: string | RegExp | (string | RegExp)[]; 36 | /** @deprecated Use `exclude` instead. */ 37 | except?: string | RegExp | (string | RegExp)[]; 38 | } 39 | 40 | export interface BobConfig extends Pick { 41 | tsc?: TSCOptions; 42 | /** 43 | * It defaults to bob-esbuild.config directory 44 | */ 45 | rootDir?: string; 46 | 47 | /** 48 | * @default "dist" 49 | */ 50 | distDir?: string; 51 | 52 | plugins?: Plugin[]; 53 | 54 | /** 55 | * Disable monorepo features and validations 56 | * 57 | * @default false 58 | */ 59 | singleBuild?: boolean; 60 | 61 | inputOptions?: InputOptions; 62 | 63 | outputOptions?: Omit; 64 | 65 | /** 66 | * If dynamic imports `await import("foo")` should be kept as `import` 67 | * and NOT be transpiled as `await Promise.resolve(require("foo"))` 68 | * 69 | * This is specially useful when is needed to import an `ES Module` from `CommonJS`, 70 | * for example, when an external package has `"type": "module"`. 71 | * 72 | * If an array of strings is specified, the dynamic imports are only kept 73 | * for those specified modules 74 | * 75 | * @default false 76 | */ 77 | keepDynamicImport?: boolean | string[] | ((moduleName: string) => boolean); 78 | 79 | /** 80 | * Skip automatic TSC build, make sure to manually call `bob-esbuild tsc` 81 | * 82 | * @default false 83 | */ 84 | skipAutoTSCBuild?: boolean; 85 | 86 | /** 87 | * Custom esbuild plugin options 88 | * 89 | * Set as `false` to not include esbuild plugin 90 | */ 91 | esbuildPluginOptions?: EsbuildPluginOptions | false; 92 | 93 | externalOptions?: ExternalsOptions; 94 | /** 95 | * Enabled debugging logs 96 | */ 97 | verbose?: boolean; 98 | 99 | /** 100 | * Manually rewrite package json and skip validation 101 | */ 102 | manualRewritePackageJson?: Record Promise | PackageJSON>; 103 | 104 | /** 105 | * Set configurations for specific packages 106 | */ 107 | packageConfigs?: Record>; 108 | 109 | /** 110 | * @default false 111 | */ 112 | useTsconfigPaths?: boolean; 113 | } 114 | 115 | export type ResolvedBobConfig = PickRequired; 116 | 117 | export interface CosmiConfigResult { 118 | filepath: string; 119 | config: ResolvedBobConfig; 120 | } 121 | 122 | export const globalConfig: Promise & { 123 | current?: CosmiConfigResult; 124 | } = cosmiconfig('bob-esbuild', { 125 | searchPlaces: ['bob-esbuild.config.ts'], 126 | loaders: { 127 | '.ts': async filepath => { 128 | const content = await fs.promises.readFile(filepath, 'utf8'); 129 | const { code } = await transform(content, { 130 | loader: 'ts', 131 | format: 'cjs', 132 | sourcemap: 'inline', 133 | target: 'node12.20', 134 | }); 135 | return importFromString(code, filepath)?.config; 136 | }, 137 | }, 138 | }) 139 | .search() 140 | .then((result): CosmiConfigResult => { 141 | if (!result) throw Error('Config could not be found!'); 142 | 143 | const filepath = result.filepath; 144 | const config: ResolvedBobConfig = result.config; 145 | 146 | config.rootDir = config.rootDir || dirname(filepath).replace(/\\/g, '/'); 147 | config.clean = config.clean ?? true; 148 | config.distDir = config.distDir || 'dist'; 149 | config.keepDynamicImport ??= false; 150 | config.singleBuild ??= false; 151 | 152 | const data = { 153 | filepath, 154 | config, 155 | }; 156 | 157 | globalConfig.current = data; 158 | 159 | return data; 160 | }) 161 | .catch(err => { 162 | error(err); 163 | process.exit(1); 164 | }); 165 | -------------------------------------------------------------------------------- /packages/bob/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cosmiconfig'; 2 | export * from './tsconfig'; 3 | export * from './rollup'; 4 | -------------------------------------------------------------------------------- /packages/bob/src/config/packageBuildConfig.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readJSON } from '../deps.js'; 3 | import type { PackageJSON } from './packageJson'; 4 | 5 | export interface PackageBuildConfig { 6 | input?: string[]; 7 | copy?: string[]; 8 | bin?: Record< 9 | string, 10 | { 11 | input: string; 12 | } 13 | >; 14 | pkg: PackageJSON; 15 | } 16 | 17 | export async function GetPackageBuildConfig(cwd: string = process.cwd()): Promise { 18 | const pkg: PackageJSON = (await readJSON(resolve(cwd, 'package.json'))) as PackageJSON; 19 | 20 | if (!pkg.name) throw Error('Invalid "name" field for package.json in ' + cwd); 21 | 22 | const buildConfig: PackageBuildConfig = { 23 | pkg, 24 | }; 25 | 26 | if (pkg.buildConfig?.input) { 27 | if (typeof pkg.buildConfig.input === 'string') { 28 | buildConfig.input = [pkg.buildConfig.input]; 29 | } else if (Array.isArray(pkg.buildConfig.input) && pkg.buildConfig.input.every(v => typeof v === 'string')) { 30 | buildConfig.input = pkg.buildConfig.input; 31 | } else { 32 | throw Error(`Invalid buildConfig.input: ${JSON.stringify(pkg.buildConfig.input)}`); 33 | } 34 | } 35 | 36 | if (Array.isArray(pkg.buildConfig?.copy)) { 37 | buildConfig.copy = pkg.buildConfig?.copy; 38 | } 39 | 40 | if (pkg.buildConfig?.bin && typeof pkg.buildConfig.bin === 'object') { 41 | buildConfig.bin = pkg.buildConfig.bin; 42 | } 43 | 44 | return { 45 | ...buildConfig, 46 | pkg, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/bob/src/config/packageJson.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import type { Plugin } from 'rollup'; 3 | import { ensureDir, get, writeJSON, makePublishManifest } from '../deps.js'; 4 | import { warn } from '../log/warn'; 5 | import type { ResolvedBobConfig } from './cosmiconfig'; 6 | import { rewriteExports } from './rewrite-exports'; 7 | import type { ProjectManifest } from '@pnpm/types'; 8 | import type { PackageBuildConfig } from './packageBuildConfig'; 9 | 10 | export interface PackageJSON extends Omit, Record { 11 | files?: string[]; 12 | type?: string; 13 | exports?: Record; 14 | buildConfig?: PackageBuildConfig; 15 | } 16 | 17 | export const getPackageJsonName = (pkg: PackageJSON): string => { 18 | if ('name' in pkg && pkg.name) return pkg.name; 19 | 20 | throw Error('Invalid package.json without name: ' + JSON.stringify(pkg)); 21 | }; 22 | 23 | export function rewritePackageJson(pkg: PackageJSON, distDir: string, cwd: string = process.cwd()) { 24 | const newPkg: PackageJSON = { 25 | name: pkg.name, 26 | }; 27 | 28 | function withoutDistDir(str?: string) { 29 | return str?.replace(`${distDir}/`, ''); 30 | } 31 | 32 | const fields = [ 33 | 'name', 34 | 'version', 35 | 'description', 36 | 'sideEffects', 37 | 'peerDependencies', 38 | 'peerDependenciesMeta', 39 | 'dependencies', 40 | 'optionalDependencies', 41 | 'repository', 42 | 'homepage', 43 | 'keywords', 44 | 'author', 45 | 'license', 46 | 'engines', 47 | 'type', 48 | ]; 49 | 50 | for (const field of fields) { 51 | if (pkg[field] != null) { 52 | newPkg[field] = pkg[field]; 53 | } 54 | } 55 | 56 | newPkg.main = withoutDistDir(pkg.main); 57 | newPkg.module = withoutDistDir(pkg.module); 58 | newPkg.types = withoutDistDir(pkg.types); 59 | 60 | if (pkg.exports) { 61 | newPkg.exports = rewriteExports(pkg.exports, withoutDistDir); 62 | } else { 63 | warn(`No "." or "./*" exports field specified in ${resolve(cwd, distDir, 'package.json')}!`); 64 | } 65 | 66 | if (pkg.bin) { 67 | newPkg.bin = {}; 68 | 69 | for (const [alias, binPath] of Object.entries(pkg.bin)) { 70 | newPkg.bin[alias] = withoutDistDir(binPath)!; 71 | } 72 | } 73 | 74 | return newPkg; 75 | } 76 | 77 | export function validatePackageJson(pkg: PackageJSON, distDir: string) { 78 | const typeModule = pkg.type === 'module'; 79 | 80 | function expect(key: string | string[], expected: string | undefined) { 81 | const received = get(pkg, key); 82 | 83 | if (expected === undefined) { 84 | if (received === undefined) return; 85 | 86 | throw Error(`"${key}" should NOT be specified in "${pkg.name}"`); 87 | } 88 | 89 | if (!received) throw Error(`"${key}" not specified in "${pkg.name}"`); 90 | 91 | if (expected !== received) { 92 | throw new Error(`${pkg.name}: "${Array.isArray(key) ? key.join(' ') : key}" equals "${received}", should be "${expected}"`); 93 | } 94 | } 95 | 96 | if (pkg.main) { 97 | expect('main', `${distDir}/index.js`); 98 | 99 | if (typeModule) { 100 | expect('module', undefined); 101 | } else { 102 | expect('module', `${distDir}/index.mjs`); 103 | } 104 | 105 | if (get(pkg, 'types')) { 106 | expect('types', `${distDir}/index.d.ts`); 107 | } else { 108 | expect('typings', `${distDir}/index.d.ts`); 109 | } 110 | 111 | if (get(pkg, 'typescript.definition')) { 112 | expect('typescript.definition', `${distDir}/index.d.ts`); 113 | } 114 | } 115 | 116 | if (get(pkg, 'publishConfig')) { 117 | expect('publishConfig.directory', distDir); 118 | } 119 | 120 | if (get(pkg, ['exports', '.'])) { 121 | if (get(pkg, ['exports', '.', 'require'])) 122 | expect(['exports', '.', 'require'], typeModule ? `./${distDir}/index.cjs` : `./${distDir}/index.js`); 123 | if (get(pkg, ['exports', '.', 'import'])) 124 | expect(['exports', '.', 'import'], typeModule ? `./${distDir}/index.js` : `./${distDir}/index.mjs`); 125 | } 126 | 127 | if (get(pkg, ['exports', './*'])) { 128 | if (get(pkg, ['exports', './*', 'require'])) 129 | expect(['exports', './*', 'require'], typeModule ? `./${distDir}/*.cjs` : `./${distDir}/*.js`); 130 | 131 | if (get(pkg, ['exports', './*', 'import'])) 132 | expect(['exports', './*', 'import'], typeModule ? `./${distDir}/*.js` : `./${distDir}/*.mjs`); 133 | } 134 | } 135 | 136 | export async function writePackageJson({ 137 | packageJson, 138 | distDir, 139 | cwd = process.cwd(), 140 | rewritePackage, 141 | }: GeneratePackageJsonOptions) { 142 | const distDirPath = resolve(cwd, distDir); 143 | await ensureDir(distDirPath); 144 | 145 | const pkg = (await makePublishManifest(cwd, rewritePackageJson(packageJson, distDir, cwd) as ProjectManifest)) as PackageJSON; 146 | 147 | await writeJSON(resolve(distDirPath, 'package.json'), rewritePackage ? await rewritePackage(pkg) : pkg, { 148 | spaces: 2, 149 | }); 150 | } 151 | 152 | export interface GeneratePackageJsonOptions { 153 | packageJson: PackageJSON; 154 | distDir: string; 155 | cwd?: string; 156 | skipValidate?: boolean; 157 | rewritePackage?: (pkg: PackageJSON) => Promise | PackageJSON; 158 | } 159 | 160 | export const generatePackageJson = (options: GeneratePackageJsonOptions, config: ResolvedBobConfig): Plugin | null => { 161 | const manualRewrite = config.manualRewritePackageJson?.[getPackageJsonName(options.packageJson)]; 162 | 163 | if (!options.packageJson.publishConfig?.directory) { 164 | if (manualRewrite) { 165 | throw Error( 166 | `manualRewritePackageJson can only be specified if publishConfig.directory is not specified for ${options.packageJson.name}` 167 | ); 168 | } 169 | 170 | if ( 171 | !Array.isArray(options.packageJson.files) || 172 | !options.packageJson.files.some(v => v === options.distDir || v === '/' + options.distDir) 173 | ) 174 | throw Error( 175 | `No valid 'files' property in ${resolve( 176 | options.cwd || process.cwd(), 177 | 'package.json' 178 | )} without using "publishConfig.directory"` 179 | ); 180 | 181 | validatePackageJson(options.packageJson, options.distDir); 182 | 183 | warn(`Skipping package.json rewrite in publish subdirectory: ${resolve(options.cwd || process.cwd())}`); 184 | return null; 185 | } 186 | 187 | const pending: Promise>[] = []; 188 | 189 | return { 190 | name: 'GeneratePackageJson', 191 | async buildStart() { 192 | if (!manualRewrite && !options.skipValidate) validatePackageJson(options.packageJson, options.distDir); 193 | 194 | pending.push( 195 | Promise.allSettled([ 196 | writePackageJson( 197 | manualRewrite 198 | ? { ...options, packageJson: await manualRewrite(options.packageJson), rewritePackage: manualRewrite } 199 | : options 200 | ), 201 | ]).then(v => v[0]) 202 | ); 203 | }, 204 | async buildEnd() { 205 | await Promise.all( 206 | pending.map(v => 207 | v.then(v => { 208 | if (v.status === 'rejected') throw v.reason; 209 | }) 210 | ) 211 | ); 212 | }, 213 | }; 214 | }; 215 | -------------------------------------------------------------------------------- /packages/bob/src/config/rewrite-exports.ts: -------------------------------------------------------------------------------- 1 | export function rewriteExports( 2 | exports: Record, 3 | withoutDistDir: (str: string) => string | undefined 4 | ) { 5 | const newExports = { ...exports }; 6 | 7 | newExports['./package.json'] = './package.json'; 8 | 9 | for (const [key, value] of Object.entries(newExports)) { 10 | if (!value) continue; 11 | 12 | let newValue = value as string | { require?: string; import?: string; types?: string }; 13 | 14 | if (typeof newValue === 'string') { 15 | newValue = withoutDistDir(newValue)!; 16 | } else if (typeof newValue === 'object' && newValue != null) { 17 | newValue = { 18 | types: newValue.types != null ? withoutDistDir(newValue.types) : undefined, 19 | require: newValue.require != null ? withoutDistDir(newValue.require) : undefined, 20 | import: newValue.import != null ? withoutDistDir(newValue.import) : undefined, 21 | }; 22 | } 23 | 24 | newExports[withoutDistDir(key)!] = newValue; 25 | } 26 | 27 | return newExports; 28 | } 29 | -------------------------------------------------------------------------------- /packages/bob/src/config/rollup.ts: -------------------------------------------------------------------------------- 1 | import { bobEsbuildPlugin } from 'bob-esbuild-plugin'; 2 | import path, { join, resolve } from 'path'; 3 | import type { ExternalOption, InputOptions, OutputOptions, Plugin, RollupBuild } from 'rollup'; 4 | import { del, externals, globby, tsconfigPaths as tsPaths, rollupJson } from '../deps.js'; 5 | import { debug } from '../log/debug'; 6 | import { cleanObject } from '../utils/object'; 7 | import { retry } from '../utils/retry'; 8 | import { copyToDist } from './copyToDist'; 9 | import { globalConfig } from './cosmiconfig'; 10 | import { GetPackageBuildConfig } from './packageBuildConfig'; 11 | import { generatePackageJson, getPackageJsonName } from './packageJson'; 12 | import { rollupBin } from './rollupBin'; 13 | 14 | export interface ConfigOptions { 15 | /** 16 | * @default process.cwd() 17 | */ 18 | cwd?: string; 19 | /** 20 | * By default it's `true` in global config 21 | */ 22 | clean?: boolean; 23 | /** 24 | * Input files 25 | * 26 | * By default it takes every ".ts" file inside src 27 | */ 28 | inputFiles?: string[]; 29 | /** 30 | * Enable bundling every entry point (no code-splitting available) 31 | * 32 | * @default false 33 | */ 34 | bundle?: boolean; 35 | 36 | /** 37 | * Only build CJS 38 | * @default false 39 | */ 40 | onlyCJS?: boolean; 41 | 42 | /** 43 | * Only build ESM 44 | * @default false 45 | */ 46 | onlyESM?: boolean; 47 | 48 | /** 49 | * Skip package.json validate 50 | * 51 | * @default false 52 | */ 53 | skipValidate?: boolean; 54 | 55 | /** 56 | * Customize external imports 57 | */ 58 | external?: ExternalOption; 59 | 60 | /** 61 | * Customize globby options 62 | */ 63 | globbyOptions?: { 64 | /** 65 | * Return the absolute path for entries. 66 | * 67 | * @default false 68 | */ 69 | absolute?: boolean; 70 | /** 71 | * If set to `true`, then patterns without slashes will be matched against 72 | * the basename of the path if it contains slashes. 73 | * 74 | * @default false 75 | */ 76 | baseNameMatch?: boolean; 77 | /** 78 | * Enables Bash-like brace expansion. 79 | * 80 | * @default true 81 | */ 82 | braceExpansion?: boolean; 83 | /** 84 | * Enables a case-sensitive mode for matching files. 85 | * 86 | * @default true 87 | */ 88 | caseSensitiveMatch?: boolean; 89 | /** 90 | * Specifies the maximum number of concurrent requests from a reader to read 91 | * directories. 92 | * 93 | * @default os.cpus().length 94 | */ 95 | concurrency?: number; 96 | /** 97 | * The current working directory in which to search. 98 | * 99 | * @default process.cwd() 100 | */ 101 | cwd?: string; 102 | /** 103 | * Specifies the maximum depth of a read directory relative to the start 104 | * directory. 105 | * 106 | * @default Infinity 107 | */ 108 | deep?: number; 109 | /** 110 | * Allow patterns to match entries that begin with a period (`.`). 111 | * 112 | * @default false 113 | */ 114 | dot?: boolean; 115 | /** 116 | * Enables Bash-like `extglob` functionality. 117 | * 118 | * @default true 119 | */ 120 | extglob?: boolean; 121 | /** 122 | * Indicates whether to traverse descendants of symbolic link directories. 123 | * 124 | * @default true 125 | */ 126 | followSymbolicLinks?: boolean; 127 | 128 | /** 129 | * Enables recursively repeats a pattern containing `**`. 130 | * If `false`, `**` behaves exactly like `*`. 131 | * 132 | * @default true 133 | */ 134 | globstar?: boolean; 135 | /** 136 | * An array of glob patterns to exclude matches. 137 | * This is an alternative way to use negative patterns. 138 | * 139 | * @default [] 140 | */ 141 | ignore?: string[]; 142 | /** 143 | * Mark the directory path with the final slash. 144 | * 145 | * @default false 146 | */ 147 | markDirectories?: boolean; 148 | /** 149 | * Returns objects (instead of strings) describing entries. 150 | * 151 | * @default false 152 | */ 153 | objectMode?: boolean; 154 | /** 155 | * Return only directories. 156 | * 157 | * @default false 158 | */ 159 | onlyDirectories?: boolean; 160 | /** 161 | * Return only files. 162 | * 163 | * @default true 164 | */ 165 | onlyFiles?: boolean; 166 | /** 167 | * Enables an object mode (`objectMode`) with an additional `stats` field. 168 | * 169 | * @default false 170 | */ 171 | stats?: boolean; 172 | /** 173 | * By default this package suppress only `ENOENT` errors. 174 | * Set to `true` to suppress any error. 175 | * 176 | * @default false 177 | */ 178 | suppressErrors?: boolean; 179 | /** 180 | * Throw an error when symbolic link is broken if `true` or safely 181 | * return `lstat` call if `false`. 182 | * 183 | * @default false 184 | */ 185 | throwErrorOnBrokenSymbolicLink?: boolean; 186 | /** 187 | * Ensures that the returned entries are unique. 188 | * 189 | * @default true 190 | */ 191 | unique?: boolean; 192 | }; 193 | 194 | /** 195 | * Customize output options 196 | */ 197 | outputOptions?: Partial; 198 | } 199 | 200 | export async function getRollupConfig(optionsArg: ConfigOptions = {}) { 201 | const cwd = optionsArg.cwd ? resolve(optionsArg.cwd) : process.cwd(); 202 | 203 | const [buildConfig, { config: globalOptions }] = await Promise.all([GetPackageBuildConfig(cwd), globalConfig]); 204 | 205 | const options = { ...globalOptions.packageConfigs?.[getPackageJsonName(buildConfig.pkg)], ...cleanObject(optionsArg) }; 206 | 207 | const clean = options.clean ?? globalOptions.clean; 208 | 209 | const inputFiles = options.inputFiles || buildConfig.input || globalOptions.inputFiles || ['src/**/*.{ts,tsx}']; 210 | 211 | if (!inputFiles.length) throw Error('No input files to check!'); 212 | 213 | const distDir = globalOptions.distDir; 214 | 215 | const input = await retry(async () => 216 | ( 217 | await Promise.all( 218 | inputFiles.map(pattern => { 219 | const glob = path.join(cwd, pattern).replace(/\\/g, '/'); 220 | debug('Checking glob pattern: ' + glob); 221 | return globby(glob, options.globbyOptions); 222 | }) 223 | ) 224 | ) 225 | .flat() 226 | .filter((file, index, self) => self.indexOf(file) === index && !!file.match(/\.(js|cjs|mjs|ts|tsx|cts|mts|ctsx|mtsx)$/)) 227 | ); 228 | 229 | if (!input.length) throw Error('No input files found!'); 230 | 231 | debug('Building', input.join(' | ')); 232 | 233 | const experimentalBundling = options.bundle ?? globalOptions.bundle ?? false; 234 | 235 | if (options.onlyESM && options.onlyCJS) throw Error('You can only restrict to either one of CJS or ESM'); 236 | 237 | const absoluteDistDir = path.resolve(cwd, distDir); 238 | 239 | const typeModule = buildConfig.pkg.type === 'module'; 240 | 241 | const cjsOpts: OutputOptions = { 242 | dir: absoluteDistDir, 243 | format: 'cjs', 244 | entryFileNames: typeModule ? '[name].cjs' : '[name].js', 245 | preserveModules: true, 246 | exports: 'auto', 247 | sourcemap: false, 248 | ...cleanObject(options.outputOptions), 249 | ...cleanObject(globalOptions.outputOptions), 250 | }; 251 | 252 | const esmOpts: OutputOptions = { 253 | dir: absoluteDistDir, 254 | format: 'es', 255 | entryFileNames: typeModule ? '[name].js' : '[name].mjs', 256 | preserveModules: true, 257 | sourcemap: false, 258 | ...cleanObject(options.outputOptions), 259 | ...cleanObject(globalOptions.outputOptions), 260 | }; 261 | 262 | const outputOptions: OutputOptions[] = options.onlyCJS ? [cjsOpts] : options.onlyESM ? [esmOpts] : [cjsOpts, esmOpts]; 263 | 264 | if (buildConfig.copy?.length) debug(`Copying ${buildConfig.copy.join(' | ')} to ${absoluteDistDir}`); 265 | 266 | const genPackageJson = generatePackageJson( 267 | { packageJson: buildConfig.pkg, distDir, cwd, skipValidate: options.skipValidate }, 268 | globalOptions 269 | ); 270 | 271 | const plugins: Plugin[] = [ 272 | externals({ 273 | packagePath: path.resolve(cwd, 'package.json'), 274 | deps: true, 275 | ...globalOptions.externalOptions, 276 | }), 277 | rollupBin(buildConfig, cwd), 278 | rollupJson({ 279 | preferConst: true, 280 | }), 281 | ...(globalOptions.plugins || []), 282 | ]; 283 | 284 | if (genPackageJson) { 285 | plugins.push(genPackageJson); 286 | } 287 | 288 | const copyDistFiles = buildConfig.copy || []; 289 | 290 | if (genPackageJson) { 291 | plugins.push( 292 | copyToDist({ 293 | cwd, 294 | distDir, 295 | files: ['README.md', 'LICENSE', ...copyDistFiles], 296 | }) 297 | ); 298 | } else if (copyDistFiles.length) { 299 | plugins.push( 300 | copyToDist({ 301 | cwd, 302 | distDir, 303 | files: copyDistFiles, 304 | }) 305 | ); 306 | } 307 | 308 | const keepDynamicImport = globalOptions.keepDynamicImport; 309 | 310 | if (keepDynamicImport) { 311 | if (Array.isArray(keepDynamicImport)) { 312 | plugins.push({ 313 | name: 'keep-dynamic-import', 314 | renderDynamicImport({ targetModuleId }) { 315 | if (!targetModuleId || !keepDynamicImport.includes(targetModuleId)) return null; 316 | 317 | return { 318 | left: 'import(', 319 | right: ')', 320 | }; 321 | }, 322 | }); 323 | } else if (typeof keepDynamicImport === 'function') { 324 | plugins.push({ 325 | name: 'keep-dynamic-import', 326 | renderDynamicImport({ targetModuleId }) { 327 | if (!targetModuleId || !keepDynamicImport(targetModuleId)) return null; 328 | 329 | return { 330 | left: 'import(', 331 | right: ')', 332 | }; 333 | }, 334 | }); 335 | } else { 336 | plugins.push({ 337 | name: 'keep-dynamic-import', 338 | renderDynamicImport() { 339 | return { 340 | left: 'import(', 341 | right: ')', 342 | }; 343 | }, 344 | }); 345 | } 346 | } 347 | 348 | if (globalOptions.esbuildPluginOptions !== false) { 349 | plugins.push( 350 | bobEsbuildPlugin({ 351 | target: 'es2019', 352 | sourceMap: false, 353 | experimentalBundling, 354 | ...globalOptions.esbuildPluginOptions, 355 | }) 356 | ); 357 | } 358 | 359 | if (clean) { 360 | plugins.push( 361 | del({ 362 | targets: [`${distDir}/**/*.js`, `${distDir}/**/*.mjs`, `${distDir}/**/*.map`], 363 | cwd, 364 | }) 365 | ); 366 | } 367 | 368 | if (globalOptions.useTsconfigPaths) { 369 | plugins.push( 370 | tsPaths({ 371 | tsConfigPath: join(globalOptions.rootDir, 'tsconfig.json'), 372 | }) 373 | ); 374 | } 375 | 376 | const inputOptions: InputOptions = { 377 | input, 378 | plugins, 379 | external: options.external, 380 | ...cleanObject(globalOptions.inputOptions), 381 | }; 382 | 383 | async function write(bundle: RollupBuild) { 384 | await Promise.all(outputOptions.map(output => bundle.write(output))); 385 | } 386 | 387 | return { inputOptions, outputOptions, write }; 388 | } 389 | -------------------------------------------------------------------------------- /packages/bob/src/config/rollupBin.ts: -------------------------------------------------------------------------------- 1 | import { bobEsbuildPlugin } from 'bob-esbuild-plugin'; 2 | import { resolve } from 'path'; 3 | import type { Plugin, InputOptions } from 'rollup'; 4 | import { externals, get, rollupJson } from '../deps.js'; 5 | import { debug } from '../log/debug'; 6 | import { globalConfig } from './cosmiconfig'; 7 | import type { PackageBuildConfig } from './packageBuildConfig'; 8 | 9 | export const rollupBin = (buildConfig: PackageBuildConfig, cwd: string = process.cwd()): Plugin => { 10 | const pending: Promise[]>[] = []; 11 | 12 | return { 13 | name: 'RollupBin', 14 | async buildStart() { 15 | const { 16 | config: { distDir, externalOptions, esbuildPluginOptions }, 17 | } = await globalConfig; 18 | if (buildConfig.bin) { 19 | const { rollup } = await import('rollup'); 20 | pending.push( 21 | Promise.allSettled( 22 | Object.entries(buildConfig.bin).map(async ([alias, options]) => { 23 | if (typeof options.input !== 'string') throw Error(`buildConfig.${alias} expected to have an input field`); 24 | 25 | const inputOptions: InputOptions = { 26 | input: options.input, 27 | plugins: [ 28 | externals({ 29 | packagePath: resolve(cwd, 'package.json'), 30 | deps: true, 31 | ...externalOptions, 32 | }), 33 | bobEsbuildPlugin({ 34 | target: 'es2019', 35 | sourceMap: false, 36 | experimentalBundling: true, 37 | ...esbuildPluginOptions, 38 | }), 39 | rollupJson({ 40 | preferConst: true, 41 | }), 42 | ], 43 | }; 44 | 45 | const pkgBin = get(buildConfig.pkg, ['bin', alias]); 46 | 47 | if (typeof pkgBin !== 'string') { 48 | throw Error(`Location on bin ${alias} could not be found!`); 49 | } 50 | 51 | const bundle = await rollup(inputOptions); 52 | 53 | const binOutputFile = resolve(cwd, distDir, pkgBin); 54 | await bundle.write({ 55 | banner: `#!/usr/bin/env node`, 56 | sourcemap: false, 57 | file: binOutputFile, 58 | format: binOutputFile.endsWith('.mjs') 59 | ? 'es' 60 | : binOutputFile.endsWith('.cjs') 61 | ? 'cjs' 62 | : buildConfig.pkg.type === 'module' 63 | ? 'es' 64 | : 'cjs', 65 | }); 66 | 67 | debug(`Bin ${alias} built in ${binOutputFile}`); 68 | }) 69 | ) 70 | ); 71 | } 72 | }, 73 | async buildEnd() { 74 | await Promise.all( 75 | pending.map(pendingPromises => 76 | pendingPromises.then(async promises => { 77 | return Promise.all( 78 | promises.map(async result => { 79 | if (result.status === 'rejected') throw result.reason; 80 | }) 81 | ); 82 | }) 83 | ) 84 | ); 85 | }, 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /packages/bob/src/config/tsconfig.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { join } from 'path'; 3 | import { parseTsconfig } from '../deps.js'; 4 | import { error } from '../log/error'; 5 | import { retry } from '../utils/retry'; 6 | import { globalConfig } from './cosmiconfig'; 7 | 8 | export const resolvedTsconfig = retry(async () => { 9 | const { 10 | config: { rootDir: configRootDir, singleBuild }, 11 | } = await globalConfig; 12 | 13 | const parseResult = await parseTsconfig(join(configRootDir, 'tsconfig.json')); 14 | 15 | const compilerOptions = parseResult.tsconfig.compilerOptions; 16 | 17 | assert(compilerOptions, 'compilerOptions not specified!'); 18 | 19 | const outDir: string = compilerOptions?.outDir; 20 | 21 | if (typeof outDir !== 'string') throw Error('outDir not specified in ' + parseResult.tsconfigFile); 22 | 23 | if (!singleBuild) { 24 | if (compilerOptions.rootDir !== '.') throw Error("tsconfig.json rootDir has to be '.'"); 25 | } 26 | 27 | return { 28 | outDir, 29 | }; 30 | }).catch(err => { 31 | error(err); 32 | process.exit(1); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/bob/src/deps.ts: -------------------------------------------------------------------------------- 1 | import { createExportableManifest } from '@pnpm/exportable-manifest'; 2 | export { default as rollupJson } from '@rollup/plugin-json'; 3 | export { default as colors } from 'chalk'; 4 | export { cosmiconfig } from 'cosmiconfig'; 5 | export { format } from 'date-fns/format'; 6 | export { execaCommand as command } from 'execa'; 7 | export { hashElement } from 'folder-hash'; 8 | export { globby } from 'globby'; 9 | export { default as get } from 'lodash.get'; 10 | export { default as del } from 'rollup-plugin-delete'; 11 | export { default as externals } from 'rollup-plugin-node-externals'; 12 | export { default as tsconfigPaths } from 'rollup-plugin-tsconfig-paths'; 13 | export { default as treeKill } from 'tree-kill'; 14 | export { parse as parseTsconfig } from 'tsconfck'; 15 | 16 | import fsExtra from 'fs-extra'; 17 | 18 | export const makePublishManifest = createExportableManifest; 19 | 20 | export const { copyFile, mkdirp, pathExists, copy, ensureDir } = fsExtra; 21 | 22 | export const readJSON: (path: string) => Promise = fsExtra.readJSON; 23 | export const writeJSON: (path: string, content: unknown, options?: { spaces?: number }) => Promise = fsExtra.writeJSON; 24 | -------------------------------------------------------------------------------- /packages/bob/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './log'; 3 | export * from './rollup'; 4 | export * from './tsc'; 5 | export * from './utils'; 6 | export * from './build'; 7 | export * from './watch'; 8 | export { writePackageJson, rewritePackageJson } from './config/packageJson'; 9 | -------------------------------------------------------------------------------- /packages/bob/src/log/debug.ts: -------------------------------------------------------------------------------- 1 | import { globalConfig } from '../config/cosmiconfig'; 2 | import { makeLabel } from './label'; 3 | 4 | export async function debug(...message: any[]) { 5 | const debugEnabled = (globalConfig.current || (await globalConfig)).config.verbose; 6 | 7 | if (debugEnabled) console.log(makeLabel('BOB', 'info'), ...message); 8 | } 9 | -------------------------------------------------------------------------------- /packages/bob/src/log/error.ts: -------------------------------------------------------------------------------- 1 | import { makeLabel } from './label'; 2 | 3 | export function error(...message: any[]) { 4 | console.error(makeLabel('BOB', 'error'), ...message); 5 | } 6 | -------------------------------------------------------------------------------- /packages/bob/src/log/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug'; 2 | export * from './error'; 3 | export * from './label'; 4 | -------------------------------------------------------------------------------- /packages/bob/src/log/label.ts: -------------------------------------------------------------------------------- 1 | import { colors, format } from '../deps.js'; 2 | 3 | export const makeLabel = (input: string, type: 'info' | 'success' | 'error' | 'warn') => 4 | colors[type === 'info' ? 'bgBlue' : type === 'warn' ? 'bgYellow' : type === 'error' ? 'bgRed' : 'bgGreen']( 5 | colors.black(`[${input.toUpperCase()}-${format(new Date(), 'kk:mm:s')}]`) 6 | ); 7 | -------------------------------------------------------------------------------- /packages/bob/src/log/warn.ts: -------------------------------------------------------------------------------- 1 | import { makeLabel } from './label'; 2 | 3 | export async function warn(...message: any[]) { 4 | console.warn(makeLabel('BOB', 'warn'), ...message); 5 | } 6 | -------------------------------------------------------------------------------- /packages/bob/src/rollup/build.ts: -------------------------------------------------------------------------------- 1 | import { ConfigOptions, getRollupConfig } from '../config/rollup'; 2 | import { debug } from '../log/debug'; 3 | 4 | export async function buildRollup(options?: ConfigOptions) { 5 | const { rollup } = await import('rollup'); 6 | const { inputOptions, outputOptions } = await getRollupConfig(options); 7 | 8 | const startTime = Date.now(); 9 | 10 | const build = await rollup(inputOptions); 11 | 12 | await Promise.all( 13 | outputOptions.map(output => { 14 | return build.write(output); 15 | }) 16 | ); 17 | 18 | debug(`JS built in ${Date.now() - startTime}ms`); 19 | } 20 | -------------------------------------------------------------------------------- /packages/bob/src/rollup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build'; 2 | export * from './watch'; 3 | -------------------------------------------------------------------------------- /packages/bob/src/rollup/watch.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'child_process'; 2 | import type { RollupWatcher } from 'rollup'; 3 | import { ConfigOptions, getRollupConfig } from '../config/rollup'; 4 | import { command, treeKill } from '../deps.js'; 5 | import { debug } from '../log/debug'; 6 | import { error } from '../log/error'; 7 | 8 | export interface WatchRollupOptions { 9 | config?: ConfigOptions; 10 | onSuccessCommand?: string; 11 | onSuccessCallback?: (builds: number) => void; 12 | onStartCallback?: (builds: number) => void; 13 | } 14 | 15 | export async function watchRollup(options: WatchRollupOptions = {}): Promise<{ 16 | watcher: RollupWatcher; 17 | cleanUp: () => void; 18 | }> { 19 | const { watch: rollupWatch } = await import('rollup'); 20 | 21 | const { inputOptions, outputOptions, write } = await getRollupConfig(options.config); 22 | 23 | let startTime = Date.now(); 24 | 25 | const watcher = rollupWatch({ 26 | ...inputOptions, 27 | output: outputOptions, 28 | 29 | watch: { 30 | skipWrite: true, 31 | buildDelay: 1000, 32 | chokidar: { 33 | ignoreInitial: false, 34 | }, 35 | }, 36 | }); 37 | 38 | let onSuccessProcess: ChildProcess | null = null; 39 | 40 | function cleanUp() { 41 | try { 42 | if (onSuccessProcess?.pid) killPromise(onSuccessProcess.pid); 43 | 44 | watcher.close(); 45 | } catch (err) { 46 | } finally { 47 | process.exit(0); 48 | } 49 | } 50 | 51 | process.on('SIGINT', cleanUp); 52 | process.on('SIGHUP', cleanUp); 53 | process.on('SIGQUIT', cleanUp); 54 | process.on('SIGTERM', cleanUp); 55 | process.on('uncaughtException', cleanUp); 56 | process.on('exit', cleanUp); 57 | 58 | let pendingKillPromise: Promise; 59 | 60 | function killPromise(pid: number) { 61 | return (pendingKillPromise = new Promise(resolve => { 62 | treeKill(pid, () => { 63 | resolve(); 64 | }); 65 | })); 66 | } 67 | 68 | let buildsDone = 0; 69 | 70 | const cwd = (options.config?.cwd || process.cwd()).replace(/\\/g, '/'); 71 | 72 | watcher.on('event', event => { 73 | switch (event.code) { 74 | case 'BUNDLE_START': { 75 | if (onSuccessProcess?.pid) { 76 | killPromise(onSuccessProcess.pid); 77 | onSuccessProcess = null; 78 | } 79 | 80 | options.onStartCallback?.(buildsDone); 81 | 82 | startTime = Date.now(); 83 | 84 | debug(`Starting build for ${cwd}`); 85 | break; 86 | } 87 | case 'BUNDLE_END': { 88 | const { result } = event; 89 | 90 | write(result) 91 | .then(async () => { 92 | debug(`JS built for ${cwd} in ${Date.now() - startTime}ms`); 93 | 94 | options.onSuccessCallback?.(buildsDone); 95 | 96 | if (pendingKillPromise) await pendingKillPromise; 97 | 98 | if (options.onSuccessCommand) { 99 | debug(`$ ${options.onSuccessCommand}`); 100 | onSuccessProcess = command(options.onSuccessCommand, { 101 | stdio: 'inherit', 102 | shell: true, 103 | }); 104 | } 105 | }) 106 | .catch(error) 107 | .finally(() => { 108 | result.close(); 109 | 110 | ++buildsDone; 111 | }); 112 | 113 | break; 114 | } 115 | case 'START': { 116 | debug(`JS watcher for ${cwd} started`); 117 | break; 118 | } 119 | case 'ERROR': { 120 | error(event.error); 121 | break; 122 | } 123 | } 124 | }); 125 | 126 | return { watcher, cleanUp }; 127 | } 128 | -------------------------------------------------------------------------------- /packages/bob/src/tsc/build.ts: -------------------------------------------------------------------------------- 1 | import { parse, resolve } from 'path'; 2 | import { globalConfig } from '../config/cosmiconfig'; 3 | import { resolvedTsconfig } from '../config/tsconfig'; 4 | import { command, copy, pathExists, globby } from '../deps.js'; 5 | import { debug } from '../log/debug'; 6 | import { warn } from '../log/warn'; 7 | import { retry } from '../utils/retry'; 8 | import { getHash } from './hash'; 9 | import type { TSCOptions } from './types'; 10 | 11 | export async function buildTsc(options: TSCOptions = {}) { 12 | const { 13 | config: { tsc: globalTsc = {}, rootDir: rootDirCwd, distDir }, 14 | } = await globalConfig; 15 | 16 | const dirs = [...(options.dirs || []), ...(globalTsc.dirs || [])]; 17 | 18 | const startTime = Date.now(); 19 | 20 | const hashPromise = Promise.allSettled([retry(getHash)]).then(v => v[0]); 21 | 22 | const tscCommand = options.tscBuildCommand || globalTsc.tscBuildCommand || 'tsc --emitDeclarationOnly'; 23 | 24 | const targetDirs = await retry(() => { 25 | return globby(dirs, { 26 | expandDirectories: false, 27 | absolute: false, 28 | onlyDirectories: true, 29 | cwd: rootDirCwd, 30 | }); 31 | }); 32 | 33 | const { shouldBuild, cleanHash } = await hashPromise.then(v => { 34 | if (v.status === 'rejected') throw v.reason; 35 | 36 | return v.value; 37 | }); 38 | 39 | if (shouldBuild) { 40 | debug(!targetDirs.length ? 'No target directories for built types!' : 'Building types for: ' + targetDirs.join(' | ')); 41 | 42 | const tsconfig = globalTsc.tsconfig; 43 | await command(tscCommand + (tsconfig ? ` -p ${tsconfig}` : ''), { 44 | cwd: rootDirCwd, 45 | stdio: 'inherit', 46 | }).catch(err => { 47 | cleanHash(); 48 | 49 | throw err; 50 | }); 51 | } 52 | 53 | const { outDir } = await resolvedTsconfig; 54 | 55 | const cwd = options.cwd ? resolve(options.cwd) : process.cwd(); 56 | 57 | if (cwd.replace(/\\/g, '/') === rootDirCwd) return; 58 | 59 | const targetDir = targetDirs.find(v => resolve(rootDirCwd, v) === cwd); 60 | 61 | if (!targetDir) { 62 | warn(`Current directory: "${cwd}" not getting typescript definitions`); 63 | 64 | return; 65 | } 66 | 67 | const from = resolve(rootDirCwd, outDir, targetDir, 'src'); 68 | 69 | if (!(await pathExists(from))) return; 70 | 71 | await retry( 72 | async () => 73 | await copy(from, resolve(rootDirCwd, targetDir, distDir), { 74 | filter(src) { 75 | // Check if is directory 76 | if (!parse(src).ext) return true; 77 | 78 | return src.endsWith('.d.ts'); 79 | }, 80 | }), 81 | 10 82 | ); 83 | 84 | debug(`Types ${shouldBuild ? 'built' : 'prepared'} in ${Date.now() - startTime}ms`); 85 | } 86 | -------------------------------------------------------------------------------- /packages/bob/src/tsc/hash.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, unlinkSync } from 'fs'; 2 | import { resolve } from 'path'; 3 | import { globalConfig } from '../config/cosmiconfig'; 4 | import { resolvedTsconfig } from '../config/tsconfig'; 5 | import { hashElement, mkdirp, readJSON, writeJSON } from '../deps.js'; 6 | import { retry } from '../utils/retry'; 7 | 8 | export async function getHash(): Promise<{ 9 | shouldBuild: boolean; 10 | cleanHash: () => void; 11 | }> { 12 | const { 13 | config: { rootDir, distDir, tsc: { hash } = {}, singleBuild }, 14 | } = await globalConfig; 15 | const { outDir } = await resolvedTsconfig; 16 | 17 | // For single build, always build typescript 18 | if (singleBuild) { 19 | return { 20 | cleanHash() {}, 21 | shouldBuild: true, 22 | }; 23 | } 24 | 25 | const typesHashJSON = resolve(rootDir, outDir, 'types-hash.json'); 26 | 27 | const [currentHash, jsonHash] = await retry(async () => 28 | Promise.all([ 29 | hashElement(rootDir, { 30 | files: { 31 | exclude: hash?.files?.exclude || ['*.d.ts'], 32 | include: hash?.files?.include || ['*.ts', '*.tsx', '*.json'], 33 | }, 34 | folders: { 35 | exclude: [ 36 | outDir, 37 | distDir, 38 | 'node_modules', 39 | 'lib', 40 | 'temp', 41 | 'dist', 42 | '.git', 43 | '.next', 44 | 'coverage', 45 | '.vscode', 46 | '.github', 47 | '.changeset', 48 | '.husky', 49 | '.bob', 50 | ...(hash?.folders?.exclude || []), 51 | ], 52 | }, 53 | }), 54 | existsSync(typesHashJSON) 55 | ? readJSON(typesHashJSON).then( 56 | v => v as { hash: string }, 57 | () => null 58 | ) 59 | : null, 60 | ]) 61 | ); 62 | 63 | const cleanHash = () => { 64 | try { 65 | unlinkSync(typesHashJSON); 66 | } catch (err) {} 67 | }; 68 | 69 | if (jsonHash?.hash !== currentHash.hash) { 70 | mkdirp(resolve(rootDir, outDir)).then( 71 | () => { 72 | writeJSON(typesHashJSON, { 73 | hash: currentHash.hash, 74 | }).catch(() => null); 75 | }, 76 | () => null 77 | ); 78 | 79 | return { 80 | shouldBuild: true, 81 | cleanHash, 82 | }; 83 | } 84 | return { 85 | shouldBuild: false, 86 | cleanHash, 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /packages/bob/src/tsc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build'; 2 | export * from './hash'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/bob/src/tsc/types.ts: -------------------------------------------------------------------------------- 1 | export interface TSCOptions { 2 | dirs?: string[]; 3 | 4 | /** 5 | * @default "tsc --emitDeclarationOnly" 6 | */ 7 | tscBuildCommand?: string; 8 | 9 | /** 10 | * Customize type diff hashing 11 | */ 12 | hash?: { 13 | files?: { 14 | /** 15 | * @default ['*.d.ts'] 16 | */ 17 | exclude?: string[]; 18 | /** 19 | * @default ['*.ts', '*.tsx', '*.json'] 20 | */ 21 | include?: string[]; 22 | }; 23 | folders?: { 24 | /** 25 | * @default [ 26 | outDir, 27 | distDir, 28 | 'node_modules', 29 | 'lib', 30 | 'temp', 31 | 'dist', 32 | '.git', 33 | '.next', 34 | 'coverage', 35 | '.vscode', 36 | '.github', 37 | '.changeset', 38 | '.husky', 39 | '.bob' 40 | ] 41 | */ 42 | exclude?: string[]; 43 | }; 44 | }; 45 | 46 | /** 47 | * Specify tsconfig location. 48 | * By default it looks for the default `tsconfig.json` besides the bob.config.ts 49 | * 50 | * IT HAS TO BE AN ABSOLUTE PATH 51 | */ 52 | tsconfig?: string; 53 | 54 | /** 55 | * @default process.cwd() 56 | */ 57 | cwd?: string; 58 | } 59 | -------------------------------------------------------------------------------- /packages/bob/src/utils/getDefault.ts: -------------------------------------------------------------------------------- 1 | export function getDefault(v: T | { default?: T }) { 2 | return ((typeof v === 'object' && v != null && 'default' in v ? v.default : v) || v) as T; 3 | } 4 | -------------------------------------------------------------------------------- /packages/bob/src/utils/importFromString.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Module from 'module'; 3 | 4 | // https://github.com/floatdrop/require-from-string/blob/master/index.js 5 | export function importFromString(code: string, filename: string, opts: { appendPaths?: string[]; prependPaths?: string[] } = {}) { 6 | const appendPaths = opts.appendPaths || []; 7 | const prependPaths = opts.prependPaths || []; 8 | 9 | // @ts-expect-error 10 | const paths = Module._nodeModulePaths(path.dirname(filename)); 11 | 12 | const m = new Module(filename); 13 | m.filename = filename; 14 | m.paths = [...prependPaths, ...paths, ...appendPaths]; 15 | // @ts-expect-error 16 | m._compile(code, filename); 17 | 18 | const exports = m.exports; 19 | 20 | return exports; 21 | } 22 | -------------------------------------------------------------------------------- /packages/bob/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './importFromString'; 2 | -------------------------------------------------------------------------------- /packages/bob/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function cleanObject(obj: Partial = {}): Partial { 2 | const clean = { ...obj }; 3 | for (const key in clean) clean[key] === undefined && delete clean[key]; 4 | return clean; 5 | } 6 | -------------------------------------------------------------------------------- /packages/bob/src/utils/retry.ts: -------------------------------------------------------------------------------- 1 | export async function retry(cb: () => Promise, times: number = 3) { 2 | for (let i = 0; i < times - 1; ++i) { 3 | try { 4 | const value = await cb(); 5 | 6 | return value; 7 | } catch (err) {} 8 | } 9 | 10 | try { 11 | const value = await cb(); 12 | 13 | return value; 14 | } catch (err) { 15 | err instanceof Error && Error.captureStackTrace(err, retry); 16 | 17 | throw err; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/bob/src/watch.ts: -------------------------------------------------------------------------------- 1 | import { error } from './log/error'; 2 | import { watchRollup, WatchRollupOptions } from './rollup/watch'; 3 | import { buildTsc } from './tsc/build'; 4 | 5 | import type { TSCOptions } from './tsc/types'; 6 | 7 | export interface WatchOptions { 8 | rollup?: WatchRollupOptions; 9 | tsc?: TSCOptions | false; 10 | } 11 | 12 | export function startWatch(options: WatchOptions = {}) { 13 | return watchRollup({ 14 | ...options.rollup, 15 | onSuccessCallback(builds) { 16 | // Only for the first build we wait until it ends 17 | if (options.tsc !== false && builds === 0) { 18 | buildTsc(options.tsc).catch(error); 19 | } 20 | }, 21 | onStartCallback(builds) { 22 | // For subsequent builds we start building types on start 23 | if (options.tsc !== false && builds > 0) { 24 | buildTsc(options.tsc).catch(error); 25 | } 26 | }, 27 | }); 28 | } 29 | 30 | export type {} from 'rollup'; 31 | -------------------------------------------------------------------------------- /packages/bob/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/esbuild-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # bob-esbuild-plugin 2 | 3 | ## 4.0.0 4 | 5 | ### Major Changes 6 | 7 | - 9fb97f8: Require Node.js >=14.13.1 8 | 9 | ## 3.1.5 10 | 11 | ### Patch Changes 12 | 13 | - ee099eb: Update dependencies 14 | 15 | ## 3.1.4 16 | 17 | ### Patch Changes 18 | 19 | - 4889dc1: Bump 20 | 21 | ## 3.1.3 22 | 23 | ### Patch Changes 24 | 25 | - ebf0d1c: Bump recommended esbuild version 26 | 27 | ## 3.1.2 28 | 29 | ### Patch Changes 30 | 31 | - 54baca4: Fix esbuild peer dependency range 32 | 33 | ## 3.1.1 34 | 35 | ### Patch Changes 36 | 37 | - 75c77c6: Update & Require esbuild>=13.14 38 | 39 | ## 3.1.0 40 | 41 | ### Patch Changes 42 | 43 | - 078402e: Improve released package 44 | 45 | ## 2.2.0 46 | 47 | ### Patch Changes 48 | 49 | - 816d97e: Fix pass define option to bundle 50 | 51 | ## 2.1.0 52 | 53 | ### Minor Changes 54 | 55 | - 19b48c5: Improve tsconfig resolution using [tsconfck](https://github.com/dominikg/tsconfck) 56 | 57 | ## 2.0.0 58 | 59 | ### Major Changes 60 | 61 | - 7678c9a: esbuild as peer dependency 62 | 63 | ## 1.0.0 64 | 65 | ### Major Changes 66 | 67 | - b8df666: set bob-esbuild as peer dependency 68 | 69 | ## 0.2.4 70 | 71 | ### Patch Changes 72 | 73 | - 582b21f: fix bundle target 74 | 75 | ## 0.2.3 76 | 77 | ### Patch Changes 78 | 79 | - 8e3f251: sourcemap disabled by default 80 | 81 | ## 0.2.0 82 | 83 | ### Minor Changes 84 | 85 | - 1d3375e: update deps 86 | 87 | ### Patch Changes 88 | 89 | - 7dadf1b: filter plugins following rollup types change 90 | 91 | ## 0.1.21 92 | 93 | ### Patch Changes 94 | 95 | - c9d38ba: fix package.json rewrite w/ workspace protocol & sync packages 96 | 97 | ## 0.1.18 98 | 99 | ### Patch Changes 100 | 101 | - 26f57cb: pnpm with publishConfig.directory 102 | 103 | ## 0.1.17 104 | 105 | ### Patch Changes 106 | 107 | - 0092814: add buildOptions.bin support 108 | - faf71da: sync 109 | 110 | ## 0.1.1 111 | 112 | ### Patch Changes 113 | 114 | - 4c678c8: improve build 115 | -------------------------------------------------------------------------------- /packages/esbuild-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bob-esbuild-plugin", 3 | "version": "4.0.0", 4 | "description": "bob-esbuild main plugin, based on https://github.com/egoist/rollup-plugin-esbuild", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/PabloSzx/bob-esbuild", 8 | "directory": "packages/esbuild-plugin" 9 | }, 10 | "license": "MIT", 11 | "author": "PabloSzx ", 12 | "exports": { 13 | ".": { 14 | "require": "./lib/index.js", 15 | "import": "./lib/index.mjs" 16 | }, 17 | "./*": { 18 | "require": "./lib/*.js", 19 | "import": "./lib/*.mjs" 20 | } 21 | }, 22 | "main": "lib/index.js", 23 | "types": "lib/index.d.ts", 24 | "files": [ 25 | "lib" 26 | ], 27 | "scripts": { 28 | "prepare": "tsup && bun self-build.ts", 29 | "postpublish": "gh-release" 30 | }, 31 | "dependencies": { 32 | "@rollup/pluginutils": "^5.1.0" 33 | }, 34 | "devDependencies": { 35 | "changesets-github-release": "^0.1.0", 36 | "esbuild": "^0.19.11", 37 | "rollup": "^4.9.2", 38 | "tsconfck": "^3.0.0", 39 | "tsup": "^8.0.1" 40 | }, 41 | "peerDependencies": { 42 | "esbuild": ">=0.14.39", 43 | "rollup": "*" 44 | }, 45 | "peerDependenciesMeta": { 46 | "esbuild": { 47 | "optional": true 48 | }, 49 | "rollup": { 50 | "optional": true 51 | } 52 | }, 53 | "publishConfig": { 54 | "access": "public", 55 | "directory": "lib", 56 | "linkDirectory": false 57 | }, 58 | "tsup": { 59 | "entryPoints": [ 60 | "src/**/*.ts" 61 | ], 62 | "outDir": "lib", 63 | "clean": true, 64 | "format": [ 65 | "cjs", 66 | "esm" 67 | ], 68 | "target": "node12.20" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/esbuild-plugin/self-build.ts: -------------------------------------------------------------------------------- 1 | import { writePackageJson } from '../bob/src/config/packageJson'; 2 | import { buildTsc } from '../bob/src/tsc/build'; 3 | import pkg from './package.json'; 4 | 5 | async function main() { 6 | await Promise.all([ 7 | writePackageJson({ 8 | packageJson: pkg, 9 | distDir: 'lib', 10 | }), 11 | buildTsc(), 12 | ]); 13 | } 14 | 15 | main().catch(err => { 16 | console.error(err); 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/esbuild-plugin/src/bundle.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { build, Loader, BuildOptions } from 'esbuild'; 3 | import path from 'path'; 4 | import type { PluginContext, Plugin, LoadResult, TransformResult } from 'rollup'; 5 | 6 | export const bundle = async ( 7 | id: string, 8 | pluginContext: PluginContext, 9 | plugins: Plugin[], 10 | loaders: { 11 | [ext: string]: string; 12 | }, 13 | target: string | string[], 14 | buildOptions?: Partial> 15 | ) => { 16 | const transform = async (inputCode: string, id: string) => { 17 | let code: string | undefined; 18 | let map: any; 19 | for (const plugin of plugins) { 20 | if (plugin.transform && plugin.name !== 'esbuild' && typeof plugin.transform === 'function') { 21 | const transformed = (await plugin.transform.call( 22 | // @ts-expect-error 23 | pluginContext, 24 | inputCode, 25 | id 26 | )) as TransformResult; 27 | if (transformed == null) continue; 28 | if (typeof transformed === 'string') { 29 | code = transformed; 30 | } else if (typeof transformed === 'object') { 31 | if (transformed.code !== null) { 32 | code = transformed.code; 33 | } 34 | if (transformed.map !== null) { 35 | map = transformed.map; 36 | } 37 | } 38 | } 39 | } 40 | return { code, map }; 41 | }; 42 | 43 | const result = await build({ 44 | entryPoints: [id], 45 | format: 'esm', 46 | target, 47 | bundle: true, 48 | write: false, 49 | sourcemap: false, 50 | outdir: 'dist', 51 | platform: 'node', 52 | plugins: [ 53 | { 54 | name: 'rollup', 55 | setup: build => { 56 | build.onResolve({ filter: /.+/ }, async args => { 57 | const resolved = await pluginContext.resolve(args.path, args.importer); 58 | if (resolved == null) return; 59 | return { 60 | external: resolved.external === 'absolute' ? true : resolved.external, 61 | path: resolved.id, 62 | }; 63 | }); 64 | 65 | build.onLoad({ filter: /.+/ }, async args => { 66 | const loader = loaders[path.extname(args.path)] as Loader | undefined; 67 | 68 | let contents: string | undefined; 69 | for (const plugin of plugins) { 70 | if (plugin.load && plugin.name !== 'esbuild' && typeof plugin.load === 'function') { 71 | const loaded = (await plugin.load.call(pluginContext, args.path)) as LoadResult; 72 | if (loaded == null) { 73 | continue; 74 | } else if (typeof loaded === 'string') { 75 | contents = loaded; 76 | break; 77 | } else if (loaded && loaded.code) { 78 | contents = loaded.code; 79 | } 80 | } 81 | } 82 | 83 | if (contents == null) { 84 | contents = await fs.promises.readFile(args.path, 'utf8'); 85 | } 86 | 87 | const transformed = await transform(contents, args.path); 88 | if (transformed.code) { 89 | let code = transformed.code; 90 | if (transformed.map) { 91 | const map = Buffer.from( 92 | typeof transformed.map === 'string' ? transformed.map : JSON.stringify(transformed.map) 93 | ).toString('base64'); 94 | code += `\n//# sourceMappingURL=data:application/json;base64,${map}`; 95 | } 96 | return { 97 | contents: code, 98 | }; 99 | } 100 | return { 101 | contents, 102 | loader: loader || 'js', 103 | }; 104 | }); 105 | }, 106 | }, 107 | ], 108 | ...buildOptions, 109 | }); 110 | 111 | return { 112 | code: result.outputFiles.find(file => file.path.endsWith('.js'))?.text, 113 | map: result.outputFiles.find(file => file.path.endsWith('.map'))?.text, 114 | }; 115 | }; 116 | -------------------------------------------------------------------------------- /packages/esbuild-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createFilter, FilterPattern } from '@rollup/pluginutils'; 2 | import { formatMessages, Loader, Message, transform } from 'esbuild'; 3 | import { existsSync, statSync } from 'fs'; 4 | import { dirname, extname, join, resolve } from 'path'; 5 | import type { Plugin, PluginContext } from 'rollup'; 6 | import { bundle } from './bundle'; 7 | import { getTypescriptConfig } from './options'; 8 | 9 | const defaultLoaders: { [ext: string]: Loader } = { 10 | '.js': 'js', 11 | '.jsx': 'jsx', 12 | '.ts': 'ts', 13 | '.tsx': 'tsx', 14 | }; 15 | 16 | export type EsbuildPluginOptions = { 17 | include?: FilterPattern; 18 | exclude?: FilterPattern; 19 | sourceMap?: boolean | 'inline' | 'external' | 'both'; 20 | minify?: boolean; 21 | minifyWhitespace?: boolean; 22 | minifyIdentifiers?: boolean; 23 | minifySyntax?: boolean; 24 | target?: string | string[]; 25 | jsxFactory?: string; 26 | jsxFragment?: string; 27 | define?: { 28 | [k: string]: string; 29 | }; 30 | experimentalBundling?: boolean; 31 | /** 32 | * Use this tsconfig file instead 33 | * Disable it by setting to `false` 34 | */ 35 | tsconfig?: string | false; 36 | /** 37 | * Map extension to esbuild loader 38 | * Note that each entry (the extension) needs to start with a dot 39 | */ 40 | loaders?: { 41 | [ext: string]: Loader | false; 42 | }; 43 | }; 44 | 45 | const warn = async (pluginContext: PluginContext, messages: Message[]) => { 46 | if (messages.length > 0) { 47 | const warnings = await formatMessages(messages, { 48 | kind: 'warning', 49 | color: true, 50 | }); 51 | warnings.forEach(warning => pluginContext.warn(warning)); 52 | } 53 | }; 54 | 55 | export const bobEsbuildPlugin = (options: EsbuildPluginOptions = {}): Plugin => { 56 | let target: string | string[]; 57 | 58 | const loaders = { 59 | ...defaultLoaders, 60 | }; 61 | 62 | if (options.loaders) { 63 | for (const key of Object.keys(options.loaders)) { 64 | const value = options.loaders[key]; 65 | if (typeof value === 'string') { 66 | loaders[key] = value; 67 | } else if (value === false) { 68 | delete loaders[key]; 69 | } 70 | } 71 | } 72 | 73 | const extensions: string[] = Object.keys(loaders); 74 | const INCLUDE_REGEXP = new RegExp(`\\.(${extensions.map(ext => ext.slice(1)).join('|')})$`); 75 | const EXCLUDE_REGEXP = /node_modules/; 76 | 77 | const filter = createFilter(options.include || INCLUDE_REGEXP, options.exclude || EXCLUDE_REGEXP); 78 | 79 | const resolveFile = (resolved: string, index: boolean = false) => { 80 | for (const ext of extensions) { 81 | const file = index ? join(resolved, `index${ext}`) : `${resolved}${ext}`; 82 | if (existsSync(file)) return file; 83 | } 84 | return null; 85 | }; 86 | 87 | let plugins: Plugin[] = []; 88 | 89 | return { 90 | name: 'esbuild', 91 | 92 | resolveId(importee, importer) { 93 | if (!importer && importee[0] === '.') return; 94 | 95 | const resolved = resolve(importer ? dirname(importer) : process.cwd(), importee); 96 | 97 | let file = resolveFile(resolved); 98 | if (file) return file; 99 | if (!file && existsSync(resolved) && statSync(resolved).isDirectory()) { 100 | file = resolveFile(resolved, true); 101 | if (file) return file; 102 | } 103 | return; 104 | }, 105 | 106 | async options(options) { 107 | const customPlugins = (await options.plugins) || []; 108 | const customPluginsList = Array.isArray(customPlugins) ? customPlugins : [customPlugins]; 109 | 110 | plugins = customPluginsList?.filter((v): v is Plugin => !!v && typeof v === 'object') || []; 111 | return null; 112 | }, 113 | 114 | async load(id) { 115 | if (!options.experimentalBundling) return; 116 | 117 | const defaultOptions = 118 | options.target || options.tsconfig === false ? {} : await getTypescriptConfig(dirname(id), options.tsconfig); 119 | 120 | target = options.target || defaultOptions.target || 'es2019'; 121 | 122 | const bundled = await bundle(id, this, plugins, loaders, target, { 123 | define: options.define, 124 | }); 125 | 126 | if (!bundled.code) return; 127 | 128 | return { 129 | code: bundled.code, 130 | map: bundled.map, 131 | }; 132 | }, 133 | 134 | async transform(code, id) { 135 | // In bundle mode transformation is handled by esbuild too 136 | if (!filter(id) || options.experimentalBundling) { 137 | return null; 138 | } 139 | 140 | const ext = extname(id); 141 | const loader = loaders[ext]; 142 | 143 | if (!loader) { 144 | return null; 145 | } 146 | 147 | const defaultOptions = options.tsconfig === false ? {} : await getTypescriptConfig(dirname(id), options.tsconfig); 148 | 149 | target = options.target || defaultOptions.target || 'es2019'; 150 | 151 | const result = await transform(code, { 152 | loader, 153 | target, 154 | jsxFactory: options.jsxFactory || defaultOptions.jsxFactory, 155 | jsxFragment: options.jsxFragment || defaultOptions.jsxFragment, 156 | define: options.define, 157 | sourcemap: options.sourceMap, 158 | sourcefile: id, 159 | }); 160 | 161 | await warn(this, result.warnings); 162 | 163 | return ( 164 | result.code && { 165 | code: result.code, 166 | map: result.map || null, 167 | } 168 | ); 169 | }, 170 | 171 | async renderChunk(code) { 172 | if (options.minify || options.minifyWhitespace || options.minifyIdentifiers || options.minifySyntax) { 173 | const result = await transform(code, { 174 | loader: 'js', 175 | minify: options.minify, 176 | minifyWhitespace: options.minifyWhitespace, 177 | minifyIdentifiers: options.minifyIdentifiers, 178 | minifySyntax: options.minifySyntax, 179 | target, 180 | sourcemap: options.sourceMap, 181 | }); 182 | await warn(this, result.warnings); 183 | if (result.code) { 184 | return { 185 | code: result.code, 186 | map: result.map || null, 187 | }; 188 | } 189 | } 190 | return null; 191 | }, 192 | }; 193 | }; 194 | -------------------------------------------------------------------------------- /packages/esbuild-plugin/src/options.ts: -------------------------------------------------------------------------------- 1 | import { parse, TSConfckCache, TSConfckParseNativeResult, TSConfckParseResult } from 'tsconfck'; 2 | 3 | const cache = new TSConfckCache(); 4 | 5 | export const getTypescriptConfig = async ( 6 | cwd: string, 7 | tsconfig?: string 8 | ): Promise<{ jsxFactory?: string; jsxFragment?: string; target?: string }> => { 9 | const config = await parse(tsconfig || cwd, { 10 | cache, 11 | }); 12 | 13 | const { jsxFactory, jsxFragmentFactory, target } = config.tsconfig.compilerOptions || {}; 14 | return { 15 | jsxFactory, 16 | jsxFragment: jsxFragmentFactory, 17 | target: target && target.toLowerCase(), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/esbuild-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 4 | "rangeStrategy": "bump", 5 | "ignoreDeps": ["pnpm", "node"], 6 | "ignorePaths": ["**/node_modules/**"] 7 | } 8 | -------------------------------------------------------------------------------- /scripts/canary-release.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const semver = require('semver'); 3 | const cp = require('child_process'); 4 | const { basename } = require('path'); 5 | 6 | const { read: readConfig } = require('@changesets/config'); 7 | const readChangesets = require('@changesets/read').default; 8 | const assembleReleasePlan = require('@changesets/assemble-release-plan').default; 9 | const applyReleasePlan = require('@changesets/apply-release-plan').default; 10 | const { getPackages } = require('@manypkg/get-packages'); 11 | 12 | function getNewVersion(version, type) { 13 | const gitHash = cp.spawnSync('git', ['rev-parse', '--short', 'HEAD']).stdout.toString().trim(); 14 | 15 | return semver.inc(version, `pre${type}`, true, 'alpha-' + gitHash); 16 | } 17 | 18 | function getRelevantChangesets(baseBranch) { 19 | const comparePoint = cp 20 | .spawnSync('git', ['merge-base', `origin/${baseBranch}`, 'HEAD']) 21 | .stdout.toString() 22 | .trim(); 23 | console.log('compare point', comparePoint); 24 | const listModifiedFiles = cp.spawnSync('git', ['diff', '--name-only', comparePoint]).stdout.toString().trim().split('\n'); 25 | console.log('listModifiedFiles', listModifiedFiles); 26 | 27 | const items = listModifiedFiles.filter(f => f.startsWith('.changeset')).map(f => basename(f, '.md')); 28 | console.log('items', items); 29 | 30 | return items; 31 | } 32 | 33 | async function updateVersions() { 34 | const cwd = process.cwd(); 35 | const packages = await getPackages(cwd); 36 | const config = await readConfig(cwd, packages); 37 | const modifiedChangesets = getRelevantChangesets(config.baseBranch); 38 | const changesets = (await readChangesets(cwd)).filter(change => modifiedChangesets.includes(change.id)); 39 | 40 | if (changesets.length === 0) { 41 | console.warn(`Unable to find any relevant package for canary publishing. Please make sure changesets exists!`); 42 | process.exit(1); 43 | } else { 44 | const releasePlan = assembleReleasePlan(changesets, packages, config, undefined, false); 45 | 46 | if (releasePlan.releases.length === 0) { 47 | console.warn(`Unable to find any relevant package for canary releasing. Please make sure changesets exists!`); 48 | process.exit(1); 49 | } else { 50 | for (const release of releasePlan.releases) { 51 | if (release.type !== 'none') { 52 | release.newVersion = getNewVersion(release.oldVersion, release.type); 53 | } 54 | } 55 | 56 | await applyReleasePlan( 57 | releasePlan, 58 | packages, 59 | { 60 | ...config, 61 | commit: false, 62 | }, 63 | false, 64 | true 65 | ); 66 | } 67 | } 68 | } 69 | 70 | updateVersions() 71 | .then(() => { 72 | console.info(`Done!`); 73 | }) 74 | .catch(err => { 75 | console.error(err); 76 | process.exit(1); 77 | }); 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true /* Enable incremental compilation */, 4 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 5 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 6 | "declaration": true /* Generates corresponding '.d.ts' file. */, 7 | "emitDeclarationOnly": true, 8 | "outDir": "lib-types" /* Redirect output structure to the directory. */, 9 | "rootDir": ".", 10 | "strict": true /* Enable all strict type-checking options. */, 11 | "noUnusedLocals": true /* Report errors on unused locals. */, 12 | "noUnusedParameters": true /* Report errors on unused parameters. */, 13 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 14 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 15 | "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, 16 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 17 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 18 | "skipLibCheck": true /* Skip type checking of declaration files. */, 19 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 20 | "paths": { 21 | "shared/*": ["examples/shared/src/*"], 22 | "shared": ["examples/shared/src/index.ts"], 23 | "bob-esbuild": ["packages/bob/src/index.ts"], 24 | "bob-esbuild/*": ["packages/bob/src/*"], 25 | "bob-esbuild-plugin": ["packages/esbuild-plugin/src/index.ts"], 26 | "bob-watch": ["packages/bob-watch/src/*"], 27 | "aliased-deep": ["examples/basic/src/aliased/deep/module.ts"], 28 | "~*": ["*"], 29 | "*": ["_"] 30 | }, 31 | "baseUrl": ".", 32 | "resolveJsonModule": true 33 | }, 34 | "include": ["**/types.d.ts", "**/src/**/*.ts", "**/test/**/*.ts", "bob-esbuild.config.ts", "packages/**/*.ts"] 35 | } 36 | --------------------------------------------------------------------------------