Merge branch 'master' into move-rspack-into-main
This commit is contained in:
commit
f9e1d6371e
@ -64,6 +64,16 @@
|
|||||||
],
|
],
|
||||||
"@nx/workspace/valid-command-object": "error"
|
"@nx/workspace/valid-command-object": "error"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["pnpm-lock.yaml"],
|
||||||
|
"parser": "./tools/eslint-rules/raw-file-parser.js",
|
||||||
|
"rules": {
|
||||||
|
"@nx/workspace/ensure-pnpm-lock-version": [
|
||||||
|
"error",
|
||||||
|
{ "version": "9.0" }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -498,5 +498,10 @@
|
|||||||
"name": "nx-github-pages",
|
"name": "nx-github-pages",
|
||||||
"description": "A small Nx plugin to make deploying static projects to GitHub Pages easy.",
|
"description": "A small Nx plugin to make deploying static projects to GitHub Pages easy.",
|
||||||
"url": "https://github.com/agentender/nx-github-pages"
|
"url": "https://github.com/agentender/nx-github-pages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nx-solhint",
|
||||||
|
"description": "Solhint generators and inferred tasks for Nx",
|
||||||
|
"url": "https://github.com/juliangsibecas/nx-solhint"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: '"The Pit of Success" w/ Ruben Casas of Postman | Nx Enterprise Podcast Episode 5'
|
||||||
|
slug: 'pit-of-success-podcast-5'
|
||||||
|
authors: ['Zack DeRose']
|
||||||
|
tags: [podcast]
|
||||||
|
cover_image: /blog/images/2024-09-18/podcast-5-thumbnail.png
|
||||||
|
podcastYoutubeId: oTZLTNtndxc
|
||||||
|
podcastSpotifyId: 7LJlqLGR708OccUwyzz9wT
|
||||||
|
podcastAmazonUrl: https://music.amazon.com/podcasts/a221fdad-36fd-4695-a5b4-038d7b99d284/episodes/352e8cef-b8df-4e81-be38-96a0cf62e0f5/the-enterprise-software-podcast-by-nx-the-enterprise-software-podcast-by-nx-5-ruben-casas-postman
|
||||||
|
podcastAppleUrl: https://podcasts.apple.com/us/podcast/the-enterprise-software-podcast-by-nx-5-ruben-casas-postman/id1752704996?i=1000669972799
|
||||||
|
podcastIHeartUrl: https://www.iheart.com/podcast/269-the-enterprise-software-po-186891508/episode/the-enterprise-software-podcast-by-nx-217668148/
|
||||||
|
---
|
||||||
|
|
||||||
|
In this episode, Zack sits down with Ruben of Postman to discuss:
|
||||||
|
|
||||||
|
- The evolution of Frontend Architecture
|
||||||
|
- The AngularJS to Angular2 transition and the rise of TS and RXJS
|
||||||
|
- Why React doesn't get enough credit
|
||||||
|
- Platform teams and the pit of success
|
||||||
112
docs/blog/2024-09-20-nx-19-8.md
Normal file
112
docs/blog/2024-09-20-nx-19-8.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
title: Nx 19.8 Update!!
|
||||||
|
slug: nx-19-8-update
|
||||||
|
authors: [Zack DeRose]
|
||||||
|
tags: [nx, release]
|
||||||
|
cover_image: /blog/images/2024-09-20/thumbnail.png
|
||||||
|
---
|
||||||
|
|
||||||
|
Nx 19.8 is here! This is our last minor release before we get ready to move ahead into Nx v20, which should land in October around the same time as the [Monorepo World Conference](https://monorepo.world/)!
|
||||||
|
|
||||||
|
As always, you can find the general details for all Nx releases on our [changelog](/changelog), as well as details on our [Github Releases for the Nx repo](https://github.com/nrwl/nx/releases).
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
In this blog post:
|
||||||
|
|
||||||
|
- [Nx Import](#nx-import)
|
||||||
|
- [Improved Task Scheduling!](#improved-task-scheduling)
|
||||||
|
- [Project Crystal Comes to Angular](#project-crystal-comes-to-angular)
|
||||||
|
- [Crystalize Your Entire Workspace In One Command](#crystalize-your-entire-workspace-in-one-command)
|
||||||
|
- [New Nx Workspaces Create with ESLint v9](#new-nx-workspaces-created-with-eslint-v9)
|
||||||
|
- [Nx Release Enhancements](#nx-release-enhancements)
|
||||||
|
- [Migrate to Latest](#migrate-to-latest)
|
||||||
|
- [Round 2 of Monorepo World Conference Speakers Announced!!](#round-2-of-monorepo-world-conference-speakers-announced)
|
||||||
|
- [Learn More](#learn-more)
|
||||||
|
|
||||||
|
## Nx Import
|
||||||
|
|
||||||
|
In Nx 19.8, [`nx import`](/nx-api/nx/documents/import) has now moved from beta support to now generally available!
|
||||||
|
|
||||||
|
Nx Import is a new [top-level command of the Nx CLI](/reference/nx-commands) which allows you to import projects along with its git history from some other repository into your current Nx worksapce.
|
||||||
|
|
||||||
|
Keep an eye out for more on Nx Import on our [YouTube Channel](https://www.youtube.com/@nxdevtools) coming soon, and in the meantime be sure to check [the documentation](/nx-api/nx/documents/import) as this is now fully documented!
|
||||||
|
|
||||||
|
## Improved Task Scheduling!
|
||||||
|
|
||||||
|
We've added some optimizations to the core of Nx - particularly around Nx's task scheduling. At the core of Nx is a task runner that supports [task dependencies](/features/run-tasks#defining-a-task-pipeline) (configurable for your entire workspace in the `targetDefaults` of your `nx.json` file, and on a per-project basis in your `project.json` files!), as well as the ability to run mulitple tasks in parallel.
|
||||||
|
|
||||||
|
With 19.8, nx will leverage historical data of previous runs of tasks to add some prioritization to the scheduling tasks that tend to take longer. This should optimize the total runtime of large batch commands in your workspace!
|
||||||
|
|
||||||
|
Read more on [running tasks with Nx](/features/run-tasks)! And checkout [this new optimization on GitHub](https://github.com/nrwl/nx/pull/27783) for more details!
|
||||||
|
|
||||||
|
## Project Crystal Comes to Angular
|
||||||
|
|
||||||
|
[Project Crystal](/concepts/inferred-tasks) has come to Angular!
|
||||||
|
|
||||||
|
Project Crystal allows Nx to _infer_ tasks for projects in your workspace - rather than requiring that they exist in every `project.json` or `angular.json` file of your workspace.
|
||||||
|
|
||||||
|
You can now run the command `nx init` in a project created by the Angular CLI, and we will generate `project.json` files for each angular project in your workspace - splitting that data out from the root `angular.json` file created by the Angular CLI.
|
||||||
|
|
||||||
|
This decision was made based on our feedback from the Angular community - where there is a strong preference for being able to split out the config.
|
||||||
|
|
||||||
|
## Crystalize Your Entire Workspace In One Command
|
||||||
|
|
||||||
|
When we initially launched [Project Crystal](/concepts/inferred-tasks), we shipped originally with `convert-to-inferred` generators, which would allow you to convert your workspace one plugin at a time.
|
||||||
|
|
||||||
|
With Nx 19.8, we've added a [`infer-targets`](/recipes/running-tasks/convert-to-inferred#migrate-all-plugins) generator, which will automatically detect all available `convert-to-inferred` generators, and run the ones you choose. You may also specify a specific project using the `--project` option of the generator.
|
||||||
|
|
||||||
|
## New Nx Workspaces Created with ESLint v9
|
||||||
|
|
||||||
|
When creating a new workspace with the command: `npx create-nx-workspace`, those workspaces will now be created with [`eslint`](https://www.npmjs.com/package/eslint) v9, and [`typescript-eslint`](https://www.npmjs.com/package/typescript-eslint) v8 - their most recent versions respectively.
|
||||||
|
|
||||||
|
Keep in mind as well that ESLint v8 faces end-of-life on October 5th, meaning only [flat config](https://eslint.org/docs/latest/use/configure/migration-guide) is supported moving forward. Nx users should migrate to this new config format using [our flat config generator](/recipes/tips-n-tricks/flat-config#switching-to-eslints-flat-config-format).
|
||||||
|
|
||||||
|
For more on eslint's flat config, and how to use our generator to get to flat conifg checkout this video:
|
||||||
|
|
||||||
|
{% youtube
|
||||||
|
src="https://www.youtube.com/watch?v=32XH909CZrY"
|
||||||
|
title="ESLint Config Automation With Nx"
|
||||||
|
/%}
|
||||||
|
|
||||||
|
## Nx Release Enhancements
|
||||||
|
|
||||||
|
[`nx release`](/nx-api/nx/documents/release) is a framework/language/platform agnostic solution to versioning, publishing, and changelogs for your monorepo. We've been continuing to invest in Nx Release in 19.8, adding support specifically for [`pnpm publish`](https://pnpm.io/cli/publish) and [Github Enterprise Server](https://github.com/nrwl/nx/pull/26482)!
|
||||||
|
|
||||||
|
We also have a new feature from Nx Champion, Jonathan Gelin - which allows you to use `groupPreVersionCommand` in addition to the `preVersionCommand` when using the release groups feature to support [building before versioning](/recipes/nx-release/build-before-versioning).
|
||||||
|
|
||||||
|
## Migrate to Latest
|
||||||
|
|
||||||
|
{% youtube
|
||||||
|
src="https://youtu.be/A0FjwsTlZ8A"
|
||||||
|
title="How Automated Code Migrations Work"
|
||||||
|
/%}
|
||||||
|
|
||||||
|
As always - updating Nx and its plugins is easy as we ship an [automated migration command](/features/automate-updating-dependencies).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx migrate latest
|
||||||
|
```
|
||||||
|
|
||||||
|
After updating your dependencies, run any necessary migrations.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx migrate --run-migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Round 2 of Monorepo World Conference Speakers Announced!!
|
||||||
|
|
||||||
|
[](https://monorepo.world)
|
||||||
|
|
||||||
|
The [Monorepo World conference](https://monorepo.world) is coming up soon on October 7, 2024 at the Computer History museum in Mountain View, California.
|
||||||
|
|
||||||
|
[Get your tickets now](https://ti.to/nx-conf/monorepoworld2024), consider [requesting access to the invite-only Enterprise Summit on October 8](https://ti.to/nx-conf/monorepoworld2024), and be sure to check out the [second round of speakers](https://monorepo.world/#speakers-title) that was just published earlier this week!
|
||||||
|
|
||||||
|
## Learn more
|
||||||
|
|
||||||
|
- [Nx Docs](/getting-started/intro)
|
||||||
|
- [X/Twitter](https://twitter.com/nxdevtools) -- [LinkedIn](https://www.linkedin.com/company/nrwl/)
|
||||||
|
- [Nx GitHub](https://github.com/nrwl/nx)
|
||||||
|
- [Nx Official Discord Server](https://go.nx.dev/community)
|
||||||
|
- [Nx Youtube Channel](https://www.youtube.com/@nxdevtools)
|
||||||
|
- [Speed up your CI](https://nx.app/)
|
||||||
BIN
docs/blog/images/2024-09-18/podcast-5-thumbnail.png
Normal file
BIN
docs/blog/images/2024-09-18/podcast-5-thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 744 KiB |
BIN
docs/blog/images/2024-09-20/thumbnail.png
Normal file
BIN
docs/blog/images/2024-09-20/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 645 KiB |
14
docs/changelog/19_8_0.md
Normal file
14
docs/changelog/19_8_0.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# [Nx 19.8](/blog/nx-19-8-update)
|
||||||
|
|
||||||
|
[](/blog/nx-19-8-update)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
{% cards cols="2" %}
|
||||||
|
{% card title="Nx Import" type="document" url="/nx-api/nx/documents/import" /%}
|
||||||
|
{% card title="Improved Task Scheduling" type="document" url="/blog/nx-19-8-update#improved-task-scheduling" /%}
|
||||||
|
{% card title="Project Crystal Comes to Angular" type="document" url="/blog/nx-19-8-update#project-crystal-comes-to-angular" /%}
|
||||||
|
{% card title="Crystalize Your Entire Workspace In One Command" type="document" url="/blog/nx-19-8-update#crystalize-your-entire-workspace-in-one-command" /%}
|
||||||
|
{% card title="New Nx Workspaces Created with ESLint v9" type="document" url="/blog/nx-19-8-update#new-nx-workspaces-created-with-eslint-v9" /%}
|
||||||
|
{% card title="Nx Release Enhancements" type="document" url="/blog/nx-19-8-update#nx-release-enhancements" /%}
|
||||||
|
{% /cards %}
|
||||||
@ -181,7 +181,7 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["vite", "webpack", "rspack"],
|
"enum": ["vite", "webpack", "rspack"],
|
||||||
"x-prompt": "Which bundler do you want to use to build the application?",
|
"x-prompt": "Which bundler do you want to use to build the application?",
|
||||||
"default": "webpack",
|
"default": "vite",
|
||||||
"x-priority": "important"
|
"x-priority": "important"
|
||||||
},
|
},
|
||||||
"minimal": {
|
"minimal": {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ The `read-write` access tokens allows task results to be stored in the remote ca
|
|||||||
|
|
||||||
You can configure an access token in CI by setting the `NX_CLOUD_ACCESS_TOKEN` environment variable. `NX_CLOUD_ACCESS_TOKEN` takes precedence over any authentication method in your `nx.json`.
|
You can configure an access token in CI by setting the `NX_CLOUD_ACCESS_TOKEN` environment variable. `NX_CLOUD_ACCESS_TOKEN` takes precedence over any authentication method in your `nx.json`.
|
||||||
|
|
||||||
The following example shows how to set the `NX_CLOUD_ACCESS_TOKEN` environment variable in a GitHub Actions workflow. You will need to add the `secrets.NX_CLOUD_ACCESS_TOKEN` secret to your repository based on instructions provided by your CI provider.
|
The following example shows how to set the `NX_CLOUD_ACCESS_TOKEN` environment variable in a GitHub Actions workflow. You will need to add the `secrets.NX_CLOUD_ACCESS_TOKEN` secret to your repository based on instructions provided by your CI provider (see [GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) or [GitLab](https://docs.gitlab.com/ee/ci/variables/#define-a-cicd-variable-in-the-ui) instructions).
|
||||||
|
|
||||||
```yml {% fileName=".github/workflows/ci.yml" highlightLines=["29-32"] %}
|
```yml {% fileName=".github/workflows/ci.yml" highlightLines=["29-32"] %}
|
||||||
name: CI
|
name: CI
|
||||||
|
|||||||
@ -15,7 +15,77 @@ Nx does this in different ways, depending on whether the task is being run on a
|
|||||||
|
|
||||||
On a developer machine, the sync generator is run in `--dry-run` mode and if files would be changed by the generator, the user is prompted to run the generator or skip it. This prompt can be disabled by setting the `sync.applyChanges` property to `true` or `false` in the `nx.json` file.
|
On a developer machine, the sync generator is run in `--dry-run` mode and if files would be changed by the generator, the user is prompted to run the generator or skip it. This prompt can be disabled by setting the `sync.applyChanges` property to `true` or `false` in the `nx.json` file.
|
||||||
|
|
||||||
In CI, the sync generator is run in `--dry-run` mode and if files would be changed by the generator, the task fails with an error provided by the sync generator. The sync generator can be skipped in CI by passing the `--skip-sync` flag when executing the task or you can skip an individual sync generator by adding that generator to the `sync.disabledTaskSyncGenerators` in `nx.json`.
|
```json {% fileName="nx.json" highlightLines=["4-6"] %}
|
||||||
|
{
|
||||||
|
"$schema": "packages/nx/schemas/nx-schema.json",
|
||||||
|
...
|
||||||
|
"sync": {
|
||||||
|
"applyChanges": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% callout type="warning" title="Opting out of automatic sync" %}
|
||||||
|
If you set `sync.applyChanges` to `false`, then developers must run `nx sync` manually before pushing changes. Otherwise, CI may fail due to the workspace being out of sync.
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
In CI, the sync generator is run in `--dry-run` mode and if files would be changed by the generator, the task fails with an error provided by the sync generator. The sync generator can be skipped in CI by passing the `--skip-sync` flag when executing the task, or you can skip an individual sync generator by adding that generator to the `sync.disabledTaskSyncGenerators` in `nx.json`.
|
||||||
|
|
||||||
|
```json {% fileName="nx.json" highlightLines=["4-6"] %}
|
||||||
|
{
|
||||||
|
"$schema": "packages/nx/schemas/nx-schema.json",
|
||||||
|
...
|
||||||
|
"sync": {
|
||||||
|
"disabledTaskSyncGenerators": ["@nx/js:typescript-sync"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the project details view to **find registered sync generators** for a given task.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx show project <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command opens up the project details view, and the registered sync generators are under the **Sync Generators** for each target. Most sync generators are inferred when using an [inference plugin](/concepts/inferred-tasks). For example, the `@nx/js/typescript` plugin registers the `@nx/js:typescript-sync` generator on `build` and `typecheck` targets.
|
||||||
|
|
||||||
|
{% project-details title="Project Details View" expandedTargets="build" %}
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project": {
|
||||||
|
"name": "foo",
|
||||||
|
"data": {
|
||||||
|
"root": " packages/foo",
|
||||||
|
"projectType": "library",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"cache": true,
|
||||||
|
"inputs": [
|
||||||
|
"{workspaceRoot}/tsconfig.base.json",
|
||||||
|
"{projectRoot}/tsconfig.lib.json",
|
||||||
|
"{projectRoot}/src/**/*.ts"
|
||||||
|
],
|
||||||
|
"outputs": ["{workspaceRoot}/packages/foo/dist"],
|
||||||
|
"syncGenerators": ["@nx/js:typescript-sync"],
|
||||||
|
"executor": "nx:run-commands",
|
||||||
|
"options": {
|
||||||
|
"command": "tsc --build tsconfig.lib.json --pretty --verbose"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceMap": {
|
||||||
|
"targets": ["packages/foo/tsconfig.ts", "@nx/js/typescript"],
|
||||||
|
"targets.build": ["packages/foo/tsconfig.ts", "@nx/js/typescript"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% /project-details %}
|
||||||
|
|
||||||
Task sync generators can be thought of like the `dependsOn` property, but for generators instead of task dependencies.
|
Task sync generators can be thought of like the `dependsOn` property, but for generators instead of task dependencies.
|
||||||
|
|
||||||
|
|||||||
@ -36,34 +36,71 @@ Here's the source code of the final result for this guide.
|
|||||||
|
|
||||||
### Create an Nx Workspace
|
### Create an Nx Workspace
|
||||||
|
|
||||||
To start with, we need to create a new Nx Workspace. We can do this easily with:
|
To start with, we need to create a new Nx Workspace and add the Nx Angular plugin. We can do this easily with:
|
||||||
|
|
||||||
{% tabs %}
|
{% tabs %}
|
||||||
{% tab label="npm" %}
|
{% tab label="npm" %}
|
||||||
|
|
||||||
|
```{% command="npx create-nx-workspace@latest ng-mf" path="~/" %}
|
||||||
|
NX Let's create a new workspace [https://nx.dev/getting-started/intro]
|
||||||
|
|
||||||
|
✔ Which stack do you want to use? · none
|
||||||
|
✔ Package-based monorepo, integrated monorepo, or standalone project? · integrated
|
||||||
|
✔ Which CI provider would you like to use? · skip
|
||||||
|
✔ Would you like remote caching to make your build faster? · skip
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Next run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npx create-nx-workspace ng-mf
|
cd ng-mf
|
||||||
|
npx nx add @nx/angular
|
||||||
```
|
```
|
||||||
|
|
||||||
{% /tab %}
|
{% /tab %}
|
||||||
{% tab label="yarn" %}
|
{% tab label="yarn" %}
|
||||||
|
|
||||||
|
```{% command="yarn create nx-workspace ng-mf" path="~/" %}
|
||||||
|
NX Let's create a new workspace [https://nx.dev/getting-started/intro]
|
||||||
|
|
||||||
|
✔ Which stack do you want to use? · none
|
||||||
|
✔ Package-based monorepo, integrated monorepo, or standalone project? · integrated
|
||||||
|
✔ Which CI provider would you like to use? · skip
|
||||||
|
✔ Would you like remote caching to make your build faster? · skip
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Next run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
yarn create nx-workspace ng-mf
|
cd ng-mf
|
||||||
|
yarn nx add @nx/angular
|
||||||
```
|
```
|
||||||
|
|
||||||
{% /tab %}
|
{% /tab %}
|
||||||
{% tab label="pnpm" %}
|
{% tab label="pnpm" %}
|
||||||
|
|
||||||
|
```{% command="pnpx create-nx-workspace@latest ng-mf" path="~/" %}
|
||||||
|
NX Let's create a new workspace [https://nx.dev/getting-started/intro]
|
||||||
|
|
||||||
|
✔ Which stack do you want to use? · none
|
||||||
|
✔ Package-based monorepo, integrated monorepo, or standalone project? · integrated
|
||||||
|
✔ Which CI provider would you like to use? · skip
|
||||||
|
✔ Would you like remote caching to make your build faster? · skip
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Next run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm create nx-workspace ng-mf
|
cd ng-mf
|
||||||
|
pnpx nx add @nx/angular
|
||||||
```
|
```
|
||||||
|
|
||||||
{% /tab %}
|
{% /tab %}
|
||||||
{% /tabs %}
|
{% /tabs %}
|
||||||
|
|
||||||
You'll be prompted a few questions. Pick the `Angular` stack, `Integrated Monorepo` layout and the `webpack` bundler. You can use the default values for the rest of the prompts. We won't use the application that gets generated by default on this guide, so you can remove it.
|
|
||||||
|
|
||||||
### Creating our applications
|
### Creating our applications
|
||||||
|
|
||||||
We need to generate two applications that support Module Federation.
|
We need to generate two applications that support Module Federation.
|
||||||
@ -140,7 +177,7 @@ This config is then used in the `webpack.config.ts` file:
|
|||||||
import { withModuleFederation } from '@nx/angular/module-federation';
|
import { withModuleFederation } from '@nx/angular/module-federation';
|
||||||
import config from './module-federation.config';
|
import config from './module-federation.config';
|
||||||
|
|
||||||
export default withModuleFederation(config);
|
export default withModuleFederation(config, { dts: false });
|
||||||
```
|
```
|
||||||
|
|
||||||
We can see the following in the **Dashboard** micro frontend configuration:
|
We can see the following in the **Dashboard** micro frontend configuration:
|
||||||
@ -168,7 +205,8 @@ We'll start by building the **Login** application, which will consist of a login
|
|||||||
|
|
||||||
### User Library
|
### User Library
|
||||||
|
|
||||||
Let's create a user data-access library that will be shared between the host application and the remote application. This will be used to determine if there is an authenticated user as well as providing logic for authenticating the user.
|
Let's create a user data-access library that will be shared between the host application and the remote application.
|
||||||
|
This will be used to determine if there is an authenticated user as well as providing logic for authenticating the user.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
nx g @nx/angular:lib libs/shared/data-access-user
|
nx g @nx/angular:lib libs/shared/data-access-user
|
||||||
@ -221,6 +259,7 @@ import { Component } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { UserService } from '@ng-mf/data-access-user';
|
import { UserService } from '@ng-mf/data-access-user';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -264,12 +303,11 @@ import { UserService } from '@ng-mf/data-access-user';
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class RemoteEntryComponent {
|
export class RemoteEntryComponent {
|
||||||
|
private userService = inject(UserService);
|
||||||
username = '';
|
username = '';
|
||||||
password = '';
|
password = '';
|
||||||
isLoggedIn$ = this.userService.isUserLoggedIn$;
|
isLoggedIn$ = this.userService.isUserLoggedIn$;
|
||||||
|
|
||||||
constructor(private userService: UserService) {}
|
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
this.userService.checkCredentials(this.username, this.password);
|
this.userService.checkCredentials(this.username, this.password);
|
||||||
}
|
}
|
||||||
@ -308,7 +346,7 @@ Next, let's add our logic to the `app.component.ts` file. Change it to match the
|
|||||||
|
|
||||||
```ts {% fileName="apps/dashboard/src/app/app.component.ts" %}
|
```ts {% fileName="apps/dashboard/src/app/app.component.ts" %}
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { Router, RouterModule } from '@angular/router';
|
import { Router, RouterModule } from '@angular/router';
|
||||||
import { UserService } from '@ng-mf/data-access-user';
|
import { UserService } from '@ng-mf/data-access-user';
|
||||||
import { distinctUntilChanged } from 'rxjs/operators';
|
import { distinctUntilChanged } from 'rxjs/operators';
|
||||||
@ -326,10 +364,10 @@ import { distinctUntilChanged } from 'rxjs/operators';
|
|||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
|
private router = inject(Router);
|
||||||
|
private userService = inject(UserService);
|
||||||
isLoggedIn$ = this.userService.isUserLoggedIn$;
|
isLoggedIn$ = this.userService.isUserLoggedIn$;
|
||||||
|
|
||||||
constructor(private userService: UserService, private router: Router) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.isLoggedIn$
|
this.isLoggedIn$
|
||||||
.pipe(distinctUntilChanged())
|
.pipe(distinctUntilChanged())
|
||||||
@ -403,9 +441,9 @@ There are 3 steps involved with this:
|
|||||||
|
|
||||||
Perhaps one of the easiest methods of fetching the Remote Definitions at runtime is to store them in a JSON file that can be present in each environment. The Host application then only has to make a GET request to the JSON file.
|
Perhaps one of the easiest methods of fetching the Remote Definitions at runtime is to store them in a JSON file that can be present in each environment. The Host application then only has to make a GET request to the JSON file.
|
||||||
|
|
||||||
We’ll start by creating this file. Add a `module-federation.manifest.json` file to the `src/assets/` folder in our **Dashboard** application with the following content:
|
We’ll start by creating this file. Add a `module-federation.manifest.json` file to the `src/public/` folder in our **Dashboard** application with the following content:
|
||||||
|
|
||||||
```json {% fileName="apps/dashboard/src/assets/module-federation.manifest.json" %}
|
```json {% fileName="apps/dashboard/src/public/module-federation.manifest.json" %}
|
||||||
{
|
{
|
||||||
"login": "http://localhost:4201"
|
"login": "http://localhost:4201"
|
||||||
}
|
}
|
||||||
@ -416,7 +454,7 @@ Next, open the `main.ts` file and replace it with the following:
|
|||||||
```ts {% fileName="apps/dashboard/src/main.ts" %}
|
```ts {% fileName="apps/dashboard/src/main.ts" %}
|
||||||
import { setRemoteDefinitions } from '@nx/angular/mf';
|
import { setRemoteDefinitions } from '@nx/angular/mf';
|
||||||
|
|
||||||
fetch('/assets/module-federation.manifest.json')
|
fetch('/module-federation.manifest.json')
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((definitions) => setRemoteDefinitions(definitions))
|
.then((definitions) => setRemoteDefinitions(definitions))
|
||||||
.then(() => import('./bootstrap').catch((err) => console.error(err)));
|
.then(() => import('./bootstrap').catch((err) => console.error(err)));
|
||||||
|
|||||||
@ -84,6 +84,7 @@ const projectDetailsLoader = async (
|
|||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
errors?: GraphError[];
|
errors?: GraphError[];
|
||||||
connectedToCloud?: boolean;
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
}> => {
|
}> => {
|
||||||
const workspaceData = await workspaceDataLoader(selectedWorkspaceId);
|
const workspaceData = await workspaceDataLoader(selectedWorkspaceId);
|
||||||
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
|
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
|
||||||
@ -104,6 +105,7 @@ const projectDetailsLoader = async (
|
|||||||
sourceMap: sourceMaps[project.data.root],
|
sourceMap: sourceMaps[project.data.root],
|
||||||
errors: workspaceData.errors,
|
errors: workspaceData.errors,
|
||||||
connectedToCloud: workspaceData.connectedToCloud,
|
connectedToCloud: workspaceData.connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators: workspaceData.disabledTaskSyncGenerators,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -21,14 +21,21 @@ import {
|
|||||||
import { ProjectDetailsHeader } from './project-details-header';
|
import { ProjectDetailsHeader } from './project-details-header';
|
||||||
|
|
||||||
export function ProjectDetailsPage() {
|
export function ProjectDetailsPage() {
|
||||||
const { project, sourceMap, hash, errors, connectedToCloud } =
|
const {
|
||||||
useRouteLoaderData('selectedProjectDetails') as {
|
project,
|
||||||
hash: string;
|
sourceMap,
|
||||||
project: ProjectGraphProjectNode;
|
hash,
|
||||||
sourceMap: Record<string, string[]>;
|
errors,
|
||||||
errors?: GraphError[];
|
connectedToCloud,
|
||||||
connectedToCloud?: boolean;
|
disabledTaskSyncGenerators,
|
||||||
};
|
} = useRouteLoaderData('selectedProjectDetails') as {
|
||||||
|
hash: string;
|
||||||
|
project: ProjectGraphProjectNode;
|
||||||
|
sourceMap: Record<string, string[]>;
|
||||||
|
errors?: GraphError[];
|
||||||
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const { environment, watch, appConfig } = useEnvironmentConfig();
|
const { environment, watch, appConfig } = useEnvironmentConfig();
|
||||||
|
|
||||||
@ -65,6 +72,7 @@ export function ProjectDetailsPage() {
|
|||||||
sourceMap={sourceMap}
|
sourceMap={sourceMap}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
connectedToCloud={connectedToCloud}
|
connectedToCloud={connectedToCloud}
|
||||||
|
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
|
||||||
></ProjectDetailsWrapper>
|
></ProjectDetailsWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ interface ProjectDetailsProps {
|
|||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
errors?: GraphError[];
|
errors?: GraphError[];
|
||||||
connectedToCloud?: boolean;
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectDetailsWrapper({
|
export function ProjectDetailsWrapper({
|
||||||
@ -29,6 +30,7 @@ export function ProjectDetailsWrapper({
|
|||||||
sourceMap,
|
sourceMap,
|
||||||
errors,
|
errors,
|
||||||
connectedToCloud,
|
connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators,
|
||||||
}: ProjectDetailsProps) {
|
}: ProjectDetailsProps) {
|
||||||
const environment = useEnvironmentConfig()?.environment;
|
const environment = useEnvironmentConfig()?.environment;
|
||||||
const externalApiService = getExternalApiService();
|
const externalApiService = getExternalApiService();
|
||||||
@ -174,6 +176,7 @@ export function ProjectDetailsWrapper({
|
|||||||
}
|
}
|
||||||
connectedToCloud={connectedToCloud}
|
connectedToCloud={connectedToCloud}
|
||||||
onNxConnect={environment === 'nx-console' ? handleNxConnect : undefined}
|
onNxConnect={environment === 'nx-console' ? handleNxConnect : undefined}
|
||||||
|
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
|
||||||
/>
|
/>
|
||||||
<ErrorToast errors={errors} />
|
<ErrorToast errors={errors} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -83,6 +83,11 @@ export const Primary = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
configurations: {},
|
configurations: {},
|
||||||
|
syncGenerators: [
|
||||||
|
'@nx/js:typescript-sync',
|
||||||
|
'@foo/bar:sync',
|
||||||
|
'@baz/qux:sync',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
dependsOn: ['build-base', 'build-native'],
|
dependsOn: ['build-base', 'build-native'],
|
||||||
@ -210,6 +215,7 @@ export const Primary = {
|
|||||||
'nx-core-build-project-json-nodes',
|
'nx-core-build-project-json-nodes',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
disabledTaskSyncGenerators: ['@foo/bar:sync'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export interface ProjectDetailsProps {
|
|||||||
errors?: GraphError[];
|
errors?: GraphError[];
|
||||||
variant?: 'default' | 'compact';
|
variant?: 'default' | 'compact';
|
||||||
connectedToCloud?: boolean;
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
onViewInProjectGraph?: (data: { projectName: string }) => void;
|
onViewInProjectGraph?: (data: { projectName: string }) => void;
|
||||||
onViewInTaskGraph?: (data: {
|
onViewInTaskGraph?: (data: {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@ -44,6 +45,7 @@ export const ProjectDetails = ({
|
|||||||
onNxConnect,
|
onNxConnect,
|
||||||
viewInProjectGraphPosition = 'top',
|
viewInProjectGraphPosition = 'top',
|
||||||
connectedToCloud,
|
connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators,
|
||||||
}: ProjectDetailsProps) => {
|
}: ProjectDetailsProps) => {
|
||||||
const projectData = project.data;
|
const projectData = project.data;
|
||||||
const isCompact = variant === 'compact';
|
const isCompact = variant === 'compact';
|
||||||
@ -153,6 +155,7 @@ export const ProjectDetails = ({
|
|||||||
onRunTarget={onRunTarget}
|
onRunTarget={onRunTarget}
|
||||||
onViewInTaskGraph={onViewInTaskGraph}
|
onViewInTaskGraph={onViewInTaskGraph}
|
||||||
connectedToCloud={connectedToCloud}
|
connectedToCloud={connectedToCloud}
|
||||||
|
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
|
||||||
onNxConnect={onNxConnect}
|
onNxConnect={onNxConnect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export interface TargetConfigurationGroupListProps {
|
|||||||
}) => void;
|
}) => void;
|
||||||
onNxConnect?: () => void;
|
onNxConnect?: () => void;
|
||||||
connectedToCloud?: boolean;
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export function TargetConfigurationGroupList({
|
|||||||
onNxConnect,
|
onNxConnect,
|
||||||
className = '',
|
className = '',
|
||||||
connectedToCloud,
|
connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators,
|
||||||
}: TargetConfigurationGroupListProps) {
|
}: TargetConfigurationGroupListProps) {
|
||||||
const targetsGroup = useMemo(() => groupTargets(project), [project]);
|
const targetsGroup = useMemo(() => groupTargets(project), [project]);
|
||||||
const hasGroups = useMemo(() => {
|
const hasGroups = useMemo(() => {
|
||||||
@ -56,6 +58,7 @@ export function TargetConfigurationGroupList({
|
|||||||
project={project}
|
project={project}
|
||||||
sourceMap={sourceMap}
|
sourceMap={sourceMap}
|
||||||
connectedToCloud={connectedToCloud}
|
connectedToCloud={connectedToCloud}
|
||||||
|
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onRunTarget={onRunTarget}
|
onRunTarget={onRunTarget}
|
||||||
onViewInTaskGraph={onViewInTaskGraph}
|
onViewInTaskGraph={onViewInTaskGraph}
|
||||||
@ -82,6 +85,7 @@ export function TargetConfigurationGroupList({
|
|||||||
project={project}
|
project={project}
|
||||||
sourceMap={sourceMap}
|
sourceMap={sourceMap}
|
||||||
connectedToCloud={connectedToCloud}
|
connectedToCloud={connectedToCloud}
|
||||||
|
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onRunTarget={onRunTarget}
|
onRunTarget={onRunTarget}
|
||||||
onViewInTaskGraph={onViewInTaskGraph}
|
onViewInTaskGraph={onViewInTaskGraph}
|
||||||
@ -105,6 +109,7 @@ export function TargetConfigurationGroupList({
|
|||||||
project={project}
|
project={project}
|
||||||
sourceMap={sourceMap}
|
sourceMap={sourceMap}
|
||||||
connectedToCloud={connectedToCloud}
|
connectedToCloud={connectedToCloud}
|
||||||
|
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onRunTarget={onRunTarget}
|
onRunTarget={onRunTarget}
|
||||||
onViewInTaskGraph={onViewInTaskGraph}
|
onViewInTaskGraph={onViewInTaskGraph}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export interface TargetConfigurationDetailsListItemProps {
|
|||||||
project: ProjectGraphProjectNode;
|
project: ProjectGraphProjectNode;
|
||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
connectedToCloud?: boolean;
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
variant?: 'default' | 'compact';
|
variant?: 'default' | 'compact';
|
||||||
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
|
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
|
||||||
onViewInTaskGraph?: (data: {
|
onViewInTaskGraph?: (data: {
|
||||||
@ -23,6 +24,7 @@ export function TargetConfigurationDetailsListItem({
|
|||||||
variant,
|
variant,
|
||||||
sourceMap,
|
sourceMap,
|
||||||
connectedToCloud,
|
connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators,
|
||||||
onRunTarget,
|
onRunTarget,
|
||||||
onViewInTaskGraph,
|
onViewInTaskGraph,
|
||||||
onNxConnect,
|
onNxConnect,
|
||||||
@ -42,6 +44,7 @@ export function TargetConfigurationDetailsListItem({
|
|||||||
targetConfiguration={target}
|
targetConfiguration={target}
|
||||||
sourceMap={sourceMap}
|
sourceMap={sourceMap}
|
||||||
connectedToCloud={connectedToCloud}
|
connectedToCloud={connectedToCloud}
|
||||||
|
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
|
||||||
onRunTarget={onRunTarget}
|
onRunTarget={onRunTarget}
|
||||||
onViewInTaskGraph={onViewInTaskGraph}
|
onViewInTaskGraph={onViewInTaskGraph}
|
||||||
onNxConnect={onNxConnect}
|
onNxConnect={onNxConnect}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { TargetExecutorTitle } from '../target-executor/target-executor-title';
|
|||||||
import { getTargetExecutorSourceMapKey } from '../target-source-info/get-target-executor-source-map-key';
|
import { getTargetExecutorSourceMapKey } from '../target-source-info/get-target-executor-source-map-key';
|
||||||
import { TargetSourceInfo } from '../target-source-info/target-source-info';
|
import { TargetSourceInfo } from '../target-source-info/target-source-info';
|
||||||
import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration';
|
import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration';
|
||||||
|
import { getTaskSyncGenerators } from '../utils/sync-generators';
|
||||||
import { FadingCollapsible } from './fading-collapsible';
|
import { FadingCollapsible } from './fading-collapsible';
|
||||||
import { TargetConfigurationProperty } from './target-configuration-property';
|
import { TargetConfigurationProperty } from './target-configuration-property';
|
||||||
import { TooltipTriggerText } from './tooltip-trigger-text';
|
import { TooltipTriggerText } from './tooltip-trigger-text';
|
||||||
@ -24,6 +25,7 @@ interface TargetConfigurationDetailsProps {
|
|||||||
targetConfiguration: TargetConfiguration;
|
targetConfiguration: TargetConfiguration;
|
||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
connectedToCloud?: boolean;
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
variant?: 'default' | 'compact';
|
variant?: 'default' | 'compact';
|
||||||
onCollapse?: (targetName: string) => void;
|
onCollapse?: (targetName: string) => void;
|
||||||
onExpand?: (targetName: string) => void;
|
onExpand?: (targetName: string) => void;
|
||||||
@ -43,6 +45,7 @@ export default function TargetConfigurationDetails({
|
|||||||
targetConfiguration,
|
targetConfiguration,
|
||||||
sourceMap,
|
sourceMap,
|
||||||
connectedToCloud,
|
connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators,
|
||||||
onViewInTaskGraph,
|
onViewInTaskGraph,
|
||||||
onRunTarget,
|
onRunTarget,
|
||||||
onNxConnect,
|
onNxConnect,
|
||||||
@ -84,6 +87,9 @@ export default function TargetConfigurationDetails({
|
|||||||
? Object.keys(configurations).length
|
? Object.keys(configurations).length
|
||||||
: true);
|
: true);
|
||||||
|
|
||||||
|
const { enabledSyncGenerators, disabledSyncGenerators } =
|
||||||
|
getTaskSyncGenerators(targetConfiguration, disabledTaskSyncGenerators);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative rounded-md border border-slate-200 dark:border-slate-700/60">
|
<div className="relative rounded-md border border-slate-200 dark:border-slate-700/60">
|
||||||
<TargetConfigurationDetailsHeader
|
<TargetConfigurationDetailsHeader
|
||||||
@ -364,7 +370,7 @@ export default function TargetConfigurationDetails({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</h4>
|
</h4>
|
||||||
<div className="group/line overflow-hidden whitespace-nowrap pl-5">
|
<div className="group/line overflow-hidden whitespace-nowrap pl-5">
|
||||||
<TargetConfigurationProperty data={{ paralelism: false }}>
|
<TargetConfigurationProperty data={{ parallelism: false }}>
|
||||||
<TargetSourceInfo
|
<TargetSourceInfo
|
||||||
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
|
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
|
||||||
propertyKey={`targets.${targetName}.parallelism`}
|
propertyKey={`targets.${targetName}.parallelism`}
|
||||||
@ -374,6 +380,68 @@ export default function TargetConfigurationDetails({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{enabledSyncGenerators.length > 0 && (
|
||||||
|
<div className="group">
|
||||||
|
<h4 className="mb-4">
|
||||||
|
<Tooltip
|
||||||
|
openAction="hover"
|
||||||
|
content={
|
||||||
|
(<PropertyInfoTooltip type="syncGenerators" />) as any
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="font-medium">
|
||||||
|
<TooltipTriggerText>Sync Generators</TooltipTriggerText>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</h4>
|
||||||
|
<ul className="mb-4 list-disc pl-5">
|
||||||
|
{enabledSyncGenerators.map((generator, idx) => (
|
||||||
|
<li
|
||||||
|
className="group/line overflow-hidden whitespace-nowrap"
|
||||||
|
key={`syncGenerators-${idx}`}
|
||||||
|
>
|
||||||
|
<TargetConfigurationProperty data={generator}>
|
||||||
|
<TargetSourceInfo
|
||||||
|
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
|
||||||
|
propertyKey={`targets.${targetName}.syncGenerators`}
|
||||||
|
sourceMap={sourceMap}
|
||||||
|
/>
|
||||||
|
</TargetConfigurationProperty>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{disabledSyncGenerators.length > 0 &&
|
||||||
|
disabledSyncGenerators.map((generator, idx) => (
|
||||||
|
<li
|
||||||
|
className="group/line overflow-hidden whitespace-nowrap"
|
||||||
|
key={`syncGenerators-${idx}`}
|
||||||
|
>
|
||||||
|
<TargetConfigurationProperty
|
||||||
|
data={generator}
|
||||||
|
disabled={true}
|
||||||
|
disabledTooltip={
|
||||||
|
<p className="max-w-sm whitespace-pre-wrap py-2 font-mono text-sm normal-case text-slate-700 dark:text-slate-400">
|
||||||
|
The Sync Generator is disabled in the{' '}
|
||||||
|
<code className="font-bold italic">
|
||||||
|
sync.disabledTaskSyncGenerators
|
||||||
|
</code>{' '}
|
||||||
|
property in the{' '}
|
||||||
|
<code className="font-bold italic">nx.json</code>{' '}
|
||||||
|
file.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TargetSourceInfo
|
||||||
|
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
|
||||||
|
propertyKey={`targets.${targetName}.syncGenerators`}
|
||||||
|
sourceMap={sourceMap}
|
||||||
|
/>
|
||||||
|
</TargetConfigurationProperty>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { Tooltip } from '@nx/graph/ui-tooltips';
|
||||||
|
import { JSX, ReactNode } from 'react';
|
||||||
|
import { TooltipTriggerText } from './tooltip-trigger-text';
|
||||||
|
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
interface TargetConfigurationPropertyTextProps {
|
||||||
|
content: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
disabledTooltip?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TargetConfigurationPropertyText({
|
||||||
|
content,
|
||||||
|
disabled,
|
||||||
|
disabledTooltip,
|
||||||
|
}: TargetConfigurationPropertyTextProps): JSX.Element | null {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className={disabled ? 'opacity-50' : ''}>{content}</span>
|
||||||
|
{disabledTooltip && (
|
||||||
|
<Tooltip openAction="hover" content={disabledTooltip}>
|
||||||
|
<span className="pl-2 font-medium">
|
||||||
|
<TooltipTriggerText>
|
||||||
|
<QuestionMarkCircleIcon className="inline h-4 w-4" />
|
||||||
|
</TooltipTriggerText>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,18 +1,27 @@
|
|||||||
import { JSX, ReactNode } from 'react';
|
import { JSX, ReactNode } from 'react';
|
||||||
|
import { TargetConfigurationPropertyText } from './target-configuration-property-text';
|
||||||
|
|
||||||
interface RenderPropertyProps {
|
interface RenderPropertyProps {
|
||||||
data: string | Record<string, any> | any[];
|
data: string | Record<string, any> | any[];
|
||||||
|
disabled?: boolean;
|
||||||
|
disabledTooltip?: ReactNode;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TargetConfigurationProperty({
|
export function TargetConfigurationProperty({
|
||||||
data,
|
data,
|
||||||
children,
|
children,
|
||||||
|
disabled,
|
||||||
|
disabledTooltip,
|
||||||
}: RenderPropertyProps): JSX.Element | null {
|
}: RenderPropertyProps): JSX.Element | null {
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
return (
|
return (
|
||||||
<span className="flex font-mono text-sm">
|
<span className="flex font-mono text-sm">
|
||||||
{data}
|
<TargetConfigurationPropertyText
|
||||||
|
content={data}
|
||||||
|
disabled={disabled}
|
||||||
|
disabledTooltip={disabledTooltip}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -21,7 +30,11 @@ export function TargetConfigurationProperty({
|
|||||||
<ul>
|
<ul>
|
||||||
{data.map((item, index) => (
|
{data.map((item, index) => (
|
||||||
<li key={index} className="flex font-mono text-sm">
|
<li key={index} className="flex font-mono text-sm">
|
||||||
{String(item)}
|
<TargetConfigurationPropertyText
|
||||||
|
content={String(item)}
|
||||||
|
disabled={disabled}
|
||||||
|
disabledTooltip={disabledTooltip}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@ -32,7 +45,15 @@ export function TargetConfigurationProperty({
|
|||||||
<ul>
|
<ul>
|
||||||
{Object.entries(data).map(([key, value], index) => (
|
{Object.entries(data).map(([key, value], index) => (
|
||||||
<li key={index} className="flex font-mono text-sm">
|
<li key={index} className="flex font-mono text-sm">
|
||||||
<strong>{key}</strong>: {String(value)}
|
<TargetConfigurationPropertyText
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<strong>{key}</strong>: {String(value)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
disabledTooltip={disabledTooltip}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
34
graph/ui-project-details/src/lib/utils/sync-generators.ts
Normal file
34
graph/ui-project-details/src/lib/utils/sync-generators.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import type { TargetConfiguration } from '@nx/devkit';
|
||||||
|
|
||||||
|
export function getTaskSyncGenerators(
|
||||||
|
targetConfiguration: TargetConfiguration,
|
||||||
|
disabledTaskSyncGenerators: string[] | undefined
|
||||||
|
): {
|
||||||
|
enabledSyncGenerators: string[];
|
||||||
|
disabledSyncGenerators: string[];
|
||||||
|
} {
|
||||||
|
const enabledSyncGenerators: string[] = [];
|
||||||
|
const disabledSyncGenerators: string[] = [];
|
||||||
|
|
||||||
|
if (!targetConfiguration.syncGenerators?.length) {
|
||||||
|
return { enabledSyncGenerators, disabledSyncGenerators };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!disabledTaskSyncGenerators?.length) {
|
||||||
|
enabledSyncGenerators.push(...targetConfiguration.syncGenerators);
|
||||||
|
return { enabledSyncGenerators, disabledSyncGenerators };
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledGeneratorsSet = new Set(disabledTaskSyncGenerators);
|
||||||
|
for (const generator of targetConfiguration.syncGenerators) {
|
||||||
|
if (disabledGeneratorsSet.has(generator)) {
|
||||||
|
disabledSyncGenerators.push(generator);
|
||||||
|
} else {
|
||||||
|
enabledSyncGenerators.push(generator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { enabledSyncGenerators, disabledSyncGenerators };
|
||||||
|
}
|
||||||
@ -2,6 +2,10 @@
|
|||||||
@apply overflow-hidden !important;
|
@apply overflow-hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.DocSearch-VisuallyHiddenForAccessibility {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
body .DocSearch-Container {
|
body .DocSearch-Container {
|
||||||
@apply fixed left-0 top-0 z-[50] flex h-screen w-screen cursor-auto flex-col bg-black/10 p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh] dark:bg-white/10;
|
@apply fixed left-0 top-0 z-[50] flex h-screen w-screen cursor-auto flex-col bg-black/10 p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh] dark:bg-white/10;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { SectionHeading } from './temp/typography';
|
|
||||||
import {
|
import {
|
||||||
BoltIcon,
|
BoltIcon,
|
||||||
ChevronDoubleRightIcon,
|
ChevronDoubleRightIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
WrenchIcon,
|
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
|
|
||||||
export function EnterpriseAddons(): JSX.Element {
|
export function EnterpriseAddons(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { SectionHeading } from './temp/typography';
|
import { ButtonLink, SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
import { ButtonLink } from '@nx/nx-dev/ui-common';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export function Hero(): JSX.Element {
|
export function Hero(): JSX.Element {
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import { BoltIcon } from '@heroicons/react/24/outline';
|
|
||||||
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
|
||||||
import { NxAgentsIcon, NxReplayIcon } from '@nx/nx-dev/ui-icons';
|
|
||||||
|
|
||||||
const features = [
|
|
||||||
{
|
|
||||||
name: 'Cache with Nx Replay',
|
|
||||||
description:
|
|
||||||
'Quis tellus eget adipiscing convallis sit sit eget aliquet quis. Suspendisse eget egestas a elementum pulvinar et feugiat blandit at. In mi viverra elit nunc.',
|
|
||||||
icon: NxReplayIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Distribution with Nx Agents',
|
|
||||||
description:
|
|
||||||
'Quis tellus eget adipiscing convallis sit sit eget aliquet quis. Suspendisse eget egestas a elementum pulvinar et feugiat blandit at. In mi viverra elit nunc.',
|
|
||||||
icon: NxAgentsIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Split tasks with Atomizer',
|
|
||||||
description:
|
|
||||||
'Quis tellus eget adipiscing convallis sit sit eget aliquet quis. Suspendisse eget egestas a elementum pulvinar et feugiat blandit at. In mi viverra elit nunc.',
|
|
||||||
icon: BoltIcon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ScaleCiAndTeams(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
||||||
<div className="max-w-3xl">
|
|
||||||
<SectionHeading as="h2" variant="display" id="scale-ci-and-teams">
|
|
||||||
Scale CI & teams
|
|
||||||
</SectionHeading>
|
|
||||||
<p className="mt-6 text-lg leading-8">
|
|
||||||
Quis tellus eget adipiscing convallis sit sit eget aliquet quis.
|
|
||||||
Suspendisse eget egestas a elementum pulvinar et feugiat blandit at.
|
|
||||||
In mi viverra elit nunc.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mt-16 max-w-2xl lg:max-w-none">
|
|
||||||
<dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none lg:grid-cols-3">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<div key={feature.name} className="flex flex-col">
|
|
||||||
<dt className="flex items-center gap-x-3 text-base font-semibold leading-7">
|
|
||||||
<feature.icon
|
|
||||||
className="h-5 w-5 flex-none"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{feature.name}
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-slate-500">
|
|
||||||
<p className="flex-auto">{feature.description}</p>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -3,11 +3,10 @@ import {
|
|||||||
Cog6ToothIcon,
|
Cog6ToothIcon,
|
||||||
CubeTransparentIcon,
|
CubeTransparentIcon,
|
||||||
IdentificationIcon,
|
IdentificationIcon,
|
||||||
PhotoIcon,
|
|
||||||
SquaresPlusIcon,
|
SquaresPlusIcon,
|
||||||
UserGroupIcon,
|
UserGroupIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { SectionHeading } from './temp/typography';
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
|
|
||||||
export function ScaleYourPeople(): JSX.Element {
|
export function ScaleYourPeople(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@ -250,28 +249,6 @@ export function ScaleYourPeople(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/*<div className="mt-16">*/}
|
|
||||||
{/* <picture className="block py-12">*/}
|
|
||||||
{/* <img*/}
|
|
||||||
{/* src="/images/enterprise/graphs.jpg"*/}
|
|
||||||
{/* alt="Product screenshot"*/}
|
|
||||||
{/* className="mx-auto max-w-full rounded-xl shadow-xl ring-1 ring-slate-400/10"*/}
|
|
||||||
{/* width={2500}*/}
|
|
||||||
{/* height={1616}*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/* </picture>*/}
|
|
||||||
{/* <div className="mx-auto mt-4 max-w-2xl">*/}
|
|
||||||
{/* <h4 className="relative text-base font-medium capitalize leading-6 text-slate-900 dark:text-slate-100">*/}
|
|
||||||
{/* Crystal clear organizations*/}
|
|
||||||
{/* </h4>*/}
|
|
||||||
{/* <p>*/}
|
|
||||||
{/* Regardless of how many Nx Workspaces your company has, Nx Enterprise*/}
|
|
||||||
{/* can give you the visibility you need to understand what they have in*/}
|
|
||||||
{/* common, how they relate, and how they differ. Developers are no*/}
|
|
||||||
{/* longer relegated to contributing to one Nx Workspace.*/}
|
|
||||||
{/* </p>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { SectionHeading } from './temp/typography';
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
|
|
||||||
export function Security(): JSX.Element {
|
export function Security(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -12,9 +12,9 @@ import {
|
|||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
|
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { SectionHeading } from './temp/typography';
|
|
||||||
import { BentoGrid, BentoGridItem } from './bento-grid';
|
import { BentoGrid, BentoGridItem } from './bento-grid';
|
||||||
import { cx } from '@nx/nx-dev/ui-primitives';
|
import { cx } from '@nx/nx-dev/ui-primitives';
|
||||||
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
|
|
||||||
export function SolveYourCi(): JSX.Element {
|
export function SolveYourCi(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,150 +0,0 @@
|
|||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { SectionHeading } from './temp/typography';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the total number of years worth of compute.
|
|
||||||
*
|
|
||||||
* @param {number} millis - The total number of seconds.
|
|
||||||
* @return {number} The total number of years.
|
|
||||||
*/
|
|
||||||
function getTotalYears(millis: number): number {
|
|
||||||
/**
|
|
||||||
* The number of millis in a year is approximately:
|
|
||||||
* 86 400 000 millis/day * 365.25 days/year ≈ 31 557 600 000 seconds/year.
|
|
||||||
*/
|
|
||||||
const yearMillis = Number(31557600000);
|
|
||||||
return Math.round(millis / yearMillis);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the time saved from a remote API.
|
|
||||||
*
|
|
||||||
* @returns {Promise} A promise that resolves to an object containing the time saved data.
|
|
||||||
* @returns {Date} The date the time saved data was retrieved.
|
|
||||||
* @returns {number} The time saved in the last 7 days.
|
|
||||||
* @returns {number} The time saved in the last 30 days.
|
|
||||||
* @returns {number} The time's saved since the start.
|
|
||||||
*/
|
|
||||||
function fetchTimeSaved(): Promise<{
|
|
||||||
date: Date;
|
|
||||||
last7days: number;
|
|
||||||
last30days: number;
|
|
||||||
sinceStart: number;
|
|
||||||
}> {
|
|
||||||
const apiUrl = 'https://cloud.nx.app/time-saved';
|
|
||||||
|
|
||||||
return fetch(apiUrl)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.catch(() => ({
|
|
||||||
date: new Date(),
|
|
||||||
last7days: Math.round(Math.random() * 1000000000),
|
|
||||||
last30days: Math.round(Math.random() * 100000000000),
|
|
||||||
sinceStart: Math.round(Math.random() * 10000000000000),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Developers using Nx',
|
|
||||||
value: 2,
|
|
||||||
suffix: 'M+',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Active workspaces',
|
|
||||||
value: '4k',
|
|
||||||
suffix: '+',
|
|
||||||
},
|
|
||||||
{ id: 2, name: 'Compute time saved', value: 800, suffix: '+ years' },
|
|
||||||
{ id: 4, name: 'Runs daily', value: 100, suffix: 'k+' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Statistics(): JSX.Element {
|
|
||||||
const variants = {
|
|
||||||
hidden: {
|
|
||||||
opacity: 0,
|
|
||||||
transition: {
|
|
||||||
when: 'afterChildren',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visible: (i: number) => ({
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
delay: i || 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
const itemVariants = {
|
|
||||||
visible: (i: number) => ({
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
delay: i * 0.25,
|
|
||||||
duration: 0.65,
|
|
||||||
ease: 'easeOut',
|
|
||||||
when: 'beforeChildren',
|
|
||||||
staggerChildren: 0.3,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
hidden: {
|
|
||||||
opacity: 0,
|
|
||||||
y: 4,
|
|
||||||
transition: {
|
|
||||||
when: 'afterChildren',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const [timeSaved, setTimeSaved] = useState<number>(800);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let ignore = false;
|
|
||||||
fetchTimeSaved().then((data) => {
|
|
||||||
if (!ignore) {
|
|
||||||
setTimeSaved(getTotalYears(data.sinceStart));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
ignore = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
||||||
<div className="mx-auto max-w-2xl lg:mx-0 lg:max-w-xl">
|
|
||||||
<SectionHeading as="h2" variant="title" id="statistics">
|
|
||||||
Trusted by startups and Fortune 500 companies
|
|
||||||
</SectionHeading>
|
|
||||||
{/*<SectionHeading as="p" variant="subtitle" className="mt-6">*/}
|
|
||||||
{/* Nx Cloud provides plans for open source projects, startups, and large*/}
|
|
||||||
{/* enterprises.*/}
|
|
||||||
{/*</SectionHeading>*/}
|
|
||||||
</div>
|
|
||||||
<motion.dl
|
|
||||||
initial="hidden"
|
|
||||||
variants={variants}
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true }}
|
|
||||||
className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-10 text-slate-950 sm:mt-20 sm:grid-cols-2 sm:gap-y-16 lg:mx-0 lg:max-w-none lg:grid-cols-4 dark:text-white"
|
|
||||||
>
|
|
||||||
{stats.map((stat, idx) => (
|
|
||||||
<motion.div
|
|
||||||
key={`statistic-${idx}`}
|
|
||||||
custom={idx}
|
|
||||||
variants={itemVariants}
|
|
||||||
className="flex flex-col gap-y-3 border-l border-black/10 pl-6 dark:border-white/10"
|
|
||||||
>
|
|
||||||
<dt className="text-sm leading-6 text-slate-600 dark:text-slate-500">
|
|
||||||
{stat.name}
|
|
||||||
</dt>
|
|
||||||
<dd className="order-first text-3xl font-semibold tracking-tight">
|
|
||||||
{stat.name === 'Compute time saved' ? timeSaved : stat.value}
|
|
||||||
{stat.suffix}
|
|
||||||
</dd>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</motion.dl>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { cx } from '@nx/nx-dev/ui-primitives';
|
|
||||||
import { ElementType, ReactNode } from 'react';
|
|
||||||
|
|
||||||
type AllowedVariants = 'title' | 'display' | 'subtitle';
|
|
||||||
|
|
||||||
type Headings = {
|
|
||||||
as: ElementType;
|
|
||||||
className?: string;
|
|
||||||
children: ReactNode | ReactNode[];
|
|
||||||
id?: string;
|
|
||||||
variant: AllowedVariants;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Description = {
|
|
||||||
as: ElementType;
|
|
||||||
className?: string;
|
|
||||||
children: ReactNode | ReactNode[];
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const variants: Record<AllowedVariants, string> = {
|
|
||||||
title:
|
|
||||||
'text-3xl font-medium tracking-tight text-slate-950 dark:text-white sm:text-5xl',
|
|
||||||
display:
|
|
||||||
'text-4xl font-medium tracking-tight text-slate-950 dark:text-white sm:text-7xl',
|
|
||||||
subtitle: 'text-lg leading-8 text-slate-700 dark:text-slate-300 sm:text-2xl',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SectionHeading({
|
|
||||||
as = 'div',
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
...rest
|
|
||||||
}: Headings): JSX.Element {
|
|
||||||
const Tag = as;
|
|
||||||
return (
|
|
||||||
<Tag className={cx(variants[variant], className)} {...rest}>
|
|
||||||
{children}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SectionDescription({
|
|
||||||
as = 'div',
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}: Description): JSX.Element {
|
|
||||||
const Tag = as;
|
|
||||||
return (
|
|
||||||
<Tag
|
|
||||||
className={cx('text-slate-700 dark:text-slate-400', className)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { ProjectDetails as ProjectDetailsUi } from '@nx/graph-internal/ui-project-details';
|
import { ProjectDetails as ProjectDetailsUi } from '@nx/graph-internal/ui-project-details';
|
||||||
import { ExpandedTargetsProvider } from '@nx/graph/shared';
|
import { ExpandedTargetsProvider } from '@nx/graph/shared';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function Loading() {
|
export function Loading() {
|
||||||
return (
|
return (
|
||||||
@ -110,7 +111,7 @@ export function ProjectDetails({
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
id="project-details-container"
|
id="project-details-container"
|
||||||
className="not-prose overflow-y-auto"
|
className={twMerge('not-prose', height && 'overflow-y-auto')}
|
||||||
style={{ height }}
|
style={{ height }}
|
||||||
ref={elementRef}
|
ref={elementRef}
|
||||||
>
|
>
|
||||||
|
|||||||
9
nx.json
9
nx.json
@ -123,7 +123,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"dependsOn": ["build-native", "^build-native"]
|
"dependsOn": [
|
||||||
|
"build-native",
|
||||||
|
"^build-native",
|
||||||
|
"@nx/nx-source:lint-pnpm-lock"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lint-pnpm-lock": {
|
||||||
|
"cache": true
|
||||||
},
|
},
|
||||||
"e2e": {
|
"e2e": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
|
|||||||
37
package.json
37
package.json
@ -22,7 +22,8 @@
|
|||||||
"preinstall": "node ./scripts/preinstall.js",
|
"preinstall": "node ./scripts/preinstall.js",
|
||||||
"test": "nx run-many -t test",
|
"test": "nx run-many -t test",
|
||||||
"e2e": "nx run-many -t e2e --projects ./e2e/*",
|
"e2e": "nx run-many -t e2e --projects ./e2e/*",
|
||||||
"build:wasm": "rustup override set nightly-2024-07-19 && rustup target add wasm32-wasip1-threads && WASI_SDK_PATH=\"$(pwd)/wasi-sdk-23.0-x86_64-linux\" CMAKE_BUILD_PARALLEL_LEVEL=2 LIBSQLITE3_FLAGS=\"-DLONGDOUBLE_TYPE=double\" pnpm exec nx run-many -t build-native-wasm && rustup override unset"
|
"build:wasm": "rustup override set nightly-2024-07-19 && rustup target add wasm32-wasip1-threads && WASI_SDK_PATH=\"$(pwd)/wasi-sdk-23.0-x86_64-linux\" CMAKE_BUILD_PARALLEL_LEVEL=2 LIBSQLITE3_FLAGS=\"-DLONGDOUBLE_TYPE=double\" pnpm exec nx run-many -t build-native-wasm && rustup override unset",
|
||||||
|
"lint-pnpm-lock": "eslint pnpm-lock.yaml"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
@ -68,21 +69,21 @@
|
|||||||
"@ngrx/store": "18.0.2",
|
"@ngrx/store": "18.0.2",
|
||||||
"@nuxt/kit": "^3.10.0",
|
"@nuxt/kit": "^3.10.0",
|
||||||
"@nuxt/schema": "^3.10.0",
|
"@nuxt/schema": "^3.10.0",
|
||||||
"@nx/angular": "19.8.0-beta.0",
|
"@nx/angular": "19.8.0-beta.2",
|
||||||
"@nx/cypress": "19.8.0-beta.0",
|
"@nx/cypress": "19.8.0-beta.2",
|
||||||
"@nx/devkit": "19.8.0-beta.0",
|
"@nx/devkit": "19.8.0-beta.2",
|
||||||
"@nx/esbuild": "19.8.0-beta.0",
|
"@nx/esbuild": "19.8.0-beta.2",
|
||||||
"@nx/eslint": "19.8.0-beta.0",
|
"@nx/eslint": "19.8.0-beta.2",
|
||||||
"@nx/eslint-plugin": "19.8.0-beta.0",
|
"@nx/eslint-plugin": "19.8.0-beta.2",
|
||||||
"@nx/jest": "19.8.0-beta.0",
|
"@nx/jest": "19.8.0-beta.2",
|
||||||
"@nx/js": "19.8.0-beta.0",
|
"@nx/js": "19.8.0-beta.2",
|
||||||
"@nx/next": "19.8.0-beta.0",
|
"@nx/next": "19.8.0-beta.2",
|
||||||
"@nx/playwright": "19.8.0-beta.0",
|
"@nx/playwright": "19.8.0-beta.2",
|
||||||
"@nx/react": "19.8.0-beta.0",
|
"@nx/react": "19.8.0-beta.2",
|
||||||
"@nx/storybook": "19.8.0-beta.0",
|
"@nx/storybook": "19.8.0-beta.2",
|
||||||
"@nx/vite": "19.8.0-beta.0",
|
"@nx/vite": "19.8.0-beta.2",
|
||||||
"@nx/web": "19.8.0-beta.0",
|
"@nx/web": "19.8.0-beta.2",
|
||||||
"@nx/webpack": "19.8.0-beta.0",
|
"@nx/webpack": "19.8.0-beta.2",
|
||||||
"@phenomnomnominal/tsquery": "~5.0.1",
|
"@phenomnomnominal/tsquery": "~5.0.1",
|
||||||
"@playwright/test": "^1.36.1",
|
"@playwright/test": "^1.36.1",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||||
@ -244,13 +245,14 @@
|
|||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"npm-package-arg": "11.0.1",
|
"npm-package-arg": "11.0.1",
|
||||||
"nuxt": "^3.10.0",
|
"nuxt": "^3.10.0",
|
||||||
"nx": "19.8.0-beta.0",
|
"nx": "19.8.0-beta.2",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
"open": "^8.4.0",
|
"open": "^8.4.0",
|
||||||
"openai": "~4.3.1",
|
"openai": "~4.3.1",
|
||||||
"ora": "5.3.0",
|
"ora": "5.3.0",
|
||||||
"parse-markdown-links": "^1.0.4",
|
"parse-markdown-links": "^1.0.4",
|
||||||
"parse5": "4.0.0",
|
"parse5": "4.0.0",
|
||||||
|
"picocolors": "^1.1.0",
|
||||||
"piscina": "^4.4.0",
|
"piscina": "^4.4.0",
|
||||||
"postcss": "8.4.38",
|
"postcss": "8.4.38",
|
||||||
"postcss-import": "~14.1.0",
|
"postcss-import": "~14.1.0",
|
||||||
@ -380,6 +382,7 @@
|
|||||||
},
|
},
|
||||||
"nx": {
|
"nx": {
|
||||||
"includedScripts": [
|
"includedScripts": [
|
||||||
|
"lint-pnpm-lock",
|
||||||
"echo",
|
"echo",
|
||||||
"check-commit",
|
"check-commit",
|
||||||
"check-format",
|
"check-format",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { merge } from 'webpack-merge';
|
|||||||
import { registerTsProject } from '@nx/js/src/internal';
|
import { registerTsProject } from '@nx/js/src/internal';
|
||||||
import { workspaceRoot } from '@nx/devkit';
|
import { workspaceRoot } from '@nx/devkit';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
|
||||||
export async function mergeCustomWebpackConfig(
|
export async function mergeCustomWebpackConfig(
|
||||||
baseWebpackConfig: any,
|
baseWebpackConfig: any,
|
||||||
@ -22,11 +23,44 @@ export async function mergeCustomWebpackConfig(
|
|||||||
|
|
||||||
// The extra Webpack configuration file can export a synchronous or asynchronous function,
|
// The extra Webpack configuration file can export a synchronous or asynchronous function,
|
||||||
// for instance: `module.exports = async config => { ... }`.
|
// for instance: `module.exports = async config => { ... }`.
|
||||||
|
let newConfig: any;
|
||||||
if (typeof config === 'function') {
|
if (typeof config === 'function') {
|
||||||
return config(baseWebpackConfig, options, target);
|
newConfig = config(baseWebpackConfig, options, target);
|
||||||
} else {
|
} else {
|
||||||
return merge(baseWebpackConfig, config);
|
newConfig = merge(baseWebpackConfig, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// license-webpack-plugin will at times try to scan the monorepo's root package.json
|
||||||
|
// This will result in an error being thrown
|
||||||
|
// Ensure root package.json is excluded
|
||||||
|
const licensePlugin = newConfig.plugins.find(
|
||||||
|
(p) => p.constructor.name === 'LicenseWebpackPlugin'
|
||||||
|
);
|
||||||
|
if (licensePlugin) {
|
||||||
|
let rootPackageJsonName: string;
|
||||||
|
const pathToRootPackageJson = join(
|
||||||
|
newConfig.context.root ?? workspaceRoot,
|
||||||
|
'package.json'
|
||||||
|
);
|
||||||
|
if (existsSync(pathToRootPackageJson)) {
|
||||||
|
try {
|
||||||
|
const rootPackageJson = JSON.parse(
|
||||||
|
readFileSync(pathToRootPackageJson, 'utf-8')
|
||||||
|
);
|
||||||
|
rootPackageJsonName = rootPackageJson.name;
|
||||||
|
licensePlugin.pluginOptions.excludedPackageTest = (pkgName: string) => {
|
||||||
|
if (!rootPackageJsonName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return pkgName === rootPackageJsonName;
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveCustomWebpackConfig(path: string, tsConfig: string) {
|
export function resolveCustomWebpackConfig(path: string, tsConfig: string) {
|
||||||
|
|||||||
@ -15,7 +15,15 @@
|
|||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
"rules": {}
|
"rules": {
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"name": "chalk",
|
||||||
|
"message": "Please use `picocolors` in place of `chalk` for rendering terminal colors"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx"],
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import chalk = require('chalk');
|
import * as pc from 'picocolors';
|
||||||
import enquirer = require('enquirer');
|
import enquirer = require('enquirer');
|
||||||
import yargs = require('yargs');
|
import yargs = require('yargs');
|
||||||
|
|
||||||
@ -26,16 +26,16 @@ import {
|
|||||||
} from 'create-nx-workspace/src/utils/nx/ab-testing';
|
} from 'create-nx-workspace/src/utils/nx/ab-testing';
|
||||||
|
|
||||||
export const yargsDecorator = {
|
export const yargsDecorator = {
|
||||||
'Options:': `${chalk.green`Options`}:`,
|
'Options:': `${pc.green(`Options`)}:`,
|
||||||
'Examples:': `${chalk.green`Examples`}:`,
|
'Examples:': `${pc.green(`Examples`)}:`,
|
||||||
boolean: `${chalk.blue`boolean`}`,
|
boolean: `${pc.blue(`boolean`)}`,
|
||||||
count: `${chalk.blue`count`}`,
|
count: `${pc.blue(`count`)}`,
|
||||||
string: `${chalk.blue`string`}`,
|
string: `${pc.blue(`string`)}`,
|
||||||
array: `${chalk.blue`array`}`,
|
array: `${pc.blue(`array`)}`,
|
||||||
required: `${chalk.blue`required`}`,
|
required: `${pc.blue(`required`)}`,
|
||||||
'default:': `${chalk.blue`default`}:`,
|
'default:': `${pc.blue(`default`)}:`,
|
||||||
'choices:': `${chalk.blue`choices`}:`,
|
'choices:': `${pc.blue(`choices`)}:`,
|
||||||
'aliases:': `${chalk.blue`aliases`}:`,
|
'aliases:': `${pc.blue(`aliases`)}:`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nxVersion = require('../package.json').version;
|
const nxVersion = require('../package.json').version;
|
||||||
@ -97,7 +97,7 @@ export const commandsObject: yargs.Argv<CreateNxPluginArguments> = yargs
|
|||||||
withOptions(
|
withOptions(
|
||||||
yargs
|
yargs
|
||||||
.positional('pluginName', {
|
.positional('pluginName', {
|
||||||
describe: chalk.dim`Plugin name`,
|
describe: pc.dim(`Plugin name`),
|
||||||
type: 'string',
|
type: 'string',
|
||||||
alias: ['name'],
|
alias: ['name'],
|
||||||
})
|
})
|
||||||
@ -121,11 +121,11 @@ export const commandsObject: yargs.Argv<CreateNxPluginArguments> = yargs
|
|||||||
},
|
},
|
||||||
[normalizeArgsMiddleware]
|
[normalizeArgsMiddleware]
|
||||||
)
|
)
|
||||||
.help('help', chalk.dim`Show help`)
|
.help('help', pc.dim(`Show help`))
|
||||||
.updateLocale(yargsDecorator)
|
.updateLocale(yargsDecorator)
|
||||||
.version(
|
.version(
|
||||||
'version',
|
'version',
|
||||||
chalk.dim`Show version`,
|
pc.dim(`Show version`),
|
||||||
nxVersion
|
nxVersion
|
||||||
) as yargs.Argv<CreateNxPluginArguments>;
|
) as yargs.Argv<CreateNxPluginArguments>;
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
"homepage": "https://nx.dev",
|
"homepage": "https://nx.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"create-nx-workspace": "file:../create-nx-workspace",
|
"create-nx-workspace": "file:../create-nx-workspace",
|
||||||
"chalk": "^4.1.0",
|
"picocolors": "^1.1.0",
|
||||||
"enquirer": "~2.3.6",
|
"enquirer": "~2.3.6",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"yargs": "^17.6.2"
|
"yargs": "^17.6.2"
|
||||||
|
|||||||
@ -28,7 +28,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
|||||||
type: 'problem',
|
type: 'problem',
|
||||||
docs: {
|
docs: {
|
||||||
description: \`\`,
|
description: \`\`,
|
||||||
recommended: 'recommended',
|
|
||||||
},
|
},
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {},
|
messages: {},
|
||||||
@ -84,7 +83,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
|||||||
type: 'problem',
|
type: 'problem',
|
||||||
docs: {
|
docs: {
|
||||||
description: \`\`,
|
description: \`\`,
|
||||||
recommended: 'recommended',
|
|
||||||
},
|
},
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {},
|
messages: {},
|
||||||
@ -140,7 +138,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
|||||||
type: 'problem',
|
type: 'problem',
|
||||||
docs: {
|
docs: {
|
||||||
description: \`\`,
|
description: \`\`,
|
||||||
recommended: 'recommended',
|
|
||||||
},
|
},
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {},
|
messages: {},
|
||||||
|
|||||||
@ -25,7 +25,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
|||||||
type: 'problem',
|
type: 'problem',
|
||||||
docs: {
|
docs: {
|
||||||
description: ``,
|
description: ``,
|
||||||
recommended: 'recommended',
|
|
||||||
},
|
},
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {},
|
messages: {},
|
||||||
|
|||||||
@ -43,7 +43,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
|||||||
type: 'problem',
|
type: 'problem',
|
||||||
docs: {
|
docs: {
|
||||||
description: \`\`,
|
description: \`\`,
|
||||||
recommended: 'recommended',
|
|
||||||
},
|
},
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {},
|
messages: {},
|
||||||
|
|||||||
@ -84,7 +84,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
|||||||
type: 'problem',
|
type: 'problem',
|
||||||
docs: {
|
docs: {
|
||||||
description: \`\`,
|
description: \`\`,
|
||||||
recommended: 'error',
|
|
||||||
},
|
},
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {},
|
messages: {},
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { NxReleaseConfig } from '../../src/command-line/release/config/config';
|
|||||||
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
||||||
import { GitCommit } from '../../src/command-line/release/utils/git';
|
import { GitCommit } from '../../src/command-line/release/utils/git';
|
||||||
import {
|
import {
|
||||||
|
GithubRepoData,
|
||||||
RepoSlug,
|
RepoSlug,
|
||||||
formatReferences,
|
formatReferences,
|
||||||
} from '../../src/command-line/release/utils/github';
|
} from '../../src/command-line/release/utils/github';
|
||||||
@ -42,6 +43,7 @@ export type DependencyBump = {
|
|||||||
* @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated
|
* @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated
|
||||||
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
|
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
|
||||||
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the commit data
|
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the commit data
|
||||||
|
* @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository
|
||||||
*/
|
*/
|
||||||
export type ChangelogRenderer = (config: {
|
export type ChangelogRenderer = (config: {
|
||||||
projectGraph: ProjectGraph;
|
projectGraph: ProjectGraph;
|
||||||
@ -53,7 +55,9 @@ export type ChangelogRenderer = (config: {
|
|||||||
entryWhenNoChanges: string | false;
|
entryWhenNoChanges: string | false;
|
||||||
changelogRenderOptions: DefaultChangelogRenderOptions;
|
changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||||
dependencyBumps?: DependencyBump[];
|
dependencyBumps?: DependencyBump[];
|
||||||
|
// TODO(v20): remove repoSlug in favour of repoData
|
||||||
repoSlug?: RepoSlug;
|
repoSlug?: RepoSlug;
|
||||||
|
repoData?: GithubRepoData;
|
||||||
// TODO(v20): Evaluate if there is a cleaner way to configure this when breaking changes are allowed
|
// TODO(v20): Evaluate if there is a cleaner way to configure this when breaking changes are allowed
|
||||||
// null if version plans are being used to generate the changelog
|
// null if version plans are being used to generate the changelog
|
||||||
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
|
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
|
||||||
@ -101,6 +105,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
|||||||
dependencyBumps,
|
dependencyBumps,
|
||||||
repoSlug,
|
repoSlug,
|
||||||
conventionalCommitsConfig,
|
conventionalCommitsConfig,
|
||||||
|
repoData,
|
||||||
}): Promise<string> => {
|
}): Promise<string> => {
|
||||||
const markdownLines: string[] = [];
|
const markdownLines: string[] = [];
|
||||||
|
|
||||||
@ -148,7 +153,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
|||||||
change,
|
change,
|
||||||
changelogRenderOptions,
|
changelogRenderOptions,
|
||||||
isVersionPlans,
|
isVersionPlans,
|
||||||
repoSlug
|
repoData
|
||||||
);
|
);
|
||||||
breakingChanges.push(line);
|
breakingChanges.push(line);
|
||||||
relevantChanges.splice(i, 1);
|
relevantChanges.splice(i, 1);
|
||||||
@ -222,7 +227,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
|||||||
change,
|
change,
|
||||||
changelogRenderOptions,
|
changelogRenderOptions,
|
||||||
isVersionPlans,
|
isVersionPlans,
|
||||||
repoSlug
|
repoData
|
||||||
);
|
);
|
||||||
markdownLines.push(line);
|
markdownLines.push(line);
|
||||||
if (change.isBreaking) {
|
if (change.isBreaking) {
|
||||||
@ -295,7 +300,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
|||||||
change,
|
change,
|
||||||
changelogRenderOptions,
|
changelogRenderOptions,
|
||||||
isVersionPlans,
|
isVersionPlans,
|
||||||
repoSlug
|
repoData
|
||||||
);
|
);
|
||||||
markdownLines.push(line + '\n');
|
markdownLines.push(line + '\n');
|
||||||
if (change.isBreaking) {
|
if (change.isBreaking) {
|
||||||
@ -350,7 +355,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to map authors to github usernames
|
// Try to map authors to github usernames
|
||||||
if (repoSlug && changelogRenderOptions.mapAuthorsToGitHubUsernames) {
|
if (repoData && changelogRenderOptions.mapAuthorsToGitHubUsernames) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[..._authors.keys()].map(async (authorName) => {
|
[..._authors.keys()].map(async (authorName) => {
|
||||||
const meta = _authors.get(authorName);
|
const meta = _authors.get(authorName);
|
||||||
@ -455,7 +460,7 @@ function formatChange(
|
|||||||
change: ChangelogChange,
|
change: ChangelogChange,
|
||||||
changelogRenderOptions: DefaultChangelogRenderOptions,
|
changelogRenderOptions: DefaultChangelogRenderOptions,
|
||||||
isVersionPlans: boolean,
|
isVersionPlans: boolean,
|
||||||
repoSlug?: RepoSlug
|
repoData?: GithubRepoData
|
||||||
): string {
|
): string {
|
||||||
let description = change.description;
|
let description = change.description;
|
||||||
let extraLines = [];
|
let extraLines = [];
|
||||||
@ -480,8 +485,8 @@ function formatChange(
|
|||||||
(!isVersionPlans && change.isBreaking ? '⚠️ ' : '') +
|
(!isVersionPlans && change.isBreaking ? '⚠️ ' : '') +
|
||||||
(!isVersionPlans && change.scope ? `**${change.scope.trim()}:** ` : '') +
|
(!isVersionPlans && change.scope ? `**${change.scope.trim()}:** ` : '') +
|
||||||
description;
|
description;
|
||||||
if (repoSlug && changelogRenderOptions.commitReferences) {
|
if (repoData && changelogRenderOptions.commitReferences) {
|
||||||
changeLine += formatReferences(change.githubReferences, repoSlug);
|
changeLine += formatReferences(change.githubReferences, repoData);
|
||||||
}
|
}
|
||||||
if (extraLinesStr) {
|
if (extraLinesStr) {
|
||||||
changeLine += '\n\n' + extraLinesStr;
|
changeLine += '\n\n' + extraLinesStr;
|
||||||
|
|||||||
@ -691,6 +691,9 @@
|
|||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"enum": [false]
|
"enum": [false]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/CreateReleaseProviderConfiguration"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -724,6 +727,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreateReleaseProviderConfiguration": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"provider": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["github-enterprise-server"]
|
||||||
|
},
|
||||||
|
"hostname": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The hostname of the VCS provider instance, e.g. github.example.com"
|
||||||
|
},
|
||||||
|
"apiBaseUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The base URL for the relevant VCS provider API. If not set, this will default to `https://${hostname}/api/v3`"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["provider", "hostname"]
|
||||||
|
},
|
||||||
"NxReleaseVersionPlansConfiguration": {
|
"NxReleaseVersionPlansConfiguration": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { workspaceRoot } from '../../utils/workspace-root';
|
||||||
|
import { ActivatePowerpackOptions } from './command-object';
|
||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { getPackageManagerCommand } from '../../utils/package-manager';
|
||||||
|
|
||||||
|
export async function handleActivatePowerpack(
|
||||||
|
options: ActivatePowerpackOptions
|
||||||
|
) {
|
||||||
|
const license =
|
||||||
|
options.license ??
|
||||||
|
(await prompt({
|
||||||
|
type: 'input',
|
||||||
|
name: 'license',
|
||||||
|
message: 'Enter your License Key',
|
||||||
|
}));
|
||||||
|
const { activatePowerpack } = await requirePowerpack();
|
||||||
|
activatePowerpack(workspaceRoot, license);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requirePowerpack(): Promise<any> {
|
||||||
|
// @ts-ignore
|
||||||
|
return import('@nx/powerpack-license').catch(async (e) => {
|
||||||
|
if ('code' in e && e.code === 'MODULE_NOT_FOUND') {
|
||||||
|
try {
|
||||||
|
execSync(
|
||||||
|
`${getPackageManagerCommand().addDev} @nx/powerpack-license@latest`
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return await import('@nx/powerpack-license');
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
'Failed to install @nx/powerpack-license. Please install @nx/powerpack-license and try again.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { CommandModule } from 'yargs';
|
||||||
|
import { withVerbose } from '../yargs-utils/shared-options';
|
||||||
|
import { handleErrors } from '../../utils/handle-errors';
|
||||||
|
|
||||||
|
export interface ActivatePowerpackOptions {
|
||||||
|
license: string;
|
||||||
|
verbose: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const yargsActivatePowerpackCommand: CommandModule<
|
||||||
|
{},
|
||||||
|
ActivatePowerpackOptions
|
||||||
|
> = {
|
||||||
|
command: 'activate-powerpack <license>',
|
||||||
|
describe: false,
|
||||||
|
// describe: 'Activate a Nx Powerpack license.',
|
||||||
|
builder: (yargs) =>
|
||||||
|
withVerbose(yargs)
|
||||||
|
.parserConfiguration({
|
||||||
|
'strip-dashed': true,
|
||||||
|
'unknown-options-as-args': true,
|
||||||
|
})
|
||||||
|
.positional('license', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'This is a License Key for Nx Powerpack.',
|
||||||
|
})
|
||||||
|
.example(
|
||||||
|
'$0 activate-powerpack <license key>',
|
||||||
|
'Activate a Nx Powerpack license'
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const exitCode = await handleErrors(args.verbose as boolean, async () => {
|
||||||
|
return (await import('./activate-powerpack')).handleActivatePowerpack(
|
||||||
|
args
|
||||||
|
);
|
||||||
|
});
|
||||||
|
process.exit(exitCode);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -8,10 +8,7 @@ export interface AddOptions {
|
|||||||
__overrides_unparsed__: string[];
|
__overrides_unparsed__: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const yargsAddCommand: CommandModule<
|
export const yargsAddCommand: CommandModule<{}, AddOptions> = {
|
||||||
Record<string, unknown>,
|
|
||||||
AddOptions
|
|
||||||
> = {
|
|
||||||
command: 'add <packageSpecifier>',
|
command: 'add <packageSpecifier>',
|
||||||
describe: 'Install a plugin and initialize it.',
|
describe: 'Install a plugin and initialize it.',
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
|
|||||||
@ -78,6 +78,7 @@ export interface ProjectGraphClientResponse {
|
|||||||
isPartial: boolean;
|
isPartial: boolean;
|
||||||
errors?: GraphError[];
|
errors?: GraphError[];
|
||||||
connectedToCloud?: boolean;
|
connectedToCloud?: boolean;
|
||||||
|
disabledTaskSyncGenerators?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskGraphClientResponse {
|
export interface TaskGraphClientResponse {
|
||||||
@ -773,13 +774,16 @@ async function createProjectGraphAndSourceMapClientResponse(
|
|||||||
let isPartial = false;
|
let isPartial = false;
|
||||||
let errors: GraphError[] | undefined;
|
let errors: GraphError[] | undefined;
|
||||||
let connectedToCloud: boolean | undefined;
|
let connectedToCloud: boolean | undefined;
|
||||||
|
let disabledTaskSyncGenerators: string[] | undefined;
|
||||||
try {
|
try {
|
||||||
const projectGraphAndSourceMaps =
|
const projectGraphAndSourceMaps =
|
||||||
await createProjectGraphAndSourceMapsAsync({ exitOnError: false });
|
await createProjectGraphAndSourceMapsAsync({ exitOnError: false });
|
||||||
projectGraph = projectGraphAndSourceMaps.projectGraph;
|
projectGraph = projectGraphAndSourceMaps.projectGraph;
|
||||||
sourceMaps = projectGraphAndSourceMaps.sourceMaps;
|
sourceMaps = projectGraphAndSourceMaps.sourceMaps;
|
||||||
|
|
||||||
connectedToCloud = isNxCloudUsed(readNxJson());
|
const nxJson = readNxJson();
|
||||||
|
connectedToCloud = isNxCloudUsed(nxJson);
|
||||||
|
disabledTaskSyncGenerators = nxJson.sync?.disabledTaskSyncGenerators;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ProjectGraphError) {
|
if (e instanceof ProjectGraphError) {
|
||||||
projectGraph = e.getPartialProjectGraph();
|
projectGraph = e.getPartialProjectGraph();
|
||||||
@ -820,6 +824,7 @@ async function createProjectGraphAndSourceMapClientResponse(
|
|||||||
sourceMaps,
|
sourceMaps,
|
||||||
errors,
|
errors,
|
||||||
connectedToCloud,
|
connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -851,6 +856,7 @@ async function createProjectGraphAndSourceMapClientResponse(
|
|||||||
isPartial,
|
isPartial,
|
||||||
errors,
|
errors,
|
||||||
connectedToCloud,
|
connectedToCloud,
|
||||||
|
disabledTaskSyncGenerators,
|
||||||
},
|
},
|
||||||
sourceMapResponse: sourceMaps,
|
sourceMapResponse: sourceMaps,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
listPlugins,
|
listPlugins,
|
||||||
} from '../../utils/plugins';
|
} from '../../utils/plugins';
|
||||||
import { workspaceRoot } from '../../utils/workspace-root';
|
import { workspaceRoot } from '../../utils/workspace-root';
|
||||||
|
import { listPowerpackPlugins } from '../../utils/plugins/output';
|
||||||
|
|
||||||
export interface ListArgs {
|
export interface ListArgs {
|
||||||
/** The name of an installed plugin to query */
|
/** The name of an installed plugin to query */
|
||||||
@ -46,6 +47,7 @@ export async function listHandler(args: ListArgs): Promise<void> {
|
|||||||
}
|
}
|
||||||
listPlugins(installedPlugins, 'Installed plugins:');
|
listPlugins(installedPlugins, 'Installed plugins:');
|
||||||
listAlsoAvailableCorePlugins(installedPlugins);
|
listAlsoAvailableCorePlugins(installedPlugins);
|
||||||
|
listPowerpackPlugins();
|
||||||
|
|
||||||
output.note({
|
output.note({
|
||||||
title: 'Community Plugins',
|
title: 'Community Plugins',
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import * as chalk from 'chalk';
|
import * as chalk from 'chalk';
|
||||||
import * as yargs from 'yargs';
|
import * as yargs from 'yargs';
|
||||||
|
|
||||||
|
import { yargsActivatePowerpackCommand } from './activate-powerpack/command-object';
|
||||||
import {
|
import {
|
||||||
yargsAffectedBuildCommand,
|
yargsAffectedBuildCommand,
|
||||||
yargsAffectedCommand,
|
yargsAffectedCommand,
|
||||||
@ -63,6 +64,7 @@ export const commandsObject = yargs
|
|||||||
.parserConfiguration(parserConfiguration)
|
.parserConfiguration(parserConfiguration)
|
||||||
.usage(chalk.bold('Smart Monorepos · Fast CI'))
|
.usage(chalk.bold('Smart Monorepos · Fast CI'))
|
||||||
.demandCommand(1, '')
|
.demandCommand(1, '')
|
||||||
|
.command(yargsActivatePowerpackCommand)
|
||||||
.command(yargsAddCommand)
|
.command(yargsAddCommand)
|
||||||
.command(yargsAffectedBuildCommand)
|
.command(yargsAffectedBuildCommand)
|
||||||
.command(yargsAffectedCommand)
|
.command(yargsAffectedCommand)
|
||||||
@ -98,9 +100,27 @@ export const commandsObject = yargs
|
|||||||
.command(yargsNxInfixCommand)
|
.command(yargsNxInfixCommand)
|
||||||
.command(yargsLoginCommand)
|
.command(yargsLoginCommand)
|
||||||
.command(yargsLogoutCommand)
|
.command(yargsLogoutCommand)
|
||||||
|
.command(resolveConformanceCommandObject())
|
||||||
.scriptName('nx')
|
.scriptName('nx')
|
||||||
.help()
|
.help()
|
||||||
// NOTE: we handle --version in nx.ts, this just tells yargs that the option exists
|
// NOTE: we handle --version in nx.ts, this just tells yargs that the option exists
|
||||||
// so that it shows up in help. The default yargs implementation of --version is not
|
// so that it shows up in help. The default yargs implementation of --version is not
|
||||||
// hit, as the implementation in nx.ts is hit first and calls process.exit(0).
|
// hit, as the implementation in nx.ts is hit first and calls process.exit(0).
|
||||||
.version();
|
.version();
|
||||||
|
|
||||||
|
function resolveConformanceCommandObject() {
|
||||||
|
try {
|
||||||
|
const { yargsConformanceCommand } = require('@nx/powerpack-conformance');
|
||||||
|
return yargsConformanceCommand;
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
command: 'conformance',
|
||||||
|
// Hide from --help output in the common case of not having the plugin installed
|
||||||
|
describe: false,
|
||||||
|
handler: () => {
|
||||||
|
// TODO: Add messaging to help with learning more about powerpack and conformance
|
||||||
|
process.exit(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { ChangelogOptions } from './command-object';
|
|||||||
import {
|
import {
|
||||||
NxReleaseConfig,
|
NxReleaseConfig,
|
||||||
createNxReleaseConfig,
|
createNxReleaseConfig,
|
||||||
|
defaultCreateReleaseProvider,
|
||||||
handleNxReleaseConfigError,
|
handleNxReleaseConfigError,
|
||||||
} from './config/config';
|
} from './config/config';
|
||||||
import { deepMergeJson } from './config/deep-merge-json';
|
import { deepMergeJson } from './config/deep-merge-json';
|
||||||
@ -58,7 +59,7 @@ import {
|
|||||||
parseCommits,
|
parseCommits,
|
||||||
parseGitCommit,
|
parseGitCommit,
|
||||||
} from './utils/git';
|
} from './utils/git';
|
||||||
import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github';
|
import { createOrUpdateGithubRelease, getGitHubRepoData } from './utils/github';
|
||||||
import { launchEditor } from './utils/launch-editor';
|
import { launchEditor } from './utils/launch-editor';
|
||||||
import { parseChangelogMarkdown } from './utils/markdown';
|
import { parseChangelogMarkdown } from './utils/markdown';
|
||||||
import { printAndFlushChanges } from './utils/print-changes';
|
import { printAndFlushChanges } from './utils/print-changes';
|
||||||
@ -411,6 +412,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
output.logSingleLine(`Creating GitHub Release`);
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
await createOrUpdateGithubRelease(
|
||||||
|
nxReleaseConfig.changelog.workspaceChangelog
|
||||||
|
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
|
||||||
|
: defaultCreateReleaseProvider,
|
||||||
workspaceChangelog.releaseVersion,
|
workspaceChangelog.releaseVersion,
|
||||||
workspaceChangelog.contents,
|
workspaceChangelog.contents,
|
||||||
latestCommit,
|
latestCommit,
|
||||||
@ -644,6 +648,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
output.logSingleLine(`Creating GitHub Release`);
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
await createOrUpdateGithubRelease(
|
||||||
|
releaseGroup.changelog
|
||||||
|
? releaseGroup.changelog.createRelease
|
||||||
|
: defaultCreateReleaseProvider,
|
||||||
projectChangelog.releaseVersion,
|
projectChangelog.releaseVersion,
|
||||||
projectChangelog.contents,
|
projectChangelog.contents,
|
||||||
latestCommit,
|
latestCommit,
|
||||||
@ -797,6 +804,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
output.logSingleLine(`Creating GitHub Release`);
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
await createOrUpdateGithubRelease(
|
||||||
|
releaseGroup.changelog
|
||||||
|
? releaseGroup.changelog.createRelease
|
||||||
|
: defaultCreateReleaseProvider,
|
||||||
projectChangelog.releaseVersion,
|
projectChangelog.releaseVersion,
|
||||||
projectChangelog.contents,
|
projectChangelog.contents,
|
||||||
latestCommit,
|
latestCommit,
|
||||||
@ -1110,7 +1120,7 @@ async function generateChangelogForWorkspace({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const githubRepoSlug = getGitHubRepoSlug(gitRemote);
|
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
||||||
|
|
||||||
let contents = await changelogRenderer({
|
let contents = await changelogRenderer({
|
||||||
projectGraph,
|
projectGraph,
|
||||||
@ -1118,7 +1128,8 @@ async function generateChangelogForWorkspace({
|
|||||||
commits,
|
commits,
|
||||||
releaseVersion: releaseVersion.rawVersion,
|
releaseVersion: releaseVersion.rawVersion,
|
||||||
project: null,
|
project: null,
|
||||||
repoSlug: githubRepoSlug,
|
repoSlug: githubRepoData?.slug,
|
||||||
|
repoData: githubRepoData,
|
||||||
entryWhenNoChanges: config.entryWhenNoChanges,
|
entryWhenNoChanges: config.entryWhenNoChanges,
|
||||||
changelogRenderOptions: config.renderOptions,
|
changelogRenderOptions: config.renderOptions,
|
||||||
conventionalCommitsConfig: nxReleaseConfig.conventionalCommits,
|
conventionalCommitsConfig: nxReleaseConfig.conventionalCommits,
|
||||||
@ -1250,10 +1261,7 @@ async function generateChangelogForProjects({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const githubRepoSlug =
|
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
||||||
config.createRelease === 'github'
|
|
||||||
? getGitHubRepoSlug(gitRemote)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let contents = await changelogRenderer({
|
let contents = await changelogRenderer({
|
||||||
projectGraph,
|
projectGraph,
|
||||||
@ -1261,7 +1269,8 @@ async function generateChangelogForProjects({
|
|||||||
commits,
|
commits,
|
||||||
releaseVersion: releaseVersion.rawVersion,
|
releaseVersion: releaseVersion.rawVersion,
|
||||||
project: project.name,
|
project: project.name,
|
||||||
repoSlug: githubRepoSlug,
|
repoSlug: githubRepoData?.slug,
|
||||||
|
repoData: githubRepoData,
|
||||||
entryWhenNoChanges:
|
entryWhenNoChanges:
|
||||||
typeof config.entryWhenNoChanges === 'string'
|
typeof config.entryWhenNoChanges === 'string'
|
||||||
? interpolate(config.entryWhenNoChanges, {
|
? interpolate(config.entryWhenNoChanges, {
|
||||||
@ -1409,7 +1418,7 @@ export function shouldCreateGitHubRelease(
|
|||||||
return createReleaseArg === 'github';
|
return createReleaseArg === 'github';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (changelogConfig || {}).createRelease === 'github';
|
return (changelogConfig || {}).createRelease !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptForGitHubRelease(): Promise<boolean> {
|
async function promptForGitHubRelease(): Promise<boolean> {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,11 @@
|
|||||||
* and easy to consume config object for all the `nx release` command implementations.
|
* and easy to consume config object for all the `nx release` command implementations.
|
||||||
*/
|
*/
|
||||||
import { join, relative } from 'node:path';
|
import { join, relative } from 'node:path';
|
||||||
import { NxJsonConfiguration } from '../../../config/nx-json';
|
import { URL } from 'node:url';
|
||||||
|
import {
|
||||||
|
NxJsonConfiguration,
|
||||||
|
NxReleaseChangelogConfiguration,
|
||||||
|
} from '../../../config/nx-json';
|
||||||
import { ProjectFileMap, ProjectGraph } from '../../../config/project-graph';
|
import { ProjectFileMap, ProjectGraph } from '../../../config/project-graph';
|
||||||
import { readJsonFile } from '../../../utils/fileutils';
|
import { readJsonFile } from '../../../utils/fileutils';
|
||||||
import { findMatchingProjects } from '../../../utils/find-matching-projects';
|
import { findMatchingProjects } from '../../../utils/find-matching-projects';
|
||||||
@ -41,15 +45,6 @@ type RemoveTrueFromProperties<T, K extends keyof T> = {
|
|||||||
type RemoveTrueFromPropertiesOnEach<T, K extends keyof T[keyof T]> = {
|
type RemoveTrueFromPropertiesOnEach<T, K extends keyof T[keyof T]> = {
|
||||||
[U in keyof T]: RemoveTrueFromProperties<T[U], K>;
|
[U in keyof T]: RemoveTrueFromProperties<T[U], K>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RemoveFalseFromType<T> = T extends false ? never : T;
|
|
||||||
type RemoveFalseFromProperties<T, K extends keyof T> = {
|
|
||||||
[P in keyof T]: P extends K ? RemoveFalseFromType<T[P]> : T[P];
|
|
||||||
};
|
|
||||||
type RemoveFalseFromPropertiesOnEach<T, K extends keyof T[keyof T]> = {
|
|
||||||
[U in keyof T]: RemoveFalseFromProperties<T[U], K>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RemoveBooleanFromType<T> = T extends boolean ? never : T;
|
type RemoveBooleanFromType<T> = T extends boolean ? never : T;
|
||||||
type RemoveBooleanFromProperties<T, K extends keyof T> = {
|
type RemoveBooleanFromProperties<T, K extends keyof T> = {
|
||||||
[P in keyof T]: P extends K ? RemoveBooleanFromType<T[P]> : T[P];
|
[P in keyof T]: P extends K ? RemoveBooleanFromType<T[P]> : T[P];
|
||||||
@ -111,7 +106,11 @@ export interface CreateNxReleaseConfigError {
|
|||||||
| 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE'
|
| 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE'
|
||||||
| 'PROJECT_MATCHES_MULTIPLE_GROUPS'
|
| 'PROJECT_MATCHES_MULTIPLE_GROUPS'
|
||||||
| 'CONVENTIONAL_COMMITS_SHORTHAND_MIXED_WITH_OVERLAPPING_GENERATOR_OPTIONS'
|
| 'CONVENTIONAL_COMMITS_SHORTHAND_MIXED_WITH_OVERLAPPING_GENERATOR_OPTIONS'
|
||||||
| 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG';
|
| 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG'
|
||||||
|
| 'CANNOT_RESOLVE_CHANGELOG_RENDERER'
|
||||||
|
| 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER'
|
||||||
|
| 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME'
|
||||||
|
| 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL';
|
||||||
data: Record<string, string | string[]>;
|
data: Record<string, string | string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,7 +565,16 @@ export async function createNxReleaseConfig(
|
|||||||
releaseGroups[releaseGroupName] = finalReleaseGroup;
|
releaseGroups[releaseGroupName] = finalReleaseGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureChangelogRenderersAreResolvable(releaseGroups, rootChangelogConfig);
|
const configError = validateChangelogConfig(
|
||||||
|
releaseGroups,
|
||||||
|
rootChangelogConfig
|
||||||
|
);
|
||||||
|
if (configError) {
|
||||||
|
return {
|
||||||
|
error: configError,
|
||||||
|
nxReleaseConfig: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: null,
|
error: null,
|
||||||
@ -766,6 +774,52 @@ export async function handleNxReleaseConfigError(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'CANNOT_RESOLVE_CHANGELOG_RENDERER': {
|
||||||
|
const nxJsonMessage = await resolveNxJsonConfigErrorMessage(['release']);
|
||||||
|
output.error({
|
||||||
|
title: `There was an error when resolving the configured changelog renderer at path: ${error.data.workspaceRelativePath}`,
|
||||||
|
bodyLines: [nxJsonMessage],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER':
|
||||||
|
{
|
||||||
|
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||||
|
'release',
|
||||||
|
]);
|
||||||
|
output.error({
|
||||||
|
title: `Your "changelog.createRelease" config specifies an unsupported provider "${
|
||||||
|
error.data.provider
|
||||||
|
}". The supported providers are ${(
|
||||||
|
error.data.supportedProviders as string[]
|
||||||
|
)
|
||||||
|
.map((p) => `"${p}"`)
|
||||||
|
.join(', ')}`,
|
||||||
|
bodyLines: [nxJsonMessage],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME':
|
||||||
|
{
|
||||||
|
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||||
|
'release',
|
||||||
|
]);
|
||||||
|
output.error({
|
||||||
|
title: `Your "changelog.createRelease" config specifies an invalid hostname "${error.data.hostname}". Please ensure you provide a valid hostname value, such as "example.com"`,
|
||||||
|
bodyLines: [nxJsonMessage],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL':
|
||||||
|
{
|
||||||
|
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||||
|
'release',
|
||||||
|
]);
|
||||||
|
output.error({
|
||||||
|
title: `Your "changelog.createRelease" config specifies an invalid apiBaseUrl "${error.data.apiBaseUrl}". Please ensure you provide a valid URL value, such as "https://example.com"`,
|
||||||
|
bodyLines: [nxJsonMessage],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled error code: ${error.code}`);
|
throw new Error(`Unhandled error code: ${error.code}`);
|
||||||
}
|
}
|
||||||
@ -950,10 +1004,16 @@ function isProjectPublic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureChangelogRenderersAreResolvable(
|
/**
|
||||||
|
* We need to ensure that changelog renderers are resolvable up front so that we do not end up erroring after performing
|
||||||
|
* actions later, and we also make sure that any configured createRelease options are valid.
|
||||||
|
*
|
||||||
|
* For the createRelease config, we also set a default apiBaseUrl if applicable.
|
||||||
|
*/
|
||||||
|
function validateChangelogConfig(
|
||||||
releaseGroups: NxReleaseConfig['groups'],
|
releaseGroups: NxReleaseConfig['groups'],
|
||||||
rootChangelogConfig: NxReleaseConfig['changelog']
|
rootChangelogConfig: NxReleaseConfig['changelog']
|
||||||
) {
|
): CreateNxReleaseConfigError | null {
|
||||||
/**
|
/**
|
||||||
* If any form of changelog config is enabled, ensure that any provided changelog renderers are resolvable
|
* If any form of changelog config is enabled, ensure that any provided changelog renderers are resolvable
|
||||||
* up front so that we do not end up erroring only after the versioning step has been completed.
|
* up front so that we do not end up erroring only after the versioning step has been completed.
|
||||||
@ -962,42 +1022,148 @@ function ensureChangelogRenderersAreResolvable(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
rootChangelogConfig.workspaceChangelog &&
|
rootChangelogConfig.workspaceChangelog &&
|
||||||
typeof rootChangelogConfig.workspaceChangelog !== 'boolean' &&
|
typeof rootChangelogConfig.workspaceChangelog !== 'boolean'
|
||||||
rootChangelogConfig.workspaceChangelog.renderer?.length
|
|
||||||
) {
|
) {
|
||||||
uniqueRendererPaths.add(rootChangelogConfig.workspaceChangelog.renderer);
|
if (rootChangelogConfig.workspaceChangelog.renderer?.length) {
|
||||||
|
uniqueRendererPaths.add(rootChangelogConfig.workspaceChangelog.renderer);
|
||||||
|
}
|
||||||
|
const createReleaseError = validateCreateReleaseConfig(
|
||||||
|
rootChangelogConfig.workspaceChangelog
|
||||||
|
);
|
||||||
|
if (createReleaseError) {
|
||||||
|
return createReleaseError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
rootChangelogConfig.projectChangelogs &&
|
rootChangelogConfig.projectChangelogs &&
|
||||||
typeof rootChangelogConfig.projectChangelogs !== 'boolean' &&
|
typeof rootChangelogConfig.projectChangelogs !== 'boolean'
|
||||||
rootChangelogConfig.projectChangelogs.renderer?.length
|
|
||||||
) {
|
) {
|
||||||
uniqueRendererPaths.add(rootChangelogConfig.projectChangelogs.renderer);
|
if (rootChangelogConfig.projectChangelogs.renderer?.length) {
|
||||||
|
uniqueRendererPaths.add(rootChangelogConfig.projectChangelogs.renderer);
|
||||||
|
}
|
||||||
|
const createReleaseError = validateCreateReleaseConfig(
|
||||||
|
rootChangelogConfig.projectChangelogs
|
||||||
|
);
|
||||||
|
if (createReleaseError) {
|
||||||
|
return createReleaseError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const group of Object.values(releaseGroups)) {
|
for (const group of Object.values(releaseGroups)) {
|
||||||
if (
|
if (group.changelog && typeof group.changelog !== 'boolean') {
|
||||||
group.changelog &&
|
if (group.changelog.renderer?.length) {
|
||||||
typeof group.changelog !== 'boolean' &&
|
uniqueRendererPaths.add(group.changelog.renderer);
|
||||||
group.changelog.renderer?.length
|
}
|
||||||
) {
|
const createReleaseError = validateCreateReleaseConfig(group.changelog);
|
||||||
uniqueRendererPaths.add(group.changelog.renderer);
|
if (createReleaseError) {
|
||||||
|
return createReleaseError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uniqueRendererPaths.size) {
|
if (!uniqueRendererPaths.size) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const rendererPath of uniqueRendererPaths) {
|
for (const rendererPath of uniqueRendererPaths) {
|
||||||
try {
|
try {
|
||||||
resolveChangelogRenderer(rendererPath);
|
resolveChangelogRenderer(rendererPath);
|
||||||
} catch (e) {
|
} catch {
|
||||||
const workspaceRelativePath = relative(workspaceRoot, rendererPath);
|
return {
|
||||||
output.error({
|
code: 'CANNOT_RESOLVE_CHANGELOG_RENDERER',
|
||||||
title: `There was an error when resolving the configured changelog renderer at path: ${workspaceRelativePath}`,
|
data: {
|
||||||
});
|
workspaceRelativePath: relative(workspaceRoot, rendererPath),
|
||||||
throw e;
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedCreateReleaseProviders = [
|
||||||
|
{
|
||||||
|
name: 'github-enterprise-server',
|
||||||
|
defaultApiBaseUrl: 'https://__hostname__/api/v3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// User opts into the default by specifying the string value 'github'
|
||||||
|
export const defaultCreateReleaseProvider = {
|
||||||
|
provider: 'github',
|
||||||
|
hostname: 'github.com',
|
||||||
|
apiBaseUrl: 'https://api.github.com',
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
function validateCreateReleaseConfig(
|
||||||
|
changelogConfig: NxReleaseChangelogConfiguration
|
||||||
|
): CreateNxReleaseConfigError | null {
|
||||||
|
const createRelease = changelogConfig.createRelease;
|
||||||
|
// Disabled: valid
|
||||||
|
if (!createRelease) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// GitHub shorthand, expand to full object form, mark as valid
|
||||||
|
if (createRelease === 'github') {
|
||||||
|
changelogConfig.createRelease = defaultCreateReleaseProvider;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Object config, ensure that properties are valid
|
||||||
|
const supportedProvider = supportedCreateReleaseProviders.find(
|
||||||
|
(p) => p.name === createRelease.provider
|
||||||
|
);
|
||||||
|
if (!supportedProvider) {
|
||||||
|
return {
|
||||||
|
code: 'INVALID_CHANGELOG_CREATE_RELEASE_PROVIDER',
|
||||||
|
data: {
|
||||||
|
provider: createRelease.provider,
|
||||||
|
supportedProviders: supportedCreateReleaseProviders.map((p) => p.name),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!isValidHostname(createRelease.hostname)) {
|
||||||
|
return {
|
||||||
|
code: 'INVALID_CHANGELOG_CREATE_RELEASE_HOSTNAME',
|
||||||
|
data: {
|
||||||
|
hostname: createRelease.hostname,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// user provided a custom apiBaseUrl, ensure it is valid (accounting for empty string case)
|
||||||
|
if (
|
||||||
|
createRelease.apiBaseUrl ||
|
||||||
|
typeof createRelease.apiBaseUrl === 'string'
|
||||||
|
) {
|
||||||
|
if (!isValidUrl(createRelease.apiBaseUrl)) {
|
||||||
|
return {
|
||||||
|
code: 'INVALID_CHANGELOG_CREATE_RELEASE_API_BASE_URL',
|
||||||
|
data: {
|
||||||
|
apiBaseUrl: createRelease.apiBaseUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set default apiBaseUrl when not provided by the user
|
||||||
|
createRelease.apiBaseUrl = supportedProvider.defaultApiBaseUrl.replace(
|
||||||
|
'__hostname__',
|
||||||
|
createRelease.hostname
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidHostname(hostname) {
|
||||||
|
// Regular expression to match a valid hostname
|
||||||
|
const hostnameRegex =
|
||||||
|
/^(?!:\/\/)(?=.{1,255}$)(?!.*\.$)(?!.*?\.\.)(?!.*?-$)(?!^-)([a-zA-Z0-9-]{1,63}\.?)+[a-zA-Z]{2,}$/;
|
||||||
|
return hostnameRegex.test(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUrl(str: string): boolean {
|
||||||
|
try {
|
||||||
|
new URL(str);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -252,6 +252,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
|
|
||||||
latestCommit = await getCommitHash('HEAD');
|
latestCommit = await getCommitHash('HEAD');
|
||||||
await createOrUpdateGithubRelease(
|
await createOrUpdateGithubRelease(
|
||||||
|
nxReleaseConfig.changelog.workspaceChangelog
|
||||||
|
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
|
||||||
|
: false,
|
||||||
changelogResult.workspaceChangelog.releaseVersion,
|
changelogResult.workspaceChangelog.releaseVersion,
|
||||||
changelogResult.workspaceChangelog.contents,
|
changelogResult.workspaceChangelog.contents,
|
||||||
latestCommit,
|
latestCommit,
|
||||||
@ -297,6 +300,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
await createOrUpdateGithubRelease(
|
||||||
|
releaseGroup.changelog
|
||||||
|
? releaseGroup.changelog.createRelease
|
||||||
|
: false,
|
||||||
changelog.releaseVersion,
|
changelog.releaseVersion,
|
||||||
changelog.contents,
|
changelog.contents,
|
||||||
latestCommit,
|
latestCommit,
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import { prompt } from 'enquirer';
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { existsSync, promises as fsp } from 'node:fs';
|
import { existsSync, promises as fsp } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
|
import { NxReleaseChangelogConfiguration } from '../../../config/nx-json';
|
||||||
import { output } from '../../../utils/output';
|
import { output } from '../../../utils/output';
|
||||||
import { joinPathFragments } from '../../../utils/path';
|
import { joinPathFragments } from '../../../utils/path';
|
||||||
|
import { defaultCreateReleaseProvider } from '../config/config';
|
||||||
import { Reference } from './git';
|
import { Reference } from './git';
|
||||||
import { printDiff } from './print-changes';
|
import { printDiff } from './print-changes';
|
||||||
import { ReleaseVersion, noDiffInChangelogMessage } from './shared';
|
import { ReleaseVersion, noDiffInChangelogMessage } from './shared';
|
||||||
@ -20,12 +22,14 @@ const axios = _axios as any as (typeof _axios)['default'];
|
|||||||
|
|
||||||
export type RepoSlug = `${string}/${string}`;
|
export type RepoSlug = `${string}/${string}`;
|
||||||
|
|
||||||
export interface GithubRequestConfig {
|
interface GithubRequestConfig {
|
||||||
repo: string;
|
repo: string;
|
||||||
|
hostname: string;
|
||||||
|
apiBaseUrl: string;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GithubRelease {
|
interface GithubRelease {
|
||||||
id?: string;
|
id?: string;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
target_commitish?: string;
|
target_commitish?: string;
|
||||||
@ -35,19 +39,46 @@ export interface GithubRelease {
|
|||||||
prerelease?: boolean;
|
prerelease?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGitHubRepoSlug(remoteName = 'origin'): RepoSlug {
|
export interface GithubRepoData {
|
||||||
|
hostname: string;
|
||||||
|
slug: RepoSlug;
|
||||||
|
apiBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGitHubRepoData(
|
||||||
|
remoteName = 'origin',
|
||||||
|
createReleaseConfig: NxReleaseChangelogConfiguration['createRelease']
|
||||||
|
): GithubRepoData | null {
|
||||||
try {
|
try {
|
||||||
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
|
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
}).trim();
|
}).trim();
|
||||||
|
|
||||||
|
// Use the default provider (github.com) if custom one is not specified or releases are disabled
|
||||||
|
let hostname = defaultCreateReleaseProvider.hostname;
|
||||||
|
let apiBaseUrl = defaultCreateReleaseProvider.apiBaseUrl;
|
||||||
|
if (
|
||||||
|
createReleaseConfig !== false &&
|
||||||
|
typeof createReleaseConfig !== 'string'
|
||||||
|
) {
|
||||||
|
hostname = createReleaseConfig.hostname;
|
||||||
|
apiBaseUrl = createReleaseConfig.apiBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the 'user/repo' part from the URL
|
// Extract the 'user/repo' part from the URL
|
||||||
const regex = /github\.com[/:]([\w-]+\/[\w-]+)/;
|
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+)(\\.git)?`;
|
||||||
|
const regex = new RegExp(regexString);
|
||||||
const match = remoteUrl.match(regex);
|
const match = remoteUrl.match(regex);
|
||||||
|
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
return match[1] as RepoSlug;
|
return {
|
||||||
|
hostname,
|
||||||
|
apiBaseUrl,
|
||||||
|
// Ensure any trailing .git is stripped
|
||||||
|
slug: match[1].replace(/\.git$/, '') as RepoSlug,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`
|
`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`
|
||||||
@ -59,13 +90,14 @@ export function getGitHubRepoSlug(remoteName = 'origin'): RepoSlug {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createOrUpdateGithubRelease(
|
export async function createOrUpdateGithubRelease(
|
||||||
|
createReleaseConfig: NxReleaseChangelogConfiguration['createRelease'],
|
||||||
releaseVersion: ReleaseVersion,
|
releaseVersion: ReleaseVersion,
|
||||||
changelogContents: string,
|
changelogContents: string,
|
||||||
latestCommit: string,
|
latestCommit: string,
|
||||||
{ dryRun }: { dryRun: boolean }
|
{ dryRun }: { dryRun: boolean }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const githubRepoSlug = getGitHubRepoSlug();
|
const githubRepoData = getGitHubRepoData(undefined, createReleaseConfig);
|
||||||
if (!githubRepoSlug) {
|
if (!githubRepoData) {
|
||||||
output.error({
|
output.error({
|
||||||
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
|
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
|
||||||
bodyLines: [
|
bodyLines: [
|
||||||
@ -75,9 +107,11 @@ export async function createOrUpdateGithubRelease(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await resolveGithubToken();
|
const token = await resolveGithubToken(githubRepoData.hostname);
|
||||||
const githubRequestConfig: GithubRequestConfig = {
|
const githubRequestConfig: GithubRequestConfig = {
|
||||||
repo: githubRepoSlug,
|
repo: githubRepoData.slug,
|
||||||
|
hostname: githubRepoData.hostname,
|
||||||
|
apiBaseUrl: githubRepoData.apiBaseUrl,
|
||||||
token,
|
token,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,7 +140,7 @@ export async function createOrUpdateGithubRelease(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logTitle = `https://github.com/${githubRepoSlug}/releases/tag/${releaseVersion.gitTag}`;
|
const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${releaseVersion.gitTag}`;
|
||||||
if (existingGithubReleaseForVersion) {
|
if (existingGithubReleaseForVersion) {
|
||||||
console.error(
|
console.error(
|
||||||
`${chalk.white('UPDATE')} ${logTitle}${
|
`${chalk.white('UPDATE')} ${logTitle}${
|
||||||
@ -304,7 +338,7 @@ async function syncGithubRelease(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveGithubToken(): Promise<string | null> {
|
async function resolveGithubToken(hostname: string): Promise<string | null> {
|
||||||
// Try and resolve from the environment
|
// Try and resolve from the environment
|
||||||
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||||
if (tokenFromEnv) {
|
if (tokenFromEnv) {
|
||||||
@ -320,15 +354,15 @@ export async function resolveGithubToken(): Promise<string | null> {
|
|||||||
const yamlContents = await fsp.readFile(ghCLIPath, 'utf8');
|
const yamlContents = await fsp.readFile(ghCLIPath, 'utf8');
|
||||||
const { load } = require('@zkochan/js-yaml');
|
const { load } = require('@zkochan/js-yaml');
|
||||||
const ghCLIConfig = load(yamlContents);
|
const ghCLIConfig = load(yamlContents);
|
||||||
if (ghCLIConfig['github.com']) {
|
if (ghCLIConfig[hostname]) {
|
||||||
// Web based session (the token is already embedded in the config)
|
// Web based session (the token is already embedded in the config)
|
||||||
if (ghCLIConfig['github.com'].oauth_token) {
|
if (ghCLIConfig[hostname].oauth_token) {
|
||||||
return ghCLIConfig['github.com'].oauth_token;
|
return ghCLIConfig[hostname].oauth_token;
|
||||||
}
|
}
|
||||||
// SSH based session (we need to dynamically resolve a token using the CLI)
|
// SSH based session (we need to dynamically resolve a token using the CLI)
|
||||||
if (
|
if (
|
||||||
ghCLIConfig['github.com'].user &&
|
ghCLIConfig[hostname].user &&
|
||||||
ghCLIConfig['github.com'].git_protocol === 'ssh'
|
ghCLIConfig[hostname].git_protocol === 'ssh'
|
||||||
) {
|
) {
|
||||||
return execSync(`gh auth token`, {
|
return execSync(`gh auth token`, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
@ -337,6 +371,11 @@ export async function resolveGithubToken(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (hostname !== 'github.com') {
|
||||||
|
console.log(
|
||||||
|
`Warning: It was not possible to automatically resolve a GitHub token from your environment for hostname ${hostname}. If you set the GITHUB_TOKEN or GH_TOKEN environment variable, that will be used for GitHub API requests.`
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,7 +398,7 @@ async function makeGithubRequest(
|
|||||||
return (
|
return (
|
||||||
await axios<any, any>(url, {
|
await axios<any, any>(url, {
|
||||||
...opts,
|
...opts,
|
||||||
baseURL: 'https://api.github.com',
|
baseURL: config.apiBaseUrl,
|
||||||
headers: {
|
headers: {
|
||||||
...(opts.headers as any),
|
...(opts.headers as any),
|
||||||
Authorization: config.token ? `Bearer ${config.token}` : undefined,
|
Authorization: config.token ? `Bearer ${config.token}` : undefined,
|
||||||
@ -395,11 +434,18 @@ async function updateGithubRelease(
|
|||||||
|
|
||||||
function githubNewReleaseURL(
|
function githubNewReleaseURL(
|
||||||
config: GithubRequestConfig,
|
config: GithubRequestConfig,
|
||||||
release: { version: string; body: string }
|
release: GithubReleaseOptions
|
||||||
) {
|
) {
|
||||||
return `https://github.com/${config.repo}/releases/new?tag=${
|
// Parameters taken from https://github.com/isaacs/github/issues/1410#issuecomment-442240267
|
||||||
|
let url = `https://${config.hostname}/${config.repo}/releases/new?tag=${
|
||||||
release.version
|
release.version
|
||||||
}&title=${release.version}&body=${encodeURIComponent(release.body)}`;
|
}&title=${release.version}&body=${encodeURIComponent(release.body)}&target=${
|
||||||
|
release.commit
|
||||||
|
}`;
|
||||||
|
if (release.prerelease) {
|
||||||
|
url += '&prerelease=true';
|
||||||
|
}
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepoProvider = 'github';
|
type RepoProvider = 'github';
|
||||||
@ -411,27 +457,30 @@ const providerToRefSpec: Record<
|
|||||||
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
|
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatReference(ref: Reference, repoSlug: `${string}/${string}`) {
|
function formatReference(ref: Reference, repoData: GithubRepoData) {
|
||||||
const refSpec = providerToRefSpec['github'];
|
const refSpec = providerToRefSpec['github'];
|
||||||
return `[${ref.value}](https://github.com/${repoSlug}/${
|
return `[${ref.value}](https://${repoData.hostname}/${repoData.slug}/${
|
||||||
refSpec[ref.type]
|
refSpec[ref.type]
|
||||||
}/${ref.value.replace(/^#/, '')})`;
|
}/${ref.value.replace(/^#/, '')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatReferences(references: Reference[], repoSlug: RepoSlug) {
|
export function formatReferences(
|
||||||
|
references: Reference[],
|
||||||
|
repoData: GithubRepoData
|
||||||
|
) {
|
||||||
const pr = references.filter((ref) => ref.type === 'pull-request');
|
const pr = references.filter((ref) => ref.type === 'pull-request');
|
||||||
const issue = references.filter((ref) => ref.type === 'issue');
|
const issue = references.filter((ref) => ref.type === 'issue');
|
||||||
if (pr.length > 0 || issue.length > 0) {
|
if (pr.length > 0 || issue.length > 0) {
|
||||||
return (
|
return (
|
||||||
' (' +
|
' (' +
|
||||||
[...pr, ...issue]
|
[...pr, ...issue]
|
||||||
.map((ref) => formatReference(ref, repoSlug))
|
.map((ref) => formatReference(ref, repoData))
|
||||||
.join(', ') +
|
.join(', ') +
|
||||||
')'
|
')'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (references.length > 0) {
|
if (references.length > 0) {
|
||||||
return ' (' + formatReference(references[0], repoSlug) + ')';
|
return ' (' + formatReference(references[0], repoData) + ')';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { getNxRequirePaths } from '../../utils/installation-directory';
|
|||||||
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
|
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
|
||||||
import { ProjectGraph } from '../../config/project-graph';
|
import { ProjectGraph } from '../../config/project-graph';
|
||||||
import { ProjectGraphError } from '../../project-graph/error-types';
|
import { ProjectGraphError } from '../../project-graph/error-types';
|
||||||
|
import { getPowerpackLicenseInformation } from '../../utils/powerpack';
|
||||||
|
|
||||||
const nxPackageJson = readJsonFile<typeof import('../../../package.json')>(
|
const nxPackageJson = readJsonFile<typeof import('../../../package.json')>(
|
||||||
join(__dirname, '../../../package.json')
|
join(__dirname, '../../../package.json')
|
||||||
@ -39,6 +40,7 @@ export const packagesWeCareAbout = [
|
|||||||
|
|
||||||
export const patternsWeIgnoreInCommunityReport: Array<string | RegExp> = [
|
export const patternsWeIgnoreInCommunityReport: Array<string | RegExp> = [
|
||||||
...packagesWeCareAbout,
|
...packagesWeCareAbout,
|
||||||
|
new RegExp('@nx/powerpack*'),
|
||||||
'@schematics/angular',
|
'@schematics/angular',
|
||||||
new RegExp('@angular/*'),
|
new RegExp('@angular/*'),
|
||||||
'@nestjs/schematics',
|
'@nestjs/schematics',
|
||||||
@ -58,7 +60,9 @@ export async function reportHandler() {
|
|||||||
const {
|
const {
|
||||||
pm,
|
pm,
|
||||||
pmVersion,
|
pmVersion,
|
||||||
|
powerpackLicense,
|
||||||
localPlugins,
|
localPlugins,
|
||||||
|
powerpackPlugins,
|
||||||
communityPlugins,
|
communityPlugins,
|
||||||
registeredPlugins,
|
registeredPlugins,
|
||||||
packageVersionsWeCareAbout,
|
packageVersionsWeCareAbout,
|
||||||
@ -88,6 +92,38 @@ export async function reportHandler() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (powerpackLicense) {
|
||||||
|
bodyLines.push(LINE_SEPARATOR);
|
||||||
|
bodyLines.push(chalk.green('Nx Powerpack'));
|
||||||
|
|
||||||
|
bodyLines.push(
|
||||||
|
`Licensed to ${powerpackLicense.organizationName} for ${
|
||||||
|
powerpackLicense.seatCount
|
||||||
|
} user${powerpackLicense.seatCount > 1 ? 's' : ''} in ${
|
||||||
|
powerpackLicense.workspaceCount
|
||||||
|
} workspace${
|
||||||
|
powerpackLicense.workspaceCount > 1 ? 's' : ''
|
||||||
|
} until ${new Date(
|
||||||
|
powerpackLicense.expiresAt * 1000
|
||||||
|
).toLocaleDateString()}`
|
||||||
|
);
|
||||||
|
bodyLines.push('');
|
||||||
|
|
||||||
|
padding =
|
||||||
|
Math.max(
|
||||||
|
...powerpackPlugins.map(
|
||||||
|
(powerpackPlugin) => powerpackPlugin.name.length
|
||||||
|
)
|
||||||
|
) + 1;
|
||||||
|
for (const powerpackPlugin of powerpackPlugins) {
|
||||||
|
bodyLines.push(
|
||||||
|
`${chalk.green(powerpackPlugin.name.padEnd(padding))} : ${chalk.bold(
|
||||||
|
powerpackPlugin.version
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (registeredPlugins.length) {
|
if (registeredPlugins.length) {
|
||||||
bodyLines.push(LINE_SEPARATOR);
|
bodyLines.push(LINE_SEPARATOR);
|
||||||
bodyLines.push('Registered Plugins:');
|
bodyLines.push('Registered Plugins:');
|
||||||
@ -147,6 +183,9 @@ export async function reportHandler() {
|
|||||||
export interface ReportData {
|
export interface ReportData {
|
||||||
pm: PackageManager;
|
pm: PackageManager;
|
||||||
pmVersion: string;
|
pmVersion: string;
|
||||||
|
// TODO(@FrozenPandaz): Provide the right type here.
|
||||||
|
powerpackLicense: any | null;
|
||||||
|
powerpackPlugins: PackageJson[];
|
||||||
localPlugins: string[];
|
localPlugins: string[];
|
||||||
communityPlugins: PackageJson[];
|
communityPlugins: PackageJson[];
|
||||||
registeredPlugins: string[];
|
registeredPlugins: string[];
|
||||||
@ -174,6 +213,7 @@ export async function getReportData(): Promise<ReportData> {
|
|||||||
|
|
||||||
const nxJson = readNxJson();
|
const nxJson = readNxJson();
|
||||||
const localPlugins = await findLocalPlugins(graph, nxJson);
|
const localPlugins = await findLocalPlugins(graph, nxJson);
|
||||||
|
const powerpackPlugins = findInstalledPowerpackPlugins();
|
||||||
const communityPlugins = findInstalledCommunityPlugins();
|
const communityPlugins = findInstalledCommunityPlugins();
|
||||||
const registeredPlugins = findRegisteredPluginsBeingUsed(nxJson);
|
const registeredPlugins = findRegisteredPluginsBeingUsed(nxJson);
|
||||||
|
|
||||||
@ -193,8 +233,15 @@ export async function getReportData(): Promise<ReportData> {
|
|||||||
|
|
||||||
const native = isNativeAvailable();
|
const native = isNativeAvailable();
|
||||||
|
|
||||||
|
let powerpackLicense = null;
|
||||||
|
try {
|
||||||
|
powerpackLicense = await getPowerpackLicenseInformation();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pm,
|
pm,
|
||||||
|
powerpackLicense,
|
||||||
|
powerpackPlugins,
|
||||||
pmVersion,
|
pmVersion,
|
||||||
localPlugins,
|
localPlugins,
|
||||||
communityPlugins,
|
communityPlugins,
|
||||||
@ -294,6 +341,13 @@ export function findMisalignedPackagesForPackage(
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findInstalledPowerpackPlugins(): PackageJson[] {
|
||||||
|
const installedPlugins = findInstalledPlugins();
|
||||||
|
return installedPlugins.filter((dep) =>
|
||||||
|
new RegExp('@nx/powerpack*').test(dep.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function findInstalledCommunityPlugins(): PackageJson[] {
|
export function findInstalledCommunityPlugins(): PackageJson[] {
|
||||||
const installedPlugins = findInstalledPlugins();
|
const installedPlugins = findInstalledPlugins();
|
||||||
return installedPlugins.filter(
|
return installedPlugins.filter(
|
||||||
|
|||||||
@ -79,7 +79,17 @@ export interface NxReleaseChangelogConfiguration {
|
|||||||
* NOTE: if createRelease is set on a group of projects, it will cause the default releaseTagPattern of
|
* NOTE: if createRelease is set on a group of projects, it will cause the default releaseTagPattern of
|
||||||
* "{projectName}@{version}" to be used for those projects, even when versioning everything together.
|
* "{projectName}@{version}" to be used for those projects, even when versioning everything together.
|
||||||
*/
|
*/
|
||||||
createRelease?: 'github' | false;
|
createRelease?:
|
||||||
|
| false
|
||||||
|
| 'github'
|
||||||
|
| {
|
||||||
|
provider: 'github-enterprise-server';
|
||||||
|
hostname: string;
|
||||||
|
/**
|
||||||
|
* If not set, this will default to `https://${hostname}/api/v3`
|
||||||
|
*/
|
||||||
|
apiBaseUrl?: string;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* This can either be set to a string value that will be written to the changelog file(s)
|
* This can either be set to a string value that will be written to the changelog file(s)
|
||||||
* at the workspace root and/or within project directories, or set to `false` to specify
|
* at the workspace root and/or within project directories, or set to `false` to specify
|
||||||
|
|||||||
95
packages/nx/src/native/cache/cache.rs
vendored
95
packages/nx/src/native/cache/cache.rs
vendored
@ -4,12 +4,12 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use fs_extra::remove_items;
|
use fs_extra::remove_items;
|
||||||
use napi::bindgen_prelude::*;
|
use napi::bindgen_prelude::*;
|
||||||
|
use regex::Regex;
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::native::cache::expand_outputs::_expand_outputs;
|
use crate::native::cache::expand_outputs::_expand_outputs;
|
||||||
use crate::native::cache::file_ops::_copy;
|
use crate::native::cache::file_ops::_copy;
|
||||||
use crate::native::machine_id::get_machine_id;
|
|
||||||
use crate::native::utils::Normalize;
|
use crate::native::utils::Normalize;
|
||||||
|
|
||||||
#[napi(object)]
|
#[napi(object)]
|
||||||
@ -26,6 +26,7 @@ pub struct NxCache {
|
|||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
cache_path: PathBuf,
|
cache_path: PathBuf,
|
||||||
db: External<Connection>,
|
db: External<Connection>,
|
||||||
|
link_task_details: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
@ -35,9 +36,9 @@ impl NxCache {
|
|||||||
workspace_root: String,
|
workspace_root: String,
|
||||||
cache_path: String,
|
cache_path: String,
|
||||||
db_connection: External<Connection>,
|
db_connection: External<Connection>,
|
||||||
|
link_task_details: Option<bool>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let machine_id = get_machine_id();
|
let cache_path = PathBuf::from(&cache_path);
|
||||||
let cache_path = PathBuf::from(&cache_path).join(machine_id);
|
|
||||||
|
|
||||||
create_dir_all(&cache_path)?;
|
create_dir_all(&cache_path)?;
|
||||||
create_dir_all(cache_path.join("terminalOutputs"))?;
|
create_dir_all(cache_path.join("terminalOutputs"))?;
|
||||||
@ -47,6 +48,7 @@ impl NxCache {
|
|||||||
workspace_root: PathBuf::from(workspace_root),
|
workspace_root: PathBuf::from(workspace_root),
|
||||||
cache_directory: cache_path.to_normalized_string(),
|
cache_directory: cache_path.to_normalized_string(),
|
||||||
cache_path,
|
cache_path,
|
||||||
|
link_task_details: link_task_details.unwrap_or(true)
|
||||||
};
|
};
|
||||||
|
|
||||||
r.setup()?;
|
r.setup()?;
|
||||||
@ -55,9 +57,8 @@ impl NxCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup(&self) -> anyhow::Result<()> {
|
fn setup(&self) -> anyhow::Result<()> {
|
||||||
self.db
|
let query = if self.link_task_details {
|
||||||
.execute_batch(
|
"BEGIN;
|
||||||
"BEGIN;
|
|
||||||
CREATE TABLE IF NOT EXISTS cache_outputs (
|
CREATE TABLE IF NOT EXISTS cache_outputs (
|
||||||
hash TEXT PRIMARY KEY NOT NULL,
|
hash TEXT PRIMARY KEY NOT NULL,
|
||||||
code INTEGER NOT NULL,
|
code INTEGER NOT NULL,
|
||||||
@ -65,8 +66,23 @@ impl NxCache {
|
|||||||
accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (hash) REFERENCES task_details (hash)
|
FOREIGN KEY (hash) REFERENCES task_details (hash)
|
||||||
);
|
);
|
||||||
COMMIT;
|
COMMIT;
|
||||||
",
|
"
|
||||||
|
} else {
|
||||||
|
"BEGIN;
|
||||||
|
CREATE TABLE IF NOT EXISTS cache_outputs (
|
||||||
|
hash TEXT PRIMARY KEY NOT NULL,
|
||||||
|
code INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
COMMIT;
|
||||||
|
"
|
||||||
|
};
|
||||||
|
|
||||||
|
self.db
|
||||||
|
.execute_batch(
|
||||||
|
query,
|
||||||
)
|
)
|
||||||
.map_err(anyhow::Error::from)
|
.map_err(anyhow::Error::from)
|
||||||
}
|
}
|
||||||
@ -116,6 +132,7 @@ impl NxCache {
|
|||||||
outputs: Vec<String>,
|
outputs: Vec<String>,
|
||||||
code: i16,
|
code: i16,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
trace!("PUT {}", &hash);
|
||||||
let task_dir = self.cache_path.join(&hash);
|
let task_dir = self.cache_path.join(&hash);
|
||||||
|
|
||||||
// Remove the task directory
|
// Remove the task directory
|
||||||
@ -143,7 +160,11 @@ impl NxCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn apply_remote_cache_results(&self, hash: String, result: CachedResult) -> anyhow::Result<()> {
|
pub fn apply_remote_cache_results(
|
||||||
|
&self,
|
||||||
|
hash: String,
|
||||||
|
result: CachedResult,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let terminal_output = result.terminal_output;
|
let terminal_output = result.terminal_output;
|
||||||
write(self.get_task_outputs_path(hash.clone()), terminal_output)?;
|
write(self.get_task_outputs_path(hash.clone()), terminal_output)?;
|
||||||
|
|
||||||
@ -153,14 +174,13 @@ impl NxCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_task_outputs_path_internal(&self, hash: &str) -> PathBuf {
|
fn get_task_outputs_path_internal(&self, hash: &str) -> PathBuf {
|
||||||
self.cache_path
|
self.cache_path.join("terminalOutputs").join(hash)
|
||||||
.join("terminalOutputs")
|
|
||||||
.join(hash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn get_task_outputs_path(&self, hash: String) -> String {
|
pub fn get_task_outputs_path(&self, hash: String) -> String {
|
||||||
self.get_task_outputs_path_internal(&hash).to_normalized_string()
|
self.get_task_outputs_path_internal(&hash)
|
||||||
|
.to_normalized_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_to_cache(&self, hash: String, code: i16) -> anyhow::Result<()> {
|
fn record_to_cache(&self, hash: String, code: i16) -> anyhow::Result<()> {
|
||||||
@ -192,11 +212,12 @@ impl NxCache {
|
|||||||
.as_slice(),
|
.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
trace!("Copying Files from Cache {:?} -> {:?}", &outputs_path, &self.workspace_root);
|
trace!(
|
||||||
_copy(
|
"Copying Files from Cache {:?} -> {:?}",
|
||||||
outputs_path,
|
&outputs_path,
|
||||||
&self.workspace_root,
|
&self.workspace_root
|
||||||
)?;
|
);
|
||||||
|
_copy(outputs_path, &self.workspace_root)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -224,4 +245,42 @@ impl NxCache {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn check_cache_fs_in_sync(&self) -> anyhow::Result<bool> {
|
||||||
|
// Checks that the number of cache records in the database
|
||||||
|
// matches the number of cache directories on the filesystem.
|
||||||
|
// If they don't match, it means that the cache is out of sync.
|
||||||
|
let cache_records_exist = self.db.query_row(
|
||||||
|
"SELECT EXISTS (SELECT 1 FROM cache_outputs)",
|
||||||
|
[],
|
||||||
|
|row| {
|
||||||
|
let exists: bool = row.get(0)?;
|
||||||
|
Ok(exists)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if !cache_records_exist {
|
||||||
|
let hash_regex = Regex::new(r"^\d+$").expect("Hash regex is invalid");
|
||||||
|
let fs_entries = std::fs::read_dir(&self.cache_path)
|
||||||
|
.map_err(anyhow::Error::from)?;
|
||||||
|
|
||||||
|
for entry in fs_entries {
|
||||||
|
let entry = entry?;
|
||||||
|
let is_dir = entry.file_type()?.is_dir();
|
||||||
|
|
||||||
|
if (is_dir) {
|
||||||
|
if let Some(file_name) = entry.file_name().to_str() {
|
||||||
|
if hash_regex.is_match(file_name) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,13 @@ use crate::native::machine_id::get_machine_id;
|
|||||||
pub fn connect_to_nx_db(
|
pub fn connect_to_nx_db(
|
||||||
cache_dir: String,
|
cache_dir: String,
|
||||||
nx_version: String,
|
nx_version: String,
|
||||||
|
db_name: Option<String>,
|
||||||
) -> anyhow::Result<External<Connection>> {
|
) -> anyhow::Result<External<Connection>> {
|
||||||
let machine_id = get_machine_id();
|
|
||||||
let cache_dir_buf = PathBuf::from(cache_dir);
|
let cache_dir_buf = PathBuf::from(cache_dir);
|
||||||
let db_path = cache_dir_buf.join(format!("{}.db", machine_id));
|
let db_path = cache_dir_buf.join(format!(
|
||||||
|
"{}.db",
|
||||||
|
db_name.unwrap_or_else(get_machine_id)
|
||||||
|
));
|
||||||
create_dir_all(cache_dir_buf)?;
|
create_dir_all(cache_dir_buf)?;
|
||||||
|
|
||||||
let c = create_connection(&db_path)?;
|
let c = create_connection(&db_path)?;
|
||||||
|
|||||||
5
packages/nx/src/native/index.d.ts
vendored
5
packages/nx/src/native/index.d.ts
vendored
@ -28,13 +28,14 @@ export declare class ImportResult {
|
|||||||
|
|
||||||
export declare class NxCache {
|
export declare class NxCache {
|
||||||
cacheDirectory: string
|
cacheDirectory: string
|
||||||
constructor(workspaceRoot: string, cachePath: string, dbConnection: ExternalObject<Connection>)
|
constructor(workspaceRoot: string, cachePath: string, dbConnection: ExternalObject<Connection>, linkTaskDetails?: boolean | undefined | null)
|
||||||
get(hash: string): CachedResult | null
|
get(hash: string): CachedResult | null
|
||||||
put(hash: string, terminalOutput: string, outputs: Array<string>, code: number): void
|
put(hash: string, terminalOutput: string, outputs: Array<string>, code: number): void
|
||||||
applyRemoteCacheResults(hash: string, result: CachedResult): void
|
applyRemoteCacheResults(hash: string, result: CachedResult): void
|
||||||
getTaskOutputsPath(hash: string): string
|
getTaskOutputsPath(hash: string): string
|
||||||
copyFilesFromCache(cachedResult: CachedResult, outputs: Array<string>): void
|
copyFilesFromCache(cachedResult: CachedResult, outputs: Array<string>): void
|
||||||
removeOldCacheRecords(): void
|
removeOldCacheRecords(): void
|
||||||
|
checkCacheFsInSync(): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class NxTaskHistory {
|
export declare class NxTaskHistory {
|
||||||
@ -96,7 +97,7 @@ export interface CachedResult {
|
|||||||
outputsPath: string
|
outputsPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare export function connectToNxDb(cacheDir: string, nxVersion: string): ExternalObject<Connection>
|
export declare export function connectToNxDb(cacheDir: string, nxVersion: string, dbName?: string | undefined | null): ExternalObject<Connection>
|
||||||
|
|
||||||
export declare export function copy(src: string, dest: string): void
|
export declare export function copy(src: string, dest: string): void
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,9 @@ describe('Cache', () => {
|
|||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dbConnection = getDbConnection(join(__dirname, 'temp-db'));
|
const dbConnection = getDbConnection({
|
||||||
|
directory: join(__dirname, 'temp-db'),
|
||||||
|
});
|
||||||
|
|
||||||
taskDetails = new TaskDetails(dbConnection);
|
taskDetails = new TaskDetails(dbConnection);
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,9 @@ describe('NxTaskHistory', () => {
|
|||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dbConnection = getDbConnection(join(__dirname, 'temp-db'));
|
const dbConnection = getDbConnection({
|
||||||
|
directory: join(__dirname, 'temp-db'),
|
||||||
|
});
|
||||||
taskHistory = new NxTaskHistory(dbConnection);
|
taskHistory = new NxTaskHistory(dbConnection);
|
||||||
taskDetails = new TaskDetails(dbConnection);
|
taskDetails = new TaskDetails(dbConnection);
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import { isNxCloudUsed } from '../utils/nx-cloud-utils';
|
|||||||
import { readNxJson } from '../config/nx-json';
|
import { readNxJson } from '../config/nx-json';
|
||||||
import { verifyOrUpdateNxCloudClient } from '../nx-cloud/update-manager';
|
import { verifyOrUpdateNxCloudClient } from '../nx-cloud/update-manager';
|
||||||
import { getCloudOptions } from '../nx-cloud/utilities/get-cloud-options';
|
import { getCloudOptions } from '../nx-cloud/utilities/get-cloud-options';
|
||||||
|
import { isCI } from '../utils/is-ci';
|
||||||
|
import { output } from '../utils/output';
|
||||||
|
|
||||||
export type CachedResult = {
|
export type CachedResult = {
|
||||||
terminalOutput: string;
|
terminalOutput: string;
|
||||||
@ -40,15 +42,20 @@ export function getCache(options: DefaultTasksRunnerOptions) {
|
|||||||
|
|
||||||
export class DbCache {
|
export class DbCache {
|
||||||
private cache = new NxCache(workspaceRoot, cacheDir, getDbConnection());
|
private cache = new NxCache(workspaceRoot, cacheDir, getDbConnection());
|
||||||
|
|
||||||
private remoteCache: RemoteCacheV2 | null;
|
private remoteCache: RemoteCacheV2 | null;
|
||||||
private remoteCachePromise: Promise<RemoteCacheV2>;
|
private remoteCachePromise: Promise<RemoteCacheV2>;
|
||||||
|
|
||||||
async setup() {
|
|
||||||
this.remoteCache = await this.getRemoteCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private readonly options: { nxCloudRemoteCache: RemoteCache }) {}
|
constructor(private readonly options: { nxCloudRemoteCache: RemoteCache }) {}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// This should be cheap because we've already loaded
|
||||||
|
this.remoteCache = await this.getRemoteCache();
|
||||||
|
if (!this.remoteCache) {
|
||||||
|
this.assertCacheIsValid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async get(task: Task): Promise<CachedResult | null> {
|
async get(task: Task): Promise<CachedResult | null> {
|
||||||
const res = this.cache.get(task.hash);
|
const res = this.cache.get(task.hash);
|
||||||
|
|
||||||
@ -58,7 +65,6 @@ export class DbCache {
|
|||||||
remote: false,
|
remote: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await this.setup();
|
|
||||||
if (this.remoteCache) {
|
if (this.remoteCache) {
|
||||||
// didn't find it locally but we have a remote cache
|
// didn't find it locally but we have a remote cache
|
||||||
// attempt remote cache
|
// attempt remote cache
|
||||||
@ -95,7 +101,6 @@ export class DbCache {
|
|||||||
return tryAndRetry(async () => {
|
return tryAndRetry(async () => {
|
||||||
this.cache.put(task.hash, terminalOutput, outputs, code);
|
this.cache.put(task.hash, terminalOutput, outputs, code);
|
||||||
|
|
||||||
await this.setup();
|
|
||||||
if (this.remoteCache) {
|
if (this.remoteCache) {
|
||||||
await this.remoteCache.store(
|
await this.remoteCache.store(
|
||||||
task.hash,
|
task.hash,
|
||||||
@ -142,9 +147,62 @@ export class DbCache {
|
|||||||
return await RemoteCacheV2.fromCacheV1(this.options.nxCloudRemoteCache);
|
return await RemoteCacheV2.fromCacheV1(this.options.nxCloudRemoteCache);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
return (
|
||||||
|
(await this.getPowerpackS3Cache()) ??
|
||||||
|
(await this.getPowerpackSharedCache()) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPowerpackS3Cache(): Promise<RemoteCacheV2 | null> {
|
||||||
|
try {
|
||||||
|
const { getRemoteCache } = await import(
|
||||||
|
this.resolvePackage('@nx/powerpack-s3-cache')
|
||||||
|
);
|
||||||
|
return getRemoteCache();
|
||||||
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getPowerpackSharedCache(): Promise<RemoteCacheV2 | null> {
|
||||||
|
try {
|
||||||
|
const { getRemoteCache } = await import(
|
||||||
|
this.resolvePackage('@nx/powerpack-shared-fs-cache')
|
||||||
|
);
|
||||||
|
return getRemoteCache();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolvePackage(pkg: string) {
|
||||||
|
return require.resolve(pkg, {
|
||||||
|
paths: [process.cwd(), workspaceRoot, __dirname],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertCacheIsValid() {
|
||||||
|
// User has customized the cache directory - this could be because they
|
||||||
|
// are using a shared cache in the custom directory. The db cache is not
|
||||||
|
// stored in the cache directory, and is keyed by machine ID so they would
|
||||||
|
// hit issues. If we detect this, we can create a fallback db cache in the
|
||||||
|
// custom directory, and check if the entries are there when the main db
|
||||||
|
// cache misses.
|
||||||
|
if (isCI() && !this.cache.checkCacheFsInSync()) {
|
||||||
|
const warningLines = [
|
||||||
|
`Nx found unrecognized artifacts in the cache directory and will not be able to use them.`,
|
||||||
|
`Nx can only restore artifacts it has metadata about.`,
|
||||||
|
`Read about this warning and how to address it here: https://nx.dev/troubleshooting/unknown-local-cache`,
|
||||||
|
``,
|
||||||
|
];
|
||||||
|
output.warn({
|
||||||
|
title: 'Unrecognized Cache Artifacts',
|
||||||
|
bodyLines: warningLines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -51,6 +51,7 @@ import { TasksRunner, TaskStatus } from './tasks-runner';
|
|||||||
import { shouldStreamOutput } from './utils';
|
import { shouldStreamOutput } from './utils';
|
||||||
import chalk = require('chalk');
|
import chalk = require('chalk');
|
||||||
import type { Observable } from 'rxjs';
|
import type { Observable } from 'rxjs';
|
||||||
|
import { printPowerpackLicense } from '../utils/powerpack';
|
||||||
|
|
||||||
async function getTerminalOutputLifeCycle(
|
async function getTerminalOutputLifeCycle(
|
||||||
initiatingProject: string,
|
initiatingProject: string,
|
||||||
@ -241,6 +242,8 @@ export async function runCommandForTasks(
|
|||||||
|
|
||||||
await renderIsDone;
|
await renderIsDone;
|
||||||
|
|
||||||
|
await printPowerpackLicense();
|
||||||
|
|
||||||
return taskResults;
|
return taskResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,7 +491,7 @@ async function promptForApplyingSyncGeneratorChanges(): Promise<boolean> {
|
|||||||
name: 'applyChanges',
|
name: 'applyChanges',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
message:
|
message:
|
||||||
'Would you like to sync the identified changes to get your worskpace up to date?',
|
'Would you like to sync the identified changes to get your workspace up to date?',
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
name: 'yes',
|
name: 'yes',
|
||||||
@ -501,7 +504,7 @@ async function promptForApplyingSyncGeneratorChanges(): Promise<boolean> {
|
|||||||
],
|
],
|
||||||
footer: () =>
|
footer: () =>
|
||||||
chalk.dim(
|
chalk.dim(
|
||||||
'\nYou can skip this prompt by setting the `sync.applyChanges` option in your `nx.json`.'
|
'\nYou can skip this prompt by setting the `sync.applyChanges` option to `true` in your `nx.json`.\nFor more information, refer to the docs: https://nx.dev/concepts/sync-generators.'
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -75,10 +75,11 @@ export class TaskOrchestrator {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
// Init the ForkedProcessTaskRunner
|
// Init the ForkedProcessTaskRunner, TasksSchedule, and Cache
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.forkedProcessTaskRunner.init(),
|
this.forkedProcessTaskRunner.init(),
|
||||||
this.tasksSchedule.init(),
|
this.tasksSchedule.init(),
|
||||||
|
'init' in this.cache ? this.cache.init() : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// initial scheduling
|
// initial scheduling
|
||||||
|
|||||||
@ -2,9 +2,32 @@ import { connectToNxDb, ExternalObject } from '../native';
|
|||||||
import { workspaceDataDirectory } from './cache-directory';
|
import { workspaceDataDirectory } from './cache-directory';
|
||||||
import { version as NX_VERSION } from '../../package.json';
|
import { version as NX_VERSION } from '../../package.json';
|
||||||
|
|
||||||
let dbConnection: ExternalObject<any>;
|
const dbConnectionMap = new Map<string, ExternalObject<any>>();
|
||||||
|
|
||||||
export function getDbConnection(directory = workspaceDataDirectory) {
|
export function getDbConnection(
|
||||||
dbConnection ??= connectToNxDb(directory, NX_VERSION);
|
opts: {
|
||||||
return dbConnection;
|
directory?: string;
|
||||||
|
dbName?: string;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
opts.directory ??= workspaceDataDirectory;
|
||||||
|
const key = `${opts.directory}:${opts.dbName ?? 'default'}`;
|
||||||
|
const connection = getEntryOrSet(dbConnectionMap, key, () =>
|
||||||
|
connectToNxDb(opts.directory, NX_VERSION, opts.dbName)
|
||||||
|
);
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryOrSet<TKey, TVal>(
|
||||||
|
map: Map<TKey, TVal>,
|
||||||
|
key: TKey,
|
||||||
|
defaultValue: () => TVal
|
||||||
|
) {
|
||||||
|
const existing = map.get(key);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
const val = defaultValue();
|
||||||
|
map.set(key, val);
|
||||||
|
return val;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,6 +61,13 @@ export function listAlsoAvailableCorePlugins(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listPowerpackPlugins(): void {
|
||||||
|
const powerpackLink = 'https://nx.dev/plugin-registry';
|
||||||
|
output.log({
|
||||||
|
title: `Available Powerpack Plugins: ${powerpackLink}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function listPluginCapabilities(
|
export async function listPluginCapabilities(
|
||||||
pluginName: string,
|
pluginName: string,
|
||||||
projects: Record<string, ProjectConfiguration>
|
projects: Record<string, ProjectConfiguration>
|
||||||
|
|||||||
44
packages/nx/src/utils/powerpack.ts
Normal file
44
packages/nx/src/utils/powerpack.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { logger } from './logger';
|
||||||
|
import { getPackageManagerCommand } from './package-manager';
|
||||||
|
import { workspaceRoot } from './workspace-root';
|
||||||
|
|
||||||
|
export async function printPowerpackLicense() {
|
||||||
|
try {
|
||||||
|
const { organizationName, seatCount, workspaceCount } =
|
||||||
|
await getPowerpackLicenseInformation();
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
`Nx Powerpack Licensed to ${organizationName} for ${seatCount} user${
|
||||||
|
seatCount > 1 ? '' : 's'
|
||||||
|
} in ${workspaceCount} workspace${workspaceCount > 1 ? '' : 's'}`
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPowerpackLicenseInformation() {
|
||||||
|
try {
|
||||||
|
const { getPowerpackLicenseInformation } = (await import(
|
||||||
|
// @ts-ignore
|
||||||
|
'@nx/powerpack-license'
|
||||||
|
// TODO(@FrozenPandaz): Provide the right type here.
|
||||||
|
)) as any;
|
||||||
|
// )) as typeof import('@nx/powerpack-license');
|
||||||
|
return getPowerpackLicenseInformation(workspaceRoot);
|
||||||
|
} catch (e) {
|
||||||
|
if ('code' in e && e.code === 'ERR_MODULE_NOT_FOUND') {
|
||||||
|
throw new NxPowerpackNotInstalledError(e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NxPowerpackNotInstalledError extends Error {
|
||||||
|
constructor(e: Error) {
|
||||||
|
super(
|
||||||
|
`The "@nx/powerpack-license" package is needed to use Nx Powerpack enabled features. Please install the @nx/powerpack-license with ${
|
||||||
|
getPackageManagerCommand().addDev
|
||||||
|
} @nx/powerpack-license`,
|
||||||
|
{ cause: e }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -187,7 +187,7 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["vite", "webpack", "rspack"],
|
"enum": ["vite", "webpack", "rspack"],
|
||||||
"x-prompt": "Which bundler do you want to use to build the application?",
|
"x-prompt": "Which bundler do you want to use to build the application?",
|
||||||
"default": "webpack",
|
"default": "vite",
|
||||||
"x-priority": "important"
|
"x-priority": "important"
|
||||||
},
|
},
|
||||||
"minimal": {
|
"minimal": {
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export async function hostGeneratorInternal(
|
|||||||
|
|
||||||
const initTask = await applicationGenerator(host, {
|
const initTask = await applicationGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
|
name: options.projectName,
|
||||||
// The target use-case is loading remotes as child routes, thus always enable routing.
|
// The target use-case is loading remotes as child routes, thus always enable routing.
|
||||||
routing: true,
|
routing: true,
|
||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export function addModuleFederationFiles(
|
|||||||
defaultRemoteManifest: { name: string; port: number }[]
|
defaultRemoteManifest: { name: string; port: number }[]
|
||||||
) {
|
) {
|
||||||
const templateVariables = {
|
const templateVariables = {
|
||||||
...names(options.name),
|
...names(options.projectName),
|
||||||
...options,
|
...options,
|
||||||
static: !options?.dynamic,
|
static: !options?.dynamic,
|
||||||
tmpl: '',
|
tmpl: '',
|
||||||
@ -26,7 +26,7 @@ export function addModuleFederationFiles(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectConfig = readProjectConfiguration(host, options.name);
|
const projectConfig = readProjectConfiguration(host, options.projectName);
|
||||||
const pathToMFManifest = joinPathFragments(
|
const pathToMFManifest = joinPathFragments(
|
||||||
projectConfig.sourceRoot,
|
projectConfig.sourceRoot,
|
||||||
'assets/module-federation.manifest.json'
|
'assets/module-federation.manifest.json'
|
||||||
|
|||||||
@ -2,16 +2,16 @@ import type { Tree } from '@nx/devkit';
|
|||||||
import { joinPathFragments, readProjectConfiguration } from '@nx/devkit';
|
import { joinPathFragments, readProjectConfiguration } from '@nx/devkit';
|
||||||
import { addTsConfigPath } from '@nx/js';
|
import { addTsConfigPath } from '@nx/js';
|
||||||
import { maybeJs } from '../../../utils/maybe-js';
|
import { maybeJs } from '../../../utils/maybe-js';
|
||||||
import type { Schema } from '../schema';
|
import { NormalizedSchema } from '../../application/schema';
|
||||||
|
|
||||||
export function setupTspathForRemote(tree: Tree, options: Schema) {
|
export function setupTspathForRemote(tree: Tree, options: NormalizedSchema) {
|
||||||
const project = readProjectConfiguration(tree, options.name);
|
const project = readProjectConfiguration(tree, options.projectName);
|
||||||
|
|
||||||
const exportPath = maybeJs(options, './src/remote-entry.ts');
|
const exportPath = maybeJs(options, './src/remote-entry.ts');
|
||||||
|
|
||||||
const exportName = 'Module';
|
const exportName = 'Module';
|
||||||
|
|
||||||
addTsConfigPath(tree, `${options.name}/${exportName}`, [
|
addTsConfigPath(tree, `${options.projectName}/${exportName}`, [
|
||||||
joinPathFragments(project.root, exportPath),
|
joinPathFragments(project.root, exportPath),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function addModuleFederationFiles(
|
|||||||
options: NormalizedSchema<Schema>
|
options: NormalizedSchema<Schema>
|
||||||
) {
|
) {
|
||||||
const templateVariables = {
|
const templateVariables = {
|
||||||
...names(options.name),
|
...names(options.projectName),
|
||||||
...options,
|
...options,
|
||||||
tmpl: '',
|
tmpl: '',
|
||||||
};
|
};
|
||||||
@ -113,16 +113,17 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
|
|||||||
if (options.dynamic) {
|
if (options.dynamic) {
|
||||||
// Dynamic remotes generate with library { type: 'var' } by default.
|
// Dynamic remotes generate with library { type: 'var' } by default.
|
||||||
// We need to ensure that the remote name is a valid variable name.
|
// We need to ensure that the remote name is a valid variable name.
|
||||||
const isValidRemote = isValidVariable(options.name);
|
const isValidRemote = isValidVariable(options.projectName);
|
||||||
if (!isValidRemote.isValid) {
|
if (!isValidRemote.isValid) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid remote name provided: ${options.name}. ${isValidRemote.message}`
|
`Invalid remote name provided: ${options.projectName}. ${isValidRemote.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initAppTask = await applicationGenerator(host, {
|
const initAppTask = await applicationGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
|
name: options.projectName,
|
||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
});
|
});
|
||||||
tasks.push(initAppTask);
|
tasks.push(initAppTask);
|
||||||
@ -201,7 +202,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
|
|||||||
);
|
);
|
||||||
addRemoteToDynamicHost(
|
addRemoteToDynamicHost(
|
||||||
host,
|
host,
|
||||||
options.name,
|
options.projectName,
|
||||||
options.devServerPort,
|
options.devServerPort,
|
||||||
pathToMFManifest
|
pathToMFManifest
|
||||||
);
|
);
|
||||||
|
|||||||
@ -105,6 +105,33 @@ export function recursivelyCollectSecondaryEntryPointsFromDirectory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectPackagesFromExports(
|
||||||
|
pkgName: string,
|
||||||
|
pkgVersion: string,
|
||||||
|
exports: any | undefined,
|
||||||
|
collectedPackages: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}[]
|
||||||
|
): void {
|
||||||
|
for (const [relativeEntryPoint, exportOptions] of Object.entries(exports)) {
|
||||||
|
if (exportOptions?.['default']?.search(/\.(js|mjs|cjs)$/)) {
|
||||||
|
let entryPointName = joinPathFragments(pkgName, relativeEntryPoint);
|
||||||
|
if (entryPointName.endsWith('.json')) {
|
||||||
|
entryPointName = dirname(entryPointName);
|
||||||
|
}
|
||||||
|
if (entryPointName === '.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (collectedPackages.find((p) => p.name === entryPointName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
collectedPackages.push({ name: entryPointName, version: pkgVersion });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function collectPackageSecondaryEntryPoints(
|
export function collectPackageSecondaryEntryPoints(
|
||||||
pkgName: string,
|
pkgName: string,
|
||||||
pkgVersion: string,
|
pkgVersion: string,
|
||||||
@ -130,6 +157,9 @@ export function collectPackageSecondaryEntryPoints(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { exports } = packageJson;
|
const { exports } = packageJson;
|
||||||
|
if (exports) {
|
||||||
|
collectPackagesFromExports(pkgName, pkgVersion, exports, collectedPackages);
|
||||||
|
}
|
||||||
const subDirs = getNonNodeModulesSubDirs(pathToPackage);
|
const subDirs = getNonNodeModulesSubDirs(pathToPackage);
|
||||||
recursivelyCollectSecondaryEntryPointsFromDirectory(
|
recursivelyCollectSecondaryEntryPointsFromDirectory(
|
||||||
pkgName,
|
pkgName,
|
||||||
|
|||||||
@ -163,20 +163,110 @@ describe('MF Share Utils', () => {
|
|||||||
]);
|
]);
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(packages).toEqual({
|
expect(packages).toEqual({
|
||||||
'@angular/core': {
|
|
||||||
singleton: true,
|
|
||||||
strictVersion: true,
|
|
||||||
requiredVersion: '~13.2.0',
|
|
||||||
},
|
|
||||||
'@angular/common': {
|
'@angular/common': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
singleton: true,
|
singleton: true,
|
||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/http': {
|
||||||
requiredVersion: '~13.2.0',
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/http/testing': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/locales/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/locales/global/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/testing': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/upgrade': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/event-dispatch-contract.min.js': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/event-dispatch': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/signals': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/rxjs-interop': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/schematics/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/testing': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
},
|
},
|
||||||
rxjs: {
|
rxjs: {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
singleton: true,
|
singleton: true,
|
||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/ajax': {
|
||||||
requiredVersion: '~7.4.0',
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/fetch': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/internal/*': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/operators': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/testing': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/webSocket': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -218,6 +308,36 @@ describe('MF Share Utils', () => {
|
|||||||
// ASSERT
|
// ASSERT
|
||||||
expect(packages).toEqual({
|
expect(packages).toEqual({
|
||||||
'@angular/core': {
|
'@angular/core': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/event-dispatch-contract.min.js': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/event-dispatch': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/signals': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/rxjs-interop': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/schematics/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/testing': {
|
||||||
singleton: true,
|
singleton: true,
|
||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
requiredVersion: '~13.2.0',
|
requiredVersion: '~13.2.0',
|
||||||
@ -227,12 +347,67 @@ describe('MF Share Utils', () => {
|
|||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
requiredVersion: '~13.2.0',
|
requiredVersion: '~13.2.0',
|
||||||
},
|
},
|
||||||
'@angular/common/http/testing': {
|
'@angular/common/http': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
singleton: true,
|
singleton: true,
|
||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/http/testing': {
|
||||||
requiredVersion: '~13.2.0',
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/locales/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/locales/global/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/testing': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/common/upgrade': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
},
|
},
|
||||||
rxjs: {
|
rxjs: {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/ajax': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/fetch': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/internal/*': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/operators': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/testing': {
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'rxjs/webSocket': {
|
||||||
singleton: true,
|
singleton: true,
|
||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
requiredVersion: '~7.4.0',
|
requiredVersion: '~7.4.0',
|
||||||
@ -285,6 +460,31 @@ describe('MF Share Utils', () => {
|
|||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
requiredVersion: '~13.2.0',
|
requiredVersion: '~13.2.0',
|
||||||
},
|
},
|
||||||
|
'@angular/core/event-dispatch-contract.min.js': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/event-dispatch': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/signals': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/rxjs-interop': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/schematics/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
'@angular/core/testing': {
|
'@angular/core/testing': {
|
||||||
singleton: true,
|
singleton: true,
|
||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
@ -361,6 +561,31 @@ describe('MF Share Utils', () => {
|
|||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
requiredVersion: '~13.2.0',
|
requiredVersion: '~13.2.0',
|
||||||
},
|
},
|
||||||
|
'@angular/core/event-dispatch-contract.min.js': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/event-dispatch': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/primitives/signals': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/rxjs-interop': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
|
'@angular/core/schematics/*': {
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
},
|
||||||
'@angular/core/testing': {
|
'@angular/core/testing': {
|
||||||
singleton: true,
|
singleton: true,
|
||||||
strictVersion: true,
|
strictVersion: true,
|
||||||
|
|||||||
21796
pnpm-lock.yaml
generated
21796
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
RULE_NAME as ensurePnpmLockVersionName,
|
||||||
|
rule as ensurePnpmLockVersion,
|
||||||
|
} from './rules/ensure-pnpm-lock-version';
|
||||||
import {
|
import {
|
||||||
RULE_NAME as validCommandObjectName,
|
RULE_NAME as validCommandObjectName,
|
||||||
rule as validCommandObject,
|
rule as validCommandObject,
|
||||||
@ -34,5 +38,6 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
[validSchemaDescriptionName]: validSchemaDescription,
|
[validSchemaDescriptionName]: validSchemaDescription,
|
||||||
[validCommandObjectName]: validCommandObject,
|
[validCommandObjectName]: validCommandObject,
|
||||||
|
[ensurePnpmLockVersionName]: ensurePnpmLockVersion,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
23
tools/eslint-rules/raw-file-parser.js
Normal file
23
tools/eslint-rules/raw-file-parser.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* We have a custom lint rule for our pnpm-lock.yaml file and naturally ESLint does not natively know how to parse it.
|
||||||
|
* Rather than using a full yaml parser for this one case (which will need to spend time creating a real AST for the giant
|
||||||
|
* lock file), we can instead use a custom parser which just immediately returns a dummy AST and then build the reading of
|
||||||
|
* the lock file into the rule itself.
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
parseForESLint: (code) => ({
|
||||||
|
ast: {
|
||||||
|
type: 'Program',
|
||||||
|
loc: { start: 0, end: code.length },
|
||||||
|
range: [0, code.length],
|
||||||
|
body: [],
|
||||||
|
comments: [],
|
||||||
|
tokens: [],
|
||||||
|
},
|
||||||
|
services: { isPlain: true },
|
||||||
|
scopeManager: null,
|
||||||
|
visitorKeys: {
|
||||||
|
Program: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
109
tools/eslint-rules/rules/ensure-pnpm-lock-version.ts
Normal file
109
tools/eslint-rules/rules/ensure-pnpm-lock-version.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* This file sets you up with structure needed for an ESLint rule.
|
||||||
|
*
|
||||||
|
* It leverages utilities from @typescript-eslint to allow TypeScript to
|
||||||
|
* provide autocompletions etc for the configuration.
|
||||||
|
*
|
||||||
|
* Your rule's custom logic will live within the create() method below
|
||||||
|
* and you can learn more about writing ESLint rules on the official guide:
|
||||||
|
*
|
||||||
|
* https://eslint.org/docs/developer-guide/working-with-rules
|
||||||
|
*
|
||||||
|
* You can also view many examples of existing rules here:
|
||||||
|
*
|
||||||
|
* https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin/src/rules
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||||
|
import { closeSync, openSync, readSync } from 'node:fs';
|
||||||
|
|
||||||
|
// NOTE: The rule will be available in ESLint configs as "@nx/workspace-ensure-pnpm-lock-version"
|
||||||
|
export const RULE_NAME = 'ensure-pnpm-lock-version';
|
||||||
|
|
||||||
|
export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
||||||
|
name: RULE_NAME,
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description: ``,
|
||||||
|
},
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
version: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
messages: {
|
||||||
|
unparseableLockfileVersion:
|
||||||
|
'Could not parse lockfile version from pnpm-lock.yaml, the file may be corrupted or the ensure-pnpm-lock-version lint rule may need to be updated.',
|
||||||
|
incorrectLockfileVersion:
|
||||||
|
'pnpm-lock.yaml has a lockfileVersion of {{version}}, but {{expectedVersion}} is required.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultOptions: [],
|
||||||
|
create(context) {
|
||||||
|
// Read upon creation of the rule, the contents should not change during linting
|
||||||
|
const lockfileFirstLine = readFirstLineSync('pnpm-lock.yaml');
|
||||||
|
// Extract the version number, it will be a string in single quotes
|
||||||
|
const lockfileVersion = lockfileFirstLine.match(
|
||||||
|
/lockfileVersion:\s*'([^']+)'/
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const options = context.options as { version: string }[];
|
||||||
|
if (!Array.isArray(options) || options.length === 0) {
|
||||||
|
throw new Error('Expected an array of options with a version property');
|
||||||
|
}
|
||||||
|
const expectedLockfileVersion = options[0].version;
|
||||||
|
return {
|
||||||
|
Program(node) {
|
||||||
|
if (!lockfileVersion) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'unparseableLockfileVersion',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lockfileVersion !== expectedLockfileVersion) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'incorrectLockfileVersion',
|
||||||
|
data: {
|
||||||
|
version: lockfileVersion,
|
||||||
|
expectedVersion: expectedLockfileVersion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pnpm-lock.yaml is a huge file, so only read the first line as efficiently as possible
|
||||||
|
* for optimum linting performance.
|
||||||
|
*/
|
||||||
|
function readFirstLineSync(filePath: string) {
|
||||||
|
const BUFFER_SIZE = 64; // Optimized for the expected line length
|
||||||
|
const buffer = Buffer.alloc(BUFFER_SIZE);
|
||||||
|
let line = '';
|
||||||
|
let bytesRead: number;
|
||||||
|
let fd: number;
|
||||||
|
try {
|
||||||
|
fd = openSync(filePath, 'r');
|
||||||
|
bytesRead = readSync(fd, buffer, 0, BUFFER_SIZE, 0);
|
||||||
|
line = buffer.toString('utf8', 0, bytesRead).split('\n')[0];
|
||||||
|
} catch (err) {
|
||||||
|
throw err; // Re-throw to allow caller to handle
|
||||||
|
} finally {
|
||||||
|
if (fd !== undefined) {
|
||||||
|
closeSync(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user