Skip to content

Commit 5870705

Browse files
Make fs-ext dependency optional (#2322)
## Motivation for the change, related issues Some folks are encountering issue with building the fs-ext module containing a native Node.js add-on. Fixes #2308 ## Implementation details This PR makes the fs-ext dependency optional. This means that the dependency will be proactively installed by `npm`, but if an optional dependency fails to install, it does not fail the entire npm install. ## Testing Instructions (or ideally a Blueprint) - Run `npm ci` - Confirm that `fs-ext` is installed under node_modules - Edit packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts and add a console.log() statement to `flockSyncNoOp()` - Make local dir test dir - Run `npx nx unbuilt-jspi playground-cli server --mountBeforeInstall=<local-test-dir-path>:/wordpress` - Confirm it starts with no errors and no `flockSyncNoOp` messages. - Delete `node_modules/fs-ext` - Run `npx nx unbuilt-jspi playground-cli server --mountBeforeInstall=<local-test-dir-path>:/wordpress` - Confirm it starts successfully and prints `flockSyncNoP` console messages.
1 parent 4710a42 commit 5870705

File tree

6 files changed

+65
-63
lines changed

6 files changed

+65
-63
lines changed

package-lock.json

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
"diff3": "0.0.4",
7878
"express": "4.21.2",
7979
"file-saver": "^2.0.5",
80-
"fs-ext": "2.1.1",
8180
"fs-extra": "11.1.1",
8281
"ini": "4.1.2",
8382
"octokit": "3.1.2",
@@ -190,5 +189,8 @@
190189
"engines": {
191190
"node": ">=20.18.3",
192191
"npm": ">=10.1.0"
192+
},
193+
"optionalDependencies": {
194+
"fs-ext": "2.1.1"
193195
}
194196
}

packages/php-wasm/node/build.js

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -73,54 +73,6 @@ const dirnamePlugin = {
7373
},
7474
};
7575

76-
// Create a require function that can be used in the nativeNodeModulesPlugin.
77-
const require = createRequire(import.meta.url);
78-
79-
/**
80-
* This is a plugin for handling native Node.js modules.
81-
*
82-
* Taken from:
83-
* https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487
84-
*/
85-
const nativeNodeModulesPlugin = {
86-
name: 'native-node-modules',
87-
setup(build) {
88-
// If a ".node" file is imported within a module in the "file" namespace, resolve
89-
// it to an absolute path and put it into the "node-file" virtual namespace.
90-
build.onResolve({ filter: /\.node$/, namespace: 'file' }, (args) => ({
91-
path: require.resolve(args.path, { paths: [args.resolveDir] }),
92-
namespace: 'node-file',
93-
}));
94-
95-
// Files in the "node-file" virtual namespace call "require()" on the
96-
// path from esbuild of the ".node" file in the output directory.
97-
build.onLoad({ filter: /.*/, namespace: 'node-file' }, (args) => ({
98-
contents: `
99-
import path from ${JSON.stringify(args.path)}
100-
try { module.exports = require(path) }
101-
catch {}
102-
`,
103-
}));
104-
105-
// If a ".node" file is imported within a module in the "node-file" namespace, put
106-
// it in the "file" namespace where esbuild's default loading behavior will handle
107-
// it. It is already an absolute path since we resolved it to one above.
108-
build.onResolve(
109-
{ filter: /\.node$/, namespace: 'node-file' },
110-
(args) => ({
111-
path: args.path,
112-
namespace: 'file',
113-
})
114-
);
115-
116-
// Tell esbuild's default loading behavior to use the "file" loader for
117-
// these ".node" files.
118-
let opts = build.initialOptions;
119-
opts.loader = opts.loader || {};
120-
opts.loader['.node'] = 'file';
121-
},
122-
};
123-
12476
/**
12577
* This is a plugin for handling imports ending with ?url.
12678
*/
@@ -171,13 +123,13 @@ async function build() {
171123
format: 'cjs',
172124
bundle: true,
173125
tsconfig: 'packages/php-wasm/node/tsconfig.json',
174-
external: ['@php-wasm/*', '@wp-playground/*', 'ws'],
126+
external: ['@php-wasm/*', '@wp-playground/*', 'ws', 'fs-ext'],
175127
loader: {
176128
'.php': 'text',
177129
'.ini': 'file',
178130
'.wasm': 'file',
179131
},
180-
plugins: [dirnamePlugin, nativeNodeModulesPlugin, importUrlPlugin],
132+
plugins: [dirnamePlugin, importUrlPlugin],
181133
});
182134

183135
await esbuild.build({
@@ -203,7 +155,14 @@ const __dirname = import.meta.dirname;
203155
packages: 'external',
204156
bundle: true,
205157
tsconfig: 'packages/php-wasm/node/tsconfig.json',
206-
external: ['@php-wasm/*', '@wp-playground/*', 'ws', 'fs', 'path'],
158+
external: [
159+
'@php-wasm/*',
160+
'@wp-playground/*',
161+
'ws',
162+
'fs',
163+
'path',
164+
'fs-ext',
165+
],
207166
supported: {
208167
'dynamic-import': true,
209168
'top-level-await': true,
@@ -214,7 +173,7 @@ const __dirname = import.meta.dirname;
214173
'.ini': 'file',
215174
'.wasm': 'file',
216175
},
217-
plugins: [dirnamePlugin, nativeNodeModulesPlugin, importUrlPlugin],
176+
plugins: [dirnamePlugin, importUrlPlugin],
218177
});
219178

220179
fs.copyFileSync(

packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { logger } from '@php-wasm/logger';
22
import { openSync, closeSync } from 'fs';
3-
import { flockSync as nativeFlockSync } from 'fs-ext';
3+
4+
type NativeFlockSync = (
5+
fd: number,
6+
flags: 'sh' | 'ex' | 'shnb' | 'exnb' | 'un'
7+
) => void;
48

59
import type {
610
FileLockManager,
@@ -16,6 +20,7 @@ type LockMode = 'exclusive' | 'shared' | 'unlock';
1620
type NativeLock = {
1721
fd: number;
1822
mode: LockMode;
23+
nativeFlockSync: NativeFlockSync;
1924
};
2025

2126
type LockedRange = RequestedRangeLock & {
@@ -31,9 +36,20 @@ const MAX_64BIT_OFFSET = BigInt(2n ** 64n - 1n);
3136
* It provides methods for locking and unlocking files, as well as finding conflicting locks.
3237
*/
3338
export class FileLockManagerForNode implements FileLockManager {
39+
nativeFlockSync: NativeFlockSync;
3440
locks: Map<string, FileLock>;
3541

36-
constructor() {
42+
/**
43+
* Create a new FileLockManagerForNode instance.
44+
*
45+
* @param nativeFlockSync A synchronous flock() function to lock files via the host OS.
46+
*/
47+
constructor(
48+
nativeFlockSync: NativeFlockSync = function flockSyncNoOp() {
49+
/* do nothing */
50+
}
51+
) {
52+
this.nativeFlockSync = nativeFlockSync;
3753
this.locks = new Map();
3854
}
3955

@@ -51,7 +67,11 @@ export class FileLockManagerForNode implements FileLockManager {
5167
return true;
5268
}
5369

54-
const maybeLock = FileLock.maybeCreate(path, op.type);
70+
const maybeLock = FileLock.maybeCreate(
71+
path,
72+
op.type,
73+
this.nativeFlockSync
74+
);
5575
if (maybeLock === undefined) {
5676
return false;
5777
}
@@ -82,7 +102,11 @@ export class FileLockManagerForNode implements FileLockManager {
82102
return true;
83103
}
84104

85-
const maybeLock = FileLock.maybeCreate(path, requestedLock.type);
105+
const maybeLock = FileLock.maybeCreate(
106+
path,
107+
requestedLock.type,
108+
this.nativeFlockSync
109+
);
86110
if (maybeLock === undefined) {
87111
return false;
88112
}
@@ -177,7 +201,8 @@ export class FileLock {
177201
*/
178202
static maybeCreate(
179203
path: string,
180-
mode: Exclude<WholeFileLock['type'], 'unlocked'>
204+
mode: Exclude<WholeFileLock['type'], 'unlocked'>,
205+
nativeFlockSync: NativeFlockSync
181206
): FileLock | undefined {
182207
let fd;
183208
try {
@@ -186,7 +211,7 @@ export class FileLock {
186211
const flockFlags = mode === 'exclusive' ? 'exnb' : 'shnb';
187212
nativeFlockSync(fd, flockFlags);
188213

189-
const nativeLock: NativeLock = { fd, mode };
214+
const nativeLock: NativeLock = { fd, mode, nativeFlockSync };
190215
return new FileLock(nativeLock);
191216
} catch {
192217
if (fd !== undefined) {
@@ -586,7 +611,7 @@ export class FileLock {
586611
'un';
587612

588613
try {
589-
nativeFlockSync(this.nativeLock.fd, flockFlags);
614+
this.nativeLock.nativeFlockSync(this.nativeLock.fd, flockFlags);
590615
this.nativeLock.mode = requiredNativeLockType;
591616
return true;
592617
} catch {

packages/playground/cli/src/run-cli.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,17 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
475475

476476
// Declare file lock manager outside scope of startServer
477477
// so we can look at it when debugging request handling.
478-
const fileLockManager = new FileLockManagerForNode();
478+
const nativeFlockSync = await import('fs-ext')
479+
.then((m) => m.flockSync)
480+
.catch(() => {
481+
logger.warn(
482+
'The fs-ext package is not installed. ' +
483+
'Internal file locking will not be integrated with ' +
484+
'host OS file locking.'
485+
);
486+
return undefined;
487+
});
488+
const fileLockManager = new FileLockManagerForNode(nativeFlockSync);
479489

480490
let wordPressReady = false;
481491

packages/playground/cli/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export default defineConfig({
8282
'readline',
8383
'worker_threads',
8484
'url',
85+
'fs-ext',
8586
],
8687
},
8788
lib: {

0 commit comments

Comments
 (0)