Merge branch 'master' into move-rspack-into-main

This commit is contained in:
Colum Ferry 2024-09-23 09:27:44 +01:00
commit f9e1d6371e
79 changed files with 11124 additions and 14333 deletions

View File

@ -64,6 +64,16 @@
],
"@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" }
]
}
}
]
}

View File

@ -498,5 +498,10 @@
"name": "nx-github-pages",
"description": "A small Nx plugin to make deploying static projects to GitHub Pages easy.",
"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"
}
]

View File

@ -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

View 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!!
[![Monorepo World](/blog/images/2024-08-01/monorepo-world.avif)](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/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

14
docs/changelog/19_8_0.md Normal file
View File

@ -0,0 +1,14 @@
# [Nx 19.8](/blog/nx-19-8-update)
[![Nx 19.8 Update!!](/blog/images/2024-09-20/thumbnail.png)](/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 %}

View File

@ -181,7 +181,7 @@
"type": "string",
"enum": ["vite", "webpack", "rspack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"default": "vite",
"x-priority": "important"
},
"minimal": {

View File

@ -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`.
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"] %}
name: CI

View File

@ -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.
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.

View File

@ -36,34 +36,71 @@ Here's the source code of the final result for this guide.
### 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 %}
{% 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
npx create-nx-workspace ng-mf
cd ng-mf
npx nx add @nx/angular
```
{% /tab %}
{% 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
yarn create nx-workspace ng-mf
cd ng-mf
yarn nx add @nx/angular
```
{% /tab %}
{% 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
pnpm create nx-workspace ng-mf
cd ng-mf
pnpx nx add @nx/angular
```
{% /tab %}
{% /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
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 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:
@ -168,7 +205,8 @@ We'll start by building the **Login** application, which will consist of a login
### 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
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 { FormsModule } from '@angular/forms';
import { UserService } from '@ng-mf/data-access-user';
import { inject } from '@angular/core';
@Component({
standalone: true,
@ -264,12 +303,11 @@ import { UserService } from '@ng-mf/data-access-user';
],
})
export class RemoteEntryComponent {
private userService = inject(UserService);
username = '';
password = '';
isLoggedIn$ = this.userService.isUserLoggedIn$;
constructor(private userService: UserService) {}
login() {
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" %}
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 { UserService } from '@ng-mf/data-access-user';
import { distinctUntilChanged } from 'rxjs/operators';
@ -326,10 +364,10 @@ import { distinctUntilChanged } from 'rxjs/operators';
`,
})
export class AppComponent implements OnInit {
private router = inject(Router);
private userService = inject(UserService);
isLoggedIn$ = this.userService.isUserLoggedIn$;
constructor(private userService: UserService, private router: Router) {}
ngOnInit() {
this.isLoggedIn$
.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.
Well 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:
Well 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"
}
@ -416,7 +454,7 @@ Next, open the `main.ts` file and replace it with the following:
```ts {% fileName="apps/dashboard/src/main.ts" %}
import { setRemoteDefinitions } from '@nx/angular/mf';
fetch('/assets/module-federation.manifest.json')
fetch('/module-federation.manifest.json')
.then((res) => res.json())
.then((definitions) => setRemoteDefinitions(definitions))
.then(() => import('./bootstrap').catch((err) => console.error(err)));

View File

@ -84,6 +84,7 @@ const projectDetailsLoader = async (
sourceMap: Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
}> => {
const workspaceData = await workspaceDataLoader(selectedWorkspaceId);
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
@ -104,6 +105,7 @@ const projectDetailsLoader = async (
sourceMap: sourceMaps[project.data.root],
errors: workspaceData.errors,
connectedToCloud: workspaceData.connectedToCloud,
disabledTaskSyncGenerators: workspaceData.disabledTaskSyncGenerators,
};
};

View File

@ -21,13 +21,20 @@ import {
import { ProjectDetailsHeader } from './project-details-header';
export function ProjectDetailsPage() {
const { project, sourceMap, hash, errors, connectedToCloud } =
useRouteLoaderData('selectedProjectDetails') as {
const {
project,
sourceMap,
hash,
errors,
connectedToCloud,
disabledTaskSyncGenerators,
} = useRouteLoaderData('selectedProjectDetails') as {
hash: string;
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
};
const { environment, watch, appConfig } = useEnvironmentConfig();
@ -65,6 +72,7 @@ export function ProjectDetailsPage() {
sourceMap={sourceMap}
errors={errors}
connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
></ProjectDetailsWrapper>
</div>
</div>

View File

@ -22,6 +22,7 @@ interface ProjectDetailsProps {
sourceMap: Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
}
export function ProjectDetailsWrapper({
@ -29,6 +30,7 @@ export function ProjectDetailsWrapper({
sourceMap,
errors,
connectedToCloud,
disabledTaskSyncGenerators,
}: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
@ -174,6 +176,7 @@ export function ProjectDetailsWrapper({
}
connectedToCloud={connectedToCloud}
onNxConnect={environment === 'nx-console' ? handleNxConnect : undefined}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
/>
<ErrorToast errors={errors} />
</>

View File

@ -83,6 +83,11 @@ export const Primary = {
],
},
configurations: {},
syncGenerators: [
'@nx/js:typescript-sync',
'@foo/bar:sync',
'@baz/qux:sync',
],
},
build: {
dependsOn: ['build-base', 'build-native'],
@ -210,6 +215,7 @@ export const Primary = {
'nx-core-build-project-json-nodes',
],
},
disabledTaskSyncGenerators: ['@foo/bar:sync'],
},
};

View File

@ -18,6 +18,7 @@ export interface ProjectDetailsProps {
errors?: GraphError[];
variant?: 'default' | 'compact';
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
onViewInProjectGraph?: (data: { projectName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string;
@ -44,6 +45,7 @@ export const ProjectDetails = ({
onNxConnect,
viewInProjectGraphPosition = 'top',
connectedToCloud,
disabledTaskSyncGenerators,
}: ProjectDetailsProps) => {
const projectData = project.data;
const isCompact = variant === 'compact';
@ -153,6 +155,7 @@ export const ProjectDetails = ({
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
onNxConnect={onNxConnect}
/>
</div>

View File

@ -18,6 +18,7 @@ export interface TargetConfigurationGroupListProps {
}) => void;
onNxConnect?: () => void;
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
className?: string;
}
@ -30,6 +31,7 @@ export function TargetConfigurationGroupList({
onNxConnect,
className = '',
connectedToCloud,
disabledTaskSyncGenerators,
}: TargetConfigurationGroupListProps) {
const targetsGroup = useMemo(() => groupTargets(project), [project]);
const hasGroups = useMemo(() => {
@ -56,6 +58,7 @@ export function TargetConfigurationGroupList({
project={project}
sourceMap={sourceMap}
connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
variant={variant}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
@ -82,6 +85,7 @@ export function TargetConfigurationGroupList({
project={project}
sourceMap={sourceMap}
connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
variant={variant}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
@ -105,6 +109,7 @@ export function TargetConfigurationGroupList({
project={project}
sourceMap={sourceMap}
connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
variant={variant}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}

View File

@ -7,6 +7,7 @@ export interface TargetConfigurationDetailsListItemProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
variant?: 'default' | 'compact';
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: {
@ -23,6 +24,7 @@ export function TargetConfigurationDetailsListItem({
variant,
sourceMap,
connectedToCloud,
disabledTaskSyncGenerators,
onRunTarget,
onViewInTaskGraph,
onNxConnect,
@ -42,6 +44,7 @@ export function TargetConfigurationDetailsListItem({
targetConfiguration={target}
sourceMap={sourceMap}
connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
onNxConnect={onNxConnect}

View File

@ -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 { TargetSourceInfo } from '../target-source-info/target-source-info';
import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration';
import { getTaskSyncGenerators } from '../utils/sync-generators';
import { FadingCollapsible } from './fading-collapsible';
import { TargetConfigurationProperty } from './target-configuration-property';
import { TooltipTriggerText } from './tooltip-trigger-text';
@ -24,6 +25,7 @@ interface TargetConfigurationDetailsProps {
targetConfiguration: TargetConfiguration;
sourceMap: Record<string, string[]>;
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
variant?: 'default' | 'compact';
onCollapse?: (targetName: string) => void;
onExpand?: (targetName: string) => void;
@ -43,6 +45,7 @@ export default function TargetConfigurationDetails({
targetConfiguration,
sourceMap,
connectedToCloud,
disabledTaskSyncGenerators,
onViewInTaskGraph,
onRunTarget,
onNxConnect,
@ -84,6 +87,9 @@ export default function TargetConfigurationDetails({
? Object.keys(configurations).length
: true);
const { enabledSyncGenerators, disabledSyncGenerators } =
getTaskSyncGenerators(targetConfiguration, disabledTaskSyncGenerators);
return (
<div className="relative rounded-md border border-slate-200 dark:border-slate-700/60">
<TargetConfigurationDetailsHeader
@ -364,7 +370,7 @@ export default function TargetConfigurationDetails({
</Tooltip>
</h4>
<div className="group/line overflow-hidden whitespace-nowrap pl-5">
<TargetConfigurationProperty data={{ paralelism: false }}>
<TargetConfigurationProperty data={{ parallelism: false }}>
<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}.parallelism`}
@ -374,6 +380,68 @@ export default function TargetConfigurationDetails({
</div>
</div>
) : 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>

View File

@ -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>
)}
</>
);
}

View File

@ -1,18 +1,27 @@
import { JSX, ReactNode } from 'react';
import { TargetConfigurationPropertyText } from './target-configuration-property-text';
interface RenderPropertyProps {
data: string | Record<string, any> | any[];
disabled?: boolean;
disabledTooltip?: ReactNode;
children?: ReactNode;
}
export function TargetConfigurationProperty({
data,
children,
disabled,
disabledTooltip,
}: RenderPropertyProps): JSX.Element | null {
if (typeof data === 'string') {
return (
<span className="flex font-mono text-sm">
{data}
<TargetConfigurationPropertyText
content={data}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
{children}
</span>
);
@ -21,7 +30,11 @@ export function TargetConfigurationProperty({
<ul>
{data.map((item, index) => (
<li key={index} className="flex font-mono text-sm">
{String(item)}
<TargetConfigurationPropertyText
content={String(item)}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
{children}
</li>
))}
@ -32,7 +45,15 @@ export function TargetConfigurationProperty({
<ul>
{Object.entries(data).map(([key, value], index) => (
<li key={index} className="flex font-mono text-sm">
<TargetConfigurationPropertyText
content={
<>
<strong>{key}</strong>: {String(value)}
</>
}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
{children}
</li>
))}

View 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 };
}

View File

@ -2,6 +2,10 @@
@apply overflow-hidden !important;
}
.DocSearch-VisuallyHiddenForAccessibility {
visibility: hidden;
}
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;
}

View File

@ -1,10 +1,9 @@
import { SectionHeading } from './temp/typography';
import {
BoltIcon,
ChevronDoubleRightIcon,
UsersIcon,
WrenchIcon,
} from '@heroicons/react/24/outline';
import { SectionHeading } from '@nx/nx-dev/ui-common';
export function EnterpriseAddons(): JSX.Element {
return (

View File

@ -1,5 +1,4 @@
import { SectionHeading } from './temp/typography';
import { ButtonLink } from '@nx/nx-dev/ui-common';
import { ButtonLink, SectionHeading } from '@nx/nx-dev/ui-common';
import Link from 'next/link';
export function Hero(): JSX.Element {

View File

@ -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>
);
}

View File

@ -3,11 +3,10 @@ import {
Cog6ToothIcon,
CubeTransparentIcon,
IdentificationIcon,
PhotoIcon,
SquaresPlusIcon,
UserGroupIcon,
} from '@heroicons/react/24/outline';
import { SectionHeading } from './temp/typography';
import { SectionHeading } from '@nx/nx-dev/ui-common';
export function ScaleYourPeople(): JSX.Element {
return (
@ -250,28 +249,6 @@ export function ScaleYourPeople(): JSX.Element {
</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>
</section>
);

View File

@ -1,4 +1,4 @@
import { SectionHeading } from './temp/typography';
import { SectionHeading } from '@nx/nx-dev/ui-common';
export function Security(): JSX.Element {
return (

View File

@ -12,9 +12,9 @@ import {
} from '@heroicons/react/24/outline';
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
import { useEffect } from 'react';
import { SectionHeading } from './temp/typography';
import { BentoGrid, BentoGridItem } from './bento-grid';
import { cx } from '@nx/nx-dev/ui-primitives';
import { SectionHeading } from '@nx/nx-dev/ui-common';
export function SolveYourCi(): JSX.Element {
return (

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -9,6 +9,7 @@ import {
} from 'react';
import { ProjectDetails as ProjectDetailsUi } from '@nx/graph-internal/ui-project-details';
import { ExpandedTargetsProvider } from '@nx/graph/shared';
import { twMerge } from 'tailwind-merge';
export function Loading() {
return (
@ -110,7 +111,7 @@ export function ProjectDetails({
)}
<div
id="project-details-container"
className="not-prose overflow-y-auto"
className={twMerge('not-prose', height && 'overflow-y-auto')}
style={{ height }}
ref={elementRef}
>

View File

@ -123,7 +123,14 @@
}
},
"lint": {
"dependsOn": ["build-native", "^build-native"]
"dependsOn": [
"build-native",
"^build-native",
"@nx/nx-source:lint-pnpm-lock"
]
},
"lint-pnpm-lock": {
"cache": true
},
"e2e": {
"cache": true,

View File

@ -22,7 +22,8 @@
"preinstall": "node ./scripts/preinstall.js",
"test": "nx run-many -t test",
"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": {
"@actions/core": "^1.10.0",
@ -68,21 +69,21 @@
"@ngrx/store": "18.0.2",
"@nuxt/kit": "^3.10.0",
"@nuxt/schema": "^3.10.0",
"@nx/angular": "19.8.0-beta.0",
"@nx/cypress": "19.8.0-beta.0",
"@nx/devkit": "19.8.0-beta.0",
"@nx/esbuild": "19.8.0-beta.0",
"@nx/eslint": "19.8.0-beta.0",
"@nx/eslint-plugin": "19.8.0-beta.0",
"@nx/jest": "19.8.0-beta.0",
"@nx/js": "19.8.0-beta.0",
"@nx/next": "19.8.0-beta.0",
"@nx/playwright": "19.8.0-beta.0",
"@nx/react": "19.8.0-beta.0",
"@nx/storybook": "19.8.0-beta.0",
"@nx/vite": "19.8.0-beta.0",
"@nx/web": "19.8.0-beta.0",
"@nx/webpack": "19.8.0-beta.0",
"@nx/angular": "19.8.0-beta.2",
"@nx/cypress": "19.8.0-beta.2",
"@nx/devkit": "19.8.0-beta.2",
"@nx/esbuild": "19.8.0-beta.2",
"@nx/eslint": "19.8.0-beta.2",
"@nx/eslint-plugin": "19.8.0-beta.2",
"@nx/jest": "19.8.0-beta.2",
"@nx/js": "19.8.0-beta.2",
"@nx/next": "19.8.0-beta.2",
"@nx/playwright": "19.8.0-beta.2",
"@nx/react": "19.8.0-beta.2",
"@nx/storybook": "19.8.0-beta.2",
"@nx/vite": "19.8.0-beta.2",
"@nx/web": "19.8.0-beta.2",
"@nx/webpack": "19.8.0-beta.2",
"@phenomnomnominal/tsquery": "~5.0.1",
"@playwright/test": "^1.36.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
@ -244,13 +245,14 @@
"node-fetch": "^2.6.7",
"npm-package-arg": "11.0.1",
"nuxt": "^3.10.0",
"nx": "19.8.0-beta.0",
"nx": "19.8.0-beta.2",
"octokit": "^2.0.14",
"open": "^8.4.0",
"openai": "~4.3.1",
"ora": "5.3.0",
"parse-markdown-links": "^1.0.4",
"parse5": "4.0.0",
"picocolors": "^1.1.0",
"piscina": "^4.4.0",
"postcss": "8.4.38",
"postcss-import": "~14.1.0",
@ -380,6 +382,7 @@
},
"nx": {
"includedScripts": [
"lint-pnpm-lock",
"echo",
"check-commit",
"check-format",

View File

@ -2,6 +2,7 @@ import { merge } from 'webpack-merge';
import { registerTsProject } from '@nx/js/src/internal';
import { workspaceRoot } from '@nx/devkit';
import { join } from 'path';
import { existsSync, readFileSync } from 'fs';
export async function mergeCustomWebpackConfig(
baseWebpackConfig: any,
@ -22,11 +23,44 @@ export async function mergeCustomWebpackConfig(
// The extra Webpack configuration file can export a synchronous or asynchronous function,
// for instance: `module.exports = async config => { ... }`.
let newConfig: any;
if (typeof config === 'function') {
return config(baseWebpackConfig, options, target);
newConfig = config(baseWebpackConfig, options, target);
} 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) {

View File

@ -15,7 +15,15 @@
"overrides": [
{
"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"],

View File

@ -1,5 +1,5 @@
#!/usr/bin/env node
import chalk = require('chalk');
import * as pc from 'picocolors';
import enquirer = require('enquirer');
import yargs = require('yargs');
@ -26,16 +26,16 @@ import {
} from 'create-nx-workspace/src/utils/nx/ab-testing';
export const yargsDecorator = {
'Options:': `${chalk.green`Options`}:`,
'Examples:': `${chalk.green`Examples`}:`,
boolean: `${chalk.blue`boolean`}`,
count: `${chalk.blue`count`}`,
string: `${chalk.blue`string`}`,
array: `${chalk.blue`array`}`,
required: `${chalk.blue`required`}`,
'default:': `${chalk.blue`default`}:`,
'choices:': `${chalk.blue`choices`}:`,
'aliases:': `${chalk.blue`aliases`}:`,
'Options:': `${pc.green(`Options`)}:`,
'Examples:': `${pc.green(`Examples`)}:`,
boolean: `${pc.blue(`boolean`)}`,
count: `${pc.blue(`count`)}`,
string: `${pc.blue(`string`)}`,
array: `${pc.blue(`array`)}`,
required: `${pc.blue(`required`)}`,
'default:': `${pc.blue(`default`)}:`,
'choices:': `${pc.blue(`choices`)}:`,
'aliases:': `${pc.blue(`aliases`)}:`,
};
const nxVersion = require('../package.json').version;
@ -97,7 +97,7 @@ export const commandsObject: yargs.Argv<CreateNxPluginArguments> = yargs
withOptions(
yargs
.positional('pluginName', {
describe: chalk.dim`Plugin name`,
describe: pc.dim(`Plugin name`),
type: 'string',
alias: ['name'],
})
@ -121,11 +121,11 @@ export const commandsObject: yargs.Argv<CreateNxPluginArguments> = yargs
},
[normalizeArgsMiddleware]
)
.help('help', chalk.dim`Show help`)
.help('help', pc.dim(`Show help`))
.updateLocale(yargsDecorator)
.version(
'version',
chalk.dim`Show version`,
pc.dim(`Show version`),
nxVersion
) as yargs.Argv<CreateNxPluginArguments>;

View File

@ -30,7 +30,7 @@
"homepage": "https://nx.dev",
"dependencies": {
"create-nx-workspace": "file:../create-nx-workspace",
"chalk": "^4.1.0",
"picocolors": "^1.1.0",
"enquirer": "~2.3.6",
"tslib": "^2.3.0",
"yargs": "^17.6.2"

View File

@ -28,7 +28,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
type: 'problem',
docs: {
description: \`\`,
recommended: 'recommended',
},
schema: [],
messages: {},
@ -84,7 +83,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
type: 'problem',
docs: {
description: \`\`,
recommended: 'recommended',
},
schema: [],
messages: {},
@ -140,7 +138,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
type: 'problem',
docs: {
description: \`\`,
recommended: 'recommended',
},
schema: [],
messages: {},

View File

@ -25,7 +25,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
type: 'problem',
docs: {
description: ``,
recommended: 'recommended',
},
schema: [],
messages: {},

View File

@ -43,7 +43,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
type: 'problem',
docs: {
description: \`\`,
recommended: 'recommended',
},
schema: [],
messages: {},

View File

@ -84,7 +84,6 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
type: 'problem',
docs: {
description: \`\`,
recommended: 'error',
},
schema: [],
messages: {},

View File

@ -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 { GitCommit } from '../../src/command-line/release/utils/git';
import {
GithubRepoData,
RepoSlug,
formatReferences,
} 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 {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 {GithubRepoData} config.repoData Resolved data for the current GitHub repository
*/
export type ChangelogRenderer = (config: {
projectGraph: ProjectGraph;
@ -53,7 +55,9 @@ export type ChangelogRenderer = (config: {
entryWhenNoChanges: string | false;
changelogRenderOptions: DefaultChangelogRenderOptions;
dependencyBumps?: DependencyBump[];
// TODO(v20): remove repoSlug in favour of repoData
repoSlug?: RepoSlug;
repoData?: GithubRepoData;
// 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
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
@ -101,6 +105,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
dependencyBumps,
repoSlug,
conventionalCommitsConfig,
repoData,
}): Promise<string> => {
const markdownLines: string[] = [];
@ -148,7 +153,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
change,
changelogRenderOptions,
isVersionPlans,
repoSlug
repoData
);
breakingChanges.push(line);
relevantChanges.splice(i, 1);
@ -222,7 +227,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
change,
changelogRenderOptions,
isVersionPlans,
repoSlug
repoData
);
markdownLines.push(line);
if (change.isBreaking) {
@ -295,7 +300,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
change,
changelogRenderOptions,
isVersionPlans,
repoSlug
repoData
);
markdownLines.push(line + '\n');
if (change.isBreaking) {
@ -350,7 +355,7 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({
}
// Try to map authors to github usernames
if (repoSlug && changelogRenderOptions.mapAuthorsToGitHubUsernames) {
if (repoData && changelogRenderOptions.mapAuthorsToGitHubUsernames) {
await Promise.all(
[..._authors.keys()].map(async (authorName) => {
const meta = _authors.get(authorName);
@ -455,7 +460,7 @@ function formatChange(
change: ChangelogChange,
changelogRenderOptions: DefaultChangelogRenderOptions,
isVersionPlans: boolean,
repoSlug?: RepoSlug
repoData?: GithubRepoData
): string {
let description = change.description;
let extraLines = [];
@ -480,8 +485,8 @@ function formatChange(
(!isVersionPlans && change.isBreaking ? '⚠️ ' : '') +
(!isVersionPlans && change.scope ? `**${change.scope.trim()}:** ` : '') +
description;
if (repoSlug && changelogRenderOptions.commitReferences) {
changeLine += formatReferences(change.githubReferences, repoSlug);
if (repoData && changelogRenderOptions.commitReferences) {
changeLine += formatReferences(change.githubReferences, repoData);
}
if (extraLinesStr) {
changeLine += '\n\n' + extraLinesStr;

View File

@ -691,6 +691,9 @@
{
"type": "boolean",
"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": {
"type": "object",
"properties": {

View File

@ -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.'
);
}
}
});
}

View File

@ -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);
},
};

View File

@ -8,10 +8,7 @@ export interface AddOptions {
__overrides_unparsed__: string[];
}
export const yargsAddCommand: CommandModule<
Record<string, unknown>,
AddOptions
> = {
export const yargsAddCommand: CommandModule<{}, AddOptions> = {
command: 'add <packageSpecifier>',
describe: 'Install a plugin and initialize it.',
builder: (yargs) =>

View File

@ -78,6 +78,7 @@ export interface ProjectGraphClientResponse {
isPartial: boolean;
errors?: GraphError[];
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
}
export interface TaskGraphClientResponse {
@ -773,13 +774,16 @@ async function createProjectGraphAndSourceMapClientResponse(
let isPartial = false;
let errors: GraphError[] | undefined;
let connectedToCloud: boolean | undefined;
let disabledTaskSyncGenerators: string[] | undefined;
try {
const projectGraphAndSourceMaps =
await createProjectGraphAndSourceMapsAsync({ exitOnError: false });
projectGraph = projectGraphAndSourceMaps.projectGraph;
sourceMaps = projectGraphAndSourceMaps.sourceMaps;
connectedToCloud = isNxCloudUsed(readNxJson());
const nxJson = readNxJson();
connectedToCloud = isNxCloudUsed(nxJson);
disabledTaskSyncGenerators = nxJson.sync?.disabledTaskSyncGenerators;
} catch (e) {
if (e instanceof ProjectGraphError) {
projectGraph = e.getPartialProjectGraph();
@ -820,6 +824,7 @@ async function createProjectGraphAndSourceMapClientResponse(
sourceMaps,
errors,
connectedToCloud,
disabledTaskSyncGenerators,
})
);
@ -851,6 +856,7 @@ async function createProjectGraphAndSourceMapClientResponse(
isPartial,
errors,
connectedToCloud,
disabledTaskSyncGenerators,
},
sourceMapResponse: sourceMaps,
};

View File

@ -12,6 +12,7 @@ import {
listPlugins,
} from '../../utils/plugins';
import { workspaceRoot } from '../../utils/workspace-root';
import { listPowerpackPlugins } from '../../utils/plugins/output';
export interface ListArgs {
/** The name of an installed plugin to query */
@ -46,6 +47,7 @@ export async function listHandler(args: ListArgs): Promise<void> {
}
listPlugins(installedPlugins, 'Installed plugins:');
listAlsoAvailableCorePlugins(installedPlugins);
listPowerpackPlugins();
output.note({
title: 'Community Plugins',

View File

@ -1,6 +1,7 @@
import * as chalk from 'chalk';
import * as yargs from 'yargs';
import { yargsActivatePowerpackCommand } from './activate-powerpack/command-object';
import {
yargsAffectedBuildCommand,
yargsAffectedCommand,
@ -63,6 +64,7 @@ export const commandsObject = yargs
.parserConfiguration(parserConfiguration)
.usage(chalk.bold('Smart Monorepos · Fast CI'))
.demandCommand(1, '')
.command(yargsActivatePowerpackCommand)
.command(yargsAddCommand)
.command(yargsAffectedBuildCommand)
.command(yargsAffectedCommand)
@ -98,9 +100,27 @@ export const commandsObject = yargs
.command(yargsNxInfixCommand)
.command(yargsLoginCommand)
.command(yargsLogoutCommand)
.command(resolveConformanceCommandObject())
.scriptName('nx')
.help()
// 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
// hit, as the implementation in nx.ts is hit first and calls process.exit(0).
.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);
},
};
}
}

View File

@ -32,6 +32,7 @@ import { ChangelogOptions } from './command-object';
import {
NxReleaseConfig,
createNxReleaseConfig,
defaultCreateReleaseProvider,
handleNxReleaseConfigError,
} from './config/config';
import { deepMergeJson } from './config/deep-merge-json';
@ -58,7 +59,7 @@ import {
parseCommits,
parseGitCommit,
} from './utils/git';
import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github';
import { createOrUpdateGithubRelease, getGitHubRepoData } from './utils/github';
import { launchEditor } from './utils/launch-editor';
import { parseChangelogMarkdown } from './utils/markdown';
import { printAndFlushChanges } from './utils/print-changes';
@ -411,6 +412,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
output.logSingleLine(`Creating GitHub Release`);
await createOrUpdateGithubRelease(
nxReleaseConfig.changelog.workspaceChangelog
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
: defaultCreateReleaseProvider,
workspaceChangelog.releaseVersion,
workspaceChangelog.contents,
latestCommit,
@ -644,6 +648,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
output.logSingleLine(`Creating GitHub Release`);
await createOrUpdateGithubRelease(
releaseGroup.changelog
? releaseGroup.changelog.createRelease
: defaultCreateReleaseProvider,
projectChangelog.releaseVersion,
projectChangelog.contents,
latestCommit,
@ -797,6 +804,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
output.logSingleLine(`Creating GitHub Release`);
await createOrUpdateGithubRelease(
releaseGroup.changelog
? releaseGroup.changelog.createRelease
: defaultCreateReleaseProvider,
projectChangelog.releaseVersion,
projectChangelog.contents,
latestCommit,
@ -1110,7 +1120,7 @@ async function generateChangelogForWorkspace({
});
}
const githubRepoSlug = getGitHubRepoSlug(gitRemote);
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
let contents = await changelogRenderer({
projectGraph,
@ -1118,7 +1128,8 @@ async function generateChangelogForWorkspace({
commits,
releaseVersion: releaseVersion.rawVersion,
project: null,
repoSlug: githubRepoSlug,
repoSlug: githubRepoData?.slug,
repoData: githubRepoData,
entryWhenNoChanges: config.entryWhenNoChanges,
changelogRenderOptions: config.renderOptions,
conventionalCommitsConfig: nxReleaseConfig.conventionalCommits,
@ -1250,10 +1261,7 @@ async function generateChangelogForProjects({
});
}
const githubRepoSlug =
config.createRelease === 'github'
? getGitHubRepoSlug(gitRemote)
: undefined;
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
let contents = await changelogRenderer({
projectGraph,
@ -1261,7 +1269,8 @@ async function generateChangelogForProjects({
commits,
releaseVersion: releaseVersion.rawVersion,
project: project.name,
repoSlug: githubRepoSlug,
repoSlug: githubRepoData?.slug,
repoData: githubRepoData,
entryWhenNoChanges:
typeof config.entryWhenNoChanges === 'string'
? interpolate(config.entryWhenNoChanges, {
@ -1409,7 +1418,7 @@ export function shouldCreateGitHubRelease(
return createReleaseArg === 'github';
}
return (changelogConfig || {}).createRelease === 'github';
return (changelogConfig || {}).createRelease !== false;
}
async function promptForGitHubRelease(): Promise<boolean> {

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,11 @@
* and easy to consume config object for all the `nx release` command implementations.
*/
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 { readJsonFile } from '../../../utils/fileutils';
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]> = {
[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 RemoveBooleanFromProperties<T, K extends keyof T> = {
[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'
| 'PROJECT_MATCHES_MULTIPLE_GROUPS'
| '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[]>;
}
@ -566,7 +565,16 @@ export async function createNxReleaseConfig(
releaseGroups[releaseGroupName] = finalReleaseGroup;
}
ensureChangelogRenderersAreResolvable(releaseGroups, rootChangelogConfig);
const configError = validateChangelogConfig(
releaseGroups,
rootChangelogConfig
);
if (configError) {
return {
error: configError,
nxReleaseConfig: null,
};
}
return {
error: null,
@ -766,6 +774,52 @@ export async function handleNxReleaseConfigError(
});
}
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:
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'],
rootChangelogConfig: NxReleaseConfig['changelog']
) {
): CreateNxReleaseConfigError | null {
/**
* 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.
@ -962,42 +1022,148 @@ function ensureChangelogRenderersAreResolvable(
if (
rootChangelogConfig.workspaceChangelog &&
typeof rootChangelogConfig.workspaceChangelog !== 'boolean' &&
rootChangelogConfig.workspaceChangelog.renderer?.length
typeof rootChangelogConfig.workspaceChangelog !== 'boolean'
) {
if (rootChangelogConfig.workspaceChangelog.renderer?.length) {
uniqueRendererPaths.add(rootChangelogConfig.workspaceChangelog.renderer);
}
const createReleaseError = validateCreateReleaseConfig(
rootChangelogConfig.workspaceChangelog
);
if (createReleaseError) {
return createReleaseError;
}
}
if (
rootChangelogConfig.projectChangelogs &&
typeof rootChangelogConfig.projectChangelogs !== 'boolean' &&
rootChangelogConfig.projectChangelogs.renderer?.length
typeof rootChangelogConfig.projectChangelogs !== 'boolean'
) {
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)) {
if (
group.changelog &&
typeof group.changelog !== 'boolean' &&
group.changelog.renderer?.length
) {
if (group.changelog && typeof group.changelog !== 'boolean') {
if (group.changelog.renderer?.length) {
uniqueRendererPaths.add(group.changelog.renderer);
}
const createReleaseError = validateCreateReleaseConfig(group.changelog);
if (createReleaseError) {
return createReleaseError;
}
}
}
if (!uniqueRendererPaths.size) {
return;
return null;
}
for (const rendererPath of uniqueRendererPaths) {
try {
resolveChangelogRenderer(rendererPath);
} catch (e) {
const workspaceRelativePath = relative(workspaceRoot, rendererPath);
output.error({
title: `There was an error when resolving the configured changelog renderer at path: ${workspaceRelativePath}`,
});
throw e;
} catch {
return {
code: 'CANNOT_RESOLVE_CHANGELOG_RENDERER',
data: {
workspaceRelativePath: relative(workspaceRoot, rendererPath),
},
};
}
}
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;
}
}

View File

@ -252,6 +252,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
latestCommit = await getCommitHash('HEAD');
await createOrUpdateGithubRelease(
nxReleaseConfig.changelog.workspaceChangelog
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
: false,
changelogResult.workspaceChangelog.releaseVersion,
changelogResult.workspaceChangelog.contents,
latestCommit,
@ -297,6 +300,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
}
await createOrUpdateGithubRelease(
releaseGroup.changelog
? releaseGroup.changelog.createRelease
: false,
changelog.releaseVersion,
changelog.contents,
latestCommit,

View File

@ -8,8 +8,10 @@ import { prompt } from 'enquirer';
import { execSync } from 'node:child_process';
import { existsSync, promises as fsp } from 'node:fs';
import { homedir } from 'node:os';
import { NxReleaseChangelogConfiguration } from '../../../config/nx-json';
import { output } from '../../../utils/output';
import { joinPathFragments } from '../../../utils/path';
import { defaultCreateReleaseProvider } from '../config/config';
import { Reference } from './git';
import { printDiff } from './print-changes';
import { ReleaseVersion, noDiffInChangelogMessage } from './shared';
@ -20,12 +22,14 @@ const axios = _axios as any as (typeof _axios)['default'];
export type RepoSlug = `${string}/${string}`;
export interface GithubRequestConfig {
interface GithubRequestConfig {
repo: string;
hostname: string;
apiBaseUrl: string;
token: string | null;
}
export interface GithubRelease {
interface GithubRelease {
id?: string;
tag_name: string;
target_commitish?: string;
@ -35,19 +39,46 @@ export interface GithubRelease {
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 {
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
encoding: 'utf8',
stdio: 'pipe',
}).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
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);
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 {
throw new Error(
`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(
createReleaseConfig: NxReleaseChangelogConfiguration['createRelease'],
releaseVersion: ReleaseVersion,
changelogContents: string,
latestCommit: string,
{ dryRun }: { dryRun: boolean }
): Promise<void> {
const githubRepoSlug = getGitHubRepoSlug();
if (!githubRepoSlug) {
const githubRepoData = getGitHubRepoData(undefined, createReleaseConfig);
if (!githubRepoData) {
output.error({
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
bodyLines: [
@ -75,9 +107,11 @@ export async function createOrUpdateGithubRelease(
process.exit(1);
}
const token = await resolveGithubToken();
const token = await resolveGithubToken(githubRepoData.hostname);
const githubRequestConfig: GithubRequestConfig = {
repo: githubRepoSlug,
repo: githubRepoData.slug,
hostname: githubRepoData.hostname,
apiBaseUrl: githubRepoData.apiBaseUrl,
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) {
console.error(
`${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
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
if (tokenFromEnv) {
@ -320,15 +354,15 @@ export async function resolveGithubToken(): Promise<string | null> {
const yamlContents = await fsp.readFile(ghCLIPath, 'utf8');
const { load } = require('@zkochan/js-yaml');
const ghCLIConfig = load(yamlContents);
if (ghCLIConfig['github.com']) {
if (ghCLIConfig[hostname]) {
// Web based session (the token is already embedded in the config)
if (ghCLIConfig['github.com'].oauth_token) {
return ghCLIConfig['github.com'].oauth_token;
if (ghCLIConfig[hostname].oauth_token) {
return ghCLIConfig[hostname].oauth_token;
}
// SSH based session (we need to dynamically resolve a token using the CLI)
if (
ghCLIConfig['github.com'].user &&
ghCLIConfig['github.com'].git_protocol === 'ssh'
ghCLIConfig[hostname].user &&
ghCLIConfig[hostname].git_protocol === 'ssh'
) {
return execSync(`gh auth token`, {
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;
}
@ -359,7 +398,7 @@ async function makeGithubRequest(
return (
await axios<any, any>(url, {
...opts,
baseURL: 'https://api.github.com',
baseURL: config.apiBaseUrl,
headers: {
...(opts.headers as any),
Authorization: config.token ? `Bearer ${config.token}` : undefined,
@ -395,11 +434,18 @@ async function updateGithubRelease(
function githubNewReleaseURL(
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
}&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';
@ -411,27 +457,30 @@ const providerToRefSpec: Record<
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'];
return `[${ref.value}](https://github.com/${repoSlug}/${
return `[${ref.value}](https://${repoData.hostname}/${repoData.slug}/${
refSpec[ref.type]
}/${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 issue = references.filter((ref) => ref.type === 'issue');
if (pr.length > 0 || issue.length > 0) {
return (
' (' +
[...pr, ...issue]
.map((ref) => formatReference(ref, repoSlug))
.map((ref) => formatReference(ref, repoData))
.join(', ') +
')'
);
}
if (references.length > 0) {
return ' (' + formatReference(references[0], repoSlug) + ')';
return ' (' + formatReference(references[0], repoData) + ')';
}
return '';
}

View File

@ -23,6 +23,7 @@ import { getNxRequirePaths } from '../../utils/installation-directory';
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
import { ProjectGraph } from '../../config/project-graph';
import { ProjectGraphError } from '../../project-graph/error-types';
import { getPowerpackLicenseInformation } from '../../utils/powerpack';
const nxPackageJson = readJsonFile<typeof import('../../../package.json')>(
join(__dirname, '../../../package.json')
@ -39,6 +40,7 @@ export const packagesWeCareAbout = [
export const patternsWeIgnoreInCommunityReport: Array<string | RegExp> = [
...packagesWeCareAbout,
new RegExp('@nx/powerpack*'),
'@schematics/angular',
new RegExp('@angular/*'),
'@nestjs/schematics',
@ -58,7 +60,9 @@ export async function reportHandler() {
const {
pm,
pmVersion,
powerpackLicense,
localPlugins,
powerpackPlugins,
communityPlugins,
registeredPlugins,
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) {
bodyLines.push(LINE_SEPARATOR);
bodyLines.push('Registered Plugins:');
@ -147,6 +183,9 @@ export async function reportHandler() {
export interface ReportData {
pm: PackageManager;
pmVersion: string;
// TODO(@FrozenPandaz): Provide the right type here.
powerpackLicense: any | null;
powerpackPlugins: PackageJson[];
localPlugins: string[];
communityPlugins: PackageJson[];
registeredPlugins: string[];
@ -174,6 +213,7 @@ export async function getReportData(): Promise<ReportData> {
const nxJson = readNxJson();
const localPlugins = await findLocalPlugins(graph, nxJson);
const powerpackPlugins = findInstalledPowerpackPlugins();
const communityPlugins = findInstalledCommunityPlugins();
const registeredPlugins = findRegisteredPluginsBeingUsed(nxJson);
@ -193,8 +233,15 @@ export async function getReportData(): Promise<ReportData> {
const native = isNativeAvailable();
let powerpackLicense = null;
try {
powerpackLicense = await getPowerpackLicenseInformation();
} catch {}
return {
pm,
powerpackLicense,
powerpackPlugins,
pmVersion,
localPlugins,
communityPlugins,
@ -294,6 +341,13 @@ export function findMisalignedPackagesForPackage(
: undefined;
}
export function findInstalledPowerpackPlugins(): PackageJson[] {
const installedPlugins = findInstalledPlugins();
return installedPlugins.filter((dep) =>
new RegExp('@nx/powerpack*').test(dep.name)
);
}
export function findInstalledCommunityPlugins(): PackageJson[] {
const installedPlugins = findInstalledPlugins();
return installedPlugins.filter(

View File

@ -79,7 +79,17 @@ export interface NxReleaseChangelogConfiguration {
* 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.
*/
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)
* at the workspace root and/or within project directories, or set to `false` to specify

View File

@ -4,12 +4,12 @@ use std::time::Instant;
use fs_extra::remove_items;
use napi::bindgen_prelude::*;
use regex::Regex;
use rusqlite::{params, Connection, OptionalExtension};
use tracing::trace;
use crate::native::cache::expand_outputs::_expand_outputs;
use crate::native::cache::file_ops::_copy;
use crate::native::machine_id::get_machine_id;
use crate::native::utils::Normalize;
#[napi(object)]
@ -26,6 +26,7 @@ pub struct NxCache {
workspace_root: PathBuf,
cache_path: PathBuf,
db: External<Connection>,
link_task_details: bool,
}
#[napi]
@ -35,9 +36,9 @@ impl NxCache {
workspace_root: String,
cache_path: String,
db_connection: External<Connection>,
link_task_details: Option<bool>,
) -> anyhow::Result<Self> {
let machine_id = get_machine_id();
let cache_path = PathBuf::from(&cache_path).join(machine_id);
let cache_path = PathBuf::from(&cache_path);
create_dir_all(&cache_path)?;
create_dir_all(cache_path.join("terminalOutputs"))?;
@ -47,6 +48,7 @@ impl NxCache {
workspace_root: PathBuf::from(workspace_root),
cache_directory: cache_path.to_normalized_string(),
cache_path,
link_task_details: link_task_details.unwrap_or(true)
};
r.setup()?;
@ -55,8 +57,7 @@ impl NxCache {
}
fn setup(&self) -> anyhow::Result<()> {
self.db
.execute_batch(
let query = if self.link_task_details {
"BEGIN;
CREATE TABLE IF NOT EXISTS cache_outputs (
hash TEXT PRIMARY KEY NOT NULL,
@ -66,7 +67,22 @@ impl NxCache {
FOREIGN KEY (hash) REFERENCES task_details (hash)
);
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)
}
@ -116,6 +132,7 @@ impl NxCache {
outputs: Vec<String>,
code: i16,
) -> anyhow::Result<()> {
trace!("PUT {}", &hash);
let task_dir = self.cache_path.join(&hash);
// Remove the task directory
@ -143,7 +160,11 @@ impl NxCache {
}
#[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;
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 {
self.cache_path
.join("terminalOutputs")
.join(hash)
self.cache_path.join("terminalOutputs").join(hash)
}
#[napi]
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<()> {
@ -192,11 +212,12 @@ impl NxCache {
.as_slice(),
)?;
trace!("Copying Files from Cache {:?} -> {:?}", &outputs_path, &self.workspace_root);
_copy(
outputs_path,
&self.workspace_root,
)?;
trace!(
"Copying Files from Cache {:?} -> {:?}",
&outputs_path,
&self.workspace_root
);
_copy(outputs_path, &self.workspace_root)?;
Ok(())
}
@ -224,4 +245,42 @@ impl NxCache {
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)
}
}
}

View File

@ -10,10 +10,13 @@ use crate::native::machine_id::get_machine_id;
pub fn connect_to_nx_db(
cache_dir: String,
nx_version: String,
db_name: Option<String>,
) -> anyhow::Result<External<Connection>> {
let machine_id = get_machine_id();
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)?;
let c = create_connection(&db_path)?;

View File

@ -28,13 +28,14 @@ export declare class ImportResult {
export declare class NxCache {
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
put(hash: string, terminalOutput: string, outputs: Array<string>, code: number): void
applyRemoteCacheResults(hash: string, result: CachedResult): void
getTaskOutputsPath(hash: string): string
copyFilesFromCache(cachedResult: CachedResult, outputs: Array<string>): void
removeOldCacheRecords(): void
checkCacheFsInSync(): boolean
}
export declare class NxTaskHistory {
@ -96,7 +97,7 @@ export interface CachedResult {
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

View File

@ -16,7 +16,9 @@ describe('Cache', () => {
force: true,
});
const dbConnection = getDbConnection(join(__dirname, 'temp-db'));
const dbConnection = getDbConnection({
directory: join(__dirname, 'temp-db'),
});
taskDetails = new TaskDetails(dbConnection);

View File

@ -17,7 +17,9 @@ describe('NxTaskHistory', () => {
force: true,
});
const dbConnection = getDbConnection(join(__dirname, 'temp-db'));
const dbConnection = getDbConnection({
directory: join(__dirname, 'temp-db'),
});
taskHistory = new NxTaskHistory(dbConnection);
taskDetails = new TaskDetails(dbConnection);

View File

@ -17,6 +17,8 @@ import { isNxCloudUsed } from '../utils/nx-cloud-utils';
import { readNxJson } from '../config/nx-json';
import { verifyOrUpdateNxCloudClient } from '../nx-cloud/update-manager';
import { getCloudOptions } from '../nx-cloud/utilities/get-cloud-options';
import { isCI } from '../utils/is-ci';
import { output } from '../utils/output';
export type CachedResult = {
terminalOutput: string;
@ -40,15 +42,20 @@ export function getCache(options: DefaultTasksRunnerOptions) {
export class DbCache {
private cache = new NxCache(workspaceRoot, cacheDir, getDbConnection());
private remoteCache: RemoteCacheV2 | null;
private remoteCachePromise: Promise<RemoteCacheV2>;
async setup() {
this.remoteCache = await this.getRemoteCache();
}
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> {
const res = this.cache.get(task.hash);
@ -58,7 +65,6 @@ export class DbCache {
remote: false,
};
}
await this.setup();
if (this.remoteCache) {
// didn't find it locally but we have a remote cache
// attempt remote cache
@ -95,7 +101,6 @@ export class DbCache {
return tryAndRetry(async () => {
this.cache.put(task.hash, terminalOutput, outputs, code);
await this.setup();
if (this.remoteCache) {
await this.remoteCache.store(
task.hash,
@ -142,9 +147,62 @@ export class DbCache {
return await RemoteCacheV2.fromCacheV1(this.options.nxCloudRemoteCache);
}
} 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;
}
}
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,
});
}
}
}
/**

View File

@ -51,6 +51,7 @@ import { TasksRunner, TaskStatus } from './tasks-runner';
import { shouldStreamOutput } from './utils';
import chalk = require('chalk');
import type { Observable } from 'rxjs';
import { printPowerpackLicense } from '../utils/powerpack';
async function getTerminalOutputLifeCycle(
initiatingProject: string,
@ -241,6 +242,8 @@ export async function runCommandForTasks(
await renderIsDone;
await printPowerpackLicense();
return taskResults;
}
@ -488,7 +491,7 @@ async function promptForApplyingSyncGeneratorChanges(): Promise<boolean> {
name: 'applyChanges',
type: 'select',
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: [
{
name: 'yes',
@ -501,7 +504,7 @@ async function promptForApplyingSyncGeneratorChanges(): Promise<boolean> {
],
footer: () =>
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.'
),
};

View File

@ -75,10 +75,11 @@ export class TaskOrchestrator {
) {}
async run() {
// Init the ForkedProcessTaskRunner
// Init the ForkedProcessTaskRunner, TasksSchedule, and Cache
await Promise.all([
this.forkedProcessTaskRunner.init(),
this.tasksSchedule.init(),
'init' in this.cache ? this.cache.init() : null,
]);
// initial scheduling

View File

@ -2,9 +2,32 @@ import { connectToNxDb, ExternalObject } from '../native';
import { workspaceDataDirectory } from './cache-directory';
import { version as NX_VERSION } from '../../package.json';
let dbConnection: ExternalObject<any>;
const dbConnectionMap = new Map<string, ExternalObject<any>>();
export function getDbConnection(directory = workspaceDataDirectory) {
dbConnection ??= connectToNxDb(directory, NX_VERSION);
return dbConnection;
export function getDbConnection(
opts: {
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;
}

View File

@ -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(
pluginName: string,
projects: Record<string, ProjectConfiguration>

View 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 }
);
}
}

View File

@ -187,7 +187,7 @@
"type": "string",
"enum": ["vite", "webpack", "rspack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"default": "vite",
"x-priority": "important"
},
"minimal": {

View File

@ -67,6 +67,7 @@ export async function hostGeneratorInternal(
const initTask = await applicationGenerator(host, {
...options,
name: options.projectName,
// The target use-case is loading remotes as child routes, thus always enable routing.
routing: true,
skipFormat: true,

View File

@ -14,7 +14,7 @@ export function addModuleFederationFiles(
defaultRemoteManifest: { name: string; port: number }[]
) {
const templateVariables = {
...names(options.name),
...names(options.projectName),
...options,
static: !options?.dynamic,
tmpl: '',
@ -26,7 +26,7 @@ export function addModuleFederationFiles(
}),
};
const projectConfig = readProjectConfiguration(host, options.name);
const projectConfig = readProjectConfiguration(host, options.projectName);
const pathToMFManifest = joinPathFragments(
projectConfig.sourceRoot,
'assets/module-federation.manifest.json'

View File

@ -2,16 +2,16 @@ import type { Tree } from '@nx/devkit';
import { joinPathFragments, readProjectConfiguration } from '@nx/devkit';
import { addTsConfigPath } from '@nx/js';
import { maybeJs } from '../../../utils/maybe-js';
import type { Schema } from '../schema';
import { NormalizedSchema } from '../../application/schema';
export function setupTspathForRemote(tree: Tree, options: Schema) {
const project = readProjectConfiguration(tree, options.name);
export function setupTspathForRemote(tree: Tree, options: NormalizedSchema) {
const project = readProjectConfiguration(tree, options.projectName);
const exportPath = maybeJs(options, './src/remote-entry.ts');
const exportName = 'Module';
addTsConfigPath(tree, `${options.name}/${exportName}`, [
addTsConfigPath(tree, `${options.projectName}/${exportName}`, [
joinPathFragments(project.root, exportPath),
]);
}

View File

@ -32,7 +32,7 @@ export function addModuleFederationFiles(
options: NormalizedSchema<Schema>
) {
const templateVariables = {
...names(options.name),
...names(options.projectName),
...options,
tmpl: '',
};
@ -113,16 +113,17 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
if (options.dynamic) {
// Dynamic remotes generate with library { type: 'var' } by default.
// 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) {
throw new Error(
`Invalid remote name provided: ${options.name}. ${isValidRemote.message}`
`Invalid remote name provided: ${options.projectName}. ${isValidRemote.message}`
);
}
}
const initAppTask = await applicationGenerator(host, {
...options,
name: options.projectName,
skipFormat: true,
});
tasks.push(initAppTask);
@ -201,7 +202,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
);
addRemoteToDynamicHost(
host,
options.name,
options.projectName,
options.devServerPort,
pathToMFManifest
);

View File

@ -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(
pkgName: string,
pkgVersion: string,
@ -130,6 +157,9 @@ export function collectPackageSecondaryEntryPoints(
}
const { exports } = packageJson;
if (exports) {
collectPackagesFromExports(pkgName, pkgVersion, exports, collectedPackages);
}
const subDirs = getNonNodeModulesSubDirs(pathToPackage);
recursivelyCollectSecondaryEntryPointsFromDirectory(
pkgName,

View File

@ -163,20 +163,110 @@ describe('MF Share Utils', () => {
]);
// ASSERT
expect(packages).toEqual({
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/common': {
requiredVersion: '~13.2.0',
singleton: true,
strictVersion: true,
},
'@angular/common/http': {
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: {
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': {
requiredVersion: '~7.4.0',
singleton: true,
strictVersion: true,
},
});
});
@ -218,6 +308,36 @@ describe('MF Share Utils', () => {
// ASSERT
expect(packages).toEqual({
'@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,
strictVersion: true,
requiredVersion: '~13.2.0',
@ -227,12 +347,67 @@ describe('MF Share Utils', () => {
strictVersion: true,
requiredVersion: '~13.2.0',
},
'@angular/common/http/testing': {
'@angular/common/http': {
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,
},
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,
strictVersion: true,
requiredVersion: '~7.4.0',
@ -285,6 +460,31 @@ describe('MF Share Utils', () => {
strictVersion: true,
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': {
singleton: true,
strictVersion: true,
@ -361,6 +561,31 @@ describe('MF Share Utils', () => {
strictVersion: true,
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': {
singleton: true,
strictVersion: true,

21796
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,7 @@
import {
RULE_NAME as ensurePnpmLockVersionName,
rule as ensurePnpmLockVersion,
} from './rules/ensure-pnpm-lock-version';
import {
RULE_NAME as validCommandObjectName,
rule as validCommandObject,
@ -34,5 +38,6 @@ module.exports = {
rules: {
[validSchemaDescriptionName]: validSchemaDescription,
[validCommandObjectName]: validCommandObject,
[ensurePnpmLockVersionName]: ensurePnpmLockVersion,
},
};

View 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: [],
},
}),
};

View 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;
}