Skip to content

Commit 05758cd

Browse files
authored
Merge pull request #1 from getlarge/feat-add-windows-support
feat: add windows support
2 parents bdd69f3 + 30d670a commit 05758cd

File tree

6 files changed

+169
-75
lines changed

6 files changed

+169
-75
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,11 @@ jobs:
1818
with:
1919
fetch-depth: 0
2020

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

2723
- uses: actions/setup-node@v4
2824
with:
29-
node-version: 20
25+
node-version: 22
3026
cache: 'npm'
3127

3228
- run: npm ci --legacy-peer-deps
@@ -39,7 +35,8 @@ jobs:
3935
needs: main
4036
strategy:
4137
matrix:
42-
os: [ubuntu-latest, macos-latest]
38+
os: [ubuntu-latest, macos-latest, windows-latest]
39+
node: [20, 22]
4340
runs-on: ${{ matrix.os }}
4441

4542
steps:
@@ -49,7 +46,7 @@ jobs:
4946

5047
- uses: actions/setup-node@v4
5148
with:
52-
node-version: 20
49+
node-version: ${{ matrix.node }}
5350
cache: 'npm'
5451

5552
- run: npm ci --legacy-peer-deps

README.md

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,116 @@
1-
# NxNodeSea
1+
# @getlarge/nx-node-sea
22

3-
<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>
3+
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).
44

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

7-
Run `npx nx graph` to visually explore what got created. Now, let's get you up to speed!
7+
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.
88

9-
## Finish your CI setup
9+
## Requirements
1010

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

13-
## Run tasks
14+
## Installation
1415

15-
To run tasks with Nx use:
16-
17-
```sh
18-
npx nx <target> <project-name>
16+
```bash
17+
npm install --save-dev @getlarge/nx-node-sea
1918
```
2019

21-
For example:
20+
## Usage
2221

23-
```sh
24-
npx nx build myproject
25-
```
22+
### 1. Create a sea-config.json file
2623

27-
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.
24+
Create a `sea-config.json` file in your project's root directory:
2825

29-
[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)
26+
```json
27+
{
28+
"main": "dist/your-app/main.js",
29+
"output": "dist/your-app/main.blob",
30+
"disableExperimentalSEAWarning": false,
31+
"useSnapshot": false,
32+
"useCodeCache": false
33+
}
34+
```
3035

31-
## Add new projects
36+
### 2. Configure the plugin in nx.json
37+
38+
Add the plugin configuration to your `nx.json` file:
39+
40+
```json
41+
{
42+
"plugins": [
43+
{
44+
"plugin": "@getlarge/nx-node-sea",
45+
"options": {
46+
"seaTargetName": "sea-build",
47+
"buildTarget": "build"
48+
}
49+
}
50+
]
51+
}
52+
```
3253

33-
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.
54+
> **Note:** The `buildTarget` option specifies the target that will be used to build your application before creating the SEA. The default value is `"build"`.
3455
35-
To install a new plugin you can use the `nx add` command. Here's an example of adding the React plugin:
56+
### 3. Build your SEA
3657

37-
```sh
38-
npx nx add @nx/react
58+
```bash
59+
nx run your-app:sea-build
3960
```
4061

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

43-
```sh
44-
# Genenerate an app
45-
npx nx g @nx/react:app demo
64+
1. Build your application using the specified build target
65+
2. Generate a SEA preparation blob
66+
3. Create a standalone executable
4667

47-
# Generate a library
48-
npx nx g @nx/react:lib some-lib
49-
```
68+
## Configuration Options
5069

51-
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.
70+
### Plugin Options
5271

53-
[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)
72+
| Option | Description | Default |
73+
| --------------- | ------------------------------------------------------------ | ------------- |
74+
| `buildTarget` | The target to build your application before creating the SEA | `"build"` |
75+
| `seaTargetName` | The name of the target that will be created to build the SEA | `"sea-build"` |
5476

55-
[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)
77+
### SEA Config Options
5678

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

59-
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.
88+
## Platform Support
6089

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

63-
## Useful links
92+
- Linux
93+
- macOS (includes code signing)
94+
- Windows
6495

65-
Learn more:
96+
## Learn More
97+
98+
- [Node.js Single Executable Applications](https://nodejs.org/api/single-executable-applications.html)
99+
- [Nx Build System](https://nx.dev/features/build)
100+
- [Postject](https://github.com/nodejs/postject) - Used for injecting the blob into the executable
101+
102+
## Example Project Structure
103+
104+
```
105+
my-app/
106+
├── sea-config.json
107+
├── project.json
108+
└── src/
109+
└── main.ts
110+
```
66111

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

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

73-
- [Discord](https://go.nx.dev/community)
74-
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
75-
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
76-
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
116+
You can find a complete working example in the e2e tests.

nx-node-sea-e2e/src/nx-node-sea.spec.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { execSync, spawn } from 'node:child_process';
22
import { once } from 'node:events';
3-
import { mkdirSync, readdirSync, rmSync } from 'node:fs';
3+
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
44
import { join, dirname, basename } from 'node:path';
55
import { inspect } from 'node:util';
66
import { NxJsonConfiguration, readJsonFile, writeJsonFile } from '@nx/devkit';
77

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

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

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

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

3637
it('should be installed', () => {
3738
// npm ls will fail if the package is not installed properly
38-
execSync('npm ls nx-node-sea', {
39+
execSync('npm ls @getlarge/nx-node-sea', {
3940
cwd: projectDirectory,
4041
stdio: 'inherit',
4142
});
4243
});
4344

4445
it('should build the SEA', async () => {
45-
const cp = spawn('nx', ['run', 'sea-build'], {
46+
const cp = spawn('nx', ['run', 'sea-build', '--verbose'], {
4647
cwd: projectDirectory,
4748
stdio: 'inherit',
48-
timeout: 10_000,
49+
timeout: 35_000,
50+
shell: true,
4951
});
5052
cp.stdout?.on('data', (data) => {
5153
console.log(data.trim().toString());
@@ -59,8 +61,8 @@ describe('nx-node-sea', () => {
5961
const outputDirectory = join(projectDirectory, dirname(seaConfig.output));
6062
const files = readdirSync(outputDirectory);
6163
expect(files).toContain(basename(seaConfig.output));
62-
expect(files).toContain('node');
63-
}, 15_000);
64+
expect(files).toContain(platform === 'win32' ? 'node.exe' : 'node');
65+
}, 45_000);
6466

6567
it.todo('should run the SEA');
6668
});
@@ -95,6 +97,17 @@ function createTestProject() {
9597
console.log(
9698
inspect(readJsonFile(join(projectDirectory, 'project.json')), { depth: 3 })
9799
);
100+
101+
// mock the build output
102+
const buildOutputDirectory = join(projectDirectory, 'dist', projectName);
103+
mkdirSync(buildOutputDirectory, { recursive: true });
104+
writeFileSync(
105+
join(buildOutputDirectory, 'main.js'),
106+
'console.log("Hello World");'
107+
);
108+
109+
console.log(`Created dummy build output in "${buildOutputDirectory}"`);
110+
98111
return projectDirectory;
99112
}
100113

@@ -104,7 +117,7 @@ function updateNxJson(projectDirectory: string): void {
104117
);
105118
nxJson.plugins ??= [];
106119
nxJson.plugins.push({
107-
plugin: 'nx-node-sea',
120+
plugin: '@getlarge/nx-node-sea',
108121
options: {
109122
seaTargetName: 'sea-build',
110123
buildTarget: 'build',

nx-node-sea/src/plugin.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@nx/devkit';
1010
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
1111
import { existsSync } from 'node:fs';
12-
import { dirname, join } from 'node:path';
12+
import { dirname, join, posix, win32 } from 'node:path';
1313
import { platform, versions } from 'node:process';
1414
import { combineGlobPatterns } from 'nx/src/utils/globs';
1515
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
@@ -144,32 +144,76 @@ function getSeaTargetConfiguration(
144144
const blobPath = nodeSeaOptions.output;
145145
const nodeBinPath = join(dirname(blobPath), 'node');
146146
// TODO: add nodeSeaOptions.assets to inputs??
147+
147148
return {
148149
cache: true,
149-
inputs: ['node', '{projectRoot}/sea-config.json', 'production'],
150+
inputs: [
151+
'node',
152+
'{projectRoot}/sea-config.json',
153+
'production',
154+
{
155+
externalDependencies: ['postject'],
156+
},
157+
{
158+
runtime: 'node --version',
159+
},
160+
{
161+
runtime: 'node --print "process.arch"',
162+
},
163+
],
150164
// TODO: check if blobPath is relative, if yes append workspaceRoot
151165
outputs: [`{workspaceRoot}/${blobPath}`, `{workspaceRoot}/${nodeBinPath}`],
152166
dependsOn: [options.buildTarget],
153167
executor: 'nx:run-commands',
154168
options: {
155169
/**
156170
* @see https://nodejs.org/api/single-executable-applications.html
157-
* @todo update commands for win support
158171
*/
159-
commands: [
160-
'node --experimental-sea-config {projectRoot}/sea-config.json',
161-
`cp $(command -v node) ${nodeBinPath}`,
162-
platform === 'darwin' && `codesign --remove-signature ${nodeBinPath}`,
163-
platform === 'darwin'
164-
? `npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA`
165-
: `npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`,
166-
platform === 'darwin' && `codesign --sign - ${nodeBinPath}`,
167-
],
172+
commands: getSeaCommands({ nodeBinPath, blobPath }),
168173
parallel: false,
169174
},
170175
};
171176
}
172177

178+
function getSeaCommands(options: {
179+
nodeBinPath: string;
180+
blobPath: string;
181+
sign?: boolean;
182+
}): string[] {
183+
const { nodeBinPath, blobPath, sign = false } = options;
184+
if (platform === 'darwin') {
185+
return [
186+
'node --experimental-sea-config {projectRoot}/sea-config.json',
187+
`cp $(command -v node) ${nodeBinPath}`,
188+
`codesign --remove-signature ${nodeBinPath}`,
189+
`npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA`,
190+
`codesign --sign - ${nodeBinPath}`,
191+
];
192+
} else if (platform === 'linux') {
193+
return [
194+
'node --experimental-sea-config {projectRoot}/sea-config.json',
195+
`cp $(command -v node) ${nodeBinPath}`,
196+
`npx postject ${nodeBinPath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`,
197+
];
198+
} else if (platform === 'win32') {
199+
const _nodeBinPath = join(
200+
dirname(nodeBinPath.replaceAll(posix.sep, win32.sep)),
201+
'node.exe'
202+
);
203+
const _blobPath = blobPath.replaceAll(posix.sep, win32.sep);
204+
return [
205+
'node --experimental-sea-config {projectRoot}/sea-config.json',
206+
`node -e "require('fs').copyFileSync(process.execPath, 'main.exe')"`,
207+
...(sign ? [`signtool remove /s 'main.exe' `] : []),
208+
`npx postject main.exe NODE_SEA_BLOB ${_blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`,
209+
...(sign ? [`signtool sign /fd SHA256 main.exe`] : []),
210+
`mv main.exe ${_nodeBinPath}`,
211+
];
212+
} else {
213+
throw new Error(`Unsupported platform: ${platform}`);
214+
}
215+
}
216+
173217
function getNodeVersion() {
174218
return versions.node;
175219
}

tools/scripts/start-local-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default async () => {
1414
global.stopLocalRegistry = await startLocalRegistry({
1515
localRegistryTarget,
1616
storage,
17-
verbose: false,
17+
verbose: true,
1818
});
1919

2020
await releaseVersion({

0 commit comments

Comments
 (0)