Skip to content

Commit a44de43

Browse files
build: replace fs-extra, node-watch, meow and rimraf with Node built-ins (#992)
* build: replace fs-extra, node-watch, meow and rimraf with Node builtins These build scripts predate much of what is now in the Node standard library. On Node 24 each maps to a builtin: fs-extra to fs.cpSync/rmSync/ mkdirSync recursive, rimraf to fs.rmSync plus fs.globSync for the one --glob case, meow to util.parseArgs, and node-watch to fs.watch recursive (which works on Linux since Node 19, and the watched src dir is small). shx stays for now: its only use is the update-ufuzzy script, which goes away together with the vendored file in the uFuzzy build-copy change, so that removal lives there rather than here. Note: AI-assisted (Claude Code). Manually verified by running yarn clean and a from-scratch yarn build so every rewritten script executed, checking fs.globSync('*.lock') matches and removes the right files, and confirming Node 19+ recursive watch on Linux before dropping node-watch. * fix: run rm.js from the package root, drop the ignored copySync arg The conda-lock and extract scripts cd into a subdir first and then called node scripts/rm.js, but that path only resolves from the repo root, so the removal failed with a module-not-found once the cwd changed. npm and yarn always start a run-script at the package root, so the natural fix is to do the removal there against a root-relative target and let the later cd handle only conda-lock and tar, which genuinely need the subdir. This avoids a ../scripts/rm.js form that would recouple the path to the cd depth. copySync already hardcodes { recursive: true } in its definition, so the third argument passed at the copy-extras call site was silently ignored; removing it keeps the recursive copy and drops the misleading option. Note: AI-assisted (Claude Code). Manually verified: rm.js from root cwd with a subdir glob and a glob-miss both remove the right paths and exit 0; copySync copies a nested tree; type-check, 443 unit tests and yarn build all pass on the merged branch. * test: cover scripts/rm.js, the native rimraf replacement rm.js was the one piece of new logic in this PR with no coverage: glob detection, glob expansion, recursive removal and missing-path tolerance all lived only in the build/conda npm scripts, which the installer CI exercises end to end but which nothing pins at the unit level. Expose an rm(args) function and keep the CLI behavior behind a require.main === module guard so the same file can be imported by the test and still run as node scripts/rm.js. The test uses a real tmpdir and absolute glob patterns so it does not depend on cwd (fs.globSync resolves relative patterns against process.cwd, and chdir is unavailable in worker threads). Note: AI-assisted (Claude Code). Manually verified: the 6 cases pass; mutation checks confirm they are not tautological (recursive:false and force:false make three of them fail, dropping the glob branch fails the glob case); the CLI form still removes via argv after the refactor; type-check, 449 unit tests and yarn build pass. * style: single-line the fs import in rm.test.ts for prettier The multi-line import tripped prettier:check (it fits on one line under the repo width), which failed the Lint step. No behavior change. Note: AI-assisted (Claude Code). Manually verified: prettier:check and eslint:check both clean on the full tree. --------- Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com>
1 parent 6c8d296 commit a44de43

7 files changed

Lines changed: 165 additions & 416 deletions

File tree

package.json

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"test:unit:watch": "vitest",
1111
"test:coverage": "vitest run --coverage",
1212
"test:e2e": "playwright test",
13-
"clean": "rimraf build dist",
13+
"clean": "node scripts/rm.js build dist",
1414
"watch:tsc": "tsc -w",
1515
"watch:assets": "node ./scripts/extract.js && node ./scripts/copyassets.js watch",
1616
"watch": "concurrently \"yarn watch:tsc\" \"yarn watch:assets\" ",
@@ -30,19 +30,19 @@
3030
"dist:osx-arm64-dev": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --macos --arm64 --publish never",
3131
"dist:win-64": "yarn build && electron-builder --win --publish never",
3232
"dist:win-arm64": "yarn build && yarn electron-builder --arm64 --publish never",
33-
"update_workflow_conda_lock": "cd workflow_env && rimraf --glob *.lock && conda-lock --kind explicit -f publish_env.yaml && cd -",
34-
"update_conda_lock": "cd env_installer && rimraf --glob *.lock && conda-lock --kind explicit -f jlab_server.yaml && cd -",
35-
"clean_env_installer": "rimraf ./env_installer/jlab_server.tar.gz && rimraf ./env_installer/jlab_server",
33+
"update_workflow_conda_lock": "node scripts/rm.js workflow_env/*.lock && cd workflow_env && conda-lock --kind explicit -f publish_env.yaml && cd -",
34+
"update_conda_lock": "node scripts/rm.js env_installer/*.lock && cd env_installer && conda-lock --kind explicit -f jlab_server.yaml && cd -",
35+
"clean_env_installer": "node scripts/rm.js ./env_installer/jlab_server.tar.gz && node scripts/rm.js ./env_installer/jlab_server",
3636
"create_env_installer": "yarn clean_env_installer && conda-lock install --no-validate-platform --prefix ./env_installer/jlab_server ./env_installer/conda-${ENV_PLATFORM}.lock && yarn copy_extras_to_bundled_env && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz",
3737
"create_env_installer:linux-64": "ENV_PLATFORM=linux-64 yarn create_env_installer",
3838
"create_env_installer:linux-aarch64": "ENV_PLATFORM=linux-aarch64 yarn create_env_installer",
3939
"create_env_installer:osx-64": "ENV_PLATFORM=osx-64 yarn create_env_installer",
4040
"create_env_installer:osx-arm64": "ENV_PLATFORM=osx-arm64 yarn create_env_installer",
4141
"create_env_installer:win-64": "yarn clean_env_installer && conda-lock install --no-validate-platform --prefix ./env_installer/jlab_server ./env_installer/conda-win-64.lock && yarn copy_extras_to_bundled_env && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz",
42-
"extract_env_installer:osx": "cd ./env_installer && rimraf jlab_server_extracted && mkdir jlab_server_extracted && tar -xzf jlab_server.tar.gz -C jlab_server_extracted && cd -",
42+
"extract_env_installer:osx": "node scripts/rm.js env_installer/jlab_server_extracted && cd ./env_installer && mkdir jlab_server_extracted && tar -xzf jlab_server.tar.gz -C jlab_server_extracted && cd -",
4343
"extract_env_installer:osx-64": "yarn extract_env_installer:osx",
4444
"extract_env_installer:osx-arm64": "yarn extract_env_installer:osx",
45-
"compress_env_installer:osx": "rimraf ./env_installer/jlab_server.tar.gz && cd ./env_installer/jlab_server_extracted && tar -czf ../jlab_server.tar.gz . && cd -",
45+
"compress_env_installer:osx": "node scripts/rm.js ./env_installer/jlab_server.tar.gz && cd ./env_installer/jlab_server_extracted && tar -czf ../jlab_server.tar.gz . && cd -",
4646
"compress_env_installer:osx-64": "yarn compress_env_installer:osx",
4747
"compress_env_installer:osx-arm64": "yarn compress_env_installer:osx",
4848
"check_version_match": "node scripts/buildutil.js --check-version-match",
@@ -199,14 +199,10 @@
199199
"electron-playwright-helpers": "^2.1.0",
200200
"eslint": "^10.5.0",
201201
"eslint-config-prettier": "^10.1.8",
202-
"fs-extra": "^11.3.5",
203202
"globals": "^17.6.0",
204203
"istextorbinary": "^9.5.0",
205-
"meow": "^9.0.0",
206-
"node-watch": "^0.7.4",
207204
"prettier": "~2.1.1",
208205
"read-package-tree": "^5.1.6",
209-
"rimraf": "^4.4.1",
210206
"shx": "^0.4.0",
211207
"typescript": "^6.0.0",
212208
"typescript-eslint": "^8.61.1",

scripts/buildutil.js

Lines changed: 24 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,33 @@
1-
const meow = require('meow');
2-
const fs = require('fs-extra');
1+
const { parseArgs } = require('util');
2+
const fs = require('fs');
33
const path = require('path');
44
const semver = require('semver');
55
const yaml = require('js-yaml');
66

7+
// Native replacements for the fs-extra helpers this script used.
8+
const readJSONSync = p => JSON.parse(fs.readFileSync(p, 'utf8'));
9+
const copySync = (src, dest) => fs.cpSync(src, dest, { recursive: true });
10+
711
const pkgjsonFilePath = path.resolve(__dirname, '../package.json');
812

9-
const cli = meow(
10-
`
11-
Usage
12-
$ node buildutil <options>
13-
14-
Options
15-
--check-version-match check for JupyterLab version match
16-
--update-binary-sign-list update binary list to sign for macOS
17-
--copy-extras-to-bundled-env copy extra files from extras to bundled environment installer
18-
--platform platform for --update-binary-sign-list. osx-64 or osx-arm64
19-
20-
Other options:
21-
--help show usage information
22-
--version show version information
23-
24-
Examples
25-
$ node buildutil --check-version-match
26-
`,
27-
{
28-
flags: {
29-
checkVersionMatch: {
30-
type: 'boolean',
31-
default: false
32-
},
33-
updateBinarySignList: {
34-
type: 'boolean',
35-
default: false
36-
},
37-
platform: {
38-
type: 'string',
39-
default: 'osx-64'
40-
}
41-
}
13+
// node buildutil <options>
14+
// --check-version-match check for JupyterLab version match
15+
// --update-binary-sign-list update binary list to sign for macOS
16+
// --copy-extras-to-bundled-env copy extras into the bundled environment
17+
// --platform <osx-64|osx-arm64> platform for --update-binary-sign-list
18+
const { values: flags } = parseArgs({
19+
options: {
20+
'check-version-match': { type: 'boolean', default: false },
21+
'update-binary-sign-list': { type: 'boolean', default: false },
22+
'copy-extras-to-bundled-env': { type: 'boolean', default: false },
23+
platform: { type: 'string', default: 'osx-64' }
4224
}
43-
);
25+
});
4426

45-
if (cli.flags.checkVersionMatch) {
27+
if (flags['check-version-match']) {
4628
// parse application version
4729
const pkgjsonFileData = fs.existsSync(pkgjsonFilePath)
48-
? fs.readJSONSync(pkgjsonFilePath)
30+
? readJSONSync(pkgjsonFilePath)
4931
: undefined;
5032
if (!pkgjsonFileData) {
5133
console.error('package.json not found!');
@@ -91,7 +73,7 @@ if (cli.flags.checkVersionMatch) {
9173
process.exit(0);
9274
}
9375

94-
if (cli.flags.updateBinarySignList) {
76+
if (flags['update-binary-sign-list']) {
9577
const { isBinary } = require('istextorbinary');
9678
const envInstallerDir = path.resolve('env_installer', 'jlab_server');
9779

@@ -172,10 +154,7 @@ if (cli.flags.updateBinarySignList) {
172154

173155
const binaries = findBinariesInDirectory(envInstallerDir);
174156
const fileContent = binaries.join('\n');
175-
const signListFile = path.join(
176-
'env_installer',
177-
`sign-${cli.flags.platform}.txt`
178-
);
157+
const signListFile = path.join('env_installer', `sign-${flags.platform}.txt`);
179158

180159
fs.writeFileSync(signListFile, `${fileContent}\n`);
181160

@@ -184,11 +163,11 @@ if (cli.flags.updateBinarySignList) {
184163
process.exit(0);
185164
}
186165

187-
if (cli.flags.copyExtrasToBundledEnv) {
166+
if (flags['copy-extras-to-bundled-env']) {
188167
const envExtrasDir = path.resolve('env_installer', 'extras');
189168
const envInstallerDir = path.resolve('env_installer', 'jlab_server');
190169

191-
fs.copySync(envExtrasDir, envInstallerDir, { recursive: true });
170+
copySync(envExtrasDir, envInstallerDir);
192171

193172
console.log(
194173
`Finished copying env extras from \n\t"${envExtrasDir}" to \n\t"${envInstallerDir}"`

scripts/copyassets.js

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const path = require('path');
2-
const fs = require('fs-extra');
3-
const watch = require('node-watch');
2+
const fs = require('fs');
3+
4+
// fs-extra's copySync replaced with the native recursive cp (Node 16.7+).
5+
const copySync = (src, dest) => fs.cpSync(src, dest, { recursive: true });
46

57
const platform = process.platform;
68
const buildDir = path.resolve('./build');
@@ -39,40 +41,38 @@ function copyAssests() {
3941
const destPath = srcPath.replace(srcDir, dest);
4042

4143
if (srcPath.includes('style') || srcPath.includes('img')) {
42-
fs.copySync(srcPath, destPath);
44+
copySync(srcPath, destPath);
4345
}
4446
});
4547

4648
const titlebarPath = path.join('main', 'titlebarview', 'titlebar.html');
47-
fs.copySync(
49+
copySync(
4850
path.join(srcDir, titlebarPath),
4951
path.join(dest, '../app-assets', 'titlebarview', 'titlebar.html')
5052
);
5153

52-
fs.copySync(path.join(srcDir, 'assets'), path.join(dest, '../app-assets'), {
53-
recursive: true
54-
});
54+
copySync(path.join(srcDir, 'assets'), path.join(dest, '../app-assets'));
5555

5656
const toolkitPath = path.join(
5757
'../node_modules',
5858
'@jupyter-notebook/web-components',
5959
'dist'
6060
);
61-
fs.copySync(
61+
copySync(
6262
path.join(srcDir, toolkitPath, 'toolkit.min.js'),
6363
path.join(dest, '../jupyter-ui-toolkit/toolkit.min.js')
6464
);
65-
fs.copySync(
65+
copySync(
6666
path.join(srcDir, toolkitPath, 'toolkit.js'),
6767
path.join(dest, '../jupyter-ui-toolkit/toolkit.js')
6868
);
6969

7070
const envInfoPath = path.join('main', 'env_info.py');
71-
fs.copySync(path.join(srcDir, envInfoPath), path.join(dest, envInfoPath));
71+
copySync(path.join(srcDir, envInfoPath), path.join(dest, envInfoPath));
7272

7373
// Copy install scripts
7474
if (platform === 'darwin') {
75-
fs.copySync(
75+
copySync(
7676
path.join(
7777
path.resolve('./'),
7878
'dist-resources',
@@ -82,7 +82,7 @@ function copyAssests() {
8282
path.join(buildDir, 'entitlements.plist')
8383
);
8484
} else if (platform === 'win32') {
85-
fs.copySync(
85+
copySync(
8686
path.join(
8787
path.resolve('./'),
8888
'electron-builder-scripts',
@@ -91,20 +91,17 @@ function copyAssests() {
9191
path.join(buildDir, 'wininstall.nsh')
9292
);
9393
} else {
94-
fs.copySync(
94+
copySync(
9595
path.join(
9696
path.resolve('./'),
9797
'electron-builder-scripts',
9898
'linux_after_install.sh'
9999
),
100100
path.join(buildDir, 'linux_after_install.sh')
101101
);
102-
fs.copySync(
102+
copySync(
103103
path.join(path.resolve('./'), 'electron-builder-scripts', 'snap-hooks'),
104-
path.join(buildDir, 'snap-hooks'),
105-
{
106-
recursive: true
107-
}
104+
path.join(buildDir, 'snap-hooks')
108105
);
109106
}
110107

@@ -113,9 +110,12 @@ function copyAssests() {
113110
copyAssests();
114111

115112
if (process.argv.length > 2 && process.argv[2] == 'watch') {
116-
watch(srcDir, { recursive: true }, function (evt, name) {
117-
if (/(\.css$)|(\.html$)/.test(name)) {
118-
console.log('Asset chage detected.');
113+
// native fs.watch replaces node-watch; recursive watch works on macOS,
114+
// Windows, and Linux (Node 19+ walks the tree itself). srcDir is small so
115+
// the per-directory inotify watches on Linux are not a concern.
116+
fs.watch(srcDir, { recursive: true }, function (evt, name) {
117+
if (name && /(\.css$)|(\.html$)/.test(name)) {
118+
console.log('Asset change detected.');
119119
copyAssests();
120120
}
121121
});

scripts/extract.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
var rpt = require('read-package-tree');
22
var path = require('path');
3-
var fs = require('fs-extra');
3+
var fs = require('fs');
44
var glob = require('glob');
55

6+
// Native replacements for the fs-extra helpers this script used.
7+
var removeSync = p => fs.rmSync(p, { recursive: true, force: true });
8+
var ensureDirSync = d => fs.mkdirSync(d, { recursive: true });
9+
var copySync = (src, dest) => fs.cpSync(src, dest, { recursive: true });
10+
611
var seen = {};
712

813
var schemaDest = path.resolve('./build/schemas');
9-
fs.removeSync(schemaDest);
10-
fs.ensureDirSync(schemaDest);
14+
removeSync(schemaDest);
15+
ensureDirSync(schemaDest);
1116

1217
var themesDest = path.resolve('./build/themes');
13-
fs.removeSync(themesDest);
14-
fs.ensureDirSync(themesDest);
18+
removeSync(themesDest);
19+
ensureDirSync(themesDest);
1520

1621
function extractNode(data) {
1722
data.children.forEach(function (child) {
@@ -32,7 +37,7 @@ function extractNode(data) {
3237
if (schemaDir) {
3338
const from = path.join(data.realpath, schemaDir);
3439
const to = path.join(schemaDest, data.package.name);
35-
fs.copySync(from, to);
40+
copySync(from, to);
3641
}
3742

3843
// Handle themes.
@@ -41,7 +46,7 @@ function extractNode(data) {
4146
var themeName = data.package.name;
4247
const from = path.join(data.realpath, themeDir);
4348
const to = path.join(themesDest, themeName);
44-
fs.copySync(from, to);
49+
copySync(from, to);
4550

4651
/**
4752
* Change relative paths. This is done at runtime by jupyterlab_launcher.

scripts/rm.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Native replacement for the rimraf CLI used by the npm scripts. Removes each
2+
// argument recursively and without erroring on missing paths. Arguments
3+
// containing a glob are expanded with the native fs.globSync (Node 22+).
4+
const fs = require('fs');
5+
6+
function rm(args) {
7+
for (const arg of args) {
8+
const targets = /[*?[]/.test(arg) ? fs.globSync(arg) : [arg];
9+
for (const target of targets) {
10+
fs.rmSync(target, { recursive: true, force: true });
11+
}
12+
}
13+
}
14+
15+
if (require.main === module) {
16+
rm(process.argv.slice(2));
17+
}
18+
19+
module.exports = { rm };

0 commit comments

Comments
 (0)