Skip to content

Commit 074ea4a

Browse files
authored
Fix Node.js without ICU support (#1144)
1 parent 607a0ff commit 074ea4a

File tree

3 files changed

+43
-8
lines changed

3 files changed

+43
-8
lines changed

lib/arguments/escape.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,23 @@ const escapeControlCharacter = character => {
3838
// Some shells do not even have a way to print those characters in an escaped fashion.
3939
// Therefore, we prioritize printing those safely, instead of allowing those to be copy-pasted.
4040
// List of Unicode character categories: https://www.fileformat.info/info/unicode/category/index.htm
41-
const SPECIAL_CHAR_REGEXP = /\p{Separator}|\p{Other}/gu;
41+
const getSpecialCharRegExp = () => {
42+
try {
43+
// This throws when using Node.js without ICU support.
44+
// When using a RegExp literal, this would throw at parsing-time, instead of runtime.
45+
// eslint-disable-next-line prefer-regex-literals
46+
return new RegExp('\\p{Separator}|\\p{Other}', 'gu');
47+
} catch {
48+
// Similar to the above RegExp, but works even when Node.js has been built without ICU support.
49+
// Unlike the above RegExp, it only covers whitespaces and C0/C1 control characters.
50+
// It does not cover some edge cases, such as Unicode reserved characters.
51+
// See https://github.com/sindresorhus/execa/issues/1143
52+
// eslint-disable-next-line no-control-regex
53+
return /[\s\u0000-\u001F\u007F-\u009F\u00AD]/g;
54+
}
55+
};
56+
57+
const SPECIAL_CHAR_REGEXP = getSpecialCharRegExp();
4258

4359
// Accepted by $'...' in Bash.
4460
// Exclude \a \e \v which are accepted in Bash but not in JavaScript (except \v) and JSON.

test/arguments/escape-no-icu.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Mimics Node.js when built without ICU support
2+
// See https://github.com/sindresorhus/execa/issues/1143
3+
globalThis.RegExp = class extends RegExp {
4+
constructor(regExpString, flags) {
5+
if (flags?.includes('u') && regExpString.includes('\\p{')) {
6+
throw new Error('Invalid property name');
7+
}
8+
9+
super(regExpString, flags);
10+
}
11+
12+
static isMocked = true;
13+
};
14+
15+
// Execa computes the RegExp when first loaded, so we must delay this import
16+
await import('./escape.js');

test/arguments/escape.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ test(testResultCommand, ' foo bar', 'foo', 'bar');
2121
test(testResultCommand, ' baz quz', 'baz', 'quz');
2222
test(testResultCommand, '');
2323

24-
const testEscapedCommand = async (t, commandArguments, expectedUnix, expectedWindows) => {
25-
const expected = isWindows ? expectedWindows : expectedUnix;
24+
// eslint-disable-next-line max-params
25+
const testEscapedCommand = async (t, commandArguments, expectedUnix, expectedWindows, expectedUnixNoIcu = expectedUnix, expectedWindowsNoIcu = expectedWindows) => {
26+
const expected = RegExp.isMocked
27+
? (isWindows ? expectedWindowsNoIcu : expectedUnixNoIcu)
28+
: (isWindows ? expectedWindows : expectedUnix);
2629

2730
t.like(
2831
await t.throwsAsync(execa('fail.js', commandArguments)),
@@ -89,12 +92,12 @@ test('result.escapedCommand - \\x01', testEscapedCommand, ['\u0001'], '\'\\u0001
8992
test('result.escapedCommand - \\x7f', testEscapedCommand, ['\u007F'], '\'\\u007f\'', '"\\u007f"');
9093
test('result.escapedCommand - \\u0085', testEscapedCommand, ['\u0085'], '\'\\u0085\'', '"\\u0085"');
9194
test('result.escapedCommand - \\u2000', testEscapedCommand, ['\u2000'], '\'\\u2000\'', '"\\u2000"');
92-
test('result.escapedCommand - \\u200E', testEscapedCommand, ['\u200E'], '\'\\u200e\'', '"\\u200e"');
95+
test('result.escapedCommand - \\u200E', testEscapedCommand, ['\u200E'], '\'\\u200e\'', '"\\u200e"', '\'\u200E\'', '"\u200E"');
9396
test('result.escapedCommand - \\u2028', testEscapedCommand, ['\u2028'], '\'\\u2028\'', '"\\u2028"');
9497
test('result.escapedCommand - \\u2029', testEscapedCommand, ['\u2029'], '\'\\u2029\'', '"\\u2029"');
9598
test('result.escapedCommand - \\u5555', testEscapedCommand, ['\u5555'], '\'\u5555\'', '"\u5555"');
96-
test('result.escapedCommand - \\uD800', testEscapedCommand, ['\uD800'], '\'\\ud800\'', '"\\ud800"');
97-
test('result.escapedCommand - \\uE000', testEscapedCommand, ['\uE000'], '\'\\ue000\'', '"\\ue000"');
99+
test('result.escapedCommand - \\uD800', testEscapedCommand, ['\uD800'], '\'\\ud800\'', '"\\ud800"', '\'\uD800\'', '"\uD800"');
100+
test('result.escapedCommand - \\uE000', testEscapedCommand, ['\uE000'], '\'\\ue000\'', '"\\ue000"', '\'\uE000\'', '"\uE000"');
98101
test('result.escapedCommand - \\U1D172', testEscapedCommand, ['\u{1D172}'], '\'\u{1D172}\'', '"\u{1D172}"');
99-
test('result.escapedCommand - \\U1D173', testEscapedCommand, ['\u{1D173}'], '\'\\U1d173\'', '"\\U1d173"');
100-
test('result.escapedCommand - \\U10FFFD', testEscapedCommand, ['\u{10FFFD}'], '\'\\U10fffd\'', '"\\U10fffd"');
102+
test('result.escapedCommand - \\U1D173', testEscapedCommand, ['\u{1D173}'], '\'\\U1d173\'', '"\\U1d173"', '\'\u{1D173}\'', '"\u{1D173}"');
103+
test('result.escapedCommand - \\U10FFFD', testEscapedCommand, ['\u{10FFFD}'], '\'\\U10fffd\'', '"\\U10fffd"', '\'\u{10FFFD}\'', '"\u{10FFFD}"');

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy