├── .commitlintrc.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yml │ ├── 2-feature-request.yml │ └── config.yml └── workflows │ ├── ci.yml │ ├── semgrep.yml │ └── validate-pr-title.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .verdaccio └── config.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── apps └── demo │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ └── nx-welcome.component.ts │ ├── assets │ │ └── .gitkeep │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── styles.scss │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── jest.config.ts ├── jest.preset.js ├── nx.json ├── package.json ├── packages └── ngx-esbuild │ ├── .eslintrc.json │ ├── README.md │ ├── executors.json │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── executors │ │ └── build │ │ │ ├── esbuild │ │ │ ├── get-esbuild-options.spec.ts │ │ │ ├── get-esbuild-options.ts │ │ │ └── plugins │ │ │ │ ├── __fixtures__ │ │ │ │ ├── angular-component-resources │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── foo │ │ │ │ │ │ ├── foo.component.html │ │ │ │ │ │ ├── foo.component.scss │ │ │ │ │ │ └── foo.component.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── angular-injectables │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── react.tsx │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── compile-fixture.ts │ │ │ │ ├── file-replacements │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── env │ │ │ │ │ │ ├── environment.dev.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── global-scripts │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── scripts │ │ │ │ │ │ ├── 1.js │ │ │ │ │ │ └── 2.js │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── global-styles │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── styles │ │ │ │ │ │ ├── 1.scss │ │ │ │ │ │ └── 2.css │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── polyfills │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── node_modules │ │ │ │ │ │ └── window-location-origin │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ └── package.json │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ └── shared-worker │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── shared-worker.worker.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── angular-component-resources.spec.ts │ │ │ │ ├── angular-component-resources.ts │ │ │ │ ├── assets.spec.ts │ │ │ │ ├── assets.ts │ │ │ │ ├── babel.spec.ts │ │ │ │ ├── babel.ts │ │ │ │ ├── babel │ │ │ │ ├── transform-angular-di │ │ │ │ │ ├── plugin.spec.ts │ │ │ │ │ └── plugin.ts │ │ │ │ ├── transform-dynamic-import-commonjs-interop │ │ │ │ │ ├── plugin.spec.ts │ │ │ │ │ └── plugin.ts │ │ │ │ ├── transform-inline-angular-component-resources │ │ │ │ │ ├── plugin.spec.ts │ │ │ │ │ └── plugin.ts │ │ │ │ ├── transform-new-worker-url │ │ │ │ │ ├── plugin.spec.ts │ │ │ │ │ └── plugin.ts │ │ │ │ └── transform-webpack-eager-mode │ │ │ │ │ ├── plugin.spec.ts │ │ │ │ │ └── plugin.ts │ │ │ │ ├── delete-output-directory.ts │ │ │ │ ├── dev-server.ts │ │ │ │ ├── dev-server │ │ │ │ ├── create-dev-server.ts │ │ │ │ ├── shared-constants.ts │ │ │ │ └── websocket-client.ts │ │ │ │ ├── global-scripts.spec.ts │ │ │ │ ├── global-scripts.ts │ │ │ │ ├── global-styles.spec.ts │ │ │ │ ├── global-styles.ts │ │ │ │ ├── log-build-state.ts │ │ │ │ ├── polyfills.spec.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── utils │ │ │ │ ├── babel-transform.ts │ │ │ │ ├── cacheable-plugin-on-load-handler.ts │ │ │ │ ├── file-read-cache.ts │ │ │ │ ├── get-file-replacements.ts │ │ │ │ ├── memory-cache.ts │ │ │ │ └── types │ │ │ │ │ └── plugin-cache.ts │ │ │ │ ├── worker.spec.ts │ │ │ │ └── worker.ts │ │ │ ├── executor.spec.ts │ │ │ ├── executor.ts │ │ │ ├── schema.d.ts │ │ │ ├── schema.json │ │ │ └── target-config │ │ │ ├── get-target-config.spec.ts │ │ │ └── get-target-config.ts │ └── index.ts │ ├── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── pnpm-lock.yaml ├── project.json ├── tools └── scripts │ └── publish.mjs └── tsconfig.base.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional", 4 | "@commitlint/config-nx-scopes" 5 | ], 6 | "rules": { 7 | "footer-max-line-length": [0], 8 | "body-max-line-length": [0] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": "*.json", 8 | "parser": "jsonc-eslint-parser", 9 | "rules": {} 10 | }, 11 | { 12 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 13 | "rules": { 14 | "@nx/enforce-module-boundaries": [ 15 | "error", 16 | { 17 | "enforceBuildableLibDependency": true, 18 | "allow": [], 19 | "depConstraints": [ 20 | { 21 | "sourceTag": "*", 22 | "onlyDependOnLibsWithTags": ["*"] 23 | } 24 | ] 25 | } 26 | ] 27 | } 28 | }, 29 | { 30 | "files": ["*.ts", "*.tsx"], 31 | "extends": ["plugin:@nx/typescript"], 32 | "rules": {} 33 | }, 34 | { 35 | "files": ["*.js", "*.jsx"], 36 | "extends": ["plugin:@nx/javascript"], 37 | "rules": {} 38 | }, 39 | { 40 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 41 | "env": { 42 | "jest": true 43 | }, 44 | "rules": {} 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Oh hi there! 8 | 9 | To expedite issue processing please search open and closed issues before submitting a new one. 10 | Existing issues often contain information about workarounds, resolution, or progress updates. 11 | - type: checkboxes 12 | id: is-regression 13 | attributes: 14 | label: Is this a regression? 15 | description: Did this behavior use to work in the previous version? 16 | options: 17 | - label: Yes, this behavior used to work in the previous version 18 | - type: input 19 | id: version-bug-was-not-present 20 | attributes: 21 | label: The previous version in which this bug was not present was 22 | validations: 23 | required: false 24 | - type: textarea 25 | id: description 26 | attributes: 27 | label: Description 28 | description: A clear and concise description of the problem. 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: minimal-reproduction 33 | attributes: 34 | label: Minimal Reproduction 35 | description: | 36 | Simple steps to reproduce this bug. 37 | 38 | **Please include:** 39 | * commands run (including args) 40 | * packages added 41 | * related code changes 42 | 43 | 44 | If reproduction steps are not enough for reproduction of your issue, please create a minimal GitHub repository with the reproduction of the issue. 45 | A good way to make a minimal reproduction is to create a new app and add the minimum possible code to show the problem. 46 | Share the link to the repo below along with step-by-step instructions to reproduce the problem, as well as expected and actual behavior. 47 | 48 | Issues that don't have enough info and can't be reproduced will be closed. 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: exception-or-error 53 | attributes: 54 | label: Exception or Error 55 | description: If the issue is accompanied by an exception or an error, please share it below. 56 | render: text 57 | validations: 58 | required: false 59 | - type: textarea 60 | id: other 61 | attributes: 62 | label: Anything else relevant? 63 | description: | 64 | Do any of these matter: operating system, IDE, package manager, HTTP server, ...? If so, please mention it below. 65 | validations: 66 | required: false 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Oh hi there! 8 | 9 | To expedite issue processing please search open and closed issues before submitting a new one. 10 | Existing issues often contain information about workarounds, resolution, or progress updates. 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: Description 15 | description: A clear and concise description of the problem or missing capability. 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: desired-solution 20 | attributes: 21 | label: Describe the solution you'd like 22 | description: If you have a solution in mind, please describe it. 23 | validations: 24 | required: false 25 | - type: textarea 26 | id: alternatives 27 | attributes: 28 | label: Describe alternatives you've considered 29 | description: Have you considered any alternative solutions or workarounds? 30 | validations: 31 | required: false 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support request 4 | url: https://github.com/clickup/ngx-esbuild/discussions 5 | about: Questions and requests for support. 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | # Needed for nx-set-shas within nx-cloud-main.yml, when run on the main branch 10 | permissions: 11 | actions: read 12 | contents: read 13 | 14 | jobs: 15 | main: 16 | name: Nx Cloud - Main Job 17 | uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.14.0 18 | with: 19 | main-branch-name: main 20 | number-of-agents: 3 21 | init-commands: | 22 | npx nx-cloud start-ci-run --stop-agents-after="build" --agent-count=3 23 | parallel-commands: | 24 | npx nx-cloud record -- npx nx format:check 25 | parallel-commands-on-agents: | 26 | npx nx affected --target=lint --parallel=3 27 | npx nx affected --target=test --parallel=3 --ci --code-coverage 28 | npx nx affected --target=build --parallel=3 29 | 30 | agents: 31 | name: Nx Cloud - Agents 32 | uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.14.0 33 | with: 34 | number-of-agents: 3 35 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | # Name of this GitHub Actions workflow. 2 | name: Semgrep 3 | 4 | on: 5 | # Scan changed files in PRs (diff-aware scanning): 6 | pull_request: 7 | branches: ['main'] 8 | 9 | # Schedule the CI job (this method uses cron syntax): 10 | schedule: 11 | - cron: '0 0 * * MON-FRI' 12 | 13 | jobs: 14 | semgrep: 15 | # User definable name of this GitHub Actions job. 16 | name: Scan 17 | # If you are self-hosting, change the following `runs-on` value: 18 | runs-on: ubuntu-latest 19 | 20 | container: 21 | # A Docker image with Semgrep installed. Do not change this. 22 | image: returntocorp/semgrep@sha256:6c7ab81e4d1fd25a09f89f1bd52c984ce107c6ff33affef6ca3bc626a4cc479b 23 | 24 | # Skip any PR created by dependabot to avoid permission issues: 25 | if: (github.actor != 'dependabot[bot]') 26 | 27 | steps: 28 | # Fetch project source with GitHub Actions Checkout. 29 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 30 | # Run the "semgrep ci" command on the command line of the docker image. 31 | - run: semgrep ci 32 | env: 33 | # Connect to Semgrep Cloud Platform through your SEMGREP_APP_TOKEN. 34 | # Generate a token from Semgrep Cloud Platform > Settings 35 | # and add it to your GitHub secrets. 36 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Validate PR title 2 | 3 | on: 4 | pull_request: 5 | types: ['opened', 'edited', 'reopened', 'synchronize'] 6 | 7 | permissions: 8 | pull-requests: read 9 | contents: read 10 | 11 | jobs: 12 | validate-pr-title: 13 | runs-on: ubuntu-latest 14 | name: 'Validate PR title' 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598 # v2.4.0 20 | - name: Setup NodeJs 21 | id: setup-node 22 | uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 23 | with: 24 | cache: pnpm 25 | - name: Install dependencies 26 | run: pnpm install 27 | - name: Lint PR title 28 | # TODO - forked until https://github.com/JulienKode/pull-request-name-linter-action/pull/227 is merged 29 | uses: mattlewis92/pull-request-name-linter-action@9c7a21391dfe3e08dbc7247a1a748422a21d640c # v0.5.0 30 | with: 31 | configuration-path: './.commitlintrc.json' 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .nx/cache 42 | 43 | .angular 44 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | .github/workflows/nx-cloud-agents.yml 6 | .github/workflows/nx-cloud-main.yml 7 | .angular 8 | pnpm-lock.yaml 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.verdaccio/config.yml: -------------------------------------------------------------------------------- 1 | # path to a directory with all packages 2 | storage: ../tmp/local-registry/storage 3 | 4 | # a list of other known repositories we can talk to 5 | uplinks: 6 | npmjs: 7 | url: https://registry.npmjs.org/ 8 | maxage: 60m 9 | 10 | packages: 11 | '**': 12 | # give all users (including non-authenticated users) full access 13 | # because it is a local registry 14 | access: $all 15 | publish: $all 16 | unpublish: $all 17 | 18 | # if package is not available locally, proxy requests to npm registry 19 | proxy: npmjs 20 | 21 | # log settings 22 | logs: 23 | type: stdout 24 | format: pretty 25 | level: warn 26 | 27 | publish: 28 | allow_offline: true # set offline to true to allow publish offline 29 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["json"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ClickUp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-esbuild 2 | 3 | [![Build Status](https://github.com/clickup/ngx-esbuild/actions/workflows/ci.yml/badge.svg)](https://github.com/clickup/ngx-esbuild/actions/workflows/ci.yml) 4 | [![npm version](https://badge.fury.io/js/@clickup%2Fngx-esbuild.svg)](http://badge.fury.io/js/@clickup%2Fngx-esbuild) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | > [ClickUp](https://clickup.com/)'s esbuild powered local dev server, open sourced so you can speed up developing your own Angular applications! 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | ## About 16 | 17 | This is an alternative local development environment for large Angular applications, powered by [esbuild](https://esbuild.github.io/). 18 | 19 | It makes a different set of trade-offs than the [official Angular CLI esbuild solution](https://angular.io/guide/esbuild) to achieve faster build times and use less memory, namely: 20 | 21 | - It does not typecheck your code 22 | - As it does not typecheck, it cannot AoT compile your code either 23 | - It is designed for local development only, and does not support building for production 24 | 25 | It mainly works by implementing a version of these 2 ideas: 26 | 27 | - https://github.com/angular/angular/issues/43131 28 | - https://github.com/angular/angular/issues/43165 29 | 30 | Hopefully one day, the Angular CLI will support some of this out of the box, but until then, this is a great alternative. 31 | 32 | ## Why would I use this? 33 | 34 | - You have a large Angular application 35 | - Local dev rebuilds are slow or use too much memory 36 | - You are not using buildable libraries or module federation 37 | - AOT / Typechecking is not essential 38 | - You've already tried the [Angular CLI's esbuild solution](https://angular.io/guide/esbuild) and it's not fast enough for you 39 | 40 | ## Getting Started 41 | 42 | > [!IMPORTANT] 43 | > Currently this only works with Nx workspaces, but we're planning on making it work with regular Angular CLI projects as well. See https://github.com/clickup/ngx-esbuild/issues/3 for more info. 44 | 45 | Install with your favorite package manager: 46 | 47 | ```bash 48 | npm install -D @clickup/ngx-esbuild 49 | ``` 50 | 51 | Add a new target to your apps `project.json` (assuming you have a `build` target using the `@angular-devkit/build-angular:browser` or `@angular-devkit/build-angular:browser-esbuild` executors): 52 | 53 | ```json 54 | "targets": { 55 | ... other targets ... 56 | "serve-esbuild": { 57 | "executor": "@clickup/ngx-esbuild:build", 58 | "options": { 59 | "serve": true 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | Run with `nx serve-esbuild ` to start the dev server powered by esbuild! 66 | 67 | ### Typechecking 68 | 69 | The builder is fast as it makes a different set of trade-offs than the Angular CLI esbuild solution. Namely, it doesn't do any typechecking. 70 | 71 | While showing type errors in your IDE works to some extent, you probably want to still be able to typecheck your entire project. 72 | 73 | So to enable typechecking, you can add another target like this: 74 | 75 | ```json 76 | "type-check": { 77 | "executor": "nx:run-commands", 78 | "options": { 79 | "command": "npx tsc -p apps/your-app/tsconfig.app.json --noEmit --watch --incremental --pretty" 80 | } 81 | } 82 | ``` 83 | 84 | Then run with `nx type-check ` 85 | 86 | If you want to type-check component templates, you can run the same command but replace `tsc` with `ngc` instead (this will use a much larger amount of memory though and may be more likely to cause performance problems): 87 | 88 | ``` 89 | "command": "npx ngc -p apps/your-app/tsconfig.app.json --noEmit --watch --incremental --pretty" 90 | ``` 91 | 92 | You can even run the dev server + typechecking side by side using [stmux](https://www.npmjs.com/package/stmux): 93 | 94 | ```bash 95 | stmux -e '' -- [ "nx serve-esbuild demo" .. "nx type-check demo" ] 96 | ``` 97 | 98 | ## Supported angular devkit options 99 | 100 | These options will be read from the existing `build` target that uses the angular devkit builder. 101 | 102 | ### Supported 103 | 104 | Many of these options only support a subset of different ways that they can be configured by the Angular CLI. If something doesn't work in your project, please file an issue and we can probably add support! 105 | 106 | - `assets` (partially supported) 107 | - `main` 108 | - `polyfills` 109 | - `tsConfig` 110 | - `scripts` (partially supported) 111 | - `styles` (partially supported) 112 | - `stylePreprocessorOptions` (only scss is supported currently) 113 | - `fileReplacements` (partially supported) 114 | - `outputPath` 115 | - `sourceMap` (partially supported) 116 | - `index` (partially supported) 117 | - `webWorkerTsConfig` 118 | 119 | ### Unsupported (none of these options will have any effect) 120 | 121 | This solution is intended to only ever work for local development, and will never support building for production. So, any options related to production builds will never be supported, for everything else it may be possible to add support in the future. 122 | 123 | - `inlineStyleLanguage` 124 | - `optimization` 125 | - `resourcesOutputPath` 126 | - `aot` 127 | - `vendorChunk` 128 | - `commonChunk` 129 | - `baseHref` 130 | - `deployUrl` 131 | - `verbose` 132 | - `progress` 133 | - `i18nMissingTranslation` 134 | - `i18nDuplicateTranslation` 135 | - `localize` 136 | - `watch` 137 | - `outputHashing` 138 | - `poll` 139 | - `deleteOutputPath` 140 | - `preserveSymlinks` 141 | - `extractLicenses` 142 | - `buildOptimizer` 143 | - `namedChunks` 144 | - `subresourceIntegrity` 145 | - `serviceWorker` 146 | - `ngswConfigPath` 147 | - `statsJson` 148 | - `budgets` 149 | - `crossOrigin` 150 | - `allowedCommonJsDependencies` 151 | 152 | ## Local development 153 | 154 | - Ensure you have Node 18 or higher installed 155 | - Install pnpm: `corepack enable` 156 | - Install local dev dependencies: `pnpm install` 157 | 158 | ### Running tests 159 | 160 | ```bash 161 | pnpm nx affected:test 162 | ``` 163 | 164 | ### Linting 165 | 166 | ```bash 167 | pnpm nx affected:lint 168 | ``` 169 | 170 | ### Running the demo app 171 | 172 | ```bash 173 | pnpm demo 174 | ``` 175 | -------------------------------------------------------------------------------- /apps/demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "clickup", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "clickup", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/demo/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'demo', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | coverageDirectory: '../../coverage/apps/demo', 7 | transform: { 8 | '^.+\\.(ts|mjs|js|html)$': [ 9 | 'jest-preset-angular', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | stringifyContentPathRegex: '\\.(html|svg)$', 13 | }, 14 | ], 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /apps/demo/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "prefix": "clickup", 6 | "sourceRoot": "apps/demo/src", 7 | "tags": [], 8 | "targets": { 9 | "build": { 10 | "executor": "@angular-devkit/build-angular:browser", 11 | "outputs": ["{options.outputPath}"], 12 | "options": { 13 | "outputPath": "dist/apps/demo", 14 | "index": "apps/demo/src/index.html", 15 | "main": "apps/demo/src/main.ts", 16 | "polyfills": ["zone.js"], 17 | "tsConfig": "apps/demo/tsconfig.app.json", 18 | "assets": ["apps/demo/src/favicon.ico", "apps/demo/src/assets"], 19 | "styles": ["apps/demo/src/styles.scss"], 20 | "scripts": [] 21 | }, 22 | "configurations": { 23 | "production": { 24 | "budgets": [ 25 | { 26 | "type": "initial", 27 | "maximumWarning": "500kb", 28 | "maximumError": "1mb" 29 | }, 30 | { 31 | "type": "anyComponentStyle", 32 | "maximumWarning": "2kb", 33 | "maximumError": "4kb" 34 | } 35 | ], 36 | "outputHashing": "all" 37 | }, 38 | "development": { 39 | "buildOptimizer": false, 40 | "optimization": false, 41 | "vendorChunk": true, 42 | "extractLicenses": false, 43 | "sourceMap": true, 44 | "namedChunks": true 45 | } 46 | }, 47 | "defaultConfiguration": "production" 48 | }, 49 | "serve": { 50 | "executor": "@angular-devkit/build-angular:dev-server", 51 | "options": { 52 | "open": true 53 | }, 54 | "configurations": { 55 | "production": { 56 | "browserTarget": "demo:build:production" 57 | }, 58 | "development": { 59 | "browserTarget": "demo:build:development" 60 | } 61 | }, 62 | "defaultConfiguration": "development" 63 | }, 64 | "extract-i18n": { 65 | "executor": "@angular-devkit/build-angular:extract-i18n", 66 | "options": { 67 | "browserTarget": "demo:build" 68 | } 69 | }, 70 | "lint": { 71 | "executor": "@nx/eslint:lint", 72 | "outputs": ["{options.outputFile}"], 73 | "options": { 74 | "lintFilePatterns": ["apps/demo/**/*.ts", "apps/demo/**/*.html"] 75 | } 76 | }, 77 | "test": { 78 | "executor": "@nx/jest:jest", 79 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 80 | "options": { 81 | "jestConfig": "apps/demo/jest.config.ts", 82 | "passWithNoTests": true 83 | }, 84 | "configurations": { 85 | "ci": { 86 | "ci": true, 87 | "codeCoverage": true 88 | } 89 | } 90 | }, 91 | "build-esbuild": { 92 | "executor": "@clickup/ngx-esbuild:build" 93 | }, 94 | "serve-esbuild": { 95 | "executor": "@clickup/ngx-esbuild:build", 96 | "options": { 97 | "serve": true 98 | } 99 | }, 100 | "type-check": { 101 | "executor": "nx:run-commands", 102 | "options": { 103 | "command": "pnpm tsc -p apps/demo/tsconfig.app.json --noEmit --watch --incremental --pretty" 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clickup/ngx-esbuild/9649dedf36ee6d36d4ee17db1368448beeccd7b3/apps/demo/src/app/app.component.scss -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { NxWelcomeComponent } from './nx-welcome.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [AppComponent, NxWelcomeComponent], 9 | }).compileComponents(); 10 | }); 11 | 12 | it('should render title', () => { 13 | const fixture = TestBed.createComponent(AppComponent); 14 | fixture.detectChanges(); 15 | const compiled = fixture.nativeElement as HTMLElement; 16 | expect(compiled.querySelector('h1')?.textContent).toContain('Welcome demo'); 17 | }); 18 | 19 | it(`should have as title 'demo'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.componentInstance; 22 | expect(app.title).toEqual('demo'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NxWelcomeComponent } from './nx-welcome.component'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [NxWelcomeComponent], 7 | selector: 'clickup-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'], 10 | }) 11 | export class AppComponent { 12 | title = 'demo'; 13 | } 14 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | 3 | export const appConfig: ApplicationConfig = { 4 | providers: [], 5 | }; 6 | -------------------------------------------------------------------------------- /apps/demo/src/app/nx-welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @Component({ 5 | selector: 'clickup-nx-welcome', 6 | standalone: true, 7 | imports: [CommonModule], 8 | template: ` 9 | 16 | 420 |
421 |
422 | 423 |
424 |

425 | Hello there, 426 | Welcome demo 👋 427 |

428 |
429 | 430 |
431 |
432 |

433 | 439 | 445 | 446 | You're up and running 447 |

448 | What's next? 449 |
450 |
451 | 457 | 460 | 461 |
462 |
463 | 464 | 764 | 765 |
766 |

Next steps

767 |

Here are some things you can do with Nx:

768 |
769 | 770 | 776 | 782 | 783 | Add UI library 784 | 785 |
# Generate UI lib
786 | nx g @nx/angular:lib ui
787 | # Add a component
788 | nx g @nx/angular:component button --project ui
789 |
790 |
791 | 792 | 798 | 804 | 805 | View interactive project graph 806 | 807 |
nx graph
808 |
809 |
810 | 811 | 817 | 823 | 824 | Run affected commands 825 | 826 |
# see what's been affected by changes
827 | nx affected:graph
828 | # run tests for current changes
829 | nx affected:test
830 | # run e2e tests for current changes
831 | nx affected:e2e
832 |
833 |
834 |

835 | Carefully crafted with 836 | 842 | 848 | 849 |

850 |
851 |
852 | `, 853 | styles: [], 854 | encapsulation: ViewEncapsulation.None, 855 | }) 856 | export class NxWelcomeComponent {} 857 | -------------------------------------------------------------------------------- /apps/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clickup/ngx-esbuild/9649dedf36ee6d36d4ee17db1368448beeccd7b3/apps/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clickup/ngx-esbuild/9649dedf36ee6d36d4ee17db1368448beeccd7b3/apps/demo/src/favicon.ico -------------------------------------------------------------------------------- /apps/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => 6 | console.error(err) 7 | ); 8 | -------------------------------------------------------------------------------- /apps/demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /apps/demo/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment 2 | globalThis.ngJest = { 3 | testEnvironmentOptions: { 4 | errorOnUnknownElements: true, 5 | errorOnUnknownProperties: true, 6 | }, 7 | }; 8 | import 'jest-preset-angular/setup-jest'; 9 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts"], 8 | "include": ["src/**/*.d.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.app.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | }, 21 | { 22 | "path": "./tsconfig.editor.json" 23 | } 24 | ], 25 | "extends": "../../tsconfig.base.json", 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "target": "es2016", 7 | "types": ["jest", "node"] 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": [ 11 | "jest.config.ts", 12 | "src/**/*.test.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nx/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "targetDefaults": { 4 | "build": { 5 | "cache": true, 6 | "dependsOn": ["^build"], 7 | "inputs": ["production", "^production"] 8 | }, 9 | "lint": { 10 | "cache": true, 11 | "inputs": [ 12 | "default", 13 | "{workspaceRoot}/.eslintrc.json", 14 | "{workspaceRoot}/.eslintignore", 15 | "{workspaceRoot}/eslint.config.js" 16 | ] 17 | }, 18 | "test": { 19 | "cache": true, 20 | "inputs": ["default", "^default", "{workspaceRoot}/jest.preset.js"] 21 | }, 22 | "e2e": { 23 | "cache": true 24 | } 25 | }, 26 | "workspaceLayout": { 27 | "appsDir": "apps", 28 | "libsDir": "packages" 29 | }, 30 | "namedInputs": { 31 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 32 | "production": ["default"], 33 | "sharedGlobals": [] 34 | }, 35 | "nxCloudAccessToken": "NDQ5ZjRlZmEtNzY5MC00NjgxLWE0ZGMtZGY3YmY0OGJkZmQzfHJlYWQtd3JpdGU=", 36 | "generators": { 37 | "@nx/angular:application": { 38 | "style": "scss", 39 | "linter": "eslint", 40 | "unitTestRunner": "jest", 41 | "e2eTestRunner": "none" 42 | }, 43 | "@nx/angular:library": { 44 | "linter": "eslint", 45 | "unitTestRunner": "jest" 46 | }, 47 | "@nx/angular:component": { 48 | "style": "scss" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clickup/root", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "demo": "stmux -e '' -- [ \"pnpm nx build-esbuild demo --serve\" .. \"pnpm nx type-check demo\" ]", 8 | "prepare": "husky install" 9 | }, 10 | "devDependencies": { 11 | "@angular-devkit/build-angular": "~16.2.0", 12 | "@angular-devkit/core": "~16.2.0", 13 | "@angular-devkit/schematics": "~16.2.0", 14 | "@angular-eslint/eslint-plugin": "~16.0.0", 15 | "@angular-eslint/eslint-plugin-template": "~16.0.0", 16 | "@angular-eslint/template-parser": "~16.0.0", 17 | "@angular/cli": "~16.2.0", 18 | "@angular/compiler-cli": "~16.2.0", 19 | "@angular/language-service": "~16.2.0", 20 | "@babel/core": "^7.23.2", 21 | "@babel/plugin-syntax-decorators": "^7.22.10", 22 | "@babel/plugin-syntax-typescript": "^7.22.5", 23 | "@babel/types": "^7.23.0", 24 | "@commitlint/cli": "^18.2.0", 25 | "@commitlint/config-conventional": "^18.1.0", 26 | "@commitlint/config-nx-scopes": "^18.3.0", 27 | "@craftamap/esbuild-plugin-html": "^0.6.1", 28 | "@nx/angular": "^17.0.2", 29 | "@nx/eslint": "17.0.2", 30 | "@nx/eslint-plugin": "17.0.2", 31 | "@nx/jest": "17.0.2", 32 | "@nx/js": "17.0.2", 33 | "@nx/plugin": "^17.0.2", 34 | "@nx/workspace": "17.0.2", 35 | "@schematics/angular": "~16.2.0", 36 | "@swc-node/register": "~1.6.7", 37 | "@swc/cli": "~0.1.62", 38 | "@swc/core": "~1.3.85", 39 | "@types/babel__core": "^7.20.3", 40 | "@types/express": "^4.17.20", 41 | "@types/jest": "^29.4.0", 42 | "@types/lodash": "^4.14.200", 43 | "@types/node": "18.7.1", 44 | "@types/postcss-import": "^14.0.2", 45 | "@types/postcss-url": "^10.0.2", 46 | "@types/react-dom": "^18.2.14", 47 | "@types/strip-comments": "^2.0.3", 48 | "@types/ws": "^8.5.8", 49 | "@typescript-eslint/eslint-plugin": "^5.60.1", 50 | "@typescript-eslint/parser": "^5.60.1", 51 | "babel-plugin-tester": "^11.0.4", 52 | "chalk": "^4.1.2", 53 | "cheerio": "1.0.0-rc.12", 54 | "esbuild": "^0.19.5", 55 | "esbuild-plugin-copy": "^2.1.1", 56 | "esbuild-sass-plugin": "^2.16.0", 57 | "eslint": "~8.46.0", 58 | "eslint-config-prettier": "^9.0.0", 59 | "express": "^4.18.2", 60 | "husky": "^8.0.3", 61 | "jest": "^29.4.1", 62 | "jest-environment-jsdom": "^29.4.1", 63 | "jest-preset-angular": "~13.1.0", 64 | "jsonc-eslint-parser": "^2.1.0", 65 | "lint-staged": "^15.0.2", 66 | "lodash": "^4.17.21", 67 | "nx": "17.0.2", 68 | "open": "^8.0.4", 69 | "postcss": "^8.4.31", 70 | "postcss-import": "^15.1.0", 71 | "postcss-url": "^10.1.3", 72 | "prettier": "^2.6.2", 73 | "react-dom": "^18.2.0", 74 | "stmux": "^1.8.7", 75 | "strip-comments": "^2.0.1", 76 | "ts-jest": "^29.1.0", 77 | "ts-node": "10.9.1", 78 | "typescript": "~5.1.3", 79 | "verdaccio": "^5.0.4", 80 | "ws": "^8.14.2" 81 | }, 82 | "packageManager": "pnpm@8.10.2", 83 | "dependencies": { 84 | "@angular/animations": "~16.2.0", 85 | "@angular/common": "~16.2.0", 86 | "@angular/compiler": "~16.2.0", 87 | "@angular/core": "~16.2.0", 88 | "@angular/forms": "~16.2.0", 89 | "@angular/platform-browser": "~16.2.0", 90 | "@angular/platform-browser-dynamic": "~16.2.0", 91 | "@angular/router": "~16.2.0", 92 | "@nx/devkit": "17.0.2", 93 | "@swc/helpers": "~0.5.2", 94 | "rxjs": "~7.8.0", 95 | "tslib": "^2.3.0", 96 | "zone.js": "~0.13.0" 97 | }, 98 | "nx": { 99 | "includedScripts": [] 100 | }, 101 | "lint-staged": { 102 | "*.ts": [ 103 | "eslint --fix", 104 | "prettier --write" 105 | ], 106 | "*.{js,json,yml,md}": [ 107 | "prettier --write" 108 | ] 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.json"], 19 | "parser": "jsonc-eslint-parser", 20 | "rules": { 21 | "@nx/dependency-checks": [ 22 | "error", 23 | { 24 | "ignoredDependencies": [ 25 | "@angular/core", 26 | "@angular/platform-browser", 27 | "strip-comments", 28 | "react-dom", 29 | "@babel/plugin-syntax-decorators", 30 | "@babel/plugin-syntax-typescript" 31 | ] 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "files": ["./package.json", "./executors.json"], 38 | "parser": "jsonc-eslint-parser", 39 | "rules": { 40 | "@nx/nx-plugin-checks": "error" 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/README.md: -------------------------------------------------------------------------------- 1 | # ngx-esbuild 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build ngx-esbuild` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test ngx-esbuild` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/executors.json: -------------------------------------------------------------------------------- 1 | { 2 | "executors": { 3 | "build": { 4 | "implementation": "./src/executors/build/executor", 5 | "schema": "./src/executors/build/schema.json", 6 | "description": "build executor" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ngx-esbuild', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 7 | }, 8 | moduleFileExtensions: ['ts', 'js', 'html'], 9 | coverageDirectory: '../../coverage/packages/ngx-esbuild', 10 | coveragePathIgnorePatterns: ['/node_modules/', '/__fixtures__/'], 11 | setupFilesAfterEnv: ['/test-setup.ts'], 12 | // Required to allow running esbuild within jest: https://github.com/jestjs/jest/issues/4422 13 | testEnvironment: 'node', 14 | globals: { 15 | Uint8Array: Uint8Array, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clickup/ngx-esbuild", 3 | "version": "0.1.2", 4 | "description": "ClickUp's esbuild powered local dev server, open sourced so you can speed up developing your own Angular applications!", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/clickup/ngx-esbuild.git" 8 | }, 9 | "keywords": [ 10 | "Angular", 11 | "esbuild", 12 | "Nx", 13 | "Nx Plugin" 14 | ], 15 | "author": "ClickUp", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/clickup/ngx-esbuild/issues" 19 | }, 20 | "homepage": "https://github.com/clickup/ngx-esbuild#readme", 21 | "dependencies": { 22 | "@nx/devkit": "^17.0.2", 23 | "tslib": "^2.3.0", 24 | "@craftamap/esbuild-plugin-html": "^0.6.1", 25 | "esbuild": "^0.19.5", 26 | "postcss": "^8.4.31", 27 | "postcss-import": "^15.1.0", 28 | "postcss-url": "^10.1.3", 29 | "esbuild-sass-plugin": "^2.16.0", 30 | "esbuild-plugin-copy": "^2.1.1", 31 | "babel-plugin-tester": "^11.0.4", 32 | "@babel/core": "^7.23.2", 33 | "@babel/plugin-syntax-decorators": "^7.22.10", 34 | "@babel/plugin-syntax-typescript": "^7.22.5", 35 | "@babel/types": "^7.23.0", 36 | "cheerio": "1.0.0-rc.12", 37 | "express": "^4.18.2", 38 | "open": "^8.0.4", 39 | "ws": "^8.14.2", 40 | "chalk": "^4.1.2", 41 | "lodash": "^4.17.21" 42 | }, 43 | "peerDependencies": { 44 | "@angular-devkit/build-angular": ">=16.0.0" 45 | }, 46 | "main": "./src/index.js", 47 | "typings": "./src/index.d.ts", 48 | "executors": "./executors.json" 49 | } 50 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-esbuild", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/ngx-esbuild/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/ngx-esbuild", 12 | "main": "packages/ngx-esbuild/src/index.ts", 13 | "tsConfig": "packages/ngx-esbuild/tsconfig.lib.json", 14 | "assets": [ 15 | "README.md", 16 | { 17 | "input": "./packages/ngx-esbuild/src", 18 | "glob": "**/!(*.ts)", 19 | "output": "./src" 20 | }, 21 | { 22 | "input": "./packages/ngx-esbuild/src", 23 | "glob": "**/*.d.ts", 24 | "output": "./src" 25 | }, 26 | { 27 | "input": "./packages/ngx-esbuild", 28 | "glob": "generators.json", 29 | "output": "." 30 | }, 31 | { 32 | "input": "./packages/ngx-esbuild", 33 | "glob": "executors.json", 34 | "output": "." 35 | } 36 | ] 37 | } 38 | }, 39 | "publish": { 40 | "command": "node tools/scripts/publish.mjs ngx-esbuild {args.ver} {args.tag}", 41 | "dependsOn": ["build"] 42 | }, 43 | "lint": { 44 | "executor": "@nx/eslint:lint", 45 | "outputs": ["{options.outputFile}"], 46 | "options": { 47 | "lintFilePatterns": [ 48 | "packages/ngx-esbuild/**/*.ts", 49 | "packages/ngx-esbuild/package.json", 50 | "packages/ngx-esbuild/executors.json" 51 | ] 52 | } 53 | }, 54 | "test": { 55 | "executor": "@nx/jest:jest", 56 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 57 | "options": { 58 | "jestConfig": "packages/ngx-esbuild/jest.config.ts", 59 | "passWithNoTests": true 60 | }, 61 | "configurations": { 62 | "ci": { 63 | "ci": true, 64 | "codeCoverage": true 65 | } 66 | } 67 | } 68 | }, 69 | "tags": [] 70 | } 71 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/get-esbuild-options.spec.ts: -------------------------------------------------------------------------------- 1 | import { getEsbuildOptions } from './get-esbuild-options'; 2 | 3 | jest.mock('node:fs', () => { 4 | return { 5 | promises: { 6 | readFile: jest.fn().mockReturnValue(''), 7 | }, 8 | }; 9 | }); 10 | 11 | describe('getEsbuildOptions', () => { 12 | it('should get all options', async () => { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | const { plugins, ...options } = await getEsbuildOptions( 15 | { 16 | main: 'main.ts', 17 | polyfills: ['polyfills.ts'], 18 | tsConfig: 'tsconfig.json', 19 | index: 'index.html', 20 | outputPath: 'dist', 21 | }, 22 | { 23 | browserTarget: 'build:browser', 24 | }, 25 | { 26 | serve: false, 27 | esbuildTarget: 'es2022', 28 | }, 29 | 'client', 30 | process.cwd() 31 | ); 32 | 33 | expect(options).toMatchInlineSnapshot(` 34 | { 35 | "bundle": true, 36 | "entryNames": "[name].[hash]", 37 | "entryPoints": [ 38 | "main.ts", 39 | ], 40 | "format": "esm", 41 | "metafile": true, 42 | "outdir": "dist", 43 | "sourcemap": true, 44 | "splitting": true, 45 | "supported": { 46 | "async-await": false, 47 | "async-generator": false, 48 | "class-field": false, 49 | "class-static-field": false, 50 | "for-await": false, 51 | }, 52 | "target": "es2022", 53 | "tsconfig": "tsconfig.json", 54 | } 55 | `); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/get-esbuild-options.ts: -------------------------------------------------------------------------------- 1 | import { Schema as BuildSchema } from '@angular-devkit/build-angular/src/builders/browser/schema'; 2 | import { Schema as ServeSchema } from '@angular-devkit/build-angular/src/builders/dev-server/schema'; 3 | import { htmlPlugin } from '@craftamap/esbuild-plugin-html'; 4 | import { BuildOptions } from 'esbuild'; 5 | import assert from 'node:assert'; 6 | import fs from 'node:fs'; 7 | import path from 'node:path'; 8 | import postcss, { AcceptedPlugin } from 'postcss'; 9 | import postcssImport from 'postcss-import'; 10 | import postcssUrl from 'postcss-url'; 11 | 12 | import { BuildExecutorSchema } from '../schema'; 13 | import { angularComponentResourcesPlugin } from './plugins/angular-component-resources'; 14 | import { assetsPlugin } from './plugins/assets'; 15 | import { babelPlugin } from './plugins/babel'; 16 | import { deleteOutputDirectoryPlugin } from './plugins/delete-output-directory'; 17 | import { devServerPlugin } from './plugins/dev-server'; 18 | import { 19 | globalScriptsPlugin, 20 | globalScriptsPluginEntryPoints, 21 | } from './plugins/global-scripts'; 22 | import { 23 | globalStylesPlugin, 24 | globalStylesPluginEntryPoints, 25 | } from './plugins/global-styles'; 26 | import { logBuildStatePlugin } from './plugins/log-build-state'; 27 | import { 28 | polyfillsPlugin, 29 | polyfillsPluginEntryPoints, 30 | } from './plugins/polyfills'; 31 | import { getFileReplacements } from './plugins/utils/get-file-replacements'; 32 | import { workerPlugin } from './plugins/worker'; 33 | import { MemoryCache } from './plugins/utils/memory-cache'; 34 | 35 | export async function getEsbuildOptions( 36 | angularBuildTarget: BuildSchema, 37 | angularServeTarget: ServeSchema, 38 | buildExecutorOptions: Omit< 39 | BuildExecutorSchema, 40 | 'buildTarget' | 'serveTarget' 41 | >, 42 | projectName: string, 43 | cwd: string 44 | ): Promise { 45 | const entryPoints: string[] = [angularBuildTarget.main]; 46 | 47 | assert( 48 | typeof angularBuildTarget.index === 'string', 49 | 'index must be a string, object form is not yet supported' 50 | ); 51 | 52 | const indexHtml = 'index.html'; 53 | 54 | const styleIncludePaths = [ 55 | ...(angularBuildTarget.stylePreprocessorOptions?.includePaths ?? []), 56 | cwd, 57 | ]; 58 | 59 | const angularComponentResourceQueryString = '?ng-template'; 60 | const workerUrlQueryString = '?worker-url'; 61 | 62 | const loadBuildStatePluginInstance = logBuildStatePlugin({ 63 | watch: buildExecutorOptions.serve, 64 | }); 65 | 66 | return { 67 | entryPoints, 68 | // Output in flat directory structure 69 | entryNames: '[name].[hash]', 70 | bundle: true, 71 | splitting: true, 72 | outdir: angularBuildTarget.outputPath, 73 | target: buildExecutorOptions.esbuildTarget, 74 | // Required for @craftamap/esbuild-plugin-html 75 | metafile: true, 76 | // TODO - see if this makes a noticeable difference to speed 77 | // treeShaking: false, 78 | sourcemap: true, 79 | // Match output of the angular CLI. Also required for code splitting with dynamic imports 80 | format: 'esm', 81 | tsconfig: angularBuildTarget.tsConfig, 82 | supported: { 83 | // Native async/await, async generators and for await are not supported with Zone.js. 84 | // Disabling support here will cause esbuild to downlevel async/await to a Zone.js supported form. 85 | 'async-await': false, 86 | 'async-generator': false, 87 | 'for-await': false, 88 | // Disable support for native class fields and static class fields and downlevel to a supported form. 89 | 'class-field': false, 90 | 'class-static-field': false, 91 | }, 92 | plugins: [ 93 | loadBuildStatePluginInstance.start, 94 | deleteOutputDirectoryPlugin(), 95 | globalStylesPlugin( 96 | angularBuildTarget.styles ?? [], 97 | { 98 | loadPaths: styleIncludePaths, 99 | }, 100 | cwd 101 | ), 102 | globalScriptsPlugin(angularBuildTarget.scripts ?? [], cwd), 103 | polyfillsPlugin(angularBuildTarget.polyfills ?? [], true, cwd), 104 | babelPlugin({ 105 | cache: new MemoryCache(), 106 | angularComponentResourceQueryString, 107 | workerUrlQueryString: angularBuildTarget.webWorkerTsConfig 108 | ? workerUrlQueryString 109 | : undefined, 110 | fileReplacements: getFileReplacements( 111 | angularBuildTarget.fileReplacements ?? [], 112 | cwd 113 | ), 114 | }), 115 | angularComponentResourcesPlugin({ 116 | angularComponentResourceQueryString, 117 | sassPluginOptions: { 118 | loadPaths: styleIncludePaths, 119 | async transform(source, resolveDir, filePath) { 120 | const postCssPlugins: AcceptedPlugin[] = []; 121 | 122 | // This allows us to do css imports to .css files in node_modules 123 | // e.g. @import '@time-loop/gantt/codebase/dhtmlxgantt.css'; 124 | // We improve perf by only transforming if the file contains a .css import 125 | if (source.includes(".css';")) { 126 | postCssPlugins.push(postcssImport()); 127 | } 128 | 129 | if (source.includes('url(')) { 130 | postCssPlugins.push( 131 | postcssUrl({ 132 | url: 'inline', 133 | }) 134 | ); 135 | } 136 | 137 | if (postCssPlugins.length === 0) { 138 | return source; 139 | } 140 | 141 | const { css } = await postcss(postCssPlugins).process(source, { 142 | from: filePath, 143 | }); 144 | return css; 145 | }, 146 | // Restore angular-cli compatibility by automatically resolving non partial extensionless imports to .scss files 147 | // e.g. replaces `@import './foo.component';` with `@import './foo.component.scss';` 148 | precompile(source) { 149 | if (source.includes('@import') || source.includes('@use')) { 150 | const imports = Array.from( 151 | source.matchAll(/@(import|use) '\.(.+)'/g) 152 | ); 153 | imports.forEach((match) => { 154 | const [fullImport, importOrUseKeyword, importPath] = match; 155 | if ( 156 | !importPath.endsWith('.scss') && 157 | !importPath.endsWith('.css') 158 | ) { 159 | source = source.replace( 160 | fullImport, 161 | `@${importOrUseKeyword} '.${importPath}.scss'` 162 | ); 163 | } 164 | }); 165 | } 166 | return source; 167 | }, 168 | }, 169 | }), 170 | workerPlugin({ 171 | queryString: workerUrlQueryString, 172 | tsconfig: angularBuildTarget.webWorkerTsConfig, 173 | }), 174 | // Create index.html with the `); 122 | return $.html(); 123 | } 124 | 125 | async function getWebsocketClientCode( 126 | buildOptions: esbuild.BuildOptions 127 | ): Promise { 128 | const bundledWsClient = await esbuild.build({ 129 | entryPoints: [getWebsocketClientEntryPoint()], 130 | bundle: true, 131 | target: buildOptions.target, 132 | metafile: true, 133 | tsconfig: buildOptions.tsconfig, 134 | write: false, 135 | }); 136 | 137 | assert( 138 | bundledWsClient.outputFiles?.length === 1, 139 | 'Expected only one output file' 140 | ); 141 | 142 | return bundledWsClient.outputFiles[0].text; 143 | } 144 | 145 | function getWebsocketClientEntryPoint(): string { 146 | const clientFileName = 'websocket-client'; 147 | try { 148 | // only works for local development in this repo 149 | return require.resolve(`./${clientFileName}.ts`); 150 | } catch { 151 | // else we are in the compiled package 152 | return require.resolve(`./${clientFileName}.js`); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/dev-server/shared-constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared constants between the dev server and the client script go here. 3 | */ 4 | 5 | export const DEV_SERVER_RECONNECT_POLL_INTERVAL = 500; 6 | export const DEV_SERVER_WEBSOCKET_PATH = '/wds'; 7 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/dev-server/websocket-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEV_SERVER_RECONNECT_POLL_INTERVAL, 3 | DEV_SERVER_WEBSOCKET_PATH, 4 | } from './shared-constants'; 5 | 6 | const { protocol, host } = new URL(window.location.href); 7 | const webSocketUrl = `ws${ 8 | protocol === 'https:' ? 's' : '' 9 | }://${host}${DEV_SERVER_WEBSOCKET_PATH}`; 10 | const webSocket = new WebSocket(webSocketUrl); 11 | 12 | webSocket.addEventListener('message', (event) => { 13 | try { 14 | const payload = JSON.parse(event.data) as unknown; 15 | if ( 16 | payload && 17 | typeof payload === 'object' && 18 | 'action' in payload && 19 | payload.action === 'reload' 20 | ) { 21 | window.location.reload(); 22 | } 23 | } catch (error: unknown) { 24 | console.error('[Dev Server] Error while handling websocket message.'); 25 | console.error(error); 26 | } 27 | }); 28 | 29 | // Approach taken from https://stackoverflow.com/questions/22431751/websocket-how-to-automatically-reconnect-after-it-dies 30 | webSocket.addEventListener('close', () => { 31 | pollForReconnect(webSocket.url); 32 | }); 33 | 34 | function pollForReconnect(url: string): void { 35 | const newWs = new WebSocket(url); 36 | 37 | newWs.addEventListener('open', () => { 38 | console.log( 39 | 'Dev server live reload connection is re-established, reloading page with latest changes' 40 | ); 41 | window.location.reload(); 42 | }); 43 | 44 | newWs.addEventListener('close', () => { 45 | console.log( 46 | `Dev server live reload connection is closed. Reconnect will be attempted in ${DEV_SERVER_RECONNECT_POLL_INTERVAL}ms.` 47 | ); 48 | setTimeout(() => { 49 | pollForReconnect(url); 50 | }, DEV_SERVER_RECONNECT_POLL_INTERVAL); 51 | }); 52 | 53 | newWs.addEventListener('error', () => { 54 | newWs.close(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/global-scripts.spec.ts: -------------------------------------------------------------------------------- 1 | import { compileFixture, getFixtureCwd } from './__fixtures__/compile-fixture'; 2 | import { globalScriptsPlugin } from './global-scripts'; 3 | 4 | describe('globalScriptsPlugin', () => { 5 | test('should add global scripts', async () => { 6 | const fixtureName = 'global-scripts'; 7 | const result = await compileFixture(fixtureName, [ 8 | globalScriptsPlugin( 9 | [ 10 | { 11 | input: 'scripts/1.js', 12 | }, 13 | 'scripts/2.js', 14 | ], 15 | getFixtureCwd(fixtureName) 16 | ), 17 | ]); 18 | 19 | expect(result['scripts.js']).toMatchInlineSnapshot(` 20 | " 21 | console.log(1); 22 | 23 | 24 | console.log(2); 25 | " 26 | `); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/global-scripts.ts: -------------------------------------------------------------------------------- 1 | import { ScriptElement } from '@angular-devkit/build-angular/src/builders/browser/schema'; 2 | import { Plugin } from 'esbuild'; 3 | import { escapeRegExp } from 'lodash'; 4 | 5 | const pluginName = 'global-scripts'; 6 | 7 | const outputFilename = 'scripts.js'; 8 | 9 | export const globalScriptsPluginEntryPoints = { 10 | esbuild: outputFilename, 11 | htmlPlugin: `${pluginName}:${outputFilename}`, 12 | }; 13 | 14 | /** 15 | * Plugin to concatenate all global scripts into a single file 16 | * Implements scripts option from angular cli 17 | * @param scripts 18 | * @param cwd 19 | */ 20 | export function globalScriptsPlugin( 21 | scripts: ScriptElement[], 22 | cwd: string 23 | ): Plugin { 24 | return { 25 | name: pluginName, 26 | setup(build) { 27 | if (Array.isArray(build.initialOptions.entryPoints)) { 28 | (build.initialOptions.entryPoints as string[]).unshift( 29 | globalScriptsPluginEntryPoints.esbuild 30 | ); 31 | } 32 | 33 | // Intercept request to scripts.js file as this will only exist virtually 34 | // Associate the request with this plugin 35 | build.onResolve( 36 | { filter: new RegExp(`^${escapeRegExp(outputFilename)}$`) }, 37 | (args) => ({ 38 | path: args.path, 39 | namespace: pluginName, 40 | }) 41 | ); 42 | 43 | // Now create a virtual file that imports everything in the apps project.json scripts entry 44 | build.onLoad({ filter: /.*/, namespace: pluginName }, () => { 45 | const mappedScripts = scripts 46 | .map((script) => { 47 | if (typeof script === 'string') { 48 | return script; 49 | } else if (typeof script === 'object' && script.input) { 50 | return script.input; 51 | } 52 | throw new Error( 53 | 'Cannot handle global script: ' + JSON.stringify(script) 54 | ); 55 | }) 56 | .filter(Boolean); 57 | return { 58 | contents: mappedScripts 59 | .map((script) => { 60 | return `import './${script}';`; 61 | }) 62 | .join('\n'), 63 | loader: 'js', 64 | resolveDir: cwd, 65 | watchFiles: mappedScripts, 66 | }; 67 | }); 68 | }, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/global-styles.spec.ts: -------------------------------------------------------------------------------- 1 | import { compileFixture, getFixtureCwd } from './__fixtures__/compile-fixture'; 2 | import { globalStylesPlugin } from './global-styles'; 3 | 4 | describe('globalStylesPlugin', () => { 5 | test('should add global styles', async () => { 6 | const fixtureName = 'global-styles'; 7 | const result = await compileFixture(fixtureName, [ 8 | globalStylesPlugin( 9 | ['styles/1.scss', 'styles/2.css'], 10 | {}, 11 | getFixtureCwd(fixtureName) 12 | ), 13 | ]); 14 | 15 | expect(result['styles.css']).toMatchInlineSnapshot(` 16 | "h1 { 17 | color: "red"; 18 | } 19 | 20 | h2 { 21 | color: hotpink; 22 | } 23 | " 24 | `); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/global-styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleElement } from '@angular-devkit/build-angular/src/builders/browser/schema'; 2 | import { Plugin } from 'esbuild'; 3 | import { sassPlugin, SassPluginOptions } from 'esbuild-sass-plugin'; 4 | import { escapeRegExp } from 'lodash'; 5 | import path from 'node:path'; 6 | 7 | const pluginName = 'global-styles'; 8 | 9 | const outputFilename = 'styles.js'; 10 | 11 | export const globalStylesPluginEntryPoints = { 12 | esbuild: `${outputFilename}`, 13 | htmlPlugin: `${pluginName}:${outputFilename}`, 14 | }; 15 | 16 | /** 17 | * Plugin to concatenate all global styles into a single file 18 | * Implements styles option from angular cli 19 | * @param styles 20 | * @param sassOptions 21 | * @param cwd 22 | */ 23 | export function globalStylesPlugin( 24 | styles: StyleElement[], 25 | sassOptions: SassPluginOptions, 26 | cwd: string 27 | ): Plugin { 28 | const namespace = 'angular:global-styles'; 29 | 30 | return { 31 | name: pluginName, 32 | setup(build) { 33 | if (Array.isArray(build.initialOptions.entryPoints)) { 34 | (build.initialOptions.entryPoints as string[]).unshift( 35 | globalStylesPluginEntryPoints.esbuild 36 | ); 37 | } 38 | 39 | // Intercept request to `outputFilename` as this will only exist virtually 40 | // Associate the request with this plugin 41 | build.onResolve( 42 | { filter: new RegExp(`^${escapeRegExp(outputFilename)}$`) }, 43 | (args) => ({ 44 | path: args.path, 45 | namespace: pluginName, 46 | }) 47 | ); 48 | 49 | // Now create a virtual file that imports everything in the apps project.json styles entry 50 | build.onLoad({ filter: /.*/, namespace: pluginName }, () => { 51 | const mappedStyles = styles 52 | .map((style) => { 53 | if (typeof style === 'string') { 54 | return style; 55 | } 56 | throw new Error( 57 | 'Cannot handle global style: ' + JSON.stringify(style) 58 | ); 59 | }) 60 | .filter(Boolean); 61 | return { 62 | contents: mappedStyles 63 | .map((style) => { 64 | return `import '${namespace}:./${style}';`; 65 | }) 66 | .join('\n'), 67 | loader: 'js', 68 | resolveDir: cwd, 69 | watchFiles: mappedStyles, 70 | }; 71 | }); 72 | 73 | // Above we will prefix all global styles in the virtual entry point with angular:global-styles: to indicate that they should be processed by this plugin 74 | // This part will intercept those requests and resolve them to the actual file path but under the namespace angular:global-styles 75 | build.onResolve({ filter: /^angular:global-styles:/ }, (args) => { 76 | return { 77 | path: path.join( 78 | args.resolveDir, 79 | args.path.replace(namespace + ':', '') 80 | ), 81 | namespace, 82 | }; 83 | }); 84 | 85 | // Next, we will intercept requests for global styles, process them with the sass plugin and return the stringified css 86 | const sassPluginInstance = sassPlugin({ 87 | ...sassOptions, 88 | }); 89 | sassPluginInstance.setup({ 90 | ...build, 91 | onLoad(_, sassPluginOnLoad) { 92 | build.onLoad( 93 | // The sass plugin doesn't support passing in a namespace 94 | // Which is why we curry it here to add the functionality we need 95 | { filter: /.*/, namespace }, 96 | sassPluginOnLoad 97 | ); 98 | }, 99 | }); 100 | }, 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/log-build-state.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@nx/devkit'; 2 | import chalk from 'chalk'; 3 | import esbuild, { Plugin } from 'esbuild'; 4 | 5 | /** 6 | * Logs the build state (success / error) and the time to the console 7 | * @param watch - whether we are in watch mode (serving the app for local dev) or not 8 | */ 9 | export function logBuildStatePlugin({ watch }: { watch: boolean }): { 10 | start: Plugin; 11 | end: Plugin; 12 | } { 13 | let time: number; 14 | return { 15 | start: { 16 | name: 'log-build-state-start', 17 | setup(build) { 18 | build.onStart(() => { 19 | time = Date.now(); 20 | if (watch) { 21 | logger.info(`${chalk.blue('[esbuild] ')}Build started`); 22 | } 23 | }); 24 | }, 25 | }, 26 | end: { 27 | name: 'log-build-state-end', 28 | setup(build) { 29 | build.onEnd((result: esbuild.BuildResult) => { 30 | const success = result.errors.length === 0; 31 | const timeString = `${chalk.yellow(`${Date.now() - time}ms`)}`; 32 | const color = success ? chalk.green : chalk.red; 33 | const suffix = watch ? ', watching for changes...' : ''; 34 | const message = success 35 | ? `Build succeeded in ${timeString}` 36 | : `Build finished in ${timeString} with errors (see above)`; 37 | 38 | logger.info(`[${color('esbuild')}] ${message}${suffix}`); 39 | }); 40 | }, 41 | }, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/polyfills.spec.ts: -------------------------------------------------------------------------------- 1 | import { compileFixture, getFixtureCwd } from './__fixtures__/compile-fixture'; 2 | import { polyfillsPlugin } from './polyfills'; 3 | 4 | describe('polyfillsPlugin', () => { 5 | test('should add polyfills', async () => { 6 | const fixtureName = 'polyfills'; 7 | const result = await compileFixture(fixtureName, [ 8 | polyfillsPlugin(['polyfills.ts'], true, getFixtureCwd(fixtureName)), 9 | ]); 10 | 11 | expect(result['polyfills.js']).toContain('CompilerFacadeImpl'); 12 | expect(result['polyfills.js']).toContain( 13 | 'Object.defineProperty(location, "origin", {' 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/polyfills.ts: -------------------------------------------------------------------------------- 1 | import { Polyfills } from '@angular-devkit/build-angular/src/builders/browser/schema'; 2 | import { Plugin } from 'esbuild'; 3 | import { escapeRegExp } from 'lodash'; 4 | 5 | const pluginName = 'polyfills'; 6 | 7 | const outputFilename = 'polyfills.js'; 8 | 9 | export const polyfillsPluginEntryPoints = { 10 | esbuild: outputFilename, 11 | htmlPlugin: `${pluginName}:${outputFilename}`, 12 | }; 13 | 14 | /** 15 | * Plugin to concatenate all polyfills into a single file 16 | * Implements polyfills option from angular cli 17 | * @param polyfills 18 | * @param jitMode 19 | * @param cwd 20 | */ 21 | export function polyfillsPlugin( 22 | polyfills: Polyfills, 23 | jitMode: boolean, 24 | cwd: string 25 | ): Plugin { 26 | return { 27 | name: pluginName, 28 | setup(build) { 29 | if (Array.isArray(build.initialOptions.entryPoints)) { 30 | (build.initialOptions.entryPoints as string[]).unshift( 31 | polyfillsPluginEntryPoints.esbuild 32 | ); 33 | } 34 | 35 | // Intercept request to polyfills.js file as this will only exist virtually 36 | // Associate the request with this plugin 37 | build.onResolve( 38 | { filter: new RegExp(`^${escapeRegExp(outputFilename)}$`) }, 39 | (args) => ({ 40 | path: args.path, 41 | namespace: pluginName, 42 | }) 43 | ); 44 | 45 | // Now create a virtual file that imports everything in the apps project.json scripts entry 46 | build.onLoad({ filter: /.*/, namespace: pluginName }, () => { 47 | const polyfillsArray = Array.isArray(polyfills) 48 | ? polyfills 49 | : [polyfills]; 50 | return { 51 | contents: [ 52 | jitMode ? '@angular/compiler' : undefined, // needed for jit mode + code splitting to work 53 | ...polyfillsArray.map((polyfill) => { 54 | if (polyfill.endsWith('.ts')) { 55 | return `./${polyfill}`; 56 | } 57 | return polyfill; 58 | }), 59 | ] 60 | .filter(Boolean) 61 | .map((script) => { 62 | return `import '${script}';`; 63 | }) 64 | .join('\n'), 65 | loader: 'ts', 66 | resolveDir: cwd, 67 | watchFiles: polyfillsArray, 68 | }; 69 | }); 70 | }, 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/utils/babel-transform.ts: -------------------------------------------------------------------------------- 1 | import { PluginItem, transformAsync, TransformOptions } from '@babel/core'; 2 | import { Loader } from 'esbuild'; 3 | 4 | /** 5 | * Transforms the given source using babel 6 | * @param source 7 | * @param filename 8 | * @param plugins 9 | * @param assumptions 10 | * @param resultLoader 11 | */ 12 | export async function babelTransform( 13 | source: string, 14 | filename: string, 15 | plugins: PluginItem[], 16 | assumptions: TransformOptions['assumptions'], 17 | resultLoader: Loader 18 | ) { 19 | const result = await transformAsync(source, { 20 | babelrc: false, 21 | sourceMaps: 'inline', 22 | filename, 23 | plugins, 24 | assumptions, 25 | // Hide this warning: "The code generator has de-optimised the styling of ... as it exceeds the max of 500KB" 26 | compact: false, 27 | }); 28 | 29 | if (!result?.code) { 30 | return; 31 | } 32 | 33 | return { 34 | contents: result.code, 35 | loader: resultLoader, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/utils/cacheable-plugin-on-load-handler.ts: -------------------------------------------------------------------------------- 1 | import { Loader } from 'esbuild'; 2 | import fs from 'node:fs'; 3 | 4 | import { PluginCache } from './types/plugin-cache'; 5 | 6 | /** 7 | * This function is used to cache the results in memory of an esbuild plugin's onLoad handler. 8 | * Based on this guide: https://esbuild.github.io/plugins/#caching-your-plugin 9 | * 10 | * @param path 11 | * @param cache 12 | * @param handler 13 | */ 14 | export async function cacheablePluginOnLoadHandler( 15 | path: string, 16 | cache: PluginCache, 17 | handler: ( 18 | contents: string, 19 | path: string 20 | ) => Promise 21 | ) { 22 | const stats = await fs.promises.stat(path); 23 | 24 | const key = path; 25 | let value = cache.get(key); 26 | 27 | const input = 28 | !value || value.mtimeMs < stats.mtimeMs 29 | ? await fs.promises.readFile(path, 'utf8') 30 | : value.input; 31 | 32 | if (!value || value.input !== input) { 33 | const output = await handler(input, path); 34 | value = { input, output, mtimeMs: stats.mtimeMs }; 35 | cache.set(key, value); 36 | } 37 | 38 | return value.output; 39 | } 40 | 41 | export interface CacheablePluginValue { 42 | input: string; 43 | output: 44 | | { 45 | contents: string; 46 | loader: Loader; 47 | } 48 | | undefined; 49 | mtimeMs: number; 50 | } 51 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/utils/file-read-cache.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | /** 4 | * This helper is used to read a files contents only if it has changed since the last time it was read. 5 | * This helps speed up incremental rebuild times as reading the file last modified time is much faster than always reading the file contents. 6 | */ 7 | export function createFileReadCache() { 8 | const fileCache = new Map(); 9 | 10 | async function readFile(path: string): Promise { 11 | const stats = await fs.promises.stat(path); 12 | const cachedFile = fileCache.get(path); 13 | 14 | if (!cachedFile || cachedFile.mTimeMs < stats.mtimeMs) { 15 | const contents = await fs.promises.readFile(path, 'utf8'); 16 | fileCache.set(path, { contents, mTimeMs: stats.mtimeMs }); 17 | return contents; 18 | } 19 | 20 | return cachedFile.contents; 21 | } 22 | 23 | return { readFile }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/utils/get-file-replacements.ts: -------------------------------------------------------------------------------- 1 | import { FileReplacement } from '@angular-devkit/build-angular/src/builders/browser/schema'; 2 | import assert from 'node:assert'; 3 | import path from 'node:path'; 4 | 5 | export function getFileReplacements( 6 | angularFileReplacements: FileReplacement[], 7 | cwd: string 8 | ): Record { 9 | return Object.fromEntries( 10 | angularFileReplacements.map((fileReplacement) => { 11 | assert( 12 | fileReplacement.replace, 13 | 'File replacement must have a `replace` property' 14 | ); 15 | assert( 16 | fileReplacement.with, 17 | 'File replacement must have a `with` property' 18 | ); 19 | return [ 20 | path.join(cwd, fileReplacement.replace), 21 | path.join(cwd, fileReplacement.with), 22 | ]; 23 | }) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/utils/memory-cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple memory cache that extends the native `Map` object. 3 | * Eventually we can add more methods to it if needed. 4 | */ 5 | export class MemoryCache extends Map {} 6 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/utils/types/plugin-cache.ts: -------------------------------------------------------------------------------- 1 | import { MemoryCache } from '../memory-cache'; 2 | 3 | export type PluginCache = MemoryCache; 4 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/worker.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { compileFixture, getFixtureCwd } from './__fixtures__/compile-fixture'; 4 | import { babelPlugin } from './babel'; 5 | import { MemoryCache } from './utils/memory-cache'; 6 | import { workerPlugin } from './worker'; 7 | 8 | const workerUrlQueryString = '?worker-url'; 9 | 10 | describe('workerPlugin', () => { 11 | test('should bundle shared worker as separate entry points', async () => { 12 | const fixtureName = 'shared-worker'; 13 | const cwd = getFixtureCwd(fixtureName); 14 | const result = await compileFixture(fixtureName, [ 15 | babelPlugin({ 16 | angularComponentResourceQueryString: '?ng-template', 17 | workerUrlQueryString, 18 | fileReplacements: {}, 19 | cache: new MemoryCache(), 20 | }), 21 | workerPlugin({ 22 | queryString: workerUrlQueryString, 23 | tsconfig: path.join(cwd, 'tsconfig.json'), 24 | }), 25 | ]); 26 | 27 | expect(result['entry.js']).toMatchInlineSnapshot(` 28 | "var __getOwnPropNames = Object.getOwnPropertyNames; 29 | var __commonJS = (cb, mod) => function __require() { 30 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; 31 | }; 32 | 33 | 34 | var require_shared_worker_worker = __commonJS({ 35 | "worker:./shared-worker.worker.js"(exports, module) { 36 | module.exports = "./shared-worker.worker.js"; 37 | } 38 | }); 39 | 40 | 41 | var worker = new SharedWorker(require_shared_worker_worker()); 42 | export { 43 | worker 44 | }; 45 | " 46 | `); 47 | 48 | expect(result['shared-worker.worker.js']).toMatchInlineSnapshot(` 49 | "(() => { 50 | 51 | console.log("I am a shared worker!"); 52 | })(); 53 | " 54 | `); 55 | }); 56 | 57 | test('should not bundle the webworker when no webworker tsconfig is provided', async () => { 58 | const fixtureName = 'shared-worker'; 59 | const result = await compileFixture(fixtureName, [ 60 | babelPlugin({ 61 | angularComponentResourceQueryString: '?ng-template', 62 | workerUrlQueryString: undefined, 63 | fileReplacements: {}, 64 | cache: new MemoryCache(), 65 | }), 66 | workerPlugin({ 67 | queryString: workerUrlQueryString, 68 | }), 69 | ]); 70 | 71 | expect(result['entry.js']).toMatchInlineSnapshot(` 72 | " 73 | var worker = new SharedWorker( 74 | new URL("./shared-worker.worker", import.meta.url) 75 | ); 76 | export { 77 | worker 78 | }; 79 | " 80 | `); 81 | expect(result['shared-worker.worker.js']).toBeUndefined(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/esbuild/plugins/worker.ts: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import { escapeRegExp } from 'lodash'; 3 | import assert from 'node:assert'; 4 | 5 | const pluginName = 'worker'; 6 | 7 | /** 8 | * Handles bundling of web workers by: 9 | * 1. Intercepting the import of the worker file with the `transformNewWorkerUrlPlugin` babel plugin 10 | * 2. Bundling the worker with esbuild as its own compilation unit 11 | * 3. Replacing the new URL reference with the path to the bundled worker file using esbuilds file loader 12 | * 13 | * This is purely a stopgap until it's implemented natively by esbuild: https://github.com/evanw/esbuild/pull/2508 14 | * @param options 15 | */ 16 | export function workerPlugin(options: { 17 | queryString: string; 18 | tsconfig?: string; 19 | }): esbuild.Plugin { 20 | return { 21 | name: pluginName, 22 | setup(build) { 23 | if (!options.tsconfig) { 24 | // match angular-cli behaviour and do nothing if no `webWorkerTsConfig` option is provided 25 | return; 26 | } 27 | 28 | const workerQueryStringRegexp = new RegExp( 29 | escapeRegExp(options.queryString) + '$' 30 | ); 31 | 32 | build.onResolve( 33 | { 34 | filter: workerQueryStringRegexp, 35 | }, 36 | async (args) => { 37 | const fullyResolvedPath = await build.resolve( 38 | args.path.replace(workerQueryStringRegexp, ''), 39 | { 40 | kind: args.kind, 41 | resolveDir: args.resolveDir, 42 | } 43 | ); 44 | 45 | return { 46 | // esbuild's plugin API doesn't allow you to rename output files 47 | // So we use this hack where we resolve the file to a .js extension to force the worker file extension to be .js instead of .ts 48 | path: fullyResolvedPath.path.replace(/\.ts$/, '.js'), 49 | namespace: pluginName, 50 | pluginData: { 51 | realPath: fullyResolvedPath.path, 52 | }, 53 | }; 54 | } 55 | ); 56 | 57 | build.onLoad({ filter: /.+/, namespace: pluginName }, async (args) => { 58 | const bundledWorker = await esbuild.build({ 59 | entryPoints: [args.pluginData.realPath], 60 | bundle: true, 61 | target: build.initialOptions.target, 62 | metafile: true, 63 | tsconfig: options.tsconfig, 64 | write: false, 65 | }); 66 | 67 | assert( 68 | bundledWorker.outputFiles?.length === 1, 69 | 'Expected only one output file' 70 | ); 71 | 72 | return { 73 | contents: bundledWorker.outputFiles[0].text, 74 | loader: 'file', 75 | watchFiles: Object.keys(bundledWorker.metafile?.inputs ?? {}), 76 | }; 77 | }); 78 | }, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/executor.spec.ts: -------------------------------------------------------------------------------- 1 | import executor from './executor'; 2 | import { BuildExecutorSchema } from './schema'; 3 | 4 | jest.mock('./esbuild/get-esbuild-options', () => { 5 | return { 6 | getEsbuildOptions: jest.fn().mockReturnValue({}), 7 | }; 8 | }); 9 | 10 | jest.mock('esbuild', () => { 11 | return { 12 | build: jest.fn(), 13 | context: jest.fn().mockResolvedValue({ 14 | watch: jest.fn(), 15 | }), 16 | }; 17 | }); 18 | 19 | const options: BuildExecutorSchema = { 20 | buildTarget: 'build', 21 | serveTarget: 'serve', 22 | serve: false, 23 | esbuildTarget: 'es2022', 24 | }; 25 | 26 | describe('Build Executor', () => { 27 | it('can run', async () => { 28 | const output = await executor(options, { 29 | root: '', 30 | cwd: '', 31 | isVerbose: false, 32 | projectName: 'client', 33 | }).next(); 34 | expect(output.value.success).toBe(true); 35 | expect(jest.requireMock('esbuild').build).toHaveBeenCalledWith( 36 | expect.anything() 37 | ); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/executor.ts: -------------------------------------------------------------------------------- 1 | import { ExecutorContext } from '@nx/devkit'; 2 | import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable'; 3 | import esbuild from 'esbuild'; 4 | import assert from 'node:assert'; 5 | 6 | import { getEsbuildOptions } from './esbuild/get-esbuild-options'; 7 | import { BuildExecutorSchema } from './schema'; 8 | import { 9 | getBuildTargetConfig, 10 | getServeTargetConfig, 11 | } from './target-config/get-target-config'; 12 | 13 | export default async function* runExecutor( 14 | options: BuildExecutorSchema, 15 | context: ExecutorContext 16 | ) { 17 | const buildTargetConfig = getBuildTargetConfig( 18 | options.buildTarget, 19 | options.configurationName, 20 | context 21 | ); 22 | const serveTargetConfig = getServeTargetConfig( 23 | options.serveTarget, 24 | options.configurationName, 25 | context 26 | ); 27 | 28 | const projectName = context.projectName ?? context.workspace?.defaultProject; 29 | 30 | assert(projectName, 'Could not find project name'); 31 | 32 | const esbuildOptions = await getEsbuildOptions( 33 | buildTargetConfig, 34 | serveTargetConfig, 35 | options, 36 | projectName, 37 | context.cwd 38 | ); 39 | 40 | if (options.serve) { 41 | return yield* createAsyncIterable<{ success: boolean }>( 42 | async ({ next, done }) => { 43 | const ctx = await esbuild.context({ 44 | ...esbuildOptions, 45 | plugins: [ 46 | ...(esbuildOptions.plugins ?? []), 47 | { 48 | name: 'nx-watch-plugin', 49 | setup(build) { 50 | build.onEnd((result) => { 51 | next({ 52 | success: result.errors.length === 0, 53 | }); 54 | }); 55 | }, 56 | }, 57 | ], 58 | }); 59 | 60 | await ctx.watch(); 61 | 62 | registerCleanupCallback(() => { 63 | ctx.dispose(); 64 | done(); // return from async iterable 65 | }); 66 | } 67 | ); 68 | } else { 69 | await esbuild.build(esbuildOptions); 70 | return { 71 | success: true, 72 | }; 73 | } 74 | } 75 | 76 | // Stolen from the official nx esbuild plugin: https://github.com/nrwl/nx/blob/master/packages/esbuild/src/executors/esbuild/esbuild.impl.ts#L237-L248 77 | function registerCleanupCallback(callback: () => void) { 78 | const wrapped = () => { 79 | callback(); 80 | process.off('SIGINT', wrapped); 81 | process.off('SIGTERM', wrapped); 82 | process.off('exit', wrapped); 83 | }; 84 | 85 | process.on('SIGINT', wrapped); 86 | process.on('SIGTERM', wrapped); 87 | process.on('exit', wrapped); 88 | } 89 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface BuildExecutorSchema { 2 | /** 3 | * Whether or not to serve the application or just build it. 4 | * 5 | * @default false 6 | */ 7 | serve: boolean; 8 | 9 | /** 10 | * The build target to read configuration from 11 | * 12 | * @default build 13 | */ 14 | buildTarget: string; 15 | 16 | /** 17 | * The serve target to read configuration from 18 | * 19 | * @default serve 20 | */ 21 | serveTarget: string; 22 | 23 | /** 24 | * The name of the configuration to use for either building or serving the application 25 | */ 26 | configurationName?: string; 27 | 28 | /** 29 | * Set the JavaScript language version for emitted JavaScript. 30 | * 31 | * @default es2022 32 | */ 33 | esbuildTarget: string; 34 | } 35 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "version": 2, 4 | "title": "Build with esbuild", 5 | "description": "Alternative to the angular CLI webpack builder that uses esbuild for blazing fast builds during local development. This builder should not be used for production!", 6 | "type": "object", 7 | "properties": { 8 | "buildTarget": { 9 | "type": "string", 10 | "default": "build" 11 | }, 12 | "configurationName": { 13 | "type": "string" 14 | }, 15 | "serve": { 16 | "type": "boolean", 17 | "default": false 18 | }, 19 | "serveTarget": { 20 | "type": "string", 21 | "default": "serve" 22 | }, 23 | "esbuildTarget": { 24 | "type": "string", 25 | "default": "es2022" 26 | } 27 | }, 28 | "required": [] 29 | } 30 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/target-config/get-target-config.spec.ts: -------------------------------------------------------------------------------- 1 | import { ExecutorContext } from '@nx/devkit'; 2 | 3 | import { getTargetConfig } from './get-target-config'; 4 | 5 | function getMockContext(options: { 6 | projectName?: string; 7 | defaultProject?: string; 8 | defaultConfiguration?: string; 9 | }): ExecutorContext { 10 | return { 11 | projectName: options.projectName, 12 | workspace: { 13 | defaultProject: options.defaultProject, 14 | projects: { 15 | 'my-app': { 16 | targets: { 17 | build: { 18 | defaultConfiguration: options.defaultConfiguration, 19 | options: { 20 | outputPath: 'dist/apps/my-app', 21 | }, 22 | configurations: { 23 | production: { 24 | outputPath: 'dist/apps/my-app-prod', 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | } as unknown as ExecutorContext['workspace'], 32 | } as ExecutorContext; 33 | } 34 | 35 | describe('getTargetConfig', () => { 36 | test('use current project', () => { 37 | expect( 38 | getTargetConfig( 39 | 'build', 40 | undefined, 41 | getMockContext({ 42 | projectName: 'my-app', 43 | }) 44 | ) 45 | ).toEqual({ 46 | outputPath: 'dist/apps/my-app', 47 | }); 48 | }); 49 | 50 | test('use default project', () => { 51 | expect( 52 | getTargetConfig( 53 | 'build', 54 | undefined, 55 | getMockContext({ 56 | defaultProject: 'my-app', 57 | }) 58 | ) 59 | ).toEqual({ 60 | outputPath: 'dist/apps/my-app', 61 | }); 62 | }); 63 | 64 | test('use default configuration', () => { 65 | expect( 66 | getTargetConfig( 67 | 'build', 68 | undefined, 69 | getMockContext({ 70 | projectName: 'my-app', 71 | defaultConfiguration: 'production', 72 | }) 73 | ) 74 | ).toEqual({ 75 | outputPath: 'dist/apps/my-app-prod', 76 | }); 77 | }); 78 | 79 | test('use build configuration', () => { 80 | expect( 81 | getTargetConfig( 82 | 'build', 83 | 'production', 84 | getMockContext({ 85 | projectName: 'my-app', 86 | }) 87 | ) 88 | ).toEqual({ 89 | outputPath: 'dist/apps/my-app-prod', 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/executors/build/target-config/get-target-config.ts: -------------------------------------------------------------------------------- 1 | import { Schema as BuildSchema } from '@angular-devkit/build-angular/src/builders/browser/schema'; 2 | import { Schema as ServeSchema } from '@angular-devkit/build-angular/src/builders/dev-server/schema'; 3 | import { ExecutorContext } from '@nx/devkit'; 4 | 5 | export function getBuildTargetConfig( 6 | target: string, 7 | configurationName: string | undefined, 8 | context: ExecutorContext 9 | ) { 10 | return getTargetConfig(target, configurationName, context); 11 | } 12 | 13 | export function getServeTargetConfig( 14 | target: string, 15 | configurationName: string | undefined, 16 | context: ExecutorContext 17 | ) { 18 | return getTargetConfig(target, configurationName, context); 19 | } 20 | 21 | export function getTargetConfig( 22 | target: string, 23 | configurationName: string | undefined, 24 | context: ExecutorContext 25 | ): T { 26 | const config = 27 | context.workspace?.projects?.[ 28 | context.projectName ?? context.workspace.defaultProject ?? '' 29 | ]?.targets?.[target]; 30 | 31 | const configuration = configurationName ?? config?.defaultConfiguration; 32 | 33 | return { 34 | ...config?.options, 35 | ...(configuration ? config?.configurations?.[configuration] : {}), 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clickup/ngx-esbuild/9649dedf36ee6d36d4ee17db1368448beeccd7b3/packages/ngx-esbuild/src/index.ts -------------------------------------------------------------------------------- /packages/ngx-esbuild/test-setup.ts: -------------------------------------------------------------------------------- 1 | import { TextDecoder, TextEncoder } from 'util'; 2 | 3 | global.TextEncoder = TextEncoder; 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 5 | // @ts-ignore - types don't match perfectly but it doesn't cause any actual issues 6 | global.TextDecoder = TextDecoder; 7 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": [ 10 | "jest.config.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.test.ts", 13 | "src/executors/build/esbuild/plugins/__fixtures__/**/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/ngx-esbuild/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clickup/root", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "local-registry": { 6 | "executor": "@nx/js:verdaccio", 7 | "options": { 8 | "port": 4873, 9 | "config": ".verdaccio/config.yml", 10 | "storage": "tmp/local-registry/storage" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tools/scripts/publish.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a minimal script to publish your package to "npm". 3 | * This is meant to be used as-is or customize as you see fit. 4 | * 5 | * This script is executed on "dist/path/to/library" as "cwd" by default. 6 | * 7 | * You might need to authenticate with NPM before running this script. 8 | */ 9 | 10 | import { execSync } from 'child_process'; 11 | import { readFileSync, writeFileSync } from 'fs'; 12 | 13 | import devkit from '@nx/devkit'; 14 | const { readCachedProjectGraph } = devkit; 15 | 16 | function invariant(condition, message) { 17 | if (!condition) { 18 | console.error(message); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | // Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag} 24 | // Default "tag" to "next" so we won't publish the "latest" tag by accident. 25 | const [, , name, version, tag = 'next'] = process.argv; 26 | 27 | // A simple SemVer validation to validate the version 28 | const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/; 29 | invariant( 30 | version && validVersion.test(version), 31 | `No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.` 32 | ); 33 | 34 | const graph = readCachedProjectGraph(); 35 | const project = graph.nodes[name]; 36 | 37 | invariant( 38 | project, 39 | `Could not find project "${name}" in the workspace. Is the project.json configured correctly?` 40 | ); 41 | 42 | const outputPath = project.data?.targets?.build?.options?.outputPath; 43 | invariant( 44 | outputPath, 45 | `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?` 46 | ); 47 | 48 | process.chdir(outputPath); 49 | 50 | // Updating the version in "package.json" before publishing 51 | try { 52 | const json = JSON.parse(readFileSync(`package.json`).toString()); 53 | json.version = version; 54 | writeFileSync(`package.json`, JSON.stringify(json, null, 2)); 55 | } catch (e) { 56 | console.error(`Error reading package.json file from library build output.`); 57 | } 58 | 59 | // Execute "npm publish" to publish 60 | execSync(`npm publish --access public --tag ${tag}`); 61 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2022", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@clickup/ngx-esbuild": ["packages/ngx-esbuild/src/index.ts"] 19 | }, 20 | "esModuleInterop": true 21 | }, 22 | "exclude": ["node_modules", "tmp"] 23 | } 24 | --------------------------------------------------------------------------------