feat(core): support compile to wasi target (#22870)

This pull request is trying to add wasm32-wasi target support for the
nx/native

To test the build, you can run the following commands:

- `rustup target add wasm32-wasip1-threads`
- `pnpm exec napi build --release --platform --package-json-path
packages/nx/package.json --manifest-path packages/nx/Cargo.toml --js
./native-bindings.js -o packages/nx/src/native --target
wasm32-wasip1-threads`

And the wasm file will be built at
packages/nx/src/native/nx.wasm32-wasi.wasm

Blocked by:

- Support @napi-rs/cli 3.0  Cammisuli/monodon#48
- https://github.com/napi-rs/napi-rs/issues/2009

The pseudo_terminal mod is excluded on the wasm32 targets, which is as
expected.

The watch mod is excluded because of the upstream `watchexec` deps
introduced by ignore-files don't support the wasi target at this moment
(but we can improve it).

## Related Issues
Fixes https://github.com/nrwl/nx/issues/21860
Fixes https://github.com/nrwl/nx/issues/23821

---------

Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
This commit is contained in:
LongYinan 2024-07-06 03:55:35 +08:00 committed by GitHub
parent 23ce6af2cc
commit 981eb30a0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 3585 additions and 2123 deletions

View File

@ -175,7 +175,7 @@ jobs:
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf -y
build: |
pnpm nx run-many --target=build-native -- --target=armv7-unknown-linux-gnueabihf
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=/usr/bin/arm-linux-gnueabihf-gcc pnpm nx run-many --target=build-native -- --target=armv7-unknown-linux-gnueabihf
# Android (not needed)
# - host: ubuntu-latest
# target: aarch64-linux-android
@ -281,7 +281,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.settings.target }}
path: packages/**/*.node
path: |
packages/**/*.node
packages/**/*.wasm
if-no-files-found: error
build-freebsd:
@ -396,7 +398,8 @@ jobs:
- name: List artifacts
run: ls -R artifacts
shell: bash
- name: Build Wasm
run: pnpm build:wasm
- name: Publish
env:
VERSION: ${{ needs.resolve-required-data.outputs.version }}

2
.gitignore vendored
View File

@ -56,3 +56,5 @@ out
.npm/
.profile
.rustup/
target
*.wasm

View File

@ -17,6 +17,11 @@ packages/nx/src/plugins/js/lock-file/__fixtures__/**/*.*
packages/**/schematics/**/files/**/*.html
packages/**/generators/**/files/**/*.html
packages/nx/src/native/**/*.rs
packages/nx/src/native/browser.js
packages/nx/src/native/nx.wasi-browser.js
packages/nx/src/native/nx.wasi.cjs
packages/nx/src/native/wasi-worker-browser.mjs
packages/nx/src/native/wasi-worker.mjs
packages/nx/src/native/native-bindings.js
packages/nx/src/native/index.d.ts
nx-dev/nx-dev/.next/

1105
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -135,6 +135,7 @@ It only uses language primitives and immutable objects
- [globAsync](../../devkit/documents/globAsync)
- [hashArray](../../devkit/documents/hashArray)
- [installPackagesTask](../../devkit/documents/installPackagesTask)
- [isDaemonEnabled](../../devkit/documents/isDaemonEnabled)
- [isWorkspacesEnabled](../../devkit/documents/isWorkspacesEnabled)
- [joinPathFragments](../../devkit/documents/joinPathFragments)
- [moveFilesToNewDirectory](../../devkit/documents/moveFilesToNewDirectory)

View File

@ -0,0 +1,7 @@
# Function: isDaemonEnabled
**isDaemonEnabled**(): `boolean`
#### Returns
`boolean`

View File

@ -135,6 +135,7 @@ It only uses language primitives and immutable objects
- [globAsync](../../devkit/documents/globAsync)
- [hashArray](../../devkit/documents/hashArray)
- [installPackagesTask](../../devkit/documents/installPackagesTask)
- [isDaemonEnabled](../../devkit/documents/isDaemonEnabled)
- [isWorkspacesEnabled](../../devkit/documents/isWorkspacesEnabled)
- [joinPathFragments](../../devkit/documents/joinPathFragments)
- [moveFilesToNewDirectory](../../devkit/documents/moveFilesToNewDirectory)

View File

@ -60,7 +60,12 @@ describe('js:tsc executor', () => {
});
const libBuildProcess = await runCommandUntil(
`build ${lib} --watch`,
(output) => output.includes(`Watching for file changes`)
(output) => output.includes(`Watching for file changes`),
{
env: {
NX_DAEMON: 'true',
},
}
);
updateFile(`libs/${lib}/README.md`, `Hello, World!`);
updateJson(`libs/${lib}/package.json`, (json) => {

View File

@ -11,6 +11,7 @@ import {
uniq,
updateFile,
updateJson,
promisifiedTreeKill,
} from '@nx/e2e/utils';
import { execSync } from 'child_process';
import { join } from 'path';
@ -82,6 +83,11 @@ describe('Node Applications + webpack', () => {
`serve ${app} --watch --runBuildTargetDependencies`,
(output) => {
return output.includes(`Hello`);
},
{
env: {
NX_DAEMON: 'true',
},
}
);
@ -106,6 +112,6 @@ describe('Node Applications + webpack', () => {
{ timeout: 60_000, ms: 200 }
);
serveProcess.kill();
await promisifiedTreeKill(serveProcess.pid, 'SIGKILL');
}, 300_000);
});

View File

@ -207,6 +207,11 @@ module.exports = {
(output) => {
process.stdout.write(output);
return output.includes(`foobar: test foo bar`);
},
{
env: {
NX_DAEMON: 'true',
},
}
);
try {
@ -263,8 +268,15 @@ module.exports = {
// checking serve
updateFile(`apps/${nodeapp}/src/assets/file.txt`, `Test`);
const p = await runCommandUntil(`serve ${nodeapp}`, (output) =>
output.includes(`Listening at http://localhost:${port}`)
const p = await runCommandUntil(
`serve ${nodeapp}`,
(output) => output.includes(`Listening at http://localhost:${port}`),
{
env: {
NX_DAEMON: 'true',
},
}
);
let result = await getData(port);
@ -312,6 +324,11 @@ module.exports = {
(output) => {
process.stdout.write(output);
return output.includes(`listening on ws://localhost:${port}`);
},
{
env: {
NX_DAEMON: 'true',
},
}
);
@ -320,7 +337,7 @@ module.exports = {
expect(e2eRsult.combinedOutput).toContain('Test Suites: 1 passed, 1 total');
await killPorts(port);
p.kill();
await promisifiedTreeKill(p.pid, 'SIGKILL');
}, 120000);
// TODO(crystal, @ndcunningham): how do we handle this now?
@ -361,10 +378,18 @@ module.exports = {
`
);
await runCLIAsync(`build ${esmapp}`);
const p = await runCommandUntil(`serve ${esmapp}`, (output) => {
const p = await runCommandUntil(
`serve ${esmapp}`,
(output) => {
return output.includes('Hello World');
});
p.kill();
},
{
env: {
NX_DAEMON: 'true',
},
}
);
await promisifiedTreeKill(p.pid, 'SIGKILL');
}, 300000);
});

View File

@ -21,7 +21,8 @@
"echo": "echo 123458",
"preinstall": "node ./scripts/preinstall.js",
"test": "nx run-many -t test",
"e2e": "nx run-many -t e2e --projects ./e2e/*"
"e2e": "nx run-many -t e2e --projects ./e2e/*",
"build:wasm": "rustup override set nightly-2024-07-04 && rustup target add wasm32-wasip1-threads && pnpm exec nx run-many -t build-native-wasm && rustup override unset"
},
"devDependencies": {
"@actions/core": "^1.10.0",
@ -52,8 +53,9 @@
"@jest/types": "^29.4.1",
"@module-federation/enhanced": "^0.2.3",
"@module-federation/sdk": "^0.2.3",
"@monodon/rust": "1.3.3",
"@napi-rs/cli": "2.14.0",
"@monodon/rust": "2.0.0-beta.1",
"@napi-rs/cli": "3.0.0-alpha.56",
"@napi-rs/wasm-runtime": "0.2.4",
"@nestjs/cli": "^10.0.2",
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
@ -165,6 +167,7 @@
"dotenv": "~16.4.5",
"dotenv-expand": "~11.0.6",
"ejs": "^3.1.7",
"emnapi": "^1.2.0",
"enhanced-resolve": "^5.8.3",
"esbuild": "0.19.5",
"eslint": "8.57.0",
@ -312,7 +315,7 @@
"@heroicons/react": "^2.1.3",
"@markdoc/markdoc": "0.2.2",
"@monaco-editor/react": "^4.4.6",
"@napi-rs/canvas": "^0.1.19",
"@napi-rs/canvas": "^0.1.52",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "0.5.7",

View File

@ -1,14 +1,14 @@
import * as chalk from 'chalk';
import { ChildProcess, exec, fork } from 'child_process';
import { ChildProcess, fork } from 'child_process';
import {
ExecutorContext,
isDaemonEnabled,
joinPathFragments,
logger,
parseTargetString,
ProjectGraphProjectNode,
readTargetOptions,
runExecutor,
Target,
} from '@nx/devkit';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import { daemonClient } from 'nx/src/daemon/client/client';
@ -258,6 +258,7 @@ export async function* nodeExecutor(
await addToQueue(childProcess, whenReady);
await debouncedProcessQueue();
};
if (isDaemonEnabled()) {
additionalExitHandler = await daemonClient.registerFileWatcher(
{
watchProjects: [context.projectName],
@ -275,6 +276,11 @@ export async function* nodeExecutor(
}
}
);
} else {
logger.warn(
`NX Daemon is not running. Node process will not restart automatically after file changes.`
);
}
await runBuild(); // run first build
} else {
// Otherwise, run the build executor, which will not run task dependencies.

View File

@ -1,4 +1,10 @@
import { ExecutorContext, TaskGraph, parseTargetString } from '@nx/devkit';
import {
ExecutorContext,
isDaemonEnabled,
output,
parseTargetString,
TaskGraph,
} from '@nx/devkit';
import { rmSync } from 'fs';
import type { BatchExecutorTaskResult } from 'nx/src/config/misc-interfaces';
import { getLastValueFromAsyncIterableIterator } from 'nx/src/utils/async-iterator';
@ -6,17 +12,17 @@ import { updatePackageJson } from '../../utils/package-json/update-package-json'
import type { ExecutorOptions } from '../../utils/schema';
import { determineModuleFormatFromTsConfig } from './tsc.impl';
import {
compileTypescriptSolution,
getProcessedTaskTsConfigs,
TypescripCompilationLogger,
TypescriptCompilationResult,
TypescriptInMemoryTsConfig,
TypescriptProjectContext,
compileTypescriptSolution,
getProcessedTaskTsConfigs,
} from './lib';
import {
TaskInfo,
createTaskInfoPerTsConfigMap,
normalizeTasksOptions,
TaskInfo,
watchTaskProjectsFileChangesForAssets,
watchTaskProjectsPackageJsonFileChanges,
} from './lib/batch';
@ -115,8 +121,13 @@ export async function* tscBatchExecutor(
afterProjectCompilationCallback: processTaskPostCompilation,
}
);
if (shouldWatch) {
if (shouldWatch && !isDaemonEnabled()) {
output.warn({
title:
'Nx Daemon is not enabled. Assets and package.json files will not be updated on file changes.',
});
}
if (shouldWatch && isDaemonEnabled()) {
const taskInfos = Object.values(tsConfigTaskInfoMap);
const watchAssetsChangesDisposer =
await watchTaskProjectsFileChangesForAssets(taskInfos);

View File

@ -1,5 +1,5 @@
import * as ts from 'typescript';
import { ExecutorContext } from '@nx/devkit';
import { ExecutorContext, isDaemonEnabled, output } from '@nx/devkit';
import type { TypeScriptCompilationOptions } from '@nx/workspace/src/utilities/typescript/compilation';
import { CopyAssetsHandler } from '../../utils/assets/copy-assets-handler';
import { checkDependencies } from '../../utils/check-dependencies';
@ -133,7 +133,14 @@ export async function* tscExecutor(
}
);
if (options.watch) {
if (!isDaemonEnabled() && options.watch) {
output.warn({
title:
'Nx Daemon is not enabled. Assets and package.json files will not be updated when files change.',
});
}
if (isDaemonEnabled() && options.watch) {
const disposeWatchAssetChanges =
await assetHandler.watchAndProcessOnAssetChange();
const disposePackageJsonChanges = await watchForSingleFileChanges(

View File

@ -1,6 +1,6 @@
import { AssetGlob } from './assets';
import { CopyAssetsHandler, FileEvent } from './copy-assets-handler';
import { ExecutorContext } from '@nx/devkit';
import { ExecutorContext, isDaemonEnabled, output } from '@nx/devkit';
export interface CopyAssetsOptions {
outputPath: string;
@ -35,7 +35,14 @@ export async function copyAssets(
success: true,
};
if (options.watch) {
if (!isDaemonEnabled() && options.watch) {
output.warn({
title:
'Nx Daemon is not enabled. Assets will not be updated when they are changed.',
});
}
if (isDaemonEnabled() && options.watch) {
result.stop = await assetHandler.watchAndProcessOnAssetChange();
}

View File

@ -1,2 +1,3 @@
native-packages/
/*.node
**/*.debug.wasm32-wasi.wasm

View File

@ -3,8 +3,15 @@ name = 'nx'
version = '0.1.0'
edition = '2021'
[profile.release-wasi]
codegen-units = 16
debug = 'full'
inherits = "release"
lto = "thin"
opt-level = "z"
strip = "none"
[dependencies]
portable-pty = { git = "https://github.com/cammisuli/wezterm", rev = "b538ee29e1e89eeb4832fb35ae095564dce34c29" }
anyhow = "1.0.71"
colored = "2"
crossbeam-channel = '0.5'
@ -12,44 +19,46 @@ dashmap = { version = "5.5.3", features = ["rayon"] }
dunce = "1"
fs_extra = "1.3.0"
globset = "0.4.10"
hashbrown = { version = "0.14.3", features = ["rayon", "rkyv"] }
hashbrown = { version = "0.14.5", features = ["rayon", "rkyv"] }
ignore = '0.4'
ignore-files = "2.0.0"
itertools = "0.10.5"
once_cell = "1.18.0"
parking_lot = { version = "0.12.1", features = ["send_guard"] }
napi = { version = '2.12.6', default-features = false, features = [
napi = { version = '2.16.0', default-features = false, features = [
'anyhow',
'napi4',
'tokio_rt',
] }
napi-derive = '2.9.3'
napi-derive = '2.16.0'
nom = '7.1.3'
regex = "1.9.1"
rayon = "1.7.0"
rkyv = { version = "0.7", features = ["validation"] }
thiserror = "1.0.40"
tokio = { version = "1.28.2", features = ["fs"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
walkdir = '2.3.3'
watchexec = "3.0.1"
watchexec-events = "2.0.1"
watchexec-filterer-ignore = "3.0.0"
watchexec-signals = "2.1.0"
xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] }
swc_common = "0.31.16"
swc_ecma_parser = { version = "0.137.1", features = ["typescript"] }
swc_ecma_visit = "0.93.0"
swc_ecma_ast = "0.107.0"
crossterm = "0.27.0"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["fileapi"] }
[target.'cfg(not(windows))'.dependencies]
[target.'cfg(all(not(windows), not(target_family = "wasm")))'.dependencies]
mio = "0.8"
[target.'cfg(not(target_family = "wasm"))'.dependencies]
portable-pty = { git = "https://github.com/cammisuli/wezterm", rev = "b538ee29e1e89eeb4832fb35ae095564dce34c29" }
crossterm = "0.27.0"
ignore-files = "2.1.0"
watchexec = "3.0.1"
watchexec-events = "2.0.1"
watchexec-filterer-ignore = "3.0.0"
watchexec-signals = "2.1.0"
[lib]
crate-type = ['cdylib']

View File

@ -37,6 +37,7 @@
},
"homepage": "https://nx.dev",
"dependencies": {
"@napi-rs/wasm-runtime": "0.2.4",
"@yarnpkg/lockfile": "^1.1.0",
"@yarnpkg/parsers": "3.0.0-rc.46",
"@zkochan/js-yaml": "0.0.7",
@ -169,12 +170,16 @@
"access": "public"
},
"napi": {
"name": "nx",
"package": {
"name": "@nx/nx"
"binaryName": "nx",
"packageName": "@nx/nx",
"wasm": {
"initialMemory": 16384,
"maximumMemory": 32768
},
"triples": {
"additional": [
"targets": [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
@ -185,4 +190,3 @@
]
}
}
}

View File

@ -4,13 +4,39 @@
"sourceRoot": "packages/nx",
"projectType": "library",
"implicitDependencies": ["graph-client"],
"release": {
"version": {
"generator": "@nx/js:release-version"
}
},
"targets": {
"build-native-wasm": {
"cache": true,
"outputs": [
"{projectRoot}/src/native/*.wasm",
"{projectRoot}/src/native/*.js",
"{projectRoot}/src/native/*.cjs",
"{projectRoot}/src/native/*.mjs",
"{projectRoot}/src/native/index.d.ts"
],
"executor": "@monodon/rust:napi",
"options": {
"target": "wasm32-wasip1-threads",
"dist": "packages/nx/src/native",
"jsFile": "native-bindings.js",
"release": true
}
},
"build-native": {
"outputs": ["{projectRoot}/src/native/*.node"],
"outputs": [
"{projectRoot}/src/native/*.node",
"{projectRoot}/src/native/*.js",
"{projectRoot}/src/native/index.d.ts"
],
"executor": "@monodon/rust:napi",
"options": {
"dist": "packages/nx/src/native",
"jsFile": "packages/nx/src/native/native-bindings.js",
"jsFile": "native-bindings.js",
"release": true
},
"configurations": {
@ -24,7 +50,7 @@
},
"artifacts": {
"dependsOn": ["copy-native-package-directories"],
"command": "pnpm napi artifacts -c build/packages/nx/package.json -d ./artifacts --dist build/packages"
"command": "pnpm napi artifacts --package-json-path build/packages/nx/package.json -d ./artifacts --npm-dir build/packages"
},
"build-base": {
"executor": "@nx/js:tsc",
@ -58,7 +84,7 @@
},
{
"input": "packages/nx",
"glob": "**/*.{js,css,html,svg}",
"glob": "**/*.{mjs,cjs,js,css,html,svg,wasm}",
"ignore": ["**/jest.config.js"],
"output": "/"
},

View File

@ -540,14 +540,22 @@ async function startServer(
environmentJs: string,
host: string,
port = 4211,
watchForchanges = true,
watchForChanges = true,
affected: string[] = [],
focus: string = null,
groupByFolder: boolean = false,
exclude: string[] = []
) {
let unregisterFileWatcher: (() => void) | undefined;
if (watchForchanges) {
if (watchForChanges && !daemonClient.enabled()) {
output.warn({
title:
'Nx Daemon is not enabled. Graph will not refresh on file changes.',
});
}
if (watchForChanges && daemonClient.enabled()) {
unregisterFileWatcher = await createFileWatcher();
}

View File

@ -159,6 +159,14 @@ export async function watch(args: WatchArguments) {
process.env.NX_VERBOSE_LOGGING = 'true';
}
if (daemonClient.enabled()) {
output.error({
title:
'Daemon is not running. The watch command is not supported without the Nx Daemon.',
});
process.exit(1);
}
if (
args.includeGlobalWorkspaceFiles &&
args.command.match(projectReplacementRegex)

View File

@ -4,7 +4,7 @@ import { readFileSync, statSync } from 'fs';
import { FileHandle, open } from 'fs/promises';
import { ensureDirSync, ensureFileSync } from 'fs-extra';
import { connect } from 'net';
import { extname, join } from 'path';
import { join } from 'path';
import { performance } from 'perf_hooks';
import { output } from '../../utils/output';
import { getFullOsSocketPath, killSocketOrPath } from '../socket-utils';
@ -16,11 +16,10 @@ import {
} from '../tmp-dir';
import { FileData, ProjectGraph } from '../../config/project-graph';
import { isCI } from '../../utils/is-ci';
import { NxJsonConfiguration } from '../../config/nx-json';
import { hasNxJson, NxJsonConfiguration } from '../../config/nx-json';
import { readNxJson } from '../../config/configuration';
import { PromisedBasedQueue } from '../../utils/promised-based-queue';
import { hasNxJson } from '../../config/nx-json';
import { Message, DaemonSocketMessenger } from './daemon-socket-messenger';
import { DaemonSocketMessenger, Message } from './daemon-socket-messenger';
import { safelyCleanUpExistingProcess } from '../cache';
import { Hash } from '../../hasher/task-hasher';
import { Task, TaskGraph } from '../../config/task-graph';
@ -29,7 +28,7 @@ import {
DaemonProjectGraphError,
ProjectGraphError,
} from '../../project-graph/error-types';
import { loadRootEnvFiles } from '../../utils/dotenv';
import { IS_WASM, NxWorkspaceFiles } from '../../native';
import { HandleGlobMessage } from '../message-types/glob';
import {
GET_NX_WORKSPACE_FILES,
@ -44,7 +43,6 @@ import {
HandleGetFilesInDirectoryMessage,
} from '../message-types/get-files-in-directory';
import { HASH_GLOB, HandleHashGlobMessage } from '../message-types/hash-glob';
import { NxWorkspaceFiles } from '../../native';
import { TaskRun } from '../../utils/task-history';
import {
HandleGetTaskHistoryForHashesMessage,
@ -70,6 +68,7 @@ enum DaemonStatus {
export class DaemonClient {
private readonly nxJson: NxJsonConfiguration | null;
constructor() {
try {
this.nxJson = readNxJson();
@ -113,6 +112,7 @@ export class DaemonClient {
// CI=true,env=undefined => no daemon
// CI=true,env=false => no daemon
// CI=true,env=true => daemon
// WASM => no daemon because file watching does not work
if (
(isCI() && env !== 'true') ||
isDocker() ||
@ -124,6 +124,12 @@ export class DaemonClient {
(useDaemonProcessOption === false && env === 'false')
) {
this._enabled = false;
} else if (IS_WASM) {
output.warn({
title:
'The Nx Daemon is unsupported in WebAssembly environments. Some things may be slower than or not function as expected.',
});
this._enabled = false;
} else {
this._enabled = true;
}
@ -550,6 +556,10 @@ export class DaemonClient {
export const daemonClient = new DaemonClient();
export function isDaemonEnabled() {
return daemonClient.enabled();
}
function isDocker() {
try {
statSync('/.dockerenv');

View File

@ -254,3 +254,5 @@ export { cacheDir } from './utils/cache-directory';
* @category Utils
*/
export { createProjectFileMapUsingProjectGraph } from './project-graph/file-map-utils';
export { isDaemonEnabled } from './daemon/client/client';

View File

@ -1,3 +1,4 @@
#![cfg_attr(target_os = "wasi", feature(wasi_ext))]
// add all the napi macros globally
#[macro_use]
extern crate napi_derive;

View File

@ -0,0 +1 @@
export * from '@nx/nx-wasm32-wasi'

View File

@ -43,6 +43,11 @@ fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<(
std::os::unix::fs::symlink(original, link)
}
#[cfg(target_os = "wasi")]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
std::os::wasi::fs::symlink_path(original, link)
}
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? {

View File

@ -22,10 +22,12 @@ pub fn hash_file(file: String) -> Option<String> {
#[inline]
pub fn hash_file_path<P: AsRef<Path>>(path: P) -> Option<String> {
let path = path.as_ref();
trace!("Reading {:?} to hash", path);
let Ok(content) = std::fs::read(path) else {
trace!("Failed to read file: {:?}", path);
return None;
};
trace!("Hashing {:?}", path);
let hash = hash(&content);
trace!("Hashed file {:?} - {:?}", path, hash);

View File

@ -1,154 +1,32 @@
/* tslint:disable */
/* auto-generated by NAPI-RS */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export class ExternalObject<T> {
export declare class ExternalObject<T> {
readonly '': {
readonly '': unique symbol
[K: symbol]: T
}
}
/**
* Expands the given entries into a list of existing directories and files.
* This is used for copying outputs to and from the cache
*/
export function expandOutputs(directory: string, entries: Array<string>): Array<string>
/**
* Expands the given outputs into a list of existing files.
* This is used when hashing outputs
*/
export function getFilesForOutputs(directory: string, entries: Array<string>): Array<string>
export function remove(src: string): void
export function copy(src: string, dest: string): void
export function hashArray(input: Array<string>): string
export function hashFile(file: string): string | null
export function findImports(projectFileMap: Record<string, Array<string>>): Array<ImportResult>
/**
* Transfer the project graph from the JS world to the Rust world, so that we can pass the project graph via memory quicker
* This wont be needed once the project graph is created in Rust
*/
export function transferProjectGraph(projectGraph: ProjectGraph): ExternalObject<ProjectGraph>
export interface ExternalNode {
packageName?: string
version: string
hash?: string
export declare class ChildProcess {
kill(): void
onExit(callback: (message: string) => void): void
onOutput(callback: (message: string) => void): void
}
export interface Target {
executor?: string
inputs?: Array<JsInputs>
outputs?: Array<string>
options?: string
configurations?: string
export declare class HashPlanner {
constructor(nxJson: NxJson, projectGraph: ExternalObject<ProjectGraph>)
getPlans(taskIds: Array<string>, taskGraph: TaskGraph): Record<string, string[]>
getPlansReference(taskIds: Array<string>, taskGraph: TaskGraph): JsExternal
}
export interface Project {
root: string
namedInputs?: Record<string, Array<JsInputs>>
tags?: Array<string>
targets: Record<string, Target>
}
export interface ProjectGraph {
nodes: Record<string, Project>
dependencies: Record<string, Array<string>>
externalNodes: Record<string, ExternalNode>
}
export interface HashDetails {
value: string
details: Record<string, string>
}
export interface HasherOptions {
selectivelyHashTsConfig: boolean
}
export interface Task {
id: string
target: TaskTarget
outputs: Array<string>
projectRoot?: string
}
export interface TaskTarget {
project: string
target: string
configuration?: string
}
export interface TaskGraph {
roots: Array<string>
tasks: Record<string, Task>
dependencies: Record<string, Array<string>>
}
export interface FileData {
file: string
hash: string
}
export interface InputsInput {
input: string
dependencies?: boolean
projects?: string | Array<string>
}
export interface FileSetInput {
fileset: string
}
export interface RuntimeInput {
runtime: string
}
export interface EnvironmentInput {
env: string
}
export interface ExternalDependenciesInput {
externalDependencies: Array<string>
}
export interface DepsOutputsInput {
dependentTasksOutputFiles: string
transitive?: boolean
}
/** Stripped version of the NxJson interface for use in rust */
export interface NxJson {
namedInputs?: Record<string, Array<JsInputs>>
}
export const enum EventType {
delete = 'delete',
update = 'update',
create = 'create'
}
export interface WatchEvent {
path: string
type: EventType
}
/** Public NAPI error codes that are for Node */
export const enum WorkspaceErrors {
ParseError = 'ParseError',
Generic = 'Generic'
}
export interface NxWorkspaceFiles {
projectFileMap: ProjectFiles
globalFiles: Array<FileData>
externalReferences?: NxWorkspaceFilesExternals
}
export interface NxWorkspaceFilesExternals {
projectFiles: ExternalObject<ProjectFiles>
globalFiles: ExternalObject<Array<FileData>>
allWorkspaceFiles: ExternalObject<Array<FileData>>
}
export interface UpdatedWorkspaceFiles {
fileMap: FileMap
externalReferences: NxWorkspaceFilesExternals
}
export interface FileMap {
projectFileMap: ProjectFiles
nonProjectFiles: Array<FileData>
}
export function testOnlyTransferFileMap(projectFiles: Record<string, Array<FileData>>, nonProjectFiles: Array<FileData>): NxWorkspaceFilesExternals
export class ImportResult {
export declare class ImportResult {
file: string
sourceProject: string
dynamicImportExpressions: Array<string>
staticImportExpressions: Array<string>
}
export class ChildProcess {
kill(): void
onExit(callback: (message: string) => void): void
onOutput(callback: (message: string) => void): void
}
export class RustPseudoTerminal {
export declare class RustPseudoTerminal {
constructor()
runCommand(command: string, commandDir?: string | undefined | null, jsEnv?: Record<string, string> | undefined | null, execArgv?: Array<string> | undefined | null, quiet?: boolean | undefined | null, tty?: boolean | undefined | null): ChildProcess
/**
@ -157,16 +35,13 @@ export class RustPseudoTerminal {
*/
fork(id: string, forkScript: string, pseudoIpcPath: string, commandDir: string | undefined | null, jsEnv: Record<string, string> | undefined | null, execArgv: Array<string> | undefined | null, quiet: boolean): ChildProcess
}
export class HashPlanner {
constructor(nxJson: NxJson, projectGraph: ExternalObject<ProjectGraph>)
getPlans(taskIds: Array<string>, taskGraph: TaskGraph): Record<string, string[]>
getPlansReference(taskIds: Array<string>, taskGraph: TaskGraph): JsExternal
}
export class TaskHasher {
export declare class TaskHasher {
constructor(workspaceRoot: string, projectGraph: ExternalObject<ProjectGraph>, projectFileMap: ExternalObject<ProjectFiles>, allWorkspaceFiles: ExternalObject<Array<FileData>>, tsConfig: Buffer, tsConfigPaths: Record<string, Array<string>>, options?: HasherOptions | undefined | null)
hashPlans(hashPlans: ExternalObject<Record<string, Array<HashInstruction>>>, jsEnv: Record<string, string>): NapiDashMap
}
export class Watcher {
export declare class Watcher {
origin: string
/**
* Creates a new Watcher instance.
@ -179,7 +54,8 @@ export class Watcher {
watch(callback: (err: string | null, events: WatchEvent[]) => void): void
stop(): Promise<void>
}
export class WorkspaceContext {
export declare class WorkspaceContext {
workspaceRoot: string
constructor(workspaceRoot: string, cacheDir: string)
getWorkspaceFiles(projectRootMap: Record<string, string>): NxWorkspaceFiles
@ -190,3 +66,167 @@ export class WorkspaceContext {
allFileData(): Array<FileData>
getFilesInDirectory(directory: string): Array<string>
}
export declare export function copy(src: string, dest: string): void
export interface DepsOutputsInput {
dependentTasksOutputFiles: string
transitive?: boolean
}
export interface EnvironmentInput {
env: string
}
export declare const enum EventType {
delete = 'delete',
update = 'update',
create = 'create'
}
/**
* Expands the given entries into a list of existing directories and files.
* This is used for copying outputs to and from the cache
*/
export declare export function expandOutputs(directory: string, entries: Array<string>): Array<string>
export interface ExternalDependenciesInput {
externalDependencies: Array<string>
}
export interface ExternalNode {
packageName?: string
version: string
hash?: string
}
export interface FileData {
file: string
hash: string
}
export interface FileMap {
projectFileMap: ProjectFiles
nonProjectFiles: Array<FileData>
}
export interface FileSetInput {
fileset: string
}
export declare export function findImports(projectFileMap: Record<string, Array<string>>): Array<ImportResult>
/**
* Expands the given outputs into a list of existing files.
* This is used when hashing outputs
*/
export declare export function getFilesForOutputs(directory: string, entries: Array<string>): Array<string>
export declare export function hashArray(input: Array<string>): string
export interface HashDetails {
value: string
details: Record<string, string>
}
export interface HasherOptions {
selectivelyHashTsConfig: boolean
}
export declare export function hashFile(file: string): string | null
export interface InputsInput {
input: string
dependencies?: boolean
projects?: string | Array<string>
}
export const IS_WASM: boolean
/** Stripped version of the NxJson interface for use in rust */
export interface NxJson {
namedInputs?: Record<string, Array<JsInputs>>
}
export interface NxWorkspaceFiles {
projectFileMap: ProjectFiles
globalFiles: Array<FileData>
externalReferences?: NxWorkspaceFilesExternals
}
export interface NxWorkspaceFilesExternals {
projectFiles: ExternalObject<ProjectFiles>
globalFiles: ExternalObject<Array<FileData>>
allWorkspaceFiles: ExternalObject<Array<FileData>>
}
export interface Project {
root: string
namedInputs?: Record<string, Array<JsInputs>>
tags?: Array<string>
targets: Record<string, Target>
}
export interface ProjectGraph {
nodes: Record<string, Project>
dependencies: Record<string, Array<string>>
externalNodes: Record<string, ExternalNode>
}
export declare export function remove(src: string): void
export interface RuntimeInput {
runtime: string
}
export interface Target {
executor?: string
inputs?: Array<JsInputs>
outputs?: Array<string>
options?: string
configurations?: string
}
export interface Task {
id: string
target: TaskTarget
outputs: Array<string>
projectRoot?: string
}
export interface TaskGraph {
roots: Array<string>
tasks: Record<string, Task>
dependencies: Record<string, Array<string>>
}
export interface TaskTarget {
project: string
target: string
configuration?: string
}
export declare export function testOnlyTransferFileMap(projectFiles: Record<string, Array<FileData>>, nonProjectFiles: Array<FileData>): NxWorkspaceFilesExternals
/**
* Transfer the project graph from the JS world to the Rust world, so that we can pass the project graph via memory quicker
* This wont be needed once the project graph is created in Rust
*/
export declare export function transferProjectGraph(projectGraph: ProjectGraph): ExternalObject<ProjectGraph>
export interface UpdatedWorkspaceFiles {
fileMap: FileMap
externalReferences: NxWorkspaceFilesExternals
}
export interface WatchEvent {
path: string
type: EventType
}
/** Public NAPI error codes that are for Node */
export declare const enum WorkspaceErrors {
ParseError = 'ParseError',
Generic = 'Generic'
}

View File

@ -4,6 +4,22 @@ const Module = require('module');
const { nxVersion } = require('../utils/versions');
const { getNativeFileCacheLocation } = require('./native-file-cache-location');
// WASI is still experimental and throws a warning when used
// We spawn many many processes so the warning gets printed a lot
// We have a different warning elsewhere to warn people using WASI
const originalEmit = process.emit;
process.emit = function (eventName, eventData) {
if (
eventName === `warning` &&
typeof eventData === `object` &&
eventData?.name === `ExperimentalWarning` &&
eventData?.message?.includes(`WASI`)
) {
return false;
}
return originalEmit.apply(process, arguments);
};
const nxPackages = new Set([
'@nx/nx-android-arm64',
'@nx/nx-android-arm-eabi',

View File

@ -4,10 +4,14 @@ pub mod hasher;
mod logger;
pub mod plugins;
pub mod project_graph;
#[cfg(not(target_arch = "wasm32"))]
pub mod pseudo_terminal;
pub mod tasks;
mod types;
mod utils;
mod walker;
#[cfg(not(target_arch = "wasm32"))]
pub mod watch;
pub mod workspace;
pub mod wasm;

View File

@ -1,268 +1,382 @@
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
// prettier-ignore
/* eslint-disable */
/* auto-generated by NAPI-RS */
const { platform, arch } = process
const { readFileSync } = require('fs')
let nativeBinding = null
let localFileExisted = false
let loadError = null
const loadErrors = []
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
const isMusl = () => {
let musl = false
if (process.platform === 'linux') {
musl = isMuslFromFilesystem()
if (musl === null) {
musl = isMuslFromReport()
}
if (musl === null) {
musl = isMuslFromChildProcess()
}
}
return musl
}
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
const isMuslFromFilesystem = () => {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim();
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
} catch {
return null
}
}
const isMuslFromReport = () => {
const report = typeof process.report.getReport === 'function' ? process.report.getReport() : null
if (!report) {
return null
}
if (report.header && report.header.glibcVersionRuntime) {
return false
}
if (Array.isArray(report.sharedObjects)) {
if (report.sharedObjects.some(isFileMusl)) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
return false
}
const isMuslFromChildProcess = () => {
try {
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
} catch (e) {
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
return false
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'nx.android-arm64.node'))
function requireNative() {
if (process.platform === 'android') {
if (process.arch === 'arm64') {
try {
if (localFileExisted) {
nativeBinding = require('./nx.android-arm64.node')
} else {
nativeBinding = require('@nx/nx-android-arm64')
}
return require('./nx.android-arm64.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'nx.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nx.android-arm-eabi.node')
} else {
nativeBinding = require('@nx/nx-android-arm-eabi')
}
return require('@nx/nx-android-arm64')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'nx.win32-x64-msvc.node')
)
} else if (process.arch === 'arm') {
try {
if (localFileExisted) {
nativeBinding = require('./nx.win32-x64-msvc.node')
} else {
nativeBinding = require('@nx/nx-win32-x64-msvc')
}
return require('./nx.android-arm-eabi.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'nx.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nx.win32-ia32-msvc.node')
} else {
nativeBinding = require('@nx/nx-win32-ia32-msvc')
}
return require('@nx/nx-android-arm-eabi')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nx.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nx.win32-arm64-msvc.node')
} else {
nativeBinding = require('@nx/nx-win32-arm64-msvc')
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
}
} else if (process.platform === 'win32') {
if (process.arch === 'x64') {
try {
return require('./nx.win32-x64-msvc.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'nx.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nx.darwin-universal.node')
} else {
nativeBinding = require('@nx/nx-darwin-universal')
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'nx.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nx.darwin-x64.node')
} else {
nativeBinding = require('@nx/nx-darwin-x64')
}
return require('@nx/nx-win32-x64-msvc')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'nx.darwin-arm64.node')
)
} else if (process.arch === 'ia32') {
try {
if (localFileExisted) {
nativeBinding = require('./nx.darwin-arm64.node')
} else {
nativeBinding = require('@nx/nx-darwin-arm64')
}
return require('./nx.win32-ia32-msvc.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(join(__dirname, 'nx.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./nx.freebsd-x64.node')
} else {
nativeBinding = require('@nx/nx-freebsd-x64')
}
return require('@nx/nx-win32-ia32-msvc')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
case 'linux':
switch (arch) {
case 'x64':
} else if (process.arch === 'arm64') {
try {
return require('./nx.win32-arm64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-win32-arm64-msvc')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
}
} else if (process.platform === 'darwin') {
try {
return require('./nx.darwin-universal.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-darwin-universal')
} catch (e) {
loadErrors.push(e)
}
if (process.arch === 'x64') {
try {
return require('./nx.darwin-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-darwin-x64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./nx.darwin-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-darwin-arm64')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
}
} else if (process.platform === 'freebsd') {
if (process.arch === 'x64') {
try {
return require('./nx.freebsd-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-freebsd-x64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./nx.freebsd-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-freebsd-arm64')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
}
} else if (process.platform === 'linux') {
if (process.arch === 'x64') {
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nx.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nx.linux-x64-musl.node')
} else {
nativeBinding = require('@nx/nx-linux-x64-musl')
}
return require('./nx.linux-x64-musl.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nx.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nx.linux-x64-gnu.node')
} else {
nativeBinding = require('@nx/nx-linux-x64-gnu')
}
return require('@nx/nx-linux-x64-musl')
} catch (e) {
loadError = e
loadErrors.push(e)
}
} else {
try {
return require('./nx.linux-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
break
case 'arm64':
try {
return require('@nx/nx-linux-x64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm64') {
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'nx.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nx.linux-arm64-musl.node')
} else {
nativeBinding = require('@nx/nx-linux-arm64-musl')
}
return require('./nx.linux-arm64-musl.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
} else {
localFileExisted = existsSync(
join(__dirname, 'nx.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./nx.linux-arm64-gnu.node')
} else {
nativeBinding = require('@nx/nx-linux-arm64-gnu')
}
return require('@nx/nx-linux-arm64-musl')
} catch (e) {
loadError = e
loadErrors.push(e)
}
}
break
case 'arm':
localFileExisted = existsSync(
join(__dirname, 'nx.linux-arm-gnueabihf.node')
)
} else {
try {
if (localFileExisted) {
nativeBinding = require('./nx.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@nx/nx-linux-arm-gnueabihf')
}
return require('./nx.linux-arm64-gnu.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
try {
return require('@nx/nx-linux-arm64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm') {
if (isMusl()) {
try {
return require('./nx.linux-arm-musleabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-linux-arm-musleabihf')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nx.linux-arm-gnueabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-linux-arm-gnueabihf')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'riscv64') {
if (isMusl()) {
try {
return require('./nx.linux-riscv64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-linux-riscv64-musl')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./nx.linux-riscv64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-linux-riscv64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ppc64') {
try {
return require('./nx.linux-ppc64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-linux-ppc64-gnu')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 's390x') {
try {
return require('./nx.linux-s390x-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@nx/nx-linux-s390x-gnu')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
}
} else {
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
}
}
nativeBinding = requireNative()
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
try {
nativeBinding = require('./nx.wasi.cjs')
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
console.error(err)
}
}
if (!nativeBinding) {
try {
nativeBinding = require('@nx/nx-wasm32-wasi')
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
console.error(err)
}
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError
if (loadErrors.length > 0) {
// TODO Link to documentation with potential fixes
// - The package owner could build/publish bindings for this arch
// - The user may need to bundle the correct files
// - The user may need to re-install node_modules to get new packages
throw new Error('Failed to load native binding', { cause: loadErrors })
}
throw new Error(`Failed to load native binding`)
}
const { expandOutputs, getFilesForOutputs, remove, copy, hashArray, hashFile, ImportResult, findImports, transferProjectGraph, ChildProcess, RustPseudoTerminal, HashPlanner, TaskHasher, EventType, Watcher, WorkspaceContext, WorkspaceErrors, testOnlyTransferFileMap } = nativeBinding
module.exports.expandOutputs = expandOutputs
module.exports.getFilesForOutputs = getFilesForOutputs
module.exports.remove = remove
module.exports.copy = copy
module.exports.hashArray = hashArray
module.exports.hashFile = hashFile
module.exports.ImportResult = ImportResult
module.exports.findImports = findImports
module.exports.transferProjectGraph = transferProjectGraph
module.exports.ChildProcess = ChildProcess
module.exports.RustPseudoTerminal = RustPseudoTerminal
module.exports.HashPlanner = HashPlanner
module.exports.TaskHasher = TaskHasher
module.exports.EventType = EventType
module.exports.Watcher = Watcher
module.exports.WorkspaceContext = WorkspaceContext
module.exports.WorkspaceErrors = WorkspaceErrors
module.exports.testOnlyTransferFileMap = testOnlyTransferFileMap
module.exports.ChildProcess = nativeBinding.ChildProcess
module.exports.HashPlanner = nativeBinding.HashPlanner
module.exports.ImportResult = nativeBinding.ImportResult
module.exports.RustPseudoTerminal = nativeBinding.RustPseudoTerminal
module.exports.TaskHasher = nativeBinding.TaskHasher
module.exports.Watcher = nativeBinding.Watcher
module.exports.WorkspaceContext = nativeBinding.WorkspaceContext
module.exports.copy = nativeBinding.copy
module.exports.EventType = nativeBinding.EventType
module.exports.expandOutputs = nativeBinding.expandOutputs
module.exports.findImports = nativeBinding.findImports
module.exports.getFilesForOutputs = nativeBinding.getFilesForOutputs
module.exports.hashArray = nativeBinding.hashArray
module.exports.hashFile = nativeBinding.hashFile
module.exports.IS_WASM = nativeBinding.IS_WASM
module.exports.remove = nativeBinding.remove
module.exports.testOnlyTransferFileMap = nativeBinding.testOnlyTransferFileMap
module.exports.transferProjectGraph = nativeBinding.transferProjectGraph
module.exports.WorkspaceErrors = nativeBinding.WorkspaceErrors

View File

@ -0,0 +1,108 @@
import {
instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync,
getDefaultContext as __emnapiGetDefaultContext,
WASI as __WASI,
createOnMessage as __wasmCreateOnMessageForFsProxy,
} from '@napi-rs/wasm-runtime'
import __wasmUrl from './nx.wasm32-wasi.wasm?url'
const __wasi = new __WASI({
version: 'preview1',
})
const __emnapiContext = __emnapiGetDefaultContext()
const __sharedMemory = new WebAssembly.Memory({
initial: 16384,
maximum: 32768,
shared: true,
})
const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())
const {
instance: __napiInstance,
module: __wasiModule,
napiModule: __napiModule,
} = __emnapiInstantiateNapiModuleSync(__wasmFile, {
context: __emnapiContext,
asyncWorkPoolSize: 4,
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
type: 'module',
})
return worker
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
__napi_rs_initialize_modules(instance)
},
})
function __napi_rs_initialize_modules(__napiInstance) {
__napiInstance.exports['__napi_register__expand_outputs_0']?.()
__napiInstance.exports['__napi_register__get_files_for_outputs_1']?.()
__napiInstance.exports['__napi_register__remove_2']?.()
__napiInstance.exports['__napi_register__copy_3']?.()
__napiInstance.exports['__napi_register__hash_array_4']?.()
__napiInstance.exports['__napi_register__hash_file_5']?.()
__napiInstance.exports['__napi_register__ImportResult_struct_6']?.()
__napiInstance.exports['__napi_register__find_imports_7']?.()
__napiInstance.exports['__napi_register__transfer_project_graph_8']?.()
__napiInstance.exports['__napi_register__ExternalNode_struct_9']?.()
__napiInstance.exports['__napi_register__Target_struct_10']?.()
__napiInstance.exports['__napi_register__Project_struct_11']?.()
__napiInstance.exports['__napi_register__ProjectGraph_struct_12']?.()
__napiInstance.exports['__napi_register__HashPlanner_struct_13']?.()
__napiInstance.exports['__napi_register__HashPlanner_impl_17']?.()
__napiInstance.exports['__napi_register__HashDetails_struct_18']?.()
__napiInstance.exports['__napi_register__HasherOptions_struct_19']?.()
__napiInstance.exports['__napi_register__TaskHasher_struct_20']?.()
__napiInstance.exports['__napi_register__TaskHasher_impl_23']?.()
__napiInstance.exports['__napi_register__Task_struct_24']?.()
__napiInstance.exports['__napi_register__TaskTarget_struct_25']?.()
__napiInstance.exports['__napi_register__TaskGraph_struct_26']?.()
__napiInstance.exports['__napi_register__FileData_struct_27']?.()
__napiInstance.exports['__napi_register__InputsInput_struct_28']?.()
__napiInstance.exports['__napi_register__FileSetInput_struct_29']?.()
__napiInstance.exports['__napi_register__RuntimeInput_struct_30']?.()
__napiInstance.exports['__napi_register__EnvironmentInput_struct_31']?.()
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_32']?.()
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_33']?.()
__napiInstance.exports['__napi_register__NxJson_struct_34']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_struct_35']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_impl_44']?.()
__napiInstance.exports['__napi_register__WorkspaceErrors_45']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_46']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_47']?.()
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_48']?.()
__napiInstance.exports['__napi_register__FileMap_struct_49']?.()
__napiInstance.exports['__napi_register____test_only_transfer_file_map_50']?.()
__napiInstance.exports['__napi_register__IS_WASM_51']?.()
}
export const HashPlanner = __napiModule.exports.HashPlanner
export const ImportResult = __napiModule.exports.ImportResult
export const TaskHasher = __napiModule.exports.TaskHasher
export const WorkspaceContext = __napiModule.exports.WorkspaceContext
export const copy = __napiModule.exports.copy
export const expandOutputs = __napiModule.exports.expandOutputs
export const findImports = __napiModule.exports.findImports
export const getFilesForOutputs = __napiModule.exports.getFilesForOutputs
export const hashArray = __napiModule.exports.hashArray
export const hashFile = __napiModule.exports.hashFile
export const IS_WASM = __napiModule.exports.IS_WASM
export const remove = __napiModule.exports.remove
export const testOnlyTransferFileMap = __napiModule.exports.testOnlyTransferFileMap
export const transferProjectGraph = __napiModule.exports.transferProjectGraph
export const WorkspaceErrors = __napiModule.exports.WorkspaceErrors

View File

@ -0,0 +1,139 @@
/* eslint-disable */
/* prettier-ignore */
/* auto-generated by NAPI-RS */
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
const { Worker } = require('node:worker_threads')
const {
instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync,
getDefaultContext: __emnapiGetDefaultContext,
createOnMessage: __wasmCreateOnMessageForFsProxy,
} = require('@napi-rs/wasm-runtime')
const __rootDir = __nodePath.parse(process.cwd()).root
const __wasi = new __nodeWASI({
version: 'preview1',
env: process.env,
preopens: {
[__rootDir]: __rootDir,
}
})
const __emnapiContext = __emnapiGetDefaultContext()
const __sharedMemory = new WebAssembly.Memory({
initial: 16384,
maximum: 32768,
shared: true,
})
let __wasmFilePath = __nodePath.join(__dirname, 'nx.wasm32-wasi.wasm')
const __wasmDebugFilePath = __nodePath.join(__dirname, 'nx.wasm32-wasi.debug.wasm')
if (__nodeFs.existsSync(__wasmDebugFilePath)) {
__wasmFilePath = __wasmDebugFilePath
} else if (!__nodeFs.existsSync(__wasmFilePath)) {
try {
__wasmFilePath = __nodePath.resolve('@nx/nx-wasm32-wasi')
} catch {
throw new Error('Cannot find nx.wasm32-wasi.wasm file, and @nx/nx-wasm32-wasi package is not installed.')
}
}
const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule } = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__wasmFilePath), {
context: __emnapiContext,
asyncWorkPoolSize: (function() {
const threadsSizeFromEnv = Number(process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE)
// NaN > 0 is false
if (threadsSizeFromEnv > 0) {
return threadsSizeFromEnv
} else {
return 4
}
})(),
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(__nodePath.join(__dirname, 'wasi-worker.mjs'), {
env: process.env,
execArgv: ['--experimental-wasi-unstable-preview1'],
})
worker.onmessage = ({ data }) => {
__wasmCreateOnMessageForFsProxy(__nodeFs)(data)
}
return worker
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
__napi_rs_initialize_modules(instance)
}
})
function __napi_rs_initialize_modules(__napiInstance) {
__napiInstance.exports['__napi_register__expand_outputs_0']?.()
__napiInstance.exports['__napi_register__get_files_for_outputs_1']?.()
__napiInstance.exports['__napi_register__remove_2']?.()
__napiInstance.exports['__napi_register__copy_3']?.()
__napiInstance.exports['__napi_register__hash_array_4']?.()
__napiInstance.exports['__napi_register__hash_file_5']?.()
__napiInstance.exports['__napi_register__ImportResult_struct_6']?.()
__napiInstance.exports['__napi_register__find_imports_7']?.()
__napiInstance.exports['__napi_register__transfer_project_graph_8']?.()
__napiInstance.exports['__napi_register__ExternalNode_struct_9']?.()
__napiInstance.exports['__napi_register__Target_struct_10']?.()
__napiInstance.exports['__napi_register__Project_struct_11']?.()
__napiInstance.exports['__napi_register__ProjectGraph_struct_12']?.()
__napiInstance.exports['__napi_register__HashPlanner_struct_13']?.()
__napiInstance.exports['__napi_register__HashPlanner_impl_17']?.()
__napiInstance.exports['__napi_register__HashDetails_struct_18']?.()
__napiInstance.exports['__napi_register__HasherOptions_struct_19']?.()
__napiInstance.exports['__napi_register__TaskHasher_struct_20']?.()
__napiInstance.exports['__napi_register__TaskHasher_impl_23']?.()
__napiInstance.exports['__napi_register__Task_struct_24']?.()
__napiInstance.exports['__napi_register__TaskTarget_struct_25']?.()
__napiInstance.exports['__napi_register__TaskGraph_struct_26']?.()
__napiInstance.exports['__napi_register__FileData_struct_27']?.()
__napiInstance.exports['__napi_register__InputsInput_struct_28']?.()
__napiInstance.exports['__napi_register__FileSetInput_struct_29']?.()
__napiInstance.exports['__napi_register__RuntimeInput_struct_30']?.()
__napiInstance.exports['__napi_register__EnvironmentInput_struct_31']?.()
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_32']?.()
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_33']?.()
__napiInstance.exports['__napi_register__NxJson_struct_34']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_struct_35']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_impl_44']?.()
__napiInstance.exports['__napi_register__WorkspaceErrors_45']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_46']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_47']?.()
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_48']?.()
__napiInstance.exports['__napi_register__FileMap_struct_49']?.()
__napiInstance.exports['__napi_register____test_only_transfer_file_map_50']?.()
__napiInstance.exports['__napi_register__IS_WASM_51']?.()
}
module.exports.HashPlanner = __napiModule.exports.HashPlanner
module.exports.ImportResult = __napiModule.exports.ImportResult
module.exports.TaskHasher = __napiModule.exports.TaskHasher
module.exports.WorkspaceContext = __napiModule.exports.WorkspaceContext
module.exports.copy = __napiModule.exports.copy
module.exports.expandOutputs = __napiModule.exports.expandOutputs
module.exports.findImports = __napiModule.exports.findImports
module.exports.getFilesForOutputs = __napiModule.exports.getFilesForOutputs
module.exports.hashArray = __napiModule.exports.hashArray
module.exports.hashFile = __napiModule.exports.hashFile
module.exports.IS_WASM = __napiModule.exports.IS_WASM
module.exports.remove = __napiModule.exports.remove
module.exports.testOnlyTransferFileMap = __napiModule.exports.testOnlyTransferFileMap
module.exports.transferProjectGraph = __napiModule.exports.transferProjectGraph
module.exports.WorkspaceErrors = __napiModule.exports.WorkspaceErrors

View File

@ -0,0 +1,34 @@
use parking_lot::{Condvar, Mutex, MutexGuard};
pub struct NxMutex<T>(Mutex<T>);
impl <T> NxMutex<T> {
pub fn new(value: T) -> Self {
Self(Mutex::new(value))
}
pub fn lock(&self) -> anyhow::Result<MutexGuard<'_, T>> {
Ok(self.0.lock())
}
}
pub struct NxCondvar(Condvar);
impl NxCondvar {
pub fn new() -> Self {
Self(Condvar::new())
}
pub fn wait<'a, T, F>(&'a self, mut guard: MutexGuard<'a, T>, condition: F) -> anyhow::Result<MutexGuard<'a, T>>
where
F: Fn(&MutexGuard<'a, T>) -> bool
{
if condition(&guard) {
self.0.wait(&mut guard);
}
Ok(guard)
}
pub fn notify_all(&self) {
self.0.notify_all();
}
}

View File

@ -0,0 +1,27 @@
use std::sync::{Condvar, LockResult, Mutex, MutexGuard};
pub struct NxMutex<T>(Mutex<T>);
impl <T> NxMutex<T> {
pub fn new(value: T) -> Self {
Self(Mutex::new(value))
}
pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
self.0.lock()
}
}
pub struct NxCondvar(Condvar);
impl NxCondvar {
pub fn new() -> Self {
Self(Condvar::new())
}
pub fn wait<'a, T, F>(&self, mutex_guard: MutexGuard<'a, T>, condition: F) -> LockResult<MutexGuard<'a, T>>
where
F: Fn(&mut T) -> bool
{
self.0.wait_while(mutex_guard, condition)
}
}

View File

@ -17,3 +17,9 @@ pub fn get_mod_time(metadata: &Metadata) -> i64 {
use std::os::unix::fs::MetadataExt;
metadata.mtime()
}
#[cfg(target_os = "wasi")]
pub fn get_mod_time(metadata: &Metadata) -> i64 {
use std::os::wasi::fs::MetadataExt;
metadata.mtim() as i64
}

View File

@ -6,3 +6,10 @@ pub mod path;
pub use find_matching_projects::*;
pub use get_mod_time::*;
pub use normalize_trait::Normalize;
#[cfg_attr(not(target_arch = "wasm32"), path = "atomics/default.rs")]
#[cfg_attr(target_arch = "wasm32", path = "atomics/wasm.rs")]
pub mod atomics;
pub use atomics::*;

View File

@ -1,10 +1,6 @@
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::thread;
use std::thread::available_parallelism;
use crossbeam_channel::unbounded;
use ignore::WalkBuilder;
use tracing::trace;
use ignore::{WalkBuilder};
use crate::native::glob::build_glob_set;
@ -60,30 +56,55 @@ where
}
/// Walk the directory and ignore files from .gitignore and .nxignore
#[cfg(target_arch = "wasm32")]
pub fn nx_walker<P>(directory: P) -> impl Iterator<Item = NxFile>
where
P: AsRef<Path>,
{
let directory: PathBuf = directory.as_ref().into();
let walker = create_walker(&directory);
let entries = walker.build();
entries.filter_map(move |entry| {
let Ok(dir_entry) = entry else {
return None;
};
if dir_entry.file_type().is_some_and(|d| d.is_dir()) {
return None;
}
let Ok(file_path) = dir_entry.path().strip_prefix(&directory) else {
return None;
};
let Ok(metadata) = dir_entry.metadata() else {
return None;
};
Some(NxFile {
full_path: String::from(dir_entry.path().to_string_lossy()),
normalized_path: file_path.to_normalized_string(),
mod_time: get_mod_time(&metadata),
})
})
}
/// Walk the directory and ignore files from .gitignore and .nxignore
#[cfg(not(target_arch = "wasm32"))]
pub fn nx_walker<P>(directory: P) -> impl Iterator<Item = NxFile>
where
P: AsRef<Path>,
{
use std::thread;
use std::thread::available_parallelism;
use crossbeam_channel::unbounded;
use tracing::trace;
let directory = directory.as_ref();
let ignore_glob_set = build_glob_set(&[
"**/node_modules",
"**/.git",
"**/.nx/cache",
"**/.nx/workspace-data",
"**/.yarn/cache",
])
.expect("These static ignores always build");
let mut walker = WalkBuilder::new(directory);
walker.hidden(false);
walker.add_custom_ignore_filename(".nxignore");
// We should make sure to always ignore node_modules and the .git folder
walker.filter_entry(move |entry| {
let path = entry.path().to_string_lossy();
!ignore_glob_set.is_match(path.as_ref())
});
let mut walker = create_walker(directory);
let cpus = available_parallelism().map_or(2, |n| n.get()) - 1;
@ -130,6 +151,34 @@ where
receiver_thread.join().unwrap()
}
fn create_walker<P>(directory: P) -> WalkBuilder
where
P: AsRef<Path>
{
let directory: PathBuf = directory.as_ref().into();
let ignore_glob_set = build_glob_set(&[
"**/node_modules",
"**/.git",
"**/.nx/cache",
"**/.nx/workspace-data",
"**/.yarn/cache",
])
.expect("These static ignores always build");
let mut walker = WalkBuilder::new(&directory);
walker.require_git(false);
walker.hidden(false);
walker.add_custom_ignore_filename(".nxignore");
// We should make sure to always ignore node_modules and the .git folder
walker.filter_entry(move |entry| {
let path = entry.path().to_string_lossy();
!ignore_glob_set.is_match(path.as_ref())
});
walker
}
#[cfg(test)]
mod test {
use std::{assert_eq, vec};

View File

@ -0,0 +1,32 @@
import { instantiateNapiModuleSync, MessageHandler, WASI } from '@napi-rs/wasm-runtime'
const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
const wasi = new WASI({
print: function () {
// eslint-disable-next-line no-console
console.log.apply(console, arguments)
},
printErr: function() {
// eslint-disable-next-line no-console
console.error.apply(console, arguments)
},
})
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: wasmMemory,
}
},
})
},
})
globalThis.onmessage = function (e) {
handler.handle(e)
}

View File

@ -0,0 +1,63 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import { parse } from "node:path";
import { WASI } from "node:wasi";
import { parentPort, Worker } from "node:worker_threads";
const require = createRequire(import.meta.url);
const { instantiateNapiModuleSync, MessageHandler, getDefaultContext } = require("@napi-rs/wasm-runtime");
if (parentPort) {
parentPort.on("message", (data) => {
globalThis.onmessage({ data });
});
}
Object.assign(globalThis, {
self: globalThis,
require,
Worker,
importScripts: function (f) {
;(0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f);
},
postMessage: function (msg) {
if (parentPort) {
parentPort.postMessage(msg);
}
},
});
const emnapiContext = getDefaultContext();
const __rootDir = parse(process.cwd()).root;
const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
const wasi = new WASI({
version: 'preview1',
env: process.env,
preopens: {
[__rootDir]: __rootDir,
},
});
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
context: emnapiContext,
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: wasmMemory
};
},
});
},
});
globalThis.onmessage = function (e) {
handler.handle(e);
};

View File

@ -0,0 +1,7 @@
#[napi]
#[cfg(target_arch = "wasm32")]
pub const IS_WASM: bool = true;
#[napi]
#[cfg(not(target_arch = "wasm32"))]
pub const IS_WASM: bool = false;

View File

@ -1,6 +1,5 @@
use std::path::{Path, PathBuf};
use napi::bindgen_prelude::*;
use tracing::trace;
use watchexec_events::filekind::CreateKind;
use watchexec_events::filekind::FileEventKind;

View File

@ -2,7 +2,7 @@ use napi::bindgen_prelude::External;
use std::collections::HashMap;
use crate::native::hasher::hash;
use crate::native::utils::{path::get_child_files, Normalize};
use crate::native::utils::{path::get_child_files, Normalize, NxMutex, NxCondvar};
use rayon::prelude::*;
use std::ops::Deref;
use std::path::{Path, PathBuf};
@ -12,7 +12,6 @@ use std::thread;
use crate::native::logger::enable_logger;
use crate::native::project_graph::utils::{find_project_for_path, ProjectRootMappings};
use crate::native::types::FileData;
use parking_lot::{Condvar, Mutex};
use tracing::{trace, warn};
use crate::native::workspace::files_archive::{read_files_archive, write_files_archive};
@ -31,27 +30,10 @@ pub struct WorkspaceContext {
type Files = Vec<(PathBuf, String)>;
struct FilesWorker(Option<Arc<(Mutex<Files>, Condvar)>>);
impl FilesWorker {
fn gather_files(workspace_root: &Path, cache_dir: String) -> Self {
if !workspace_root.exists() {
warn!(
"workspace root does not exist: {}",
workspace_root.display()
);
return FilesWorker(None);
}
fn gather_and_hash_files(workspace_root: &Path, cache_dir: String) -> Vec<(PathBuf, String)>{
let archived_files = read_files_archive(&cache_dir);
let files_lock = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
let files_lock_clone = Arc::clone(&files_lock);
let workspace_root = workspace_root.to_owned();
thread::spawn(move || {
trace!("locking files");
let (lock, cvar) = &*files_lock_clone;
let mut workspace_files = lock.lock();
trace!("Gathering files in {}", workspace_root.display());
let now = std::time::Instant::now();
let file_hashes = if let Some(archived_files) = archived_files {
selective_files_hash(&workspace_root, archived_files)
@ -66,28 +48,78 @@ impl FilesWorker {
files.par_sort();
trace!("hashed and sorted files in {:?}", now.elapsed());
write_files_archive(&cache_dir, file_hashes);
files
}
struct FilesWorker(Option<Arc<(NxMutex<Files>, NxCondvar)>>);
impl FilesWorker {
#[cfg(not(target_arch = "wasm32"))]
fn gather_files(workspace_root: &Path, cache_dir: String) -> Self {
if !workspace_root.exists() {
warn!(
"workspace root does not exist: {}",
workspace_root.display()
);
return FilesWorker(None);
}
let files_lock = Arc::new((NxMutex::new(Vec::new()), NxCondvar::new()));
let files_lock_clone = Arc::clone(&files_lock);
let workspace_root = workspace_root.to_owned();
thread::spawn(move || {
let (lock, cvar) = &*files_lock_clone;
trace!("Initially locking files");
let mut workspace_files = lock.lock().expect("Should be the first time locking files");
let files = gather_and_hash_files(&workspace_root, cache_dir);
*workspace_files = files;
let files_len = workspace_files.len();
trace!(?files_len, "files retrieved");
drop(workspace_files);
cvar.notify_all();
write_files_archive(&cache_dir, file_hashes);
});
FilesWorker(Some(files_lock))
}
pub fn get_files(&self) -> Vec<FileData> {
#[cfg(target_arch = "wasm32")]
fn gather_files(workspace_root: &Path, cache_dir: String) -> Self {
if !workspace_root.exists() {
warn!(
"workspace root does not exist: {}",
workspace_root.display()
);
return FilesWorker(None);
}
let workspace_root = workspace_root.to_owned();
let files = gather_and_hash_files(&workspace_root, cache_dir);
trace!("{} files retrieved", files.len());
let files_lock = Arc::new((NxMutex::new(files), NxCondvar::new()));
FilesWorker(Some(files_lock))
}
fn get_files(&self) -> Vec<FileData> {
if let Some(files_sync) = &self.0 {
let (files_lock, cvar) = files_sync.deref();
trace!("locking files");
let mut files = files_lock.lock();
let files_len = files.len();
if files_len == 0 {
trace!("waiting for files");
cvar.wait(&mut files);
}
trace!("waiting for files to be available");
let files = files_lock.lock().expect("Should be able to lock files");
#[cfg(target_arch = "wasm32")]
let mut files = cvar.wait(files, |guard| guard.len() == 0).expect("Should be able to wait for files");
#[cfg(not(target_arch = "wasm32"))]
let files = cvar.wait(files, |guard| guard.len() == 0).expect("Should be able to wait for files");
let file_data = files
.iter()
@ -118,7 +150,7 @@ impl FilesWorker {
};
let (files_lock, _) = &files_sync.deref();
let mut files = files_lock.lock();
let mut files = files_lock.lock().expect("Should always be able to update files");
let mut map: HashMap<PathBuf, String> = files.drain(..).collect();
for deleted_path in deleted_files_and_directories {

View File

@ -11,6 +11,7 @@ use crate::native::workspace::files_archive::{NxFileHashed, NxFileHashes};
pub fn full_files_hash(workspace_root: &Path) -> NxFileHashes {
let files = nx_walker(workspace_root).collect::<Vec<_>>();
trace!("Found {} files", files.len());
hash_files(files).into_iter().collect()
}
@ -50,15 +51,17 @@ fn hash_files(files: Vec<NxFile>) -> Vec<(String, NxFileHashed)> {
let chunks = files.len() / num_parallelism;
let now = std::time::Instant::now();
let files = if chunks < num_parallelism {
let files = if cfg!(target_arch = "wasm32") || chunks < num_parallelism {
trace!("hashing workspace files in parallel");
files
.into_iter()
.into_par_iter()
.filter_map(|file| {
hash_file_path(&file.full_path)
.map(|hash| (file.normalized_path, NxFileHashed(hash, file.mod_time)))
})
.collect::<Vec<_>>()
} else {
trace!("hashing workspace files in {} chunks of {}", num_parallelism, chunks);
files
.par_chunks(chunks)
.flat_map_iter(|chunks| {

View File

@ -1,4 +1,4 @@
import { ChildProcess, RustPseudoTerminal } from '../native';
import { ChildProcess, RustPseudoTerminal, IS_WASM } from '../native';
import { PseudoIPCServer } from './pseudo-ipc';
import { getForkedProcessOsSocketPath } from '../daemon/socket-utils';
import { Serializable } from 'child_process';
@ -206,6 +206,9 @@ function messageToCode(message: string): number {
}
function supportedPtyPlatform() {
if (IS_WASM) {
return false;
}
if (process.platform !== 'win32') {
return true;
}

View File

@ -57,14 +57,24 @@ export function nxViteBuildCoordinationPlugin(
);
}
let firstBuildStart = true;
return {
name: 'nx-vite-build-coordination-plugin',
async buildStart() {
if (!unregisterFileWatcher) {
if (firstBuildStart) {
firstBuildStart = false;
await buildChangedProjects();
if (daemonClient.enabled()) {
unregisterFileWatcher = await createFileWatcher();
process.on('exit', () => unregisterFileWatcher());
process.on('SIGINT', () => process.exit());
} else {
output.warn({
title:
'Nx Daemon is not enabled. Projects will not be rebuilt when files change.',
});
}
}
},
};

View File

@ -2,6 +2,7 @@ import { execFileSync, fork } from 'child_process';
import * as chalk from 'chalk';
import {
ExecutorContext,
output,
parseTargetString,
readTargetOptions,
} from '@nx/devkit';
@ -184,9 +185,13 @@ export default async function* fileServerExecutor(
}
};
if (options.watch) {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
if (!daemonClient.enabled() && options.watch) {
output.warn({
title:
'Nx Daemon is not enabled. Static server is not watching for changes.',
});
}
if (daemonClient.enabled() && options.watch) {
disposeWatch = await createFileWatcher(context.projectName, run);
}

View File

@ -1,6 +1,6 @@
import { exec } from 'child_process';
import type { Compiler } from 'webpack';
import { daemonClient } from 'nx/src/daemon/client/client';
import { daemonClient, isDaemonEnabled } from 'nx/src/daemon/client/client';
import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch';
import { output } from 'nx/src/utils/output';
@ -12,7 +12,14 @@ export class WebpackNxBuildCoordinationPlugin {
if (!skipInitialBuild) {
this.buildChangedProjects();
}
if (isDaemonEnabled()) {
this.startWatchingBuildableLibs();
} else {
output.warn({
title:
'Nx Daemon is not enabled. Buildable libs will not be rebuilt on file changes.',
});
}
}
apply(compiler: Compiler) {

2632
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,9 @@ const glob = require('fast-glob');
const p = process.argv[2];
const nativeFiles = glob.sync(`packages/${p}/**/*.node`);
const nativeFiles = glob.sync(`packages/${p}/**/*.{node,wasm,js,mjs,cjs}`);
console.log({ nativeFiles });
nativeFiles.forEach((file) => {
fs.copyFileSync(file, `build/${file}`);