Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a295d8f
feat: refactor getSeaTargetConfiguration to improve command handling …
getlarge Mar 9, 2025
b7f7336
feat: update CI workflow to support Node.js 22 and add Windows to the…
getlarge Mar 9, 2025
f4b65ef
fix: update package name references to @getlarge/nx-node-sea in tests…
getlarge Mar 9, 2025
2016c2f
fix: update CI cache key and correct command syntax in plugin
getlarge Mar 9, 2025
8d9a01b
fix: enable shell option in child process for improved command execution
getlarge Mar 9, 2025
c439bf0
feat: add mock build output generation in test project setup
getlarge Mar 9, 2025
298e96f
fix: increase timeout values and add verbose flag for SEA build tests
getlarge Mar 9, 2025
2715bce
fix: update Windows command handling in getSeaCommands function
getlarge Mar 9, 2025
090965c
fix: conditionally include sign commands in getSeaCommands function
getlarge Mar 9, 2025
85b5948
fix: update path handling for Windows in getSeaCommands function
getlarge Mar 10, 2025
db5a61d
fix: increase timeout values for SEA build tests
getlarge Mar 10, 2025
1a8c70d
fix: refactor build output directory handling and update command path…
getlarge Mar 10, 2025
ec96ec5
fix: update SEA command handling to use 'main.exe' instead of 'node.exe'
getlarge Mar 10, 2025
32de7b8
fix: remove unnecessary cache key from CI workflow
getlarge Mar 10, 2025
77fc69b
fix: update Windows postject params
getlarge Mar 10, 2025
8096a62
fix: update file existence check to use stringContaining matcher
getlarge Mar 10, 2025
b7898d5
fix: update file existence check to handle platform-specific executab…
getlarge Mar 10, 2025
30d670a
docs: update README for @getlarge/nx-node-sea plugin with installatio…
getlarge Mar 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,11 @@ jobs:
with:
fetch-depth: 0

# This enables task distribution via Nx Cloud
# Run this command as early as possible, before dependencies are installed
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
# Uncomment this line to enable task distribution
# - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build"

- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: 'npm'

- run: npm ci --legacy-peer-deps
Expand All @@ -39,7 +35,8 @@ jobs:
needs: main
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
node: [20, 22]
runs-on: ${{ matrix.os }}

steps:
Expand All @@ -49,7 +46,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 20
node-version: ${{ matrix.node }}
cache: 'npm'

- run: npm ci --legacy-peer-deps
Expand Down
130 changes: 85 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,116 @@
# NxNodeSea
# @getlarge/nx-node-sea

<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
A plugin for [Nx](https://nx.dev) that provides integration with [Node.js Single Executable Applications (SEA)](https://nodejs.org/api/single-executable-applications.html).

✨ Your new, shiny [Nx workspace](https://nx.dev) is almost ready ✨.
## Overview

Run `npx nx graph` to visually explore what got created. Now, let's get you up to speed!
This plugin helps you create Node.js Single Executable Applications (SEA) within your Nx workspace. It automates the process of generating SEA preparation blobs and creating standalone executables that bundle your Node.js application.

## Finish your CI setup
## Requirements

[Click here to finish setting up your workspace!](https://cloud.nx.app/connect/oTjir14WOJ)
- Node.js 20 or higher (SEA feature requirement)
- Nx 20.0.6 or higher

## Run tasks
## Installation

To run tasks with Nx use:

```sh
npx nx <target> <project-name>
```bash
npm install --save-dev @getlarge/nx-node-sea
```

For example:
## Usage

```sh
npx nx build myproject
```
### 1. Create a sea-config.json file

These targets are either [inferred automatically](https://nx.dev/concepts/inferred-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or defined in the `project.json` or `package.json` files.
Create a `sea-config.json` file in your project's root directory:

[More about running tasks in the docs &raquo;](https://nx.dev/features/run-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
```json
{
"main": "dist/your-app/main.js",
"output": "dist/your-app/main.blob",
"disableExperimentalSEAWarning": false,
"useSnapshot": false,
"useCodeCache": false
}
```

## Add new projects
### 2. Configure the plugin in nx.json

Add the plugin configuration to your `nx.json` file:

```json
{
"plugins": [
{
"plugin": "@getlarge/nx-node-sea",
"options": {
"seaTargetName": "sea-build",
"buildTarget": "build"
}
}
]
}
```

While you could add new projects to your workspace manually, you might want to leverage [Nx plugins](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) and their [code generation](https://nx.dev/features/generate-code?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) feature.
> **Note:** The `buildTarget` option specifies the target that will be used to build your application before creating the SEA. The default value is `"build"`.

To install a new plugin you can use the `nx add` command. Here's an example of adding the React plugin:
### 3. Build your SEA

```sh
npx nx add @nx/react
```bash
nx run your-app:sea-build
```

Use the plugin's generator to create new projects. For example, to create a new React app or library:
This will:

```sh
# Genenerate an app
npx nx g @nx/react:app demo
1. Build your application using the specified build target
2. Generate a SEA preparation blob
3. Create a standalone executable

# Generate a library
npx nx g @nx/react:lib some-lib
```
## Configuration Options

You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx list <plugin-name>` to learn about more specific capabilities of a particular plugin. Alternatively, [install Nx Console](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) to browse plugins and generators in your IDE.
### Plugin Options

[Learn more about Nx plugins &raquo;](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) | [Browse the plugin registry &raquo;](https://nx.dev/plugin-registry?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
| Option | Description | Default |
| --------------- | ------------------------------------------------------------ | ------------- |
| `buildTarget` | The target to build your application before creating the SEA | `"build"` |
| `seaTargetName` | The name of the target that will be created to build the SEA | `"sea-build"` |

[Learn more about Nx on CI](https://nx.dev/ci/intro/ci-with-nx#ready-get-started-with-your-provider?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
### SEA Config Options

## Install Nx Console
| Option | Description | Required |
| ------------------------------- | ---------------------------------------------------- | -------- |
| `main` | Path to the main JavaScript file of your application | Yes |
| `output` | Path where the SEA blob will be generated | Yes |
| `disableExperimentalSEAWarning` | Disable warnings about experimental feature | No |
| `useSnapshot` | Use V8 snapshot for faster startup | No |
| `useCodeCache` | Use code cache for faster startup | No |
| `assets` | Record of assets to include in the blob | No |

Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
## Platform Support

[Install Nx Console &raquo;](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
The plugin automatically handles platform-specific differences for:

## Useful links
- Linux
- macOS (includes code signing)
- Windows

Learn more:
## Learn More

- [Node.js Single Executable Applications](https://nodejs.org/api/single-executable-applications.html)
- [Nx Build System](https://nx.dev/features/build)
- [Postject](https://github.com/nodejs/postject) - Used for injecting the blob into the executable

## Example Project Structure

```
my-app/
├── sea-config.json
├── project.json
└── src/
└── main.ts
```

- [Learn about Nx on CI](https://nx.dev/ci/intro/ci-with-nx?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
- [Releasing Packages with Nx release](https://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
- [What are Nx plugins?](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
The plugin will create a standalone executable in the directory specified in `sea-config.json` (`output`).

And join the Nx community:
On macOS and Linux, the binary will be named `node`. On Windows, it will be named `node.exe`.

- [Discord](https://go.nx.dev/community)
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
You can find a complete working example in the e2e tests.
31 changes: 22 additions & 9 deletions nx-node-sea-e2e/src/nx-node-sea.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { execSync, spawn } from 'node:child_process';
import { once } from 'node:events';
import { mkdirSync, readdirSync, rmSync } from 'node:fs';
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { inspect } from 'node:util';
import { NxJsonConfiguration, readJsonFile, writeJsonFile } from '@nx/devkit';

import type { NodeSeaPluginOptions, NodeSeaOptions } from 'nx-node-sea';
import { platform } from 'node:process';

describe('nx-node-sea', () => {
let projectDirectory: string;
Expand All @@ -16,14 +17,14 @@ describe('nx-node-sea', () => {

// The plugin has been built and published to a local registry in the jest globalSetup
// Install the plugin built with the latest source code into the test repo
execSync(`npm install nx-node-sea@e2e`, {
execSync(`npm install @getlarge/nx-node-sea@e2e`, {
cwd: projectDirectory,
stdio: 'inherit',
env: process.env,
});
updateNxJson(projectDirectory);
seaConfig = createSeaConfig(projectDirectory);
}, 10_000);
}, 15_000);

afterAll(() => {
// Cleanup the test project
Expand All @@ -35,17 +36,18 @@ describe('nx-node-sea', () => {

it('should be installed', () => {
// npm ls will fail if the package is not installed properly
execSync('npm ls nx-node-sea', {
execSync('npm ls @getlarge/nx-node-sea', {
cwd: projectDirectory,
stdio: 'inherit',
});
});

it('should build the SEA', async () => {
const cp = spawn('nx', ['run', 'sea-build'], {
const cp = spawn('nx', ['run', 'sea-build', '--verbose'], {
cwd: projectDirectory,
stdio: 'inherit',
timeout: 10_000,
timeout: 35_000,
shell: true,
});
cp.stdout?.on('data', (data) => {
console.log(data.trim().toString());
Expand All @@ -59,8 +61,8 @@ describe('nx-node-sea', () => {
const outputDirectory = join(projectDirectory, dirname(seaConfig.output));
const files = readdirSync(outputDirectory);
expect(files).toContain(basename(seaConfig.output));
expect(files).toContain('node');
}, 15_000);
expect(files).toContain(platform === 'win32' ? 'node.exe' : 'node');
}, 45_000);

it.todo('should run the SEA');
});
Expand Down Expand Up @@ -95,6 +97,17 @@ function createTestProject() {
console.log(
inspect(readJsonFile(join(projectDirectory, 'project.json')), { depth: 3 })
);

// mock the build output
const buildOutputDirectory = join(projectDirectory, 'dist', projectName);
mkdirSync(buildOutputDirectory, { recursive: true });
writeFileSync(
join(buildOutputDirectory, 'main.js'),
'console.log("Hello World");'
);

console.log(`Created dummy build output in "${buildOutputDirectory}"`);

return projectDirectory;
}

Expand All @@ -104,7 +117,7 @@ function updateNxJson(projectDirectory: string): void {
);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: 'nx-node-sea',
plugin: '@getlarge/nx-node-sea',
options: {
seaTargetName: 'sea-build',
buildTarget: 'build',
Expand Down
68 changes: 56 additions & 12 deletions nx-node-sea/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { dirname, join, posix, win32 } from 'node:path';
import { platform, versions } from 'node:process';
import { combineGlobPatterns } from 'nx/src/utils/globs';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
Expand Down Expand Up @@ -144,32 +144,76 @@ function getSeaTargetConfiguration(
const blobPath = nodeSeaOptions.output;
const nodeBinPath = join(dirname(blobPath), 'node');
// TODO: add nodeSeaOptions.assets to inputs??

return {
cache: true,
inputs: ['node', '{projectRoot}/sea-config.json', 'production'],
inputs: [
'node',
'{projectRoot}/sea-config.json',
'production',
{
externalDependencies: ['postject'],
},
{
runtime: 'node --version',
},
{
runtime: 'node --print "process.arch"',
},
],
// TODO: check if blobPath is relative, if yes append workspaceRoot
outputs: [`{workspaceRoot}/${blobPath}`, `{workspaceRoot}/${nodeBinPath}`],
dependsOn: [options.buildTarget],
executor: 'nx:run-commands',
options: {
/**
* @see https://nodejs.org/api/single-executable-applications.html
* @todo update commands for win support
*/
commands: [
'node --experimental-sea-config {projectRoot}/sea-config.json',
`cp $(command -v node) ${nodeBinPath}`,
platform === 'darwin' && `codesign --remove-signature ${nodeBinPath}`,
platform === 'darwin'
? `npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA`
: `npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`,
platform === 'darwin' && `codesign --sign - ${nodeBinPath}`,
],
commands: getSeaCommands({ nodeBinPath, blobPath }),
parallel: false,
},
};
}

function getSeaCommands(options: {
nodeBinPath: string;
blobPath: string;
sign?: boolean;
}): string[] {
const { nodeBinPath, blobPath, sign = false } = options;
if (platform === 'darwin') {
return [
'node --experimental-sea-config {projectRoot}/sea-config.json',
`cp $(command -v node) ${nodeBinPath}`,
`codesign --remove-signature ${nodeBinPath}`,
`npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA`,
`codesign --sign - ${nodeBinPath}`,
];
} else if (platform === 'linux') {
return [
'node --experimental-sea-config {projectRoot}/sea-config.json',
`cp $(command -v node) ${nodeBinPath}`,
`npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`,
];
} else if (platform === 'win32') {
const _nodeBinPath = join(
dirname(nodeBinPath.replaceAll(posix.sep, win32.sep)),
'node.exe'
);
const _blobPath = blobPath.replaceAll(posix.sep, win32.sep);
return [
'node --experimental-sea-config {projectRoot}/sea-config.json',
`node -e "require('fs').copyFileSync(process.execPath, 'main.exe')"`,
...(sign ? [`signtool remove /s 'main.exe' `] : []),
`npx postject main.exe NODE_SEA_BLOB ${_blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`,
...(sign ? [`signtool sign /fd SHA256 main.exe`] : []),
`mv main.exe ${_nodeBinPath}`,
];
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
}

function getNodeVersion() {
return versions.node;
}
Expand Down
2 changes: 1 addition & 1 deletion tools/scripts/start-local-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async () => {
global.stopLocalRegistry = await startLocalRegistry({
localRegistryTarget,
storage,
verbose: false,
verbose: true,
});

await releaseVersion({
Expand Down
Loading