├── .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 | [](https://github.com/clickup/ngx-esbuild/actions/workflows/ci.yml)
4 | [](http://badge.fury.io/js/@clickup%2Fngx-esbuild)
5 | [](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 |
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 |
--------------------------------------------------------------------------------