From c9ef8ca3a1659a64af9054910ab30a44d256df5c Mon Sep 17 00:00:00 2001 From: theMackabu Date: Fri, 10 Apr 2026 12:24:30 -0700 Subject: [PATCH] expand Node compatibility coverage across builtins and packages - Add Object.getOwnPropertyDescriptors support - Treat native C functions as objects for Object prototype operations. - Preserve display names for registered builtin module namespaces and use node: aliases in missing named export errors. - Attach module context metadata to cached builtin namespaces. - Register fs constants as the constants module. - Add fs chmod/chmodSync support with numeric and octal-string modes. - Add fs readlink/readlinkSync support. - Export fs constants with additional open and stat mode constants. - Add fs.realpath.native callback compatibility. - Add path/posix and path/win32 registered module variants. - Add buffer constants, kMaxLength, kStringMaxLength, and INSPECT_MAX_BYTES exports. - Brand AbortSignal objects and validate abort signal APIs by brand. - Add process.emitWarning formatting and warning event payload support. - Add stream default high water mark getters and setters. - Add util.callbackify. - Add util.aborted for AbortSignal promises. - Add parser support for parenthesized comma expressions in arrow parsing. - Allow class members named static when they are methods or fields. - Add super spread-call support through a SUPER_APPLY opcode. - Add package resolver support for npm: alias dependency specs. - Include parent path in package traversal de-duplication keys. - Reuse cached package entries with file counts during exec temp linking. - Avoid skipping installed packages when cached file counts indicate incomplete installs. - Infer package binary names from package.json for exec temp runs. --- examples/results.txt | 2 +- examples/spec/arrow.js | 9 + examples/spec/atomics.js | 35 ++ examples/spec/buffer.js | 25 +- examples/spec/child_process.js | 54 ++- examples/spec/classes.js | 31 ++ examples/spec/events.js | 79 ++++- examples/spec/iterators.js | 16 +- examples/spec/objects.js | 46 ++- examples/spec/path.js | 7 + examples/spec/process.js | 8 + include/common.h | 5 +- include/gc/modules.h | 1 + include/modules/fs.h | 1 + include/modules/path.h | 2 + include/silver/opcode.h | 1 + src/ant.c | 82 ++++- src/esm/library.c | 29 +- src/gc/objects.c | 1 + src/main.c | 4 + src/modules/abort.c | 13 +- src/modules/buffer.c | 17 + src/modules/child_process.c | 40 ++- src/modules/events.c | 199 ++++++++++- src/modules/fs.c | 228 ++++++++++++- src/modules/path.c | 14 + src/modules/process.c | 202 ++++++++--- src/modules/stream.c | 41 ++- src/modules/util.c | 105 +++++- src/pkg/linker.zig | 27 +- src/pkg/resolver.zig | 313 +++++++++++------- src/pkg/root.zig | 124 ++++++- src/silver/ast.c | 27 +- src/silver/compiler.c | 27 +- src/silver/engine.c | 7 +- src/silver/ops/calls.h | 23 ++ src/silver/ops/coercion.h | 11 +- src/types/buffer.d.ts | 17 + src/types/modules/child_process.d.ts | 4 +- src/types/modules/constants.d.ts | 12 + src/types/modules/fs.d.ts | 6 + src/types/modules/stream.d.ts | 16 +- src/types/modules/util.d.ts | 2 + .../builtin_missing_named_export_importer.mjs | 3 + tests/test_builtin_missing_named_export.mjs | 21 ++ tests/test_fs_chmod_sync.mjs | 40 +++ tests/test_process_emit_warning.cjs | 66 ++++ 47 files changed, 1794 insertions(+), 249 deletions(-) create mode 100644 src/types/modules/constants.d.ts create mode 100644 tests/builtin_missing_named_export_importer.mjs create mode 100644 tests/test_builtin_missing_named_export.mjs create mode 100644 tests/test_fs_chmod_sync.mjs create mode 100644 tests/test_process_emit_warning.cjs diff --git a/examples/results.txt b/examples/results.txt index 44776b0..bc6fbec 100644 --- a/examples/results.txt +++ b/examples/results.txt @@ -1143,7 +1143,7 @@ compat-table/es2017/Atomics.sub.js: OK compat-table/es2017/Atomics.wait.js: OK compat-table/es2017/Atomics.xor.js: OK compat-table/es2017/Object.entries.js: OK -compat-table/es2017/Object.getOwnPropertyDescriptors.js: TypeError: undefined is not a function +compat-table/es2017/Object.getOwnPropertyDescriptors.js: OK compat-table/es2017/Object.getOwnPropertyDescriptors.no-undefined.js: TypeError: undefined is not a function compat-table/es2017/Object.values.js: OK compat-table/es2017/SharedArrayBuffer.Symbol.species.js: failed diff --git a/examples/spec/arrow.js b/examples/spec/arrow.js index f3cc32f..1dfd26f 100644 --- a/examples/spec/arrow.js +++ b/examples/spec/arrow.js @@ -17,6 +17,15 @@ test('arrow no params', noParams(), 42); const implicit = (a, b) => a - b; test('arrow implicit return', implicit(10, 3), 7); +const trailingSingle = a => a + 1; +test('arrow trailing comma single param', trailingSingle(4), 5); + +const trailingMulti = (a, b) => a * b; +test('arrow trailing comma multi param', trailingMulti(4, 5), 20); + +const trailingDestructure = ({ value }, extra) => value + extra; +test('arrow trailing comma destructuring param', trailingDestructure({ value: 7 }, 3), 10); + const block = (a, b) => { const sum = a + b; return sum * 2; diff --git a/examples/spec/atomics.js b/examples/spec/atomics.js index 9f569f4..812d869 100644 --- a/examples/spec/atomics.js +++ b/examples/spec/atomics.js @@ -38,4 +38,39 @@ Atomics.store(int32, 0, 5); test('Atomics.compareExchange no match', Atomics.compareExchange(int32, 0, 99, 10), 5); test('after no match', Atomics.load(int32, 0), 5); +Atomics.store(int32, 0, 1); +const asyncNotEqual = Atomics.waitAsync(int32, 0, 2); +test('Atomics.waitAsync not-equal sync', asyncNotEqual.async, false); +test('Atomics.waitAsync not-equal value', asyncNotEqual.value, 'not-equal'); + +const asyncTimedOutSync = Atomics.waitAsync(int32, 0, 1, 0); +test('Atomics.waitAsync zero timeout sync', asyncTimedOutSync.async, false); +test('Atomics.waitAsync zero timeout value', asyncTimedOutSync.value, 'timed-out'); + +const timedWait = Atomics.waitAsync(int32, 0, 1, 5); +test('Atomics.waitAsync timeout is async', timedWait.async, true); +test('Atomics.waitAsync timeout value', await timedWait.value, 'timed-out'); + +Atomics.store(int32, 0, 3); +const notifiedWait = Atomics.waitAsync(int32, 0, 3, 1000); +test('Atomics.waitAsync notify is async', notifiedWait.async, true); +test('Atomics.notify resolves async waiter count', Atomics.notify(int32, 0, 1), 1); +test('Atomics.waitAsync notify value', await notifiedWait.value, 'ok'); + +Atomics.store(int32, 0, 4); +let keepChecking = true; +let waitAsyncChecks = 0; +function checkWithoutSpinning() { + if (!keepChecking) return; + waitAsyncChecks++; + const result = Atomics.waitAsync(int32, 0, 4, 1000); + if (result.async) result.value.then(checkWithoutSpinning); + else setImmediate(checkWithoutSpinning); +} +checkWithoutSpinning(); +await new Promise(resolve => setImmediate(resolve)); +test('pending waitAsync yields to immediates', waitAsyncChecks, 1); +keepChecking = false; +Atomics.notify(int32, 0, 1); + summary(); diff --git a/examples/spec/buffer.js b/examples/spec/buffer.js index 40c5b50..9c4d700 100644 --- a/examples/spec/buffer.js +++ b/examples/spec/buffer.js @@ -1,4 +1,5 @@ import { test, testThrows, summary } from './helpers.js'; +import * as buffer from 'node:buffer'; console.log('Buffer Tests\n'); @@ -71,34 +72,34 @@ const bu64 = new BigUint64Array(2); test('BigUint64Array length', bu64.length, 2); test('BigUint64Array BYTES_PER_ELEMENT', bu64.BYTES_PER_ELEMENT, 8); -const buffer = new ArrayBuffer(16); -const view8 = new Uint8Array(buffer); -const view16 = new Uint16Array(buffer); -const view32 = new Uint32Array(buffer); -const view16f = new Float16Array(buffer); +const backingBuffer = new ArrayBuffer(16); +const view8 = new Uint8Array(backingBuffer); +const view16 = new Uint16Array(backingBuffer); +const view32 = new Uint32Array(backingBuffer); +const view16f = new Float16Array(backingBuffer); test('Uint8Array view length', view8.length, 16); test('Uint16Array view length', view16.length, 8); test('Uint32Array view length', view32.length, 4); test('Float16Array view length', view16f.length, 8); -const viewWithOffset = new Uint8Array(buffer, 4); +const viewWithOffset = new Uint8Array(backingBuffer, 4); test('view with offset length', viewWithOffset.length, 12); test('view with offset byteOffset', viewWithOffset.byteOffset, 4); -const viewWithOffsetAndLength = new Uint8Array(buffer, 4, 8); +const viewWithOffsetAndLength = new Uint8Array(backingBuffer, 4, 8); test('view offset+length length', viewWithOffsetAndLength.length, 8); test('view offset+length byteOffset', viewWithOffsetAndLength.byteOffset, 4); -const dv = new DataView(buffer); +const dv = new DataView(backingBuffer); test('DataView byteLength', dv.byteLength, 16); test('DataView byteOffset', dv.byteOffset, 0); -const dv2 = new DataView(buffer, 4); +const dv2 = new DataView(backingBuffer, 4); test('DataView offset byteLength', dv2.byteLength, 12); test('DataView offset byteOffset', dv2.byteOffset, 4); -const dv3 = new DataView(buffer, 4, 8); +const dv3 = new DataView(backingBuffer, 4, 8); test('DataView offset+length byteLength', dv3.byteLength, 8); test('DataView offset+length byteOffset', dv3.byteOffset, 4); @@ -167,6 +168,10 @@ test('Buffer.isEncoding empty', Buffer.isEncoding(''), false); test('Buffer.byteLength string', Buffer.byteLength('Hello'), 5); test('Buffer.byteLength buffer', Buffer.byteLength(Buffer.from('Hello')), 5); test('Buffer.byteLength empty', Buffer.byteLength(''), 0); +test('buffer.constants exists', typeof buffer.constants, 'object'); +test('buffer.constants.MAX_STRING_LENGTH exists', typeof buffer.constants.MAX_STRING_LENGTH, 'number'); +test('buffer kStringMaxLength mirrors constants', buffer.kStringMaxLength, buffer.constants.MAX_STRING_LENGTH); +test('buffer kMaxLength mirrors constants', buffer.kMaxLength, buffer.constants.MAX_LENGTH); const concatBuf1 = Buffer.from('Hello'); const concatBuf2 = Buffer.from(' '); diff --git a/examples/spec/child_process.js b/examples/spec/child_process.js index d3f2094..a92d154 100644 --- a/examples/spec/child_process.js +++ b/examples/spec/child_process.js @@ -1,5 +1,6 @@ import { test, testThrows, summary } from './helpers.js'; import { spawn, exec, execSync, spawnSync } from 'child_process'; +import { ChildProcess as NodeChildProcess } from 'node:child_process'; console.log('Child Process Tests\n'); @@ -45,12 +46,22 @@ let spawnClosed = false; const child = spawn('sh', ['-c', 'echo line1; echo line2']); +test('node:child_process exports ChildProcess', typeof NodeChildProcess, 'function'); test('spawn returns object', typeof child, 'object'); +test('spawn returns ChildProcess instance', child instanceof NodeChildProcess, true); test('spawn has pid', child.pid > 0, true); test('spawn has on method', typeof child.on, 'function'); test('spawn has once method', typeof child.once, 'function'); test('spawn has kill method', typeof child.kill, 'function'); +const constructedChild = new NodeChildProcess(); +let constructedChildEvent = false; +constructedChild.on('ping', () => { + constructedChildEvent = true; +}); +constructedChild.emit('ping'); +test('new ChildProcess has EventEmitter behavior', constructedChildEvent, true); + child.on('stdout', data => { spawnStdout += data; }); @@ -59,13 +70,15 @@ child.on('exit', code => { spawnExitCode = code; }); -const childClosedP = new Promise(resolve => child.on('close', () => { - spawnClosed = true; - test('spawn collects stdout', spawnStdout.trim(), 'line1\nline2'); - test('spawn exit code', spawnExitCode, 0); - test('spawn close event fired', spawnClosed, true); - resolve(); -})); +const childClosedP = new Promise(resolve => + child.on('close', () => { + spawnClosed = true; + test('spawn collects stdout', spawnStdout.trim(), 'line1\nline2'); + test('spawn exit code', spawnExitCode, 0); + test('spawn close event fired', spawnClosed, true); + resolve(); + }) +); let stdinRoundTrip = ''; const stdinChild = spawn('cat'); @@ -75,18 +88,23 @@ stdinChild.on('stdout', data => { stdinChild.stdin.write('ping'); stdinChild.stdin.end(); -const stdinChildDoneP = new Promise(resolve => stdinChild.on('close', () => { - test('spawn stdin round trip', stdinRoundTrip, 'ping'); - resolve(); -})); - -const shellChildDoneP = childClosedP.then(() => new Promise(resolve => { - const shellChild = spawn('echo $HOME', [], { shell: true }); - shellChild.on('close', () => { - test('spawn with shell option', shellChild.stdout.length > 0, true); +const stdinChildDoneP = new Promise(resolve => + stdinChild.on('close', () => { + test('spawn stdin round trip', stdinRoundTrip, 'ping'); resolve(); - }); -})); + }) +); + +const shellChildDoneP = childClosedP.then( + () => + new Promise(resolve => { + const shellChild = spawn('echo $HOME', [], { shell: true }); + shellChild.on('close', () => { + test('spawn with shell option', shellChild.stdout.length > 0, true); + resolve(); + }); + }) +); const stderrChild = spawn('sh', ['-c', 'echo err >&2']); let stderrData = ''; diff --git a/examples/spec/classes.js b/examples/spec/classes.js index 72fe505..7aa72a8 100644 --- a/examples/spec/classes.js +++ b/examples/spec/classes.js @@ -160,6 +160,21 @@ class Bird extends Animal { let bird = new Bird('Tweety'); test('super[method]()', bird.speak(), 'Tweety makes a sound - chirp!'); +class SpreadBase { + constructor(a, b) { + this.sum = a + b; + } +} + +class SpreadDerived extends SpreadBase { + constructor() { + super(...arguments); + } +} + +let spreadDerived = new SpreadDerived(3, 4); +test('super spread arguments', spreadDerived.sum, 7); + class Static { static value = 42; static method() { @@ -170,6 +185,22 @@ class Static { test('static property', Static.value, 42); test('static method', Static.method(), 'static'); +class StaticNamedElements { + static(value) { + return value + 1; + } +} + +let staticNamedElements = new StaticNamedElements(); +test('instance method named static', staticNamedElements.static(4), 5); + +class StaticNamedField { + static = 7; +} + +let staticNamedField = new StaticNamedField(); +test('instance field named static', staticNamedField.static, 7); + class BankAccount { #balance = 0; diff --git a/examples/spec/events.js b/examples/spec/events.js index e4b7733..66af0c8 100644 --- a/examples/spec/events.js +++ b/examples/spec/events.js @@ -1,5 +1,6 @@ -import { test, summary } from './helpers.js'; +import { test, testDeep, summary } from './helpers.js'; import { EventEmitter } from 'ant:events'; +import { once as nodeOnce, addAbortListener, getMaxListeners, setMaxListeners } from 'node:events'; console.log('EventEmitter Tests\n'); @@ -207,4 +208,80 @@ test('global dispatchEvent with CustomEvent', ceGlobal, 42); const ce3 = new CustomEvent('tagged'); test('CustomEvent Symbol.toStringTag', Object.prototype.toString.call(ce3), '[object CustomEvent]'); +console.log('\nnode:events Helper Tests\n'); + +const eeNodeOnce = new EventEmitter(); +const eeNodeOncePromise = nodeOnce(eeNodeOnce, 'ready'); +eeNodeOnce.emit('ready', 'ok', 7); +testDeep('events.once resolves emitted args', await eeNodeOncePromise, ['ok', 7]); + +const etNodeOnce = new EventTarget(); +const etNodeOncePromise = nodeOnce(etNodeOnce, 'ping'); +etNodeOnce.dispatchEvent(new Event('ping')); +const etNodeOnceArgs = await etNodeOncePromise; +test('events.once supports EventTarget', etNodeOnceArgs[0].type, 'ping'); + +const abortController = new AbortController(); +const abortPromise = nodeOnce(abortController.signal, 'abort'); +abortController.abort('stop'); +const abortArgs = await abortPromise; +test('events.once supports AbortSignal', abortArgs[0].type, 'abort'); + +let duckOnceRejected = false; +try { + await nodeOnce( + { + once(_name, listener) { + listener('bad'); + } + }, + 'ready' + ); +} catch { + duckOnceRejected = true; +} +test('events.once rejects once-shaped plain objects', duckOnceRejected, true); + +let protoEmitterRejected = false; +try { + await nodeOnce(Object.create(EventEmitter.prototype), 'ready'); +} catch { + protoEmitterRejected = true; +} +test('events.once rejects EventEmitter prototype spoofing', protoEmitterRejected, true); + +let duckTargetRejected = false; +try { + await nodeOnce( + { + addEventListener(_name, listener) { + listener(new Event('bad')); + } + }, + 'ready' + ); +} catch { + duckTargetRejected = true; +} +test('events.once rejects EventTarget-shaped plain objects', duckTargetRejected, true); + +let protoTargetRejected = false; +try { + await nodeOnce(Object.create(EventTarget.prototype), 'ready'); +} catch { + protoTargetRejected = true; +} +test('events.once rejects EventTarget prototype spoofing', protoTargetRejected, true); + +let addAbortValue = 0; +const addAbortController = new AbortController(); +const disposable = addAbortListener(addAbortController.signal, () => { + addAbortValue++; +}); +addAbortController.abort(); +test('events.addAbortListener fires on abort', addAbortValue, 1); +test('events.addAbortListener returns disposable', typeof disposable.dispose, 'function'); +test('events.getMaxListeners default', getMaxListeners(eeNodeOnce), 10); +test('events.setMaxListeners no-op return', setMaxListeners(20, eeNodeOnce), undefined); + summary(); diff --git a/examples/spec/iterators.js b/examples/spec/iterators.js index 0503367..4f14372 100644 --- a/examples/spec/iterators.js +++ b/examples/spec/iterators.js @@ -16,7 +16,10 @@ test('string iterator second', strIter.next().value, 'b'); test('string iterator third', strIter.next().value, 'c'); test('string iterator done', strIter.next().done, true); -const map = new Map([['a', 1], ['b', 2]]); +const map = new Map([ + ['a', 1], + ['b', 2] +]); const mapIter = map.entries(); testDeep('map entries first', mapIter.next().value, ['a', 1]); testDeep('map entries second', mapIter.next().value, ['b', 2]); @@ -59,4 +62,15 @@ for (const n of custom) { } test('custom iterator', customSum, 60); +let capturedNestedForOf = 0; +const capturedFns = {}; +for (const { values = [] } of [{ values: ['a', 'bb'] }]) { + for (const value of values) { + capturedFns[value] = () => value.length; + capturedNestedForOf++; + } +} +test('captured nested for-of count', capturedNestedForOf, 2); +test('captured nested for-of closure', capturedFns.bb(), 2); + summary(); diff --git a/examples/spec/objects.js b/examples/spec/objects.js index 2bd89bc..671e8da 100644 --- a/examples/spec/objects.js +++ b/examples/spec/objects.js @@ -60,8 +60,8 @@ test('destructuring rename', renamed, 5); let { y = 10 } = {}; test('destructuring default', y, 10); -test('hasOwnProperty true', ({ a: 1 }).hasOwnProperty('a'), true); -test('hasOwnProperty false', ({ a: 1 }).hasOwnProperty('b'), false); +test('hasOwnProperty true', { a: 1 }.hasOwnProperty('a'), true); +test('hasOwnProperty false', { a: 1 }.hasOwnProperty('b'), false); let frozen = Object.freeze({ a: 1 }); test('Object.isFrozen', Object.isFrozen(frozen), true); @@ -75,6 +75,46 @@ test('Object.create inherits', child.inherited, true); test('Object.getPrototypeOf', Object.getPrototypeOf([]) === Array.prototype, true); -testDeep('Object.fromEntries', Object.fromEntries([['a', 1], ['b', 2]]), { a: 1, b: 2 }); +testDeep( + 'Object.fromEntries', + Object.fromEntries([ + ['a', 1], + ['b', 2] + ]), + { a: 1, b: 2 } +); + +let descriptorSource = {}; +const descriptorSymbol = Symbol('descriptor'); +Object.defineProperty(descriptorSource, 'hidden', { + value: 42, + enumerable: false, + writable: false, + configurable: false +}); +Object.defineProperty(descriptorSource, 'computed', { + get() { + return 7; + }, + enumerable: true, + configurable: true +}); +descriptorSource.visible = 'yes'; +descriptorSource[descriptorSymbol] = 'symbol value'; + +const descriptors = Object.getOwnPropertyDescriptors(descriptorSource); +test('Object.getOwnPropertyDescriptors exists', typeof Object.getOwnPropertyDescriptors, 'function'); +test('descriptors data value', descriptors.visible.value, 'yes'); +test('descriptors hidden value', descriptors.hidden.value, 42); +test('descriptors hidden enumerable', descriptors.hidden.enumerable, false); +test('descriptors hidden writable', descriptors.hidden.writable, false); +test('descriptors hidden configurable', descriptors.hidden.configurable, false); +test('descriptors accessor getter', typeof descriptors.computed.get, 'function'); +test('descriptors accessor enumerable', descriptors.computed.enumerable, true); +test('descriptors symbol value', descriptors[descriptorSymbol].value, 'symbol value'); + +const arrayDescriptors = Object.getOwnPropertyDescriptors(['item']); +test('array descriptor index value', arrayDescriptors[0].value, 'item'); +test('array descriptor length value', arrayDescriptors.length.value, 1); summary(); diff --git a/examples/spec/path.js b/examples/spec/path.js index 59f73b3..dcb234d 100644 --- a/examples/spec/path.js +++ b/examples/spec/path.js @@ -1,5 +1,8 @@ import { test, summary } from './helpers.js'; import path from 'ant:path'; +import nodePathPosix from 'node:path/posix'; +import antPathPosix from 'ant:path/posix'; +import barePathPosix from 'path/posix'; console.log('Path Tests\n'); @@ -33,4 +36,8 @@ test('path.format', path.format({ dir: '/home/user', base: 'file.txt' }), '/home test('path.sep', path.sep, '/'); test('path.delimiter', path.delimiter, ':'); +test('node:path/posix join', nodePathPosix.join('a', 'b'), 'a/b'); +test('ant:path/posix sep', antPathPosix.sep, '/'); +test('path/posix dirname', barePathPosix.dirname('/a/b/c.txt'), '/a/b'); + summary(); diff --git a/examples/spec/process.js b/examples/spec/process.js index c02e113..3617267 100644 --- a/examples/spec/process.js +++ b/examples/spec/process.js @@ -1,8 +1,16 @@ import { test, summary } from './helpers.js'; +import nodeProcess, { cwd as nodeCwd, execPath as nodeExecPath, hrtime as nodeHrtime, stdout as nodeStdout } from 'node:process'; console.log('Process Tests\n'); test('process exists', typeof process, 'object'); +test('node:process default is global process', nodeProcess, process); +test('node:process named cwd is function', typeof nodeCwd, 'function'); +test('node:process named execPath is string', typeof nodeExecPath, 'string'); +test('node:process named hrtime is function', typeof nodeHrtime, 'function'); +test('node:process named hrtime.bigint is function', typeof nodeHrtime.bigint, 'function'); +test('node:process named hrtime matches process.hrtime', nodeHrtime, process.hrtime); +test('node:process named stdout is global stdout', nodeStdout, process.stdout); test('process toStringTag', Object.prototype.toString.call(process), '[object process]'); test('pid is number', typeof process.pid, 'number'); diff --git a/include/common.h b/include/common.h index c3532a8..cc8e1b3 100644 --- a/include/common.h +++ b/include/common.h @@ -114,7 +114,10 @@ typedef enum { BRAND_WASM_TAG, BRAND_WASM_EXCEPTION, BRAND_DATE, - BRAND_MODULE_NAMESPACE + BRAND_MODULE_NAMESPACE, + BRAND_ABORT_SIGNAL, + BRAND_EVENTEMITTER, + BRAND_EVENTTARGET } object_brand_id_t; static inline void *mantissa_chk(void *p, const char *func) { diff --git a/include/gc/modules.h b/include/gc/modules.h index cfb8126..57feccd 100644 --- a/include/gc/modules.h +++ b/include/gc/modules.h @@ -6,6 +6,7 @@ typedef void (*gc_mark_fn)(ant_t *js, ant_value_t v); void gc_mark_timers(ant_t *js, gc_mark_fn mark); +void gc_mark_atomics(ant_t *js, gc_mark_fn mark); void gc_mark_ffi(ant_t *js, gc_mark_fn mark); void gc_mark_fetch(ant_t *js, gc_mark_fn mark); void gc_mark_fs(ant_t *js, gc_mark_fn mark); diff --git a/include/modules/fs.h b/include/modules/fs.h index b8f4a8f..7aec8e3 100644 --- a/include/modules/fs.h +++ b/include/modules/fs.h @@ -5,6 +5,7 @@ ant_value_t fs_library(ant_t *js); ant_value_t fs_promises_library(ant_t *js); +ant_value_t fs_constants_library(ant_t *js); int has_pending_fs_ops(void); void init_fs_module(void); diff --git a/include/modules/path.h b/include/modules/path.h index 59cc1d3..09e1f19 100644 --- a/include/modules/path.h +++ b/include/modules/path.h @@ -4,5 +4,7 @@ #include "types.h" ant_value_t path_library(ant_t *js); +ant_value_t path_posix_library(ant_t *js); +ant_value_t path_win32_library(ant_t *js); #endif diff --git a/include/silver/opcode.h b/include/silver/opcode.h index 17fc763..7822cc0 100644 --- a/include/silver/opcode.h +++ b/include/silver/opcode.h @@ -162,6 +162,7 @@ OP_DEF( TAIL_CALL, 3, 1, 0, npop) /* tail-position call */ OP_DEF( TAIL_CALL_METHOD, 3, 2, 0, npop) OP_DEF( NEW, 3, 2, 1, npop) /* func new.target args -> obj */ OP_DEF( APPLY, 3, 3, 1, u16) /* func this [args] -> result */ +OP_DEF( SUPER_APPLY, 3, 3, 1, u16) /* super this [args] -> result */ OP_DEF( NEW_APPLY, 3, 2, 1, u16) /* func new.target [args] -> obj */ OP_DEF( EVAL, 5, 1, 1, npop) /* direct eval */ OP_DEF( RETURN, 1, 1, 0, none) diff --git a/src/ant.c b/src/ant.c index 85c43bb..ea08ab4 100644 --- a/src/ant.c +++ b/src/ant.c @@ -5604,6 +5604,7 @@ static ant_value_t builtin_object_getPrototypeOf(ant_t *js, ant_value_t *args, i uint8_t t = vtype(obj); if (t == T_STR || t == T_NUM || t == T_BOOL || t == T_BIGINT) return get_prototype_for_type(js, t); + if (t == T_CFUNC) return get_prototype_for_type(js, t); if (is_object_type(obj)) return get_proto(js, obj); return js_mknull(); @@ -5616,11 +5617,21 @@ static ant_value_t builtin_object_setPrototypeOf(ant_t *js, ant_value_t *args, i ant_value_t proto = args[1]; uint8_t t = vtype(obj); + if (t == T_CFUNC) { + obj = js_cfunc_promote(js, obj); + t = T_FUNC; + } + if (t != T_OBJ && t != T_ARR && t != T_FUNC) { return js_mkerr(js, "Object.setPrototypeOf: first argument must be an object"); } uint8_t pt = vtype(proto); + if (pt == T_CFUNC) { + proto = js_cfunc_promote(js, proto); + pt = T_FUNC; + } + if (pt != T_OBJ && pt != T_ARR && pt != T_FUNC && pt != T_NULL) { return js_mkerr(js, "Object.setPrototypeOf: prototype must be an object or null"); } @@ -6327,6 +6338,19 @@ static ant_value_t builtin_object_getOwnPropertyDescriptor(ant_t *js, ant_value_ return proxy_get_own_property_descriptor(js, as_obj, key); } + bool is_arr_obj = array_obj_ptr(as_obj) != NULL; + bool is_arr_length = !is_sym && is_arr_obj && is_length_key(key_str, key_len); + bool has_arr_index = false; + ant_value_t arr_index_val = js_mkundef(); + + if (!is_sym && is_arr_obj && is_array_index(key_str, key_len)) { + unsigned long idx; + if ( + parse_array_index(key_str, key_len, get_array_length(js, as_obj), &idx) && + arr_has(js, as_obj, (ant_offset_t)idx) + ) { has_arr_index = true; arr_index_val = arr_get(js, as_obj, (ant_offset_t)idx); } + } + ant_offset_t sym_off = is_sym ? (ant_offset_t)vdata(key) : 0; prop_meta_t sym_meta; prop_meta_t str_meta; @@ -6334,7 +6358,7 @@ static ant_value_t builtin_object_getOwnPropertyDescriptor(ant_t *js, ant_value_ bool has_str_meta = is_sym ? false : lookup_string_prop_meta(js, as_obj, key_str, (size_t)key_len, &str_meta); ant_offset_t prop_off = is_sym ? lkp_sym(js, as_obj, sym_off) : lkp(js, as_obj, key_str, key_len); - if (prop_off == 0 && !(is_sym ? has_sym_meta : has_str_meta)) { + if (prop_off == 0 && !(is_sym ? has_sym_meta : has_str_meta) && !is_arr_length && !has_arr_index) { return js_mkundef(); } @@ -6378,7 +6402,10 @@ static ant_value_t builtin_object_getOwnPropertyDescriptor(ant_t *js, ant_value_ if (prop_off != 0) { prop_val = propref_load(js, prop_off); has_value_out = true; - } else if (!is_sym && is_length_key(key_str, key_len) && array_obj_ptr(as_obj)) { + } else if (has_arr_index) { + prop_val = arr_index_val; + has_value_out = true; + } else if (is_arr_length) { prop_val = tov((double)get_array_length(js, as_obj)); has_value_out = true; } @@ -6473,6 +6500,51 @@ static ant_value_t builtin_object_getOwnPropertySymbols(ant_t *js, ant_value_t * return mkval(T_ARR, vdata(arr)); } +static ant_value_t object_add_descriptors_for_keys( + ant_t *js, + ant_value_t result, + ant_value_t obj, + ant_value_t keys +) { + ant_offset_t len = get_array_length(js, keys); + + for (ant_offset_t i = 0; i < len; i++) { + ant_value_t key = arr_get(js, keys, i); + if (vtype(key) == T_UNDEF) continue; + + ant_value_t desc_args[2] = { obj, key }; + ant_value_t desc = builtin_object_getOwnPropertyDescriptor(js, desc_args, 2); + if (is_err(desc)) return desc; + if (vtype(desc) == T_UNDEF) continue; + + ant_value_t set_result = js_setprop(js, result, key, desc); + if (is_err(set_result)) return set_result; + } + + return js_mkundef(); +} + +static ant_value_t builtin_object_getOwnPropertyDescriptors(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t result = js_mkobj(js); + if (nargs == 0) return result; + + ant_value_t obj = args[0]; + uint8_t t = vtype(obj); + if (t != T_OBJ && t != T_ARR && t != T_FUNC) return result; + + ant_value_t names = builtin_object_getOwnPropertyNames(js, &obj, 1); + if (is_err(names)) return names; + ant_value_t err = object_add_descriptors_for_keys(js, result, obj, names); + if (is_err(err)) return err; + + ant_value_t symbols = builtin_object_getOwnPropertySymbols(js, &obj, 1); + if (is_err(symbols)) return symbols; + err = object_add_descriptors_for_keys(js, result, obj, symbols); + if (is_err(err)) return err; + + return result; +} + static ant_value_t builtin_object_isExtensible(ant_t *js, ant_value_t *args, int nargs) { if (nargs == 0) return js_true; @@ -11819,18 +11891,21 @@ ant_value_t js_create_module_context(ant_t *js, const char *filename, bool is_ma GC_ROOT_RESTORE(js, root_mark); return filename_val; } + GC_ROOT_PIN(js, filename_val); setprop_cstr(js, module_ctx, "filename", 8, filename_val); + setprop_cstr(js, module_ctx, "displayName", 11, filename_val); ant_value_t import_meta = js_create_import_meta_for_context(js, module_ctx, filename, is_main); if (is_err(import_meta)) { GC_ROOT_RESTORE(js, root_mark); return import_meta; } + GC_ROOT_PIN(js, import_meta); setprop_cstr(js, module_ctx, "meta", 4, import_meta); - GC_ROOT_RESTORE(js, root_mark); + return module_ctx; } @@ -12688,6 +12763,7 @@ ant_t *js_create(void *buf, size_t len) { defmethod(js, obj_func_obj, "isSealed", 8, js_mkfun(builtin_object_isSealed)); defmethod(js, obj_func_obj, "fromEntries", 11, js_mkfun(builtin_object_fromEntries)); defmethod(js, obj_func_obj, "getOwnPropertyDescriptor", 24, js_mkfun(builtin_object_getOwnPropertyDescriptor)); + defmethod(js, obj_func_obj, "getOwnPropertyDescriptors", 25, js_mkfun(builtin_object_getOwnPropertyDescriptors)); defmethod(js, obj_func_obj, "getOwnPropertyNames", 19, js_mkfun(builtin_object_getOwnPropertyNames)); defmethod(js, obj_func_obj, "getOwnPropertySymbols", 21, js_mkfun(builtin_object_getOwnPropertySymbols)); defmethod(js, obj_func_obj, "isExtensible", 12, js_mkfun(builtin_object_isExtensible)); diff --git a/src/esm/library.c b/src/esm/library.c index 9dc3e84..90b88ea 100644 --- a/src/esm/library.c +++ b/src/esm/library.c @@ -1,6 +1,7 @@ #include "esm/library.h" #include "gc/roots.h" #include "ant.h" +#include "internal.h" #include #include @@ -8,6 +9,7 @@ typedef struct ant_library_entry { char name[256]; + char display_name[256]; ant_library_init_fn init_fn; ant_value_t cached_ns; bool ns_initialized; @@ -17,6 +19,15 @@ typedef struct ant_library_entry { static ant_library_entry_t *library_registry = NULL; +static bool ant_library_display_name_is_better(const char *candidate, const char *current) { + if (!candidate || !candidate[0]) return false; + if (!current || !current[0]) return true; + + bool candidate_is_node = strncmp(candidate, "node:", 5) == 0; + bool current_is_node = strncmp(current, "node:", 5) == 0; + return candidate_is_node && !current_is_node; +} + void ant_register_library(ant_library_init_fn init_fn, const char *name, ...) { va_list args; const char *alias = name; @@ -26,15 +37,20 @@ void ant_register_library(ant_library_init_fn init_fn, const char *name, ...) { while (alias != NULL) { ant_library_entry_t *lib = (ant_library_entry_t *)ant_calloc(sizeof(ant_library_entry_t)); if (!lib) break; - + strncpy(lib->name, alias, sizeof(lib->name) - 1); lib->name[sizeof(lib->name) - 1] = '\0'; lib->init_fn = init_fn; lib->ns_initialized = false; - + if (!canonical_entry) canonical_entry = lib; lib->canonical = canonical_entry; - + + if (ant_library_display_name_is_better(alias, canonical_entry->display_name)) { + strncpy(canonical_entry->display_name, alias, sizeof(canonical_entry->display_name) - 1); + canonical_entry->display_name[sizeof(canonical_entry->display_name) - 1] = '\0'; + } + HASH_ADD_STR(library_registry, name, lib); alias = va_arg(args, const char *); } @@ -70,6 +86,13 @@ ant_value_t js_esm_load_registered_library(ant_t *js, const char *specifier, siz if (canon->ns_initialized) return canon->cached_ns; canon->cached_ns = canon->init_fn(js); + if (is_object_type(canon->cached_ns)) { + const char *display_name = canon->display_name[0] ? canon->display_name : canon->name; + ant_value_t module_ctx = js_create_module_context(js, display_name, false); + if (is_err(module_ctx)) return module_ctx; + if (is_object_type(module_ctx)) js_set_slot_wb(js, canon->cached_ns, SLOT_MODULE_CTX, module_ctx); + } + canon->ns_initialized = true; gc_register_root(&canon->cached_ns); diff --git a/src/gc/objects.c b/src/gc/objects.c index dfbde5d..0c5b451 100644 --- a/src/gc/objects.c +++ b/src/gc/objects.c @@ -486,6 +486,7 @@ static void gc_mark_roots(ant_t *js) { gc_visit_roots(js, gc_mark_value); gc_mark_timers(js, gc_mark_value); + gc_mark_atomics(js, gc_mark_value); gc_mark_ffi(js, gc_mark_value); gc_mark_fetch(js, gc_mark_value); gc_mark_fs(js, gc_mark_value); diff --git a/src/main.c b/src/main.c index 5a7a9ed..c675b53 100644 --- a/src/main.c +++ b/src/main.c @@ -643,6 +643,7 @@ int main(int argc, char *argv[]) { ant_standard_library("buffer", buffer_library); ant_standard_library("path", path_library); ant_standard_library("fs", fs_library); + ant_standard_library("constants", fs_constants_library); ant_standard_library("os", os_library); ant_standard_library("url", url_library); ant_standard_library("perf_hooks", perf_hooks_library); @@ -660,6 +661,9 @@ int main(int argc, char *argv[]) { ant_standard_library("stream", stream_library); ant_standard_library("timers", timers_library); + ant_register_library(path_posix_library, "path/posix", "ant:path/posix", "node:path/posix", NULL); + ant_register_library(path_win32_library, "path/win32", "ant:path/win32", "node:path/win32", NULL); + ant_standard_library("fs/promises", fs_promises_library); ant_standard_library("timers/promises", timers_promises_library); ant_standard_library("readline/promises", readline_promises_library); diff --git a/src/modules/abort.c b/src/modules/abort.c index b51b377..a911588 100644 --- a/src/modules/abort.c +++ b/src/modules/abort.c @@ -56,7 +56,7 @@ static abort_signal_data_t *get_signal_data(ant_value_t obj) { static abort_signal_data_t *get_signal_data_if_signal_object(ant_value_t obj) { if (!g_initialized || !is_object_type(obj)) return NULL; - if (js_get_slot(obj, SLOT_PROTO) != g_signal_proto) return NULL; + if (!js_check_brand(obj, BRAND_ABORT_SIGNAL)) return NULL; return get_signal_data(obj); } @@ -155,7 +155,7 @@ void signal_do_abort(ant_t *js, ant_value_t signal_obj, ant_value_t reason) { } void abort_signal_remove_listener(ant_t *js, ant_value_t signal, ant_value_t callback) { - abort_signal_data_t *data = get_signal_data(signal); + abort_signal_data_t *data = get_signal_data_if_signal_object(signal); if (!data) return; unsigned int n = abort_array_len(data->listeners); @@ -187,6 +187,7 @@ static ant_value_t make_new_signal(ant_t *js) { ant_value_t obj = js_mkobj(js); js_set_slot(obj, SLOT_DATA, ANT_PTR(data)); + js_set_slot(obj, SLOT_BRAND, js_mknum(BRAND_ABORT_SIGNAL)); if (g_initialized) js_set_slot_wb(js, obj, SLOT_PROTO, g_signal_proto); js_set(js, obj, "aborted", js_false); @@ -472,21 +473,21 @@ void gc_mark_abort_signal_object(ant_t *js, ant_value_t signal, gc_mark_fn mark) } bool abort_signal_is_aborted(ant_value_t signal) { - abort_signal_data_t *data = get_signal_data(signal); + abort_signal_data_t *data = get_signal_data_if_signal_object(signal); return data && data->aborted; } bool abort_signal_is_signal(ant_value_t signal) { - return get_signal_data(signal) != NULL; + return get_signal_data_if_signal_object(signal) != NULL; } ant_value_t abort_signal_get_reason(ant_value_t signal) { - abort_signal_data_t *data = get_signal_data(signal); + abort_signal_data_t *data = get_signal_data_if_signal_object(signal); return data ? data->reason : js_mkundef(); } void abort_signal_add_listener(ant_t *js, ant_value_t signal, ant_value_t callback) { - abort_signal_data_t *data = get_signal_data(signal); + abort_signal_data_t *data = get_signal_data_if_signal_object(signal); if (!data) return; if (data->aborted) { diff --git a/src/modules/buffer.c b/src/modules/buffer.c index bfb649e..231a105 100644 --- a/src/modules/buffer.c +++ b/src/modules/buffer.c @@ -19,6 +19,12 @@ #define BUFFER_REGISTRY_INITIAL_CAP 64 +// Node compatibility exports only +// Ant does not enforce these as allocation limits +#define BUFFER_COMPAT_MAX_LENGTH 4294967296.0 +#define BUFFER_COMPAT_MAX_STRING_LENGTH 536870888.0 +#define BUFFER_COMPAT_INSPECT_MAX_BYTES 50.0 + static size_t ta_metadata_bytes = 0; static size_t buffer_registry_count = 0; static size_t buffer_registry_cap = 0; @@ -2362,6 +2368,13 @@ static ant_value_t js_sharedarraybuffer_constructor(ant_t *js, ant_value_t *args return obj; } +static ant_value_t buffer_make_constants(ant_t *js) { + ant_value_t constants = js_newobj(js); + js_set(js, constants, "MAX_LENGTH", js_mknum(BUFFER_COMPAT_MAX_LENGTH)); + js_set(js, constants, "MAX_STRING_LENGTH", js_mknum(BUFFER_COMPAT_MAX_STRING_LENGTH)); + return constants; +} + ant_value_t buffer_library(ant_t *js) { ant_value_t glob = js_glob(js); ant_value_t lib = js_newobj(js); @@ -2371,6 +2384,10 @@ ant_value_t buffer_library(ant_t *js) { js_set(js, lib, "File", js_get(js, glob, "File")); js_set(js, lib, "atob", js_get(js, glob, "atob")); js_set(js, lib, "btoa", js_get(js, glob, "btoa")); + js_set(js, lib, "constants", buffer_make_constants(js)); + js_set(js, lib, "kMaxLength", js_mknum(BUFFER_COMPAT_MAX_LENGTH)); + js_set(js, lib, "kStringMaxLength", js_mknum(BUFFER_COMPAT_MAX_STRING_LENGTH)); + js_set(js, lib, "INSPECT_MAX_BYTES", js_mknum(BUFFER_COMPAT_INSPECT_MAX_BYTES)); return lib; } diff --git a/src/modules/child_process.c b/src/modules/child_process.c index 6e83299..8f9d5ff 100644 --- a/src/modules/child_process.c +++ b/src/modules/child_process.c @@ -31,6 +31,7 @@ #include "gc/modules.h" #include "modules/child_process.h" #include "modules/buffer.h" +#include "modules/events.h" #include "modules/symbol.h" #define MAX_CHILD_LISTENERS 16 @@ -109,6 +110,9 @@ struct child_process_s { static child_process_t *pending_children_head = NULL; static child_process_t *pending_children_tail = NULL; +static ant_value_t g_child_process_proto = 0; +static ant_value_t g_child_process_ctor = 0; + static void fprint_js_str_raw(FILE *out, ant_t *js, ant_value_t s) { if (vtype(s) != T_STR) { fprintf(out, "%s\n", js_str(js, s)); @@ -501,7 +505,6 @@ static ant_value_t child_on(ant_t *js, ant_value_t *args, int nargs) { ant_value_t cp_ptr = js_get_slot(this_obj, SLOT_DATA); if (vtype(cp_ptr) == T_UNDEF) return js_mkerr(js, "Invalid child process object"); - child_process_t *cp = (child_process_t *)(uintptr_t)js_getnum(cp_ptr); size_t name_len; @@ -679,6 +682,38 @@ static ant_value_t child_end(ant_t *js, ant_value_t *args, int nargs) { return child_end_impl(cp); } +static ant_value_t child_process_ctor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t obj = js_mkobj(js); + if (is_object_type(g_child_process_proto)) js_set_proto_init(obj, g_child_process_proto); + + js_set(js, obj, "pid", js_mkundef()); + js_set(js, obj, "exitCode", js_mknull()); + js_set(js, obj, "signalCode", js_mknull()); + js_set(js, obj, "killed", js_false); + js_set(js, obj, "connected", js_false); + js_set(js, obj, "stdin", js_mknull()); + js_set(js, obj, "stdout", js_mknull()); + js_set(js, obj, "stderr", js_mknull()); + + return obj; +} + +static void child_process_init_constructor(ant_t *js) { + if (g_child_process_ctor && g_child_process_proto) return; + ant_value_t ee_proto = eventemitter_prototype(js); + + g_child_process_proto = js_mkobj(js); + if (is_object_type(ee_proto)) js_set_proto_init(g_child_process_proto, ee_proto); + js_set(js, g_child_process_proto, "kill", js_mkfun(child_kill)); + js_set(js, g_child_process_proto, "ref", js_mkfun(child_ref)); + js_set(js, g_child_process_proto, "unref", js_mkfun(child_unref)); + + g_child_process_ctor = js_make_ctor( + js, child_process_ctor, + g_child_process_proto, "ChildProcess", 12 + ); +} + static uv_handle_t *child_stream_handle(child_process_t *cp, child_stream_kind_t kind) { if (!cp) return NULL; switch (kind) { @@ -863,6 +898,7 @@ static ant_value_t create_child_stream_object(ant_t *js, child_process_t *cp, ch static ant_value_t create_child_object(ant_t *js, child_process_t *cp) { ant_value_t obj = js_mkobj(js); + if (is_object_type(g_child_process_proto)) js_set_proto_init(obj, g_child_process_proto); js_set_slot(obj, SLOT_DATA, ANT_PTR(cp)); js_set(js, obj, "pid", js_mknum((double)cp->process.pid)); @@ -1703,7 +1739,9 @@ static ant_value_t builtin_fork(ant_t *js, ant_value_t *args, int nargs) { ant_value_t child_process_library(ant_t *js) { ant_value_t lib = js_mkobj(js); + child_process_init_constructor(js); + js_set(js, lib, "ChildProcess", g_child_process_ctor); js_set(js, lib, "spawn", js_mkfun(builtin_spawn)); js_set(js, lib, "exec", js_mkfun(builtin_exec)); js_set(js, lib, "execFile", js_mkfun(builtin_execFile)); diff --git a/src/modules/events.c b/src/modules/events.c index ae96655..aeb79f1 100644 --- a/src/modules/events.c +++ b/src/modules/events.c @@ -28,6 +28,7 @@ typedef struct { static ant_value_t g_isTrusted_getter = 0; static ant_value_t g_eventemitter_ctor = 0; static ant_value_t g_eventemitter_proto = 0; +static ant_value_t g_eventtarget_proto = 0; static ant_value_t g_event_proto = 0; static ant_value_t g_customevent_proto = 0; static ant_value_t g_errorevent_proto = 0; @@ -206,6 +207,14 @@ static inline ant_value_t evt_key_from_arg(ant_value_t arg) { return (t == T_STR || t == T_SYMBOL) ? arg : 0; } +static bool is_eventemitter_instance(ant_value_t target) { + return js_check_brand(target, BRAND_EVENTEMITTER); +} + +static bool is_eventtarget_instance(ant_value_t target) { + return js_check_brand(target, BRAND_EVENTTARGET); +} + static void js_init_event_obj(ant_t *js, ant_value_t obj, ant_value_t type_val, bool bubbles, bool cancelable) { js_set(js, obj, "type", type_val); js_set(js, obj, "target", js_mknull()); @@ -260,6 +269,15 @@ static ant_value_t js_event_get_isTrusted(ant_t *js, ant_value_t *args, int narg return js_false; } +static ant_value_t js_eventemitter_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "EventEmitter constructor requires 'new'"); + + ant_value_t this_obj = js_getthis(js); + if (is_object_type(this_obj)) js_set_slot(this_obj, SLOT_BRAND, js_mknum(BRAND_EVENTEMITTER)); + return js_mkundef(); +} + static ant_value_t js_event_preventDefault(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_obj = js_getthis(js); ant_value_t cancelable = js_get(js, this_obj, "cancelable"); @@ -437,6 +455,15 @@ static ant_value_t js_promiserejectionevent_ctor(ant_t *js, ant_value_t *args, i return this_obj; } +static ant_value_t js_eventtarget_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (vtype(js->new_target) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "EventTarget constructor requires 'new'"); + + ant_value_t this_obj = js_getthis(js); + if (is_object_type(this_obj)) js_set_slot(this_obj, SLOT_BRAND, js_mknum(BRAND_EVENTTARGET)); + return js_mkundef(); +} + static bool parse_addEventListener_options(ant_t *js, ant_value_t *args, int nargs, bool *once, bool *capture, ant_value_t *signal) { *once = false; *capture = false; *signal = js_mkundef(); if (nargs < 3) return true; @@ -450,7 +477,7 @@ static bool parse_addEventListener_options(ant_t *js, ant_value_t *args, int nar ant_value_t sig = js_get(js, opts, "signal"); if (vtype(sig) == T_NULL) return false; if (vtype(sig) != T_UNDEF) { - if (!is_object_type(sig)) return false; + if (!abort_signal_is_signal(sig)) return false; *signal = sig; } @@ -871,11 +898,171 @@ static ant_value_t js_eventemitter_eventNames(ant_t *js, ant_value_t *args, int return result; } +static ant_value_t js_events_once_listener(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (!is_object_type(state)) return js_mkundef(); + + ant_value_t promise = js_get_slot(state, SLOT_DATA); + if (vtype(promise) != T_PROMISE) return js_mkundef(); + + ant_value_t settled = js_get_slot(state, SLOT_SETTLED); + if (vtype(settled) == T_BOOL && settled == js_true) return js_mkundef(); + js_set_slot(state, SLOT_SETTLED, js_true); + + ant_value_t values = js_mkarr(js); + for (int i = 0; i < nargs; i++) js_arr_push(js, values, args[i]); + js_resolve_promise(js, promise, values); + + return js_mkundef(); +} + +static ant_value_t js_events_once_abort_listener(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (!is_object_type(state)) return js_mkundef(); + + ant_value_t promise = js_get_slot(state, SLOT_DATA); + if (vtype(promise) != T_PROMISE) return js_mkundef(); + + ant_value_t settled = js_get_slot(state, SLOT_SETTLED); + if (vtype(settled) == T_BOOL && settled == js_true) return js_mkundef(); + js_set_slot(state, SLOT_SETTLED, js_true); + + ant_value_t signal = js_get(js, state, "signal"); + ant_value_t reason = abort_signal_get_reason(signal); + if (vtype(reason) == T_UNDEF) reason = js_mkerr(js, "The operation was aborted"); + js_reject_promise(js, promise, reason); + + return js_mkundef(); +} + +static bool js_events_once_is_abort_key(ant_t *js, ant_value_t key) { + size_t key_len = 0; + const char *key_str = vtype(key) == T_STR ? js_getstr(js, key, &key_len) : NULL; + return key_str && key_len == 5 && memcmp(key_str, "abort", 5) == 0; +} + +static void js_events_once_reject_aborted(ant_t *js, ant_value_t promise, ant_value_t signal) { + ant_value_t reason = abort_signal_get_reason(signal); + if (vtype(reason) == T_UNDEF) reason = js_mkerr(js, "The operation was aborted"); + js_reject_promise(js, promise, reason); +} + +static ant_value_t js_events_once_attach( + ant_t *js, + ant_value_t promise, + ant_value_t target, + ant_value_t key, + ant_value_t listener, + ant_value_t signal +) { + if (abort_signal_is_signal(target)) { + if (!js_events_once_is_abort_key(js, key)) { + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "AbortSignal only supports the abort event")); + return promise; + } + abort_signal_add_listener(js, target, listener); + return promise; + } + + if (is_eventemitter_instance(target)) { + if (!eventemitter_add_listener_val(js, target, key, listener, true)) + js_reject_promise(js, promise, js_mkerr(js, "listener must be a function")); + return promise; + } + + if (is_eventtarget_instance(target)) { + ant_value_t listener_options = js_mkobj(js); + js_set(js, listener_options, "once", js_true); + if (abort_signal_is_signal(signal)) js_set(js, listener_options, "signal", signal); + + ant_value_t call_args[3] = { key, listener, listener_options }; + ant_value_t result = add_listener_to(js, call_args, 3, find_or_create_emitter_event_type(js, target, key)); + if (is_err(result)) js_reject_promise(js, promise, result); + return promise; + } + + js_reject_promise(js, promise, js_mkerr_typed(js, JS_ERR_TYPE, "target is not an EventEmitter or EventTarget")); + return promise; +} + +static ant_value_t js_events_once(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2 || !is_object_type(args[0])) + return js_mkerr_typed(js, JS_ERR_TYPE, "events.once requires an emitter and event name"); + + ant_value_t key = evt_key_from_arg(args[1]); + if (!key) return js_mkerr_typed(js, JS_ERR_TYPE, "event must be a string or Symbol"); + + ant_value_t promise = js_mkpromise(js); + if (is_err(promise)) return promise; + + ant_value_t state = js_mkobj(js); + js_set_slot(state, SLOT_DATA, promise); + js_set_slot(state, SLOT_SETTLED, js_false); + + ant_value_t listener = js_heavy_mkfun(js, js_events_once_listener, state); + ant_value_t target = args[0]; + ant_value_t options = nargs >= 3 ? args[2] : js_mkundef(); + ant_value_t signal = js_mkundef(); + + if (is_object_type(options)) signal = js_get(js, options, "signal"); + if (abort_signal_is_signal(signal)) { + if (abort_signal_is_aborted(signal)) { + js_events_once_reject_aborted(js, promise, signal); + return promise; + } + js_set(js, state, "signal", signal); + abort_signal_add_listener(js, signal, js_heavy_mkfun(js, js_events_once_abort_listener, state)); + } + + return js_events_once_attach(js, promise, target, key, listener, signal); +} + +// TODO: fix stub +static ant_value_t js_events_disposable_dispose(ant_t *js, ant_value_t *args, int nargs) { + return js_mkundef(); +} + +static ant_value_t js_events_add_abort_listener(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2 || !abort_signal_is_signal(args[0]) || !is_callable(args[1])) + return js_mkerr_typed(js, JS_ERR_TYPE, "events.addAbortListener requires an AbortSignal and listener"); + + if (abort_signal_is_aborted(args[0])) { + ant_value_t event = js_mkobj(js); + js_set(js, event, "type", js_mkstr(js, "abort", 5)); + sv_vm_call(js->vm, js, args[1], args[0], &event, 1, NULL, false); + } else abort_signal_add_listener(js, args[0], args[1]); + + ant_value_t disposable = js_mkobj(js); + js_set(js, disposable, "dispose", js_mkfun(js_events_disposable_dispose)); + + return disposable; +} + +// TODO: fix stub +static ant_value_t js_events_set_max_listeners(ant_t *js, ant_value_t *args, int nargs) { + return js_mkundef(); +} + +// TODO: fix stub +static ant_value_t js_events_get_max_listeners(ant_t *js, ant_value_t *args, int nargs) { + return js_mknum(10); +} + +// TODO: fix stub +static ant_value_t js_events_on(ant_t *js, ant_value_t *args, int nargs) { + return js_mkerr_typed(js, JS_ERR_TYPE, "events.on async iterator is not implemented"); +} + ant_value_t events_library(ant_t *js) { ant_value_t lib = js_mkobj(js); eventemitter_prototype(js); js_set_module_default(js, lib, g_eventemitter_ctor, "EventEmitter"); + js_set(js, lib, "once", js_mkfun(js_events_once)); + js_set(js, lib, "on", js_mkfun(js_events_on)); + js_set(js, lib, "addAbortListener", js_mkfun(js_events_add_abort_listener)); + js_set(js, lib, "setMaxListeners", js_mkfun(js_events_set_max_listeners)); + js_set(js, lib, "getMaxListeners", js_mkfun(js_events_get_max_listeners)); js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "events", 6)); return lib; @@ -900,12 +1087,15 @@ ant_value_t eventemitter_prototype(ant_t *js) { js_set(js, eventemitter_proto, "eventNames", js_mkfun(js_eventemitter_eventNames)); js_set_sym(js, eventemitter_proto, get_toStringTag_sym(), js_mkstr(js, "EventEmitter", 12)); + js_set_slot(eventemitter_ctor, SLOT_CFUNC, js_mkfun(js_eventemitter_ctor)); js_mkprop_fast(js, eventemitter_ctor, "prototype", 9, eventemitter_proto); js_mkprop_fast(js, eventemitter_ctor, "name", 4, ANT_STRING("EventEmitter")); js_set_descriptor(js, eventemitter_ctor, "name", 4, 0); g_eventemitter_proto = eventemitter_proto; - g_eventemitter_ctor = js_obj_to_func_ex(eventemitter_ctor, SV_CALL_IS_DEFAULT_CTOR); + g_eventemitter_ctor = js_obj_to_func(eventemitter_ctor); + js_set(js, eventemitter_proto, "constructor", g_eventemitter_ctor); + js_set_descriptor(js, eventemitter_proto, "constructor", 11, JS_DESC_W | JS_DESC_C); return g_eventemitter_proto; } @@ -956,16 +1146,18 @@ void init_events_module(void) { js_set(js, global, "PromiseRejectionEvent", pre_fn); ant_value_t eventtarget_proto = js_mkobj(js); + g_eventtarget_proto = eventtarget_proto; js_set(js, eventtarget_proto, "addEventListener", js_mkfun(js_add_event_listener_method)); js_set(js, eventtarget_proto, "removeEventListener", js_mkfun(js_remove_event_listener_method)); js_set(js, eventtarget_proto, "dispatchEvent", js_mkfun(js_dispatch_event_method)); js_set_sym(js, eventtarget_proto, get_toStringTag_sym(), js_mkstr(js, "EventTarget", 11)); ant_value_t eventtarget_ctor = js_mkobj(js); + js_set_slot(eventtarget_ctor, SLOT_CFUNC, js_mkfun(js_eventtarget_ctor)); js_mkprop_fast(js, eventtarget_ctor, "prototype", 9, eventtarget_proto); js_mkprop_fast(js, eventtarget_ctor, "name", 4, ANT_STRING("EventTarget")); js_set_descriptor(js, eventtarget_ctor, "name", 4, 0); - ant_value_t eventtarget_fn = js_obj_to_func_ex(eventtarget_ctor, SV_CALL_IS_DEFAULT_CTOR); + ant_value_t eventtarget_fn = js_obj_to_func(eventtarget_ctor); js_set(js, eventtarget_proto, "constructor", eventtarget_fn); js_set_descriptor(js, eventtarget_proto, "constructor", 11, JS_DESC_W | JS_DESC_C); @@ -994,6 +1186,7 @@ void gc_mark_events(ant_t *js, gc_mark_fn mark) { if (g_isTrusted_getter) mark(js, g_isTrusted_getter); if (g_eventemitter_ctor) mark(js, g_eventemitter_ctor); if (g_eventemitter_proto) mark(js, g_eventemitter_proto); + if (g_eventtarget_proto) mark(js, g_eventtarget_proto); if (g_event_proto) mark(js, g_event_proto); if (g_customevent_proto) mark(js, g_customevent_proto); if (g_errorevent_proto) mark(js, g_errorevent_proto); diff --git a/src/modules/fs.c b/src/modules/fs.c index c38b4cc..bcc50b2 100644 --- a/src/modules/fs.c +++ b/src/modules/fs.c @@ -60,7 +60,8 @@ typedef enum { FS_OP_READ_FD, FS_OP_OPEN, FS_OP_CLOSE, - FS_OP_MKDTEMP + FS_OP_MKDTEMP, + FS_OP_CHMOD } fs_op_type_t; typedef struct fs_request_s { @@ -200,6 +201,34 @@ static ant_value_t fs_mk_errno_error( const char *syscall, const char *path, const char *dest ); +static bool fs_parse_mode(ant_t *js, ant_value_t arg, mode_t *out) { + if (vtype(arg) == T_NUM) { + double mode = js_getnum(arg); + if (mode < 0) return false; + *out = (mode_t)mode; + return true; + } + + if (vtype(arg) != T_STR) return false; + + size_t len = 0; + const char *str = js_getstr(js, arg, &len); + if (!str) return false; + + size_t start = 0; + if (len > 2 && str[0] == '0' && (str[1] == 'o' || str[1] == 'O')) start = 2; + if (start == len) return false; + + mode_t mode = 0; + for (size_t i = start; i < len; i++) { + if (str[i] < '0' || str[i] > '7') return false; + mode = (mode_t)((mode << 3) + (str[i] - '0')); + } + + *out = mode; + return true; +} + static ant_value_t fs_stream_error(ant_t *js, ant_value_t stream_obj, const char *op, int uv_code) { ant_value_t props = js_mkobj(js); ant_value_t path_val = js_get(js, stream_obj, "path"); @@ -1644,6 +1673,22 @@ static void on_access_complete(uv_fs_t *uv_req) { free_fs_request(req); } +static void on_chmod_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + fs_request_fail(req, (int)uv_req->result); + req->completed = 1; + complete_request(req); + return; + } + + req->completed = 1; + js_resolve_promise(req->js, req->promise, js_mkundef()); + remove_pending_request(req); + free_fs_request(req); +} + static void on_readdir_complete(uv_fs_t *uv_req) { fs_request_t *req = (fs_request_t *)uv_req->data; @@ -2734,6 +2779,82 @@ static ant_value_t builtin_fs_accessSync(ant_t *js, ant_value_t *args, int nargs return js_mkundef(); } +static ant_value_t builtin_fs_chmodSync(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "chmodSync() requires a path argument"); + if (nargs < 2) return js_mkerr(js, "chmodSync() requires a mode argument"); + + ant_value_t path_val = fs_coerce_path(js, args[0]); + if (vtype(path_val) != T_STR) return js_mkerr(js, "chmodSync() path must be a string or URL"); + + mode_t mode = 0; + if (!fs_parse_mode(js, args[1], &mode)) { + return js_mkerr(js, "chmodSync() mode must be a number or octal string"); + } + + size_t path_len = 0; + char *path = js_getstr(js, path_val, &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + char *path_cstr = strndup(path, path_len); + if (!path_cstr) return js_mkerr(js, "Out of memory"); + + uv_fs_t req; + int result = uv_fs_chmod(uv_default_loop(), &req, path_cstr, mode, NULL); + + if (result < 0) { + ant_value_t err = fs_mk_uv_error(js, result, "chmod", path_cstr, NULL); + uv_fs_req_cleanup(&req); + free(path_cstr); + return err; + } + + uv_fs_req_cleanup(&req); + free(path_cstr); + return js_mkundef(); +} + +static ant_value_t builtin_fs_chmod(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "chmod() requires a path argument"); + if (nargs < 2) return js_mkerr(js, "chmod() requires a mode argument"); + + ant_value_t path_val = fs_coerce_path(js, args[0]); + if (vtype(path_val) != T_STR) return js_mkerr(js, "chmod() path must be a string or URL"); + + mode_t mode = 0; + if (!fs_parse_mode(js, args[1], &mode)) { + return js_mkerr(js, "chmod() mode must be a number or octal string"); + } + + size_t path_len = 0; + char *path = js_getstr(js, path_val, &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_CHMOD; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + if (!req->path) { + free_fs_request(req); + return js_mkerr(js, "Out of memory"); + } + + utarray_push_back(pending_requests, &req); + int result = uv_fs_chmod(uv_default_loop(), &req->uv_req, req->path, mode, on_chmod_complete); + + if (result < 0) { + fs_request_fail(req, result); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + static ant_value_t builtin_fs_access(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "access() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "access() path must be a string"); @@ -2823,6 +2944,71 @@ static ant_value_t builtin_fs_realpath(ant_t *js, ant_value_t *args, int nargs) return req->promise; } +static ant_value_t builtin_fs_readlinkSync(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "readlinkSync() requires a path argument"); + + ant_value_t path_val = fs_coerce_path(js, args[0]); + if (vtype(path_val) != T_STR) return js_mkerr(js, "readlinkSync() path must be a string"); + + size_t path_len = 0; + const char *path = js_getstr(js, path_val, &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + char *path_cstr = strndup(path, path_len); + if (!path_cstr) return js_mkerr(js, "Out of memory"); + + uv_fs_t req; + int result = uv_fs_readlink(NULL, &req, path_cstr, NULL); + + if (result < 0 || !req.ptr) { + ant_value_t err = fs_mk_uv_error(js, result, "readlink", path_cstr, NULL); + uv_fs_req_cleanup(&req); + free(path_cstr); + return err; + } + + ant_value_t link = js_mkstr(js, (const char *)req.ptr, strlen((const char *)req.ptr)); + uv_fs_req_cleanup(&req); + free(path_cstr); + return link; +} + +static ant_value_t builtin_fs_readlink(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "readlink() requires a path argument"); + + ant_value_t path_val = fs_coerce_path(js, args[0]); + if (vtype(path_val) != T_STR) return js_mkerr(js, "readlink() path must be a string"); + + size_t path_len = 0; + const char *path = js_getstr(js, path_val, &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_REALPATH; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + if (!req->path) { + free_fs_request(req); + return js_mkerr(js, "Out of memory"); + } + + utarray_push_back(pending_requests, &req); + int result = uv_fs_readlink(uv_default_loop(), &req->uv_req, req->path, on_realpath_complete); + + if (result < 0) { + fs_request_fail(req, result); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + static ant_value_t builtin_fs_readdirSync(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "readdirSync() requires a path argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "readdirSync() path must be a string"); @@ -3802,11 +3988,15 @@ static void fs_set_promise_methods(ant_t *js, ant_value_t lib) { js_set(js, lib, "futimes", js_mkfun(builtin_fs_futimes)); js_set(js, lib, "exists", js_mkfun(builtin_fs_exists)); js_set(js, lib, "access", js_mkfun(builtin_fs_access)); + js_set(js, lib, "chmod", js_mkfun(builtin_fs_chmod)); js_set(js, lib, "readdir", js_mkfun(builtin_fs_readdir)); js_set(js, lib, "realpath", js_mkfun(builtin_fs_realpath)); + js_set(js, lib, "readlink", js_mkfun(builtin_fs_readlink)); } static void fs_set_callback_compatible_methods(ant_t *js, ant_value_t lib) { + ant_value_t realpath = fs_make_callback_wrapper(js, js_mkfun(builtin_fs_realpath), false); + js_set(js, lib, "readFile", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_readFile), false)); js_set(js, lib, "open", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_open_fd), false)); js_set(js, lib, "close", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_close_fd), false)); @@ -3824,12 +4014,16 @@ static void fs_set_callback_compatible_methods(ant_t *js, ant_value_t lib) { js_set(js, lib, "futimes", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_futimes), false)); js_set(js, lib, "exists", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_exists), true)); js_set(js, lib, "access", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_access), false)); + js_set(js, lib, "chmod", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_chmod), false)); js_set(js, lib, "readdir", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_readdir), false)); - js_set(js, lib, "realpath", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_realpath), false)); + js_set(js, lib, "readlink", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_readlink), false)); + + js_set(js, realpath, "native", realpath); + js_set(js, lib, "realpath", realpath); } static ant_value_t fs_make_constants(ant_t *js) { - ant_value_t constants = js_mkobj(js); + ant_value_t constants = js_newobj(js); js_set(js, constants, "F_OK", js_mknum(F_OK)); js_set(js, constants, "R_OK", js_mknum(R_OK)); js_set(js, constants, "W_OK", js_mknum(W_OK)); @@ -3841,6 +4035,24 @@ static ant_value_t fs_make_constants(ant_t *js) { js_set(js, constants, "O_EXCL", js_mknum(O_EXCL)); js_set(js, constants, "O_TRUNC", js_mknum(O_TRUNC)); js_set(js, constants, "O_APPEND", js_mknum(O_APPEND)); +#ifdef O_SYMLINK + js_set(js, constants, "O_SYMLINK", js_mknum(O_SYMLINK)); +#endif +#ifdef O_NOFOLLOW + js_set(js, constants, "O_NOFOLLOW", js_mknum(O_NOFOLLOW)); +#endif +#ifdef S_IFMT + js_set(js, constants, "S_IFMT", js_mknum(S_IFMT)); +#endif +#ifdef S_IFREG + js_set(js, constants, "S_IFREG", js_mknum(S_IFREG)); +#endif +#ifdef S_IFDIR + js_set(js, constants, "S_IFDIR", js_mknum(S_IFDIR)); +#endif +#ifdef S_IFLNK + js_set(js, constants, "S_IFLNK", js_mknum(S_IFLNK)); +#endif return constants; } @@ -3887,8 +4099,10 @@ ant_value_t fs_library(ant_t *js) { js_set(js, lib, "futimesSync", js_mkfun(builtin_fs_futimesSync)); js_set(js, lib, "existsSync", js_mkfun(builtin_fs_existsSync)); js_set(js, lib, "accessSync", js_mkfun(builtin_fs_accessSync)); + js_set(js, lib, "chmodSync", js_mkfun(builtin_fs_chmodSync)); js_set(js, lib, "readdirSync", js_mkfun(builtin_fs_readdirSync)); js_set(js, lib, "realpathSync", realpath_sync); + js_set(js, lib, "readlinkSync", js_mkfun(builtin_fs_readlinkSync)); js_set(js, lib, "watch", js_mkfun(builtin_fs_watch)); js_set(js, lib, "watchFile", js_mkfun(builtin_fs_watchFile)); js_set(js, lib, "unwatchFile", js_mkfun(builtin_fs_unwatchFile)); @@ -3920,6 +4134,14 @@ ant_value_t fs_promises_library(ant_t *js) { return lib; } +ant_value_t fs_constants_library(ant_t *js) { + ant_value_t constants = fs_make_constants(js); + js_set(js, constants, "default", constants); + js_set_slot_wb(js, constants, SLOT_DEFAULT, constants); + js_set_sym(js, constants, get_toStringTag_sym(), js_mkstr(js, "constants", 9)); + return constants; +} + int has_pending_fs_ops(void) { return pending_requests && utarray_len(pending_requests) > 0; } diff --git a/src/modules/path.c b/src/modules/path.c index ff9037a..1bc2c8b 100644 --- a/src/modules/path.c +++ b/src/modules/path.c @@ -872,3 +872,17 @@ ant_value_t path_library(ant_t *js) { return lib; } + +ant_value_t path_posix_library(ant_t *js) { + path_api_t api = path_build_api_for_style(js, PATH_STYLE_POSIX); + ant_value_t lib = path_make_variant(js, &api, PATH_STYLE_POSIX); + js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "path", 4)); + return lib; +} + +ant_value_t path_win32_library(ant_t *js) { + path_api_t api = path_build_api_for_style(js, PATH_STYLE_WIN32); + ant_value_t lib = path_make_variant(js, &api, PATH_STYLE_WIN32); + js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "path", 4)); + return lib; +} diff --git a/src/modules/process.c b/src/modules/process.c index 6e53831..764dcab 100644 --- a/src/modules/process.c +++ b/src/modules/process.c @@ -1638,6 +1638,96 @@ static ant_value_t process_emit(ant_t *js, ant_value_t *args, int nargs) { return js_true; } +static bool process_is_error_object(ant_value_t value) { + if (is_err(value)) return true; + return is_object_type(value) && js_get_slot(value, SLOT_ERROR_BRAND) == js_true; +} + +static void process_get_warning_options( + ant_t *js, ant_value_t options, + const char **type, const char **code, const char **detail, + ant_offset_t *detail_len +) { + if (!is_object_type(options)) return; + + ant_value_t type_val = js_get(js, options, "type"); + ant_value_t code_val = js_get(js, options, "code"); + ant_value_t detail_val = js_get(js, options, "detail"); + + if (vtype(type_val) == T_STR) *type = js_getstr(js, type_val, NULL); + if (vtype(code_val) == T_STR) *code = js_getstr(js, code_val, NULL); + if (vtype(detail_val) == T_STR) *detail = (const char *)(uintptr_t)vstr(js, detail_val, detail_len); +} + +static ant_value_t process_make_warning_object( + ant_t *js, const char *type, js_cstr_t msg, const char *code, + const char *detail, ant_offset_t detail_len +) { + ant_value_t warning_obj = js_mkobj(js); + js_set_proto_init(warning_obj, js_get_ctor_proto(js, "Error", 5)); + js_set_slot(warning_obj, SLOT_ERROR_BRAND, js_true); + js_set(js, warning_obj, "name", js_mkstr(js, type, strlen(type))); + js_set(js, warning_obj, "message", js_mkstr(js, msg.ptr, msg.len)); + + if (code) js_set(js, warning_obj, "code", js_mkstr(js, code, strlen(code))); + if (detail) js_set(js, warning_obj, "detail", js_mkstr(js, detail, detail_len)); + + js_capture_stack(js, warning_obj); + return warning_obj; +} + +static ant_value_t process_emit_warning(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkundef(); + + ant_value_t warning = args[0]; + const char *type = "Warning"; + const char *code = NULL; + const char *detail = NULL; + ant_offset_t detail_len = 0; + + if (nargs >= 2) { + if (vtype(args[1]) == T_STR) type = js_getstr(js, args[1], NULL); + else process_get_warning_options(js, args[1], &type, &code, &detail, &detail_len); + } + + if (nargs >= 3 && vtype(args[2]) == T_STR) code = js_getstr(js, args[2], NULL); + + char msg_buf[512]; + js_cstr_t msg = {0}; + ant_value_t warning_event_arg = warning; + bool is_error = process_is_error_object(warning); + + if (is_error) { + ant_value_t warning_obj = js_as_obj(warning); + ant_offset_t prop_len = 0; + const char *name_prop = get_str_prop(js, warning_obj, "name", 4, &prop_len); + if (name_prop) type = name_prop; + + const char *message_prop = get_str_prop(js, warning_obj, "message", 7, &prop_len); + msg.ptr = message_prop ? message_prop : ""; + msg.len = message_prop ? prop_len : 0; + msg.needs_free = false; + + code = get_str_prop(js, warning_obj, "code", 4, NULL); + detail = get_str_prop(js, warning_obj, "detail", 6, &detail_len); + warning_event_arg = warning_obj; + } else { + msg = js_to_cstr(js, warning, msg_buf, sizeof(msg_buf)); + warning_event_arg = process_make_warning_object(js, type, msg, code, detail, detail_len); + } + + fprintf(stderr, "(%s:%d) ", "ant", (int)getpid()); + if (code) fprintf(stderr, "[%s] ", code); + + fprintf(stderr, "%s: %.*s\n", type ? type : "Warning", (int)msg.len, msg.ptr); + if (detail) fprintf(stderr, "%.*s\n", (int)detail_len, detail); + + emit_process_event("warning", &warning_event_arg, 1); + if (msg.needs_free) free((void *)msg.ptr); + + return js_mkundef(); +} + static ant_value_t process_listener_count(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mknum(0); @@ -1665,10 +1755,6 @@ static ant_value_t process_get_max_listeners(ant_t *js, ant_value_t *args, int n return js_mknum(max_listeners); } -ant_value_t process_library(ant_t *js) { - return js_get(js, js_glob(js), "process"); -} - static ant_value_t process_next_tick(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "process.nextTick requires a callback"); @@ -1682,61 +1768,76 @@ static ant_value_t process_next_tick(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); } -void init_process_module() { - ant_t *js = rt->js; - ant_value_t global = js_glob(js); - - process_start_time = uv_hrtime(); - ant_value_t process_proto = js_mkobj(js); - - js_set(js, process_proto, "exit", js_mkfun(process_exit)); - js_set(js, process_proto, "on", js_mkfun(process_on)); - js_set(js, process_proto, "addListener", js_mkfun(process_on)); - js_set(js, process_proto, "once", js_mkfun(process_once)); - js_set(js, process_proto, "off", js_mkfun(process_off)); - js_set(js, process_proto, "removeListener", js_mkfun(process_off)); - js_set(js, process_proto, "removeAllListeners", js_mkfun(process_remove_all_listeners)); - js_set(js, process_proto, "emit", js_mkfun(process_emit)); - js_set(js, process_proto, "listenerCount", js_mkfun(process_listener_count)); - js_set(js, process_proto, "setMaxListeners", js_mkfun(process_set_max_listeners)); - js_set(js, process_proto, "getMaxListeners", js_mkfun(process_get_max_listeners)); - js_set(js, process_proto, "cwd", js_mkfun(process_cwd)); - js_set(js, process_proto, "chdir", js_mkfun(process_chdir)); - js_set(js, process_proto, "uptime", js_mkfun(process_uptime)); - js_set(js, process_proto, "cpuUsage", js_mkfun(process_cpu_usage)); - js_set(js, process_proto, "kill", js_mkfun(process_kill)); - js_set(js, process_proto, "abort", js_mkfun(process_abort)); - js_set(js, process_proto, "umask", js_mkfun(process_umask)); - js_set(js, process_proto, "nextTick", js_mkfun(process_next_tick)); - +static void process_set_methods(ant_t *js, ant_value_t obj, bool include_event_methods) { + js_set(js, obj, "exit", js_mkfun(process_exit)); + js_set(js, obj, "cwd", js_mkfun(process_cwd)); + js_set(js, obj, "chdir", js_mkfun(process_chdir)); + js_set(js, obj, "uptime", js_mkfun(process_uptime)); + js_set(js, obj, "cpuUsage", js_mkfun(process_cpu_usage)); + js_set(js, obj, "kill", js_mkfun(process_kill)); + js_set(js, obj, "abort", js_mkfun(process_abort)); + js_set(js, obj, "umask", js_mkfun(process_umask)); + js_set(js, obj, "nextTick", js_mkfun(process_next_tick)); + js_set(js, obj, "emitWarning", js_mkfun(process_emit_warning)); + js_set(js, obj, "dlopen", js_mkfun(napi_process_dlopen_js)); + ant_value_t mem_usage_fn = js_heavy_mkfun(js, process_memory_usage, js_mkundef()); js_set(js, mem_usage_fn, "rss", js_mkfun(process_memory_usage_rss)); - js_set(js, process_proto, "memoryUsage", mem_usage_fn); - + js_set(js, obj, "memoryUsage", mem_usage_fn); + ant_value_t hrtime_fn = js_heavy_mkfun(js, process_hrtime, js_mkundef()); js_set(js, hrtime_fn, "bigint", js_mkfun(process_hrtime_bigint)); - js_set(js, process_proto, "hrtime", hrtime_fn); - js_set(js, process_proto, "dlopen", js_mkfun(napi_process_dlopen_js)); - + js_set(js, obj, "hrtime", hrtime_fn); + + if (include_event_methods) { + js_set(js, obj, "on", js_mkfun(process_on)); + js_set(js, obj, "addListener", js_mkfun(process_on)); + js_set(js, obj, "once", js_mkfun(process_once)); + js_set(js, obj, "off", js_mkfun(process_off)); + js_set(js, obj, "removeListener", js_mkfun(process_off)); + js_set(js, obj, "removeAllListeners", js_mkfun(process_remove_all_listeners)); + js_set(js, obj, "emit", js_mkfun(process_emit)); + js_set(js, obj, "listenerCount", js_mkfun(process_listener_count)); + js_set(js, obj, "setMaxListeners", js_mkfun(process_set_max_listeners)); + js_set(js, obj, "getMaxListeners", js_mkfun(process_get_max_listeners)); + } + #ifndef _WIN32 - js_set(js, process_proto, "getuid", js_mkfun(process_getuid)); - js_set(js, process_proto, "geteuid", js_mkfun(process_geteuid)); - js_set(js, process_proto, "getgid", js_mkfun(process_getgid)); - js_set(js, process_proto, "getegid", js_mkfun(process_getegid)); - js_set(js, process_proto, "getgroups", js_mkfun(process_getgroups)); - js_set(js, process_proto, "setuid", js_mkfun(process_setuid)); - js_set(js, process_proto, "setgid", js_mkfun(process_setgid)); - js_set(js, process_proto, "seteuid", js_mkfun(process_seteuid)); - js_set(js, process_proto, "setegid", js_mkfun(process_setegid)); - js_set(js, process_proto, "setgroups", js_mkfun(process_setgroups)); - js_set(js, process_proto, "initgroups", js_mkfun(process_initgroups)); + js_set(js, obj, "getuid", js_mkfun(process_getuid)); + js_set(js, obj, "geteuid", js_mkfun(process_geteuid)); + js_set(js, obj, "getgid", js_mkfun(process_getgid)); + js_set(js, obj, "getegid", js_mkfun(process_getegid)); + js_set(js, obj, "getgroups", js_mkfun(process_getgroups)); + js_set(js, obj, "setuid", js_mkfun(process_setuid)); + js_set(js, obj, "setgid", js_mkfun(process_setgid)); + js_set(js, obj, "seteuid", js_mkfun(process_seteuid)); + js_set(js, obj, "setegid", js_mkfun(process_setegid)); + js_set(js, obj, "setgroups", js_mkfun(process_setgroups)); + js_set(js, obj, "initgroups", js_mkfun(process_initgroups)); #endif - +} + +ant_value_t process_library(ant_t *js) { + ant_value_t process_obj = js_get(js, js_glob(js), "process"); + js_set_slot_wb(js, process_obj, SLOT_DEFAULT, process_obj); + return process_obj; +} + +void init_process_module() { + ant_t *js = rt->js; + ant_value_t global = js_glob(js); + + process_start_time = uv_hrtime(); + ant_value_t process_proto = js_mkobj(js); + + process_set_methods(js, process_proto, true); js_set_sym(js, process_proto, get_toStringTag_sym(), js_mkstr(js, "process", 7)); - + ant_value_t process_obj = js_mkobj(js); ant_value_t env_obj = js_mkobj(js); + js_set_proto_init(process_obj, process_proto); + process_set_methods(js, process_obj, false); load_dotenv_file(js, env_obj); js_set_keys(env_obj, env_keys); @@ -1861,9 +1962,8 @@ void gc_mark_process(ant_t *js, gc_mark_fn mark) { ProcessEventType *tables[] = {process_events, stdin_events, stdout_events, stderr_events}; for (int t = 0; t < 4; t++) { ProcessEventType *evt, *tmp; - HASH_ITER(hh, tables[t], evt, tmp) { + HASH_ITER(hh, tables[t], evt, tmp) for (int i = 0; i < evt->listener_count; i++) mark(js, evt->listeners[i].listener); - } } } diff --git a/src/modules/stream.c b/src/modules/stream.c index 9af3d55..cc256b3 100644 --- a/src/modules/stream.c +++ b/src/modules/stream.c @@ -34,6 +34,9 @@ static ant_value_t g_transform_ctor = 0; static ant_value_t g_passthrough_proto = 0; static ant_value_t g_passthrough_ctor = 0; +static double g_default_high_water_mark = 16384.0; +static double g_default_object_high_water_mark = 16.0; + static ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj); static ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj); static ant_value_t stream_readable_push_value(ant_t *js, ant_value_t stream_obj, ant_value_t chunk, ant_value_t encoding); @@ -260,6 +263,17 @@ static ant_value_t stream_get_option(ant_t *js, ant_value_t options, const char return js_get(js, options, name); } +static double stream_default_high_water_mark(bool object_mode) { + return object_mode ? g_default_object_high_water_mark : g_default_high_water_mark; +} + +static double stream_high_water_mark_from_options(ant_t *js, ant_value_t options, bool object_mode) { + ant_value_t hwm = stream_get_option(js, options, "highWaterMark"); + return (vtype(hwm) == T_NUM && js_getnum(hwm) > 0) + ? js_getnum(hwm) + : stream_default_high_water_mark(object_mode); +} + static ant_value_t stream_make_base_object(ant_t *js, ant_value_t proto) { ant_value_t obj = js_mkobj(js); if (is_object_type(proto)) js_set_proto_init(obj, proto); @@ -281,15 +295,16 @@ static void stream_init_readable(ant_t *js, ant_value_t obj, ant_value_t raw_opt ant_value_t options = is_object_type(raw_options) ? raw_options : js_mkobj(js); ant_value_t state = js_mkobj(js); ant_value_t read_fn = stream_get_option(js, options, "read"); - ant_value_t hwm = stream_get_option(js, options, "highWaterMark"); - double high_water_mark = (vtype(hwm) == T_NUM && js_getnum(hwm) > 0) ? js_getnum(hwm) : 16384.0; + + bool object_mode = js_truthy(js, stream_get_option(js, options, "objectMode")); + double high_water_mark = stream_high_water_mark_from_options(js, options, object_mode); stream_init_base(js, obj, raw_options); js_set(js, obj, "readable", js_true); js_set(js, obj, "writable", js_false); js_set(js, obj, "readableEnded", js_false); - js_set(js, state, "objectMode", js_bool(js_truthy(js, stream_get_option(js, options, "objectMode")))); + js_set(js, state, "objectMode", js_bool(object_mode)); js_set(js, state, "ended", js_false); js_set(js, state, "endEmitted", js_false); js_set(js, state, "flowing", js_false); @@ -1588,6 +1603,22 @@ ant_value_t stream_readable_push(ant_t *js, ant_value_t stream_obj, ant_value_t return stream_readable_push_value(js, stream_obj, chunk, encoding); } +static ant_value_t js_stream_get_default_high_water_mark(ant_t *js, ant_value_t *args, int nargs) { + bool object_mode = nargs > 0 && js_truthy(js, args[0]); + return js_mknum(stream_default_high_water_mark(object_mode)); +} + +static ant_value_t js_stream_set_default_high_water_mark(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 2 || vtype(args[1]) != T_NUM || js_getnum(args[1]) < 0) + return js_mkerr_typed(js, JS_ERR_RANGE, "setDefaultHighWaterMark requires a non-negative number"); + + bool object_mode = js_truthy(js, args[0]); + if (object_mode) g_default_object_high_water_mark = js_getnum(args[1]); + else g_default_high_water_mark = js_getnum(args[1]); + + return js_mkundef(); +} + ant_value_t stream_library(ant_t *js) { ant_value_t lib = js_mkobj(js); ant_value_t promises = js_mkobj(js); @@ -1604,6 +1635,8 @@ ant_value_t stream_library(ant_t *js) { js_set(js, lib, "PassThrough", g_passthrough_ctor); js_set(js, lib, "pipeline", js_mkfun(js_stream_pipeline)); js_set(js, lib, "finished", js_mkfun(js_stream_finished)); + js_set(js, lib, "getDefaultHighWaterMark", js_mkfun(js_stream_get_default_high_water_mark)); + js_set(js, lib, "setDefaultHighWaterMark", js_mkfun(js_stream_set_default_high_water_mark)); js_set(js, lib, "promises", promises); js_set(js, g_stream_ctor, "Readable", g_readable_ctor); @@ -1613,6 +1646,8 @@ ant_value_t stream_library(ant_t *js) { js_set(js, g_stream_ctor, "PassThrough", g_passthrough_ctor); js_set(js, g_stream_ctor, "pipeline", js_get(js, lib, "pipeline")); js_set(js, g_stream_ctor, "finished", js_get(js, lib, "finished")); + js_set(js, g_stream_ctor, "getDefaultHighWaterMark", js_get(js, lib, "getDefaultHighWaterMark")); + js_set(js, g_stream_ctor, "setDefaultHighWaterMark", js_get(js, lib, "setDefaultHighWaterMark")); js_set(js, g_stream_ctor, "promises", promises); js_set(js, promises, "default", promises); diff --git a/src/modules/util.c b/src/modules/util.c index 4536953..84570fc 100644 --- a/src/modules/util.c +++ b/src/modules/util.c @@ -19,6 +19,7 @@ #include "modules/json.h" #include "modules/symbol.h" #include "modules/util.h" +#include "modules/abort.h" #include "modules/collections.h" typedef struct { @@ -33,7 +34,7 @@ typedef struct { const char *close; } util_style_entry_t; -// migrate to crprintf +// TODO: migrate to crprintf static const util_style_entry_t util_styles[] = { {"bold", "\x1b[1m", "\x1b[22m"}, {"dim", "\x1b[2m", "\x1b[22m"}, @@ -865,6 +866,79 @@ static ant_value_t util_promisified_call(ant_t *js, ant_value_t *args, int nargs return promise; } +static ant_value_t util_callbackify_success(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (!is_object_type(state)) return js_mkundef(); + + ant_value_t callback = js_get_slot(state, SLOT_DATA); + if (!is_callable(callback)) return js_mkundef(); + + ant_value_t cb_args[2] = { + js_mknull(), + nargs > 0 ? args[0] : js_mkundef() + }; + + return sv_vm_call(js->vm, js, callback, js_mkundef(), cb_args, 2, NULL, false); +} + +static ant_value_t util_callbackify_error(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (!is_object_type(state)) return js_mkundef(); + + ant_value_t callback = js_get_slot(state, SLOT_DATA); + if (!is_callable(callback)) return js_mkundef(); + + ant_value_t err = nargs > 0 ? args[0] : js_mkerr(js, "Promise was rejected"); + ant_value_t cb_args[1] = { err }; + return sv_vm_call(js->vm, js, callback, js_mkundef(), cb_args, 1, NULL, false); +} + +static ant_value_t util_callbackified_call(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t fn = js_getcurrentfunc(js); + ant_value_t original = js_get_slot(fn, SLOT_DATA); + + if (!is_callable(original)) return js_mkerr(js, "callbackified target is not callable"); + if (nargs < 1 || !is_callable(args[nargs - 1])) + return js_mkerr_typed(js, JS_ERR_TYPE, "callbackified function requires a callback"); + + ant_value_t callback = args[nargs - 1]; + int call_nargs = nargs - 1; + ant_value_t *call_args = NULL; + + if (call_nargs > 0) { + call_args = (ant_value_t *)malloc((size_t)call_nargs * sizeof(ant_value_t)); + if (!call_args) return js_mkerr(js, "Out of memory"); + for (int i = 0; i < call_nargs; i++) call_args[i] = args[i]; + } + + ant_value_t result = sv_vm_call(js->vm, js, original, js_getthis(js), call_args, call_nargs, NULL, false); + free(call_args); + + if (is_err(result) || js->thrown_exists) { + ant_value_t ex = js->thrown_exists ? js->thrown_value : result; + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + js->thrown_stack = js_mkundef(); + ant_value_t cb_args[1] = { ex }; + sv_vm_call(js->vm, js, callback, js_mkundef(), cb_args, 1, NULL, false); + return js_mkundef(); + } + + if (vtype(result) != T_PROMISE) { + ant_value_t cb_args[2] = { js_mknull(), result }; + sv_vm_call(js->vm, js, callback, js_mkundef(), cb_args, 2, NULL, false); + return js_mkundef(); + } + + ant_value_t state = js_mkobj(js); + js_set_slot(state, SLOT_DATA, callback); + ant_value_t success = js_heavy_mkfun(js, util_callbackify_success, state); + ant_value_t error = js_heavy_mkfun(js, util_callbackify_error, state); + js_promise_then(js, result, success, error); + + return js_mkundef(); +} + static ant_value_t util_deprecated_call(ant_t *js, ant_value_t *args, int nargs) { ant_value_t fn = js_getcurrentfunc(js); ant_value_t ctx = js_get_slot(fn, SLOT_DATA); @@ -903,6 +977,33 @@ static ant_value_t util_promisify(ant_t *js, ant_value_t *args, int nargs) { return js_heavy_mkfun(js, util_promisified_call, args[0]); } +static ant_value_t util_callbackify(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !is_callable(args[0])) { + return js_mkerr(js, "callbackify(fn) requires a function"); + } + return js_heavy_mkfun(js, util_callbackified_call, args[0]); +} + +static ant_value_t util_aborted_listener(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t promise = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (vtype(promise) == T_PROMISE) js_resolve_promise(js, promise, js_mkundef()); + return js_mkundef(); +} + +static ant_value_t util_aborted(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !abort_signal_is_signal(args[0])) + return js_mkerr_typed(js, JS_ERR_TYPE, "aborted(signal, resource) requires an AbortSignal"); + + ant_value_t promise = js_mkpromise(js); + if (abort_signal_is_aborted(args[0])) { + js_resolve_promise(js, promise, js_mkundef()); + return promise; + } + + abort_signal_add_listener(js, args[0], js_heavy_mkfun(js, util_aborted_listener, promise)); + return promise; +} + static ant_value_t util_inherits(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 2 || !is_callable(args[0]) || !is_callable(args[1])) { return js_mkerr(js, "inherits(ctor, superCtor) requires constructor functions"); @@ -935,6 +1036,8 @@ ant_value_t util_library(ant_t *js) { js_set(js, lib, "inherits", js_mkfun(util_inherits)); js_set(js, lib, "parseEnv", js_mkfun(util_parse_env)); js_set(js, lib, "promisify", js_mkfun(util_promisify)); + js_set(js, lib, "callbackify", js_mkfun(util_callbackify)); + js_set(js, lib, "aborted", js_mkfun(util_aborted)); js_set(js, lib, "stripVTControlCharacters", js_mkfun(util_strip_vt_control_characters)); js_set(js, lib, "styleText", js_mkfun(util_style_text)); js_set(js, lib, "types", types); diff --git a/src/pkg/linker.zig b/src/pkg/linker.zig index d2550a4..96b351c 100644 --- a/src/pkg/linker.zig +++ b/src/pkg/linker.zig @@ -193,7 +193,7 @@ pub const Linker = struct { var has_existing_package = false; { - const existing = node_modules.openDir(install_path, .{}) catch null; + const existing = node_modules.openDir(install_path, .{ .iterate = true }) catch null; if (existing) |dir| { has_existing_install = true; var installed_dir = dir; @@ -202,7 +202,8 @@ pub const Linker = struct { const installed_version = readPackageVersion(self.allocator, installed_dir); defer if (installed_version) |v| self.allocator.free(v); has_existing_package = installed_version != null; - should_skip = packageVersionsMatch(source_version, installed_version); + should_skip = packageVersionsMatch(source_version, installed_version) and + installedFileCountMatches(installed_dir, pkg.file_count); } } @@ -283,6 +284,28 @@ pub const Linker = struct { return std.mem.eql(u8, src, dst); } + fn installedFileCountMatches(dir: std.fs.Dir, expected_files: u32) bool { + if (expected_files == 0) return true; + const actual_files = countFilesRecursive(dir) catch return false; + return actual_files >= expected_files; + } + + fn countFilesRecursive(dir: std.fs.Dir) !u32 { + var count: u32 = 0; + var iter = dir.iterate(); + while (try iter.next()) |entry| { + switch (entry.kind) { + .file => count += 1, + .directory => { + var child = dir.openDir(entry.name, .{ .iterate = true }) catch continue; + defer child.close(); + count += try countFilesRecursive(child); + }, + else => {}, + }} + return count; + } + fn createBinSymlink(self: *Linker, pkg_name: []const u8, cmd_name: []const u8, bin_path: []const u8, bin_dir: std.fs.Dir) !void { var normalized_path = bin_path; if (std.mem.startsWith(u8, normalized_path, "./")) { diff --git a/src/pkg/resolver.zig b/src/pkg/resolver.zig index 6ae8256..2c29258 100644 --- a/src/pkg/resolver.zig +++ b/src/pkg/resolver.zig @@ -251,6 +251,58 @@ pub const Constraint = struct { } }; +const DependencySpec = struct { + install_name: []const u8, + package_name: []const u8, + constraint: []const u8, +}; + +const NpmAliasSpec = struct { + package_name: []const u8, + constraint: []const u8, +}; + +fn parseNpmAliasSpec(spec: []const u8) ?NpmAliasSpec { + if (!std.mem.startsWith(u8, spec, "npm:")) return null; + + const rest = spec["npm:".len..]; + if (rest.len == 0) return null; + + const version_at: ?usize = if (rest[0] == '@') blk: { + const slash = std.mem.indexOfScalar(u8, rest, '/') orelse return null; + if (slash + 1 >= rest.len) return null; + if (std.mem.indexOfScalar(u8, rest[slash + 1 ..], '@')) |rel_at| { + break :blk slash + 1 + rel_at; + } + break :blk null; + } else std.mem.indexOfScalar(u8, rest, '@'); + + const package_name = if (version_at) |at| rest[0..at] else rest; + if (package_name.len == 0) return null; + + const constraint = if (version_at) |at| blk: { + const alias_constraint = rest[at + 1 ..]; + break :blk if (alias_constraint.len > 0) alias_constraint else "latest"; + } else "latest"; + + return .{ .package_name = package_name, .constraint = constraint }; +} + +fn dependencySpec(install_name: []const u8, constraint: []const u8) DependencySpec { + if (parseNpmAliasSpec(constraint)) |alias| { + return .{ + .install_name = install_name, + .package_name = alias.package_name, + .constraint = alias.constraint, + }; + } + return .{ + .install_name = install_name, + .package_name = install_name, + .constraint = constraint, + }; +} + pub const VersionInfo = struct { version: Version, version_str: []const u8, @@ -260,7 +312,9 @@ pub const VersionInfo = struct { optional_dependencies: std.StringHashMap([]const u8), peer_dependencies: std.StringHashMap([]const u8), peer_dependencies_meta: std.StringHashMap(bool), - os: ?[]const u8, cpu: ?[]const u8, libc: ?[]const u8, + os: ?[]const u8, + cpu: ?[]const u8, + libc: ?[]const u8, bin: std.StringHashMap([]const u8), allocator: std.mem.Allocator, @@ -311,7 +365,7 @@ pub const VersionInfo = struct { .freebsd => "freebsd", else => "unknown", }; - + const current_cpu = comptime switch (builtin.cpu.arch) { .aarch64 => "arm64", .x86_64 => "x64", @@ -319,11 +373,8 @@ pub const VersionInfo = struct { .arm => "arm", else => "unknown", }; - - const current_libc: ?[]const u8 = comptime if (builtin.os.tag != .linux) null - else if (builtin.abi == .gnu or builtin.abi == .gnueabi or builtin.abi == .gnueabihf) "glibc" - else if (builtin.abi == .musl or builtin.abi == .musleabi or builtin.abi == .musleabihf) "musl" - else null; + + const current_libc: ?[]const u8 = comptime if (builtin.os.tag != .linux) null else if (builtin.abi == .gnu or builtin.abi == .gnueabi or builtin.abi == .gnueabihf) "glibc" else if (builtin.abi == .musl or builtin.abi == .musleabi or builtin.abi == .musleabihf) "musl" else null; if (self.os) |os_filter| if (!matchesFilter(os_filter, current_os)) return false; if (self.cpu) |cpu_filter| if (!matchesFilter(cpu_filter, current_cpu)) return false; @@ -356,7 +407,7 @@ pub const VersionInfo = struct { } }; - fn parseDepsMap( +fn parseDepsMap( allocator: std.mem.Allocator, maybe_obj: ?std.json.Value, ) std.StringHashMap([]const u8) { @@ -367,19 +418,19 @@ pub const VersionInfo = struct { for (deps_obj.object.keys(), deps_obj.object.values()) |dep_name, dep_ver| { if (dep_ver != .string) continue; - + const key = allocator.dupe(u8, dep_name) catch continue; const val = allocator.dupe(u8, dep_ver.string) catch { allocator.free(key); continue; }; - + map.put(key, val) catch { - allocator.free(key); - allocator.free(val); + allocator.free(key); + allocator.free(val); }; } - + return map; } @@ -388,16 +439,15 @@ fn parsePeerMeta( maybe_obj: ?std.json.Value, ) std.StringHashMap(bool) { var map = std.StringHashMap(bool).init(allocator); - + const meta_obj = maybe_obj orelse return map; if (meta_obj != .object) return map; - + for (meta_obj.object.keys(), meta_obj.object.values()) |dep_name, meta_val| { if (meta_val != .object) continue; - - const is_optional = if (meta_val.object.get("optional")) |opt| (opt == .bool and opt.bool) - else false; - + + const is_optional = if (meta_val.object.get("optional")) |opt| (opt == .bool and opt.bool) else false; + if (is_optional) { const key = allocator.dupe(u8, dep_name) catch continue; map.put(key, true) catch allocator.free(key); @@ -436,7 +486,8 @@ pub const PackageMetadata = struct { pub fn parseFromJson(allocator: std.mem.Allocator, json_data: []const u8) !PackageMetadata { const parsed = std.json.parseFromSlice(std.json.Value, allocator, json_data, .{}) catch { return error.ParseError; - }; defer parsed.deinit(); + }; + defer parsed.deinit(); const root = parsed.value; if (root != .object) return error.ParseError; @@ -557,13 +608,13 @@ pub const PackageMetadata = struct { if (version_data.object.get("bin")) |bin_val| { if (bin_val == .object) { for (bin_val.object.keys(), bin_val.object.values()) |key, val| { - if (val == .string) bin.put(allocator.dupe(u8, key) - catch continue, allocator.dupe(u8, val.string) catch continue) catch {}; + if (val == .string) bin.put(allocator.dupe(u8, key) catch continue, allocator.dupe(u8, val.string) catch continue) catch {}; } } else if (bin_val == .string) { const bin_name = allocator.dupe(u8, name) catch continue; const bin_path = allocator.dupe(u8, bin_val.string) catch { - allocator.free(bin_name); continue; + allocator.free(bin_name); + continue; }; bin.put(bin_name, bin_path) catch { allocator.free(bin_name); @@ -581,8 +632,11 @@ pub const PackageMetadata = struct { .optional_dependencies = opt_deps, .peer_dependencies = peer_deps, .peer_dependencies_meta = peer_meta, - .os = os_filter, .cpu = cpu_filter, .libc = libc_filter, - .bin = bin, .allocator = allocator, + .os = os_filter, + .cpu = cpu_filter, + .libc = libc_filter, + .bin = bin, + .allocator = allocator, }); } @@ -626,15 +680,12 @@ pub const ResolvedPackage = struct { pub fn installPath(self: *const ResolvedPackage, allocator: std.mem.Allocator) ![]const u8 { if (self.parent_path) |parent| { return std.fmt.allocPrint(allocator, "{s}/node_modules/{s}", .{ parent, self.name.slice() }); - } return allocator.dupe(u8, self.name.slice()); + } + return allocator.dupe(u8, self.name.slice()); } }; - -pub const OnPackageResolvedFn = *const fn ( - pkg: *const ResolvedPackage, - user_data: ?*anyopaque -) void; +pub const OnPackageResolvedFn = *const fn (pkg: *const ResolvedPackage, user_data: ?*anyopaque) void; pub const Resolver = struct { allocator: std.mem.Allocator, @@ -685,18 +736,19 @@ pub const Resolver = struct { while (key_iter.next()) |key| { self.allocator.free(key.*); } - + var iter = self.resolved.valueIterator(); while (iter.next()) |pkg| { pkg.*.deinit(); self.allocator.destroy(pkg.*); - } self.resolved.deinit(); + } + self.resolved.deinit(); var cons_key_iter = self.constraints.keyIterator(); while (cons_key_iter.next()) |key| { self.allocator.free(key.*); } - + var cons_iter = self.constraints.valueIterator(); while (cons_iter.next()) |list| { list.deinit(self.allocator); @@ -733,7 +785,8 @@ pub const Resolver = struct { } entry.value_ptr.deinit(self.allocator); self.allocator.free(entry.key_ptr.*); - } all_constraints.deinit(); + } + all_constraints.deinit(); } const CollectItem = struct { @@ -762,7 +815,7 @@ pub const Resolver = struct { .depth = 0, }); } - + var dev_iter = pkg_json.dev_dependencies.iterator(); while (dev_iter.next()) |entry| { try collect_queue.append(self.allocator, .{ @@ -781,16 +834,17 @@ pub const Resolver = struct { defer to_fetch.deinit(self.allocator); for (collect_queue.items) |item| { - if (!self.metadata_cache.contains(item.name)) { + const spec = dependencySpec(item.name, item.constraint_str); + if (!self.metadata_cache.contains(spec.package_name)) { var loaded_from_disk = false; if (self.cache_db) |db| { - if (db.lookupMetadata(item.name, self.allocator)) |json_data| { + if (db.lookupMetadata(spec.package_name, self.allocator)) |json_data| { const metadata = PackageMetadata.parseFromJson(self.cache_allocator, json_data) catch { self.allocator.free(json_data); continue; }; self.allocator.free(json_data); - const cache_key = self.cache_allocator.dupe(u8, item.name) catch continue; + const cache_key = self.cache_allocator.dupe(u8, spec.package_name) catch continue; self.metadata_cache.put(cache_key, metadata) catch { self.cache_allocator.free(cache_key); continue; @@ -801,12 +855,12 @@ pub const Resolver = struct { if (!loaded_from_disk) { var already_listed = false; for (to_fetch.items) |f| { - if (std.mem.eql(u8, f, item.name)) { + if (std.mem.eql(u8, f, spec.package_name)) { already_listed = true; break; } } - if (!already_listed) try to_fetch.append(self.allocator, item.name); + if (!already_listed) try to_fetch.append(self.allocator, spec.package_name); } } } @@ -833,25 +887,28 @@ pub const Resolver = struct { }; for (ctx.collect_queue_items) |item| { - if (!std.mem.eql(u8, item.name, name)) continue; + const spec = dependencySpec(item.name, item.constraint_str); + if (!std.mem.eql(u8, spec.package_name, name)) continue; - const constraint = Constraint.parse(item.constraint_str) catch continue; + const constraint = Constraint.parse(spec.constraint) catch continue; const best = ctx.resolver.selectBestVersion(&metadata, constraint) orelse continue; if (!best.matchesPlatform()) continue; var dep_it = best.dependencies.iterator(); while (dep_it.next()) |entry| { - const dep_name = entry.key_ptr.*; - if (ctx.resolver.metadata_cache.contains(dep_name)) continue; + const dep_spec = dependencySpec(entry.key_ptr.*, entry.value_ptr.*); + if (ctx.resolver.metadata_cache.contains(dep_spec.package_name)) continue; var already_queued = false; for (ctx.prefetch_queue.items) |q| { - if (std.mem.eql(u8, q, dep_name)) { - already_queued = true; break; + if (std.mem.eql(u8, q, dep_spec.package_name)) { + already_queued = true; + break; } } - if (!already_queued) ctx.prefetch_queue.append(ctx.allocator, dep_name) catch {}; - } break; + if (!already_queued) ctx.prefetch_queue.append(ctx.allocator, dep_spec.package_name) catch {}; + } + break; } } }; @@ -896,20 +953,21 @@ pub const Resolver = struct { } try seen_collect.put(seen_key, {}); - const constraint = Constraint.parse(item.constraint_str) catch continue; - const gop = try all_constraints.getOrPut(item.name); + const spec = dependencySpec(item.name, item.constraint_str); + const constraint = Constraint.parse(spec.constraint) catch continue; + const gop = try all_constraints.getOrPut(spec.package_name); if (!gop.found_existing) { - gop.key_ptr.* = try self.allocator.dupe(u8, item.name); + gop.key_ptr.* = try self.allocator.dupe(u8, spec.package_name); gop.value_ptr.* = .{}; } try gop.value_ptr.append(self.allocator, .{ .constraint = constraint, - .constraint_str = try self.allocator.dupe(u8, item.constraint_str), + .constraint_str = try self.allocator.dupe(u8, spec.constraint), .requester = try self.allocator.dupe(u8, item.requester), .depth = item.depth, }); - if (self.metadata_cache.get(item.name)) |metadata| { + if (self.metadata_cache.get(spec.package_name)) |metadata| { const best = self.selectBestVersion(&metadata, constraint) orelse continue; if (!best.matchesPlatform()) continue; @@ -925,8 +983,9 @@ pub const Resolver = struct { var opt_it = best.optional_dependencies.iterator(); while (opt_it.next()) |entry| { - if (self.metadata_cache.get(entry.key_ptr.*)) |opt_meta| { - const opt_con = Constraint.parse(entry.value_ptr.*) catch continue; + const opt_spec = dependencySpec(entry.key_ptr.*, entry.value_ptr.*); + if (self.metadata_cache.get(opt_spec.package_name)) |opt_meta| { + const opt_con = Constraint.parse(opt_spec.constraint) catch continue; const opt_best = self.selectBestVersion(&opt_meta, opt_con) orelse continue; if (!opt_best.matchesPlatform()) continue; } @@ -982,12 +1041,12 @@ pub const Resolver = struct { if (best) |b| { if (b.version.prerelease) |pre| { debug.log(" {s}: optimal={d}.{d}.{d}-{s} (satisfies {d}/{d} constraints)", .{ - pkg_name, b.version.major, b.version.minor, b.version.patch, pre, + pkg_name, b.version.major, b.version.minor, b.version.patch, pre, self.countSatisfied(&metadata, b, constraint_list), constraint_list.len, }); } else { debug.log(" {s}: optimal={d}.{d}.{d} (satisfies {d}/{d} constraints)", .{ - pkg_name, b.version.major, b.version.minor, b.version.patch, + pkg_name, b.version.major, b.version.minor, b.version.patch, self.countSatisfied(&metadata, b, constraint_list), constraint_list.len, }); } @@ -1051,7 +1110,11 @@ pub const Resolver = struct { errdefer next_queue.deinit(self.allocator); for (queue.items) |item| { - const key = std.fmt.allocPrint(self.allocator, "{s}@{s}", .{ item.name, item.constraint }) catch continue; + const key = std.fmt.allocPrint(self.allocator, "{s}|{s}@{s}", .{ + item.parent_name orelse "", + item.name, + item.constraint, + }) catch continue; if (processed.contains(key)) { self.allocator.free(key); continue; @@ -1067,7 +1130,11 @@ pub const Resolver = struct { defer self.allocator.free(pkg_install_path); for (pkg.dependencies.items) |dep| { - const dep_key = std.fmt.allocPrint(self.allocator, "{s}@{s}", .{ dep.name.slice(), dep.constraint }) catch continue; + const dep_key = std.fmt.allocPrint(self.allocator, "{s}|{s}@{s}", .{ + pkg_install_path, + dep.name.slice(), + dep.constraint, + }) catch continue; defer self.allocator.free(dep_key); if (!processed.contains(dep_key)) { try next_queue.append(self.allocator, .{ @@ -1108,7 +1175,10 @@ pub const Resolver = struct { var want_prerelease = false; for (constraint_list) |info| { - if (info.constraint.version.prerelease != null) { want_prerelease = true; break; } + if (info.constraint.version.prerelease != null) { + want_prerelease = true; + break; + } } for (metadata.versions.items) |*v| { @@ -1141,9 +1211,10 @@ pub const Resolver = struct { parent_name: ?[]const u8, optimal_versions: *std.StringHashMap(*const VersionInfo), ) !*ResolvedPackage { - const constraint = try Constraint.parse(constraint_str); + const dep_spec = dependencySpec(name, constraint_str); + const constraint = try Constraint.parse(dep_spec.constraint); - if (self.resolved.get(name)) |existing_pkg| { + if (self.resolved.get(dep_spec.install_name)) |existing_pkg| { if (constraint.satisfies(existing_pkg.version)) { if (direct) existing_pkg.direct = true; if (depth < existing_pkg.depth) existing_pkg.depth = depth; @@ -1151,12 +1222,12 @@ pub const Resolver = struct { } if (parent_name) |parent| { - var metadata = try self.fetchMetadata(name); + var metadata = try self.fetchMetadata(dep_spec.package_name); const nested_best = self.selectBestVersion(&metadata, constraint) orelse return existing_pkg; if (!nested_best.matchesPlatform()) return existing_pkg; debug.log(" nested: {s}@{d}.{d}.{d} under {s} (hoisted: {d}.{d}.{d})", .{ - name, + dep_spec.install_name, nested_best.version.major, nested_best.version.minor, nested_best.version.patch, @@ -1166,15 +1237,15 @@ pub const Resolver = struct { existing_pkg.version.patch, }); - return try self.createNestedPackage(name, nested_best, depth, parent); + return try self.createNestedPackage(dep_spec.install_name, nested_best, depth, parent); } return existing_pkg; } - var metadata = try self.fetchMetadata(name); + var metadata = try self.fetchMetadata(dep_spec.package_name); const version_info = blk: { - if (optimal_versions.get(name)) |optimal| { + if (optimal_versions.get(dep_spec.package_name)) |optimal| { if (constraint.satisfies(optimal.version) and optimal.matchesPlatform()) break :blk optimal; } break :blk self.selectBestVersion(&metadata, constraint) orelse return error.NoMatchingVersion; @@ -1182,17 +1253,18 @@ pub const Resolver = struct { if (!version_info.matchesPlatform()) return error.PlatformMismatch; - const cons_gop = try self.constraints.getOrPut(name); + const cons_gop = try self.constraints.getOrPut(dep_spec.package_name); if (!cons_gop.found_existing) { - cons_gop.key_ptr.* = try self.allocator.dupe(u8, name); + cons_gop.key_ptr.* = try self.allocator.dupe(u8, dep_spec.package_name); cons_gop.value_ptr.* = .{}; - } try cons_gop.value_ptr.append(self.allocator, constraint); + } + try cons_gop.value_ptr.append(self.allocator, constraint); const pkg = try self.allocator.create(ResolvedPackage); errdefer self.allocator.destroy(pkg); pkg.* = .{ - .name = try self.string_pool.intern(name), + .name = try self.string_pool.intern(dep_spec.install_name), .version = version_info.version, .integrity = version_info.integrity, .tarball_url = try self.allocator.dupe(u8, version_info.tarball_url), @@ -1204,7 +1276,7 @@ pub const Resolver = struct { .allocator = self.allocator, }; - const name_key = try self.allocator.dupe(u8, name); + const name_key = try self.allocator.dupe(u8, dep_spec.install_name); errdefer self.allocator.free(name_key); try self.resolved.put(name_key, pkg); @@ -1219,8 +1291,9 @@ pub const Resolver = struct { var opt_it = version_info.optional_dependencies.iterator(); while (opt_it.next()) |entry| { - if (self.metadata_cache.get(entry.key_ptr.*)) |opt_meta| { - const opt_con = Constraint.parse(entry.value_ptr.*) catch continue; + const opt_spec = dependencySpec(entry.key_ptr.*, entry.value_ptr.*); + if (self.metadata_cache.get(opt_spec.package_name)) |opt_meta| { + const opt_con = Constraint.parse(opt_spec.constraint) catch continue; const opt_best = self.selectBestVersion(&opt_meta, opt_con) orelse continue; if (!opt_best.matchesPlatform()) continue; } @@ -1249,23 +1322,24 @@ pub const Resolver = struct { } fn resolveSingle(self: *Resolver, name: []const u8, constraint_str: []const u8, depth: u32, direct: bool, parent_name: ?[]const u8) !*ResolvedPackage { - const constraint = try Constraint.parse(constraint_str); + const dep_spec = dependencySpec(name, constraint_str); + const constraint = try Constraint.parse(dep_spec.constraint); - if (self.resolved.get(name)) |existing_pkg| { + if (self.resolved.get(dep_spec.install_name)) |existing_pkg| { if (constraint.satisfies(existing_pkg.version)) { if (direct) existing_pkg.direct = true; if (depth < existing_pkg.depth) existing_pkg.depth = depth; return existing_pkg; } - const cons_gop = try self.constraints.getOrPut(name); + const cons_gop = try self.constraints.getOrPut(dep_spec.package_name); if (!cons_gop.found_existing) { - cons_gop.key_ptr.* = try self.allocator.dupe(u8, name); + cons_gop.key_ptr.* = try self.allocator.dupe(u8, dep_spec.package_name); cons_gop.value_ptr.* = .{}; } try cons_gop.value_ptr.append(self.allocator, constraint); - var metadata = try self.fetchMetadata(name); + var metadata = try self.fetchMetadata(dep_spec.package_name); const all_constraints = cons_gop.value_ptr.items; const best = self.selectBestVersionForConstraints(&metadata, all_constraints); @@ -1276,7 +1350,7 @@ pub const Resolver = struct { if (b.version.order(existing_pkg.version) != .eq) { debug.log(" re-resolve {s}: {d}.{d}.{d} -> {d}.{d}.{d}", .{ - name, + dep_spec.install_name, existing_pkg.version.major, existing_pkg.version.minor, existing_pkg.version.patch, @@ -1302,13 +1376,13 @@ pub const Resolver = struct { } else { if (parent_name) |parent| { const nested_best = self.selectBestVersion(&metadata, constraint) orelse { - debug.log(" nested: no version for {s} {s}", .{ name, constraint_str }); + debug.log(" nested: no version for {s} {s}", .{ dep_spec.install_name, dep_spec.constraint }); return existing_pkg; }; if (!nested_best.matchesPlatform()) return existing_pkg; debug.log(" nested: {s}@{d}.{d}.{d} under {s} (hoisted: {d}.{d}.{d})", .{ - name, + dep_spec.install_name, nested_best.version.major, nested_best.version.minor, nested_best.version.patch, @@ -1318,22 +1392,22 @@ pub const Resolver = struct { existing_pkg.version.patch, }); - return try self.createNestedPackage(name, nested_best, depth, parent); + return try self.createNestedPackage(dep_spec.install_name, nested_best, depth, parent); } else { - debug.log(" version conflict for {s}: no version satisfies all constraints", .{name}); + debug.log(" version conflict for {s}: no version satisfies all constraints", .{dep_spec.install_name}); return existing_pkg; } } } - const cons_gop = try self.constraints.getOrPut(name); + const cons_gop = try self.constraints.getOrPut(dep_spec.package_name); if (!cons_gop.found_existing) { - cons_gop.key_ptr.* = try self.allocator.dupe(u8, name); + cons_gop.key_ptr.* = try self.allocator.dupe(u8, dep_spec.package_name); cons_gop.value_ptr.* = .{}; } try cons_gop.value_ptr.append(self.allocator, constraint); - var metadata = try self.fetchMetadata(name); + var metadata = try self.fetchMetadata(dep_spec.package_name); const best = self.selectBestVersion(&metadata, constraint) orelse { return error.NoMatchingVersion; }; @@ -1346,7 +1420,7 @@ pub const Resolver = struct { errdefer self.allocator.destroy(pkg); pkg.* = .{ - .name = try self.string_pool.intern(name), + .name = try self.string_pool.intern(dep_spec.install_name), .version = best.version, .integrity = best.integrity, .tarball_url = try self.allocator.dupe(u8, best.tarball_url), @@ -1358,7 +1432,7 @@ pub const Resolver = struct { .allocator = self.allocator, }; - const name_key = try self.allocator.dupe(u8, name); + const name_key = try self.allocator.dupe(u8, dep_spec.install_name); errdefer self.allocator.free(name_key); try self.resolved.put(name_key, pkg); @@ -1378,9 +1452,10 @@ pub const Resolver = struct { while (opt_iter.next()) |entry| { const dep_name = entry.key_ptr.*; const dep_constraint = entry.value_ptr.*; + const opt_spec = dependencySpec(dep_name, dep_constraint); - if (self.metadata_cache.get(dep_name)) |opt_metadata| { - const opt_constraint = Constraint.parse(dep_constraint) catch continue; + if (self.metadata_cache.get(opt_spec.package_name)) |opt_metadata| { + const opt_constraint = Constraint.parse(opt_spec.constraint) catch continue; const opt_best = self.selectBestVersion(&opt_metadata, opt_constraint) orelse continue; if (!opt_best.matchesPlatform()) continue; @@ -1453,9 +1528,10 @@ pub const Resolver = struct { while (opt_iter.next()) |entry| { const dep_name = entry.key_ptr.*; const dep_constraint = entry.value_ptr.*; + const opt_spec = dependencySpec(dep_name, dep_constraint); - if (self.metadata_cache.get(dep_name)) |opt_metadata| { - const opt_constraint = Constraint.parse(dep_constraint) catch continue; + if (self.metadata_cache.get(opt_spec.package_name)) |opt_metadata| { + const opt_constraint = Constraint.parse(opt_spec.constraint) catch continue; const opt_best = self.selectBestVersion(&opt_metadata, opt_constraint) orelse continue; if (!opt_best.matchesPlatform()) continue; } @@ -1485,32 +1561,33 @@ pub const Resolver = struct { self.allocator.free(in_progress_key); } - const constraint = try Constraint.parse(constraint_str); - const cons_gop = try self.constraints.getOrPut(name); - + const dep_spec = dependencySpec(name, constraint_str); + const constraint = try Constraint.parse(dep_spec.constraint); + const cons_gop = try self.constraints.getOrPut(dep_spec.package_name); + if (!cons_gop.found_existing) { - cons_gop.key_ptr.* = try self.allocator.dupe(u8, name); + cons_gop.key_ptr.* = try self.allocator.dupe(u8, dep_spec.package_name); cons_gop.value_ptr.* = .{}; } try cons_gop.value_ptr.append(self.allocator, constraint); - if (self.resolved.get(name)) |existing_pkg| { + if (self.resolved.get(dep_spec.install_name)) |existing_pkg| { if (constraint.satisfies(existing_pkg.version)) { if (depth == 0) existing_pkg.direct = true; if (depth < existing_pkg.depth) existing_pkg.depth = depth; return existing_pkg; } - var metadata = try self.fetchMetadata(name); + var metadata = try self.fetchMetadata(dep_spec.package_name); const all_constraints = cons_gop.value_ptr.items; const best = self.selectBestVersionForConstraints(&metadata, all_constraints) orelse { - debug.log(" version conflict for {s}: no version satisfies all constraints", .{name}); + debug.log(" version conflict for {s}: no version satisfies all constraints", .{dep_spec.install_name}); return existing_pkg; }; if (best.version.order(existing_pkg.version) != .eq) { debug.log(" re-resolve {s}: {d}.{d}.{d} -> {d}.{d}.{d}", .{ - name, + dep_spec.install_name, existing_pkg.version.major, existing_pkg.version.minor, existing_pkg.version.patch, @@ -1531,7 +1608,7 @@ pub const Resolver = struct { return existing_pkg; } - var metadata = try self.fetchMetadata(name); + var metadata = try self.fetchMetadata(dep_spec.package_name); const all_constraints = cons_gop.value_ptr.items; const best = self.selectBestVersionForConstraints(&metadata, all_constraints) orelse { return error.NoMatchingVersion; @@ -1541,7 +1618,7 @@ pub const Resolver = struct { errdefer self.allocator.destroy(pkg); pkg.* = .{ - .name = try self.string_pool.intern(name), + .name = try self.string_pool.intern(dep_spec.install_name), .version = best.version, .integrity = best.integrity, .tarball_url = try self.allocator.dupe(u8, best.tarball_url), @@ -1553,7 +1630,7 @@ pub const Resolver = struct { .allocator = self.allocator, }; - const name_key = try self.allocator.dupe(u8, name); + const name_key = try self.allocator.dupe(u8, dep_spec.install_name); errdefer self.allocator.free(name_key); try self.resolved.put(name_key, pkg); @@ -1583,12 +1660,13 @@ pub const Resolver = struct { continue; }; - const opt_metadata = self.fetchMetadata(dep_name) catch continue; - const opt_constraint = Constraint.parse(dep_constraint) catch continue; + const opt_spec = dependencySpec(dep_name, dep_constraint); + const opt_metadata = self.fetchMetadata(opt_spec.package_name) catch continue; + const opt_constraint = Constraint.parse(opt_spec.constraint) catch continue; const opt_best = self.selectBestVersion(&opt_metadata, opt_constraint) orelse continue; if (!opt_best.matchesPlatform()) { - if (self.resolved.fetchRemove(dep_name)) |kv| { + if (self.resolved.fetchRemove(opt_spec.install_name)) |kv| { self.allocator.free(kv.key); kv.value.deinit(); self.allocator.destroy(kv.value); @@ -1659,7 +1737,7 @@ pub const Resolver = struct { const metadata = try PackageMetadata.parseFromJson(self.cache_allocator, json_data); const cache_key = try self.cache_allocator.dupe(u8, name); - + try self.metadata_cache.put(cache_key, metadata); return self.metadata_cache.get(name).?; } @@ -1711,7 +1789,8 @@ pub const Resolver = struct { var satisfies_all = true; for (all_constraints) |c| { if (!c.satisfies(v.version)) { - satisfies_all = false; break; + satisfies_all = false; + break; } } @@ -1725,7 +1804,10 @@ pub const Resolver = struct { for (metadata.versions.items) |*v| { var satisfies_all = true; for (all_constraints) |c| { - if (!c.satisfies(v.version)) { satisfies_all = false; break; } + if (!c.satisfies(v.version)) { + satisfies_all = false; + break; + } } if (satisfies_all and (best == null or v.version.order(best.?.version) == .gt)) best = v; } @@ -1760,10 +1842,12 @@ pub const Resolver = struct { const url_ref = try writer.internString(pkg.tarball_url); const prerelease_ref = if (pkg.version.prerelease) |pre| try writer.internString(pre) - else lockfile.StringRef.empty; + else + lockfile.StringRef.empty; const parent_ref = if (pkg.parent_path) |parent| try writer.internString(parent) - else lockfile.StringRef.empty; + else + lockfile.StringRef.empty; const deps_start: u32 = @intCast(writer.dependencies.items.len); const pkg_install_path = try pkg.installPath(self.allocator); @@ -1790,7 +1874,8 @@ pub const Resolver = struct { .dev = dep.flags.dev, .optional = dep.flags.optional, }, - }); deps_written += 1; + }); + deps_written += 1; } } diff --git a/src/pkg/root.zig b/src/pkg/root.zig index 055f08f..b310aad 100644 --- a/src/pkg/root.zig +++ b/src/pkg/root.zig @@ -2637,6 +2637,118 @@ export fn pkg_info_get_dependency(ctx: ?*const PkgContext, index: u32, out: *Dep return .ok; } +const BinSelection = struct { + err: PkgError, + name: []const u8 = "", +}; + +fn packageSimpleName(pkg_name: []const u8) []const u8 { + if (std.mem.lastIndexOfScalar(u8, pkg_name, '/')) |slash| { + return pkg_name[slash + 1 ..]; + } + return pkg_name; +} + +fn normalizedBinTarget(path: []const u8) []const u8 { + if (std.mem.startsWith(u8, path, "./")) return path[2..]; + return path; +} + +fn appendBinName(list: *std.ArrayList(u8), allocator: std.mem.Allocator, name: []const u8) !void { + if (list.items.len > 0) try list.appendSlice(allocator, ", "); + try list.appendSlice(allocator, name); +} + +fn selectPackageBinName( + c: *PkgContext, + allocator: std.mem.Allocator, + node_modules_path: []const u8, + pkg_name: []const u8, +) BinSelection { + const simple_name = packageSimpleName(pkg_name); + + const pkg_json_path = std.fmt.allocPrint(allocator, "{s}/{s}/package.json", .{ + node_modules_path, + pkg_name, + }) catch return .{ .err = .out_of_memory }; + + const file = std.fs.cwd().openFile(pkg_json_path, .{}) catch { + c.setErrorFmt("Package '{s}' was installed, but its package.json could not be opened", .{pkg_name}); + return .{ .err = .io_error }; + }; + defer file.close(); + + const content = file.readToEndAlloc(allocator, 1024 * 1024) catch { + c.setErrorFmt("Package '{s}' was installed, but its package.json could not be read", .{pkg_name}); + return .{ .err = .io_error }; + }; + + var doc = json.JsonDoc.parse(content) catch { + c.setErrorFmt("Package '{s}' was installed, but its package.json could not be parsed", .{pkg_name}); + return .{ .err = .io_error }; + }; + defer doc.deinit(); + + const root_val = doc.root(); + + if (root_val.getString("bin")) |_| { + const selected = allocator.dupe(u8, simple_name) catch return .{ .err = .out_of_memory }; + return .{ .err = .ok, .name = selected }; + } + + const bin_obj = root_val.getObject("bin") orelse { + c.setErrorFmt("Package '{s}' does not declare any binaries", .{pkg_name}); + return .{ .err = .not_found }; + }; + + var iter = bin_obj.objectIterator() orelse { + c.setErrorFmt("Package '{s}' does not declare any usable binaries", .{pkg_name}); + return .{ .err = .not_found }; + }; + + var valid_count: usize = 0; + var first_name: []const u8 = ""; + var first_target: []const u8 = ""; + var package_named_bin: ?[]const u8 = null; + var all_targets_same = true; + var names: std.ArrayList(u8) = .empty; + + while (iter.next()) |entry| { + const target = entry.value.asString() orelse continue; + + if (valid_count == 0) { + first_name = entry.key; + first_target = normalizedBinTarget(target); + } else if (!std.mem.eql(u8, normalizedBinTarget(target), first_target)) { + all_targets_same = false; + } + + appendBinName(&names, allocator, entry.key) catch return .{ .err = .out_of_memory }; + valid_count += 1; + + if (std.mem.eql(u8, entry.key, simple_name)) { + package_named_bin = entry.key; + } + } + + const selected = package_named_bin orelse if (valid_count == 1 or all_targets_same) first_name else ""; + if (selected.len > 0) { + const selected_copy = allocator.dupe(u8, selected) catch return .{ .err = .out_of_memory }; + return .{ .err = .ok, .name = selected_copy }; + } + + if (valid_count == 0) { + c.setErrorFmt("Package '{s}' does not declare any usable binaries", .{pkg_name}); + } else { + c.setErrorFmt("Package '{s}' exposes multiple binaries ({s}) and no default could be inferred", .{ + pkg_name, + names.items, + }); + } + + return .{ .err = .not_found }; +} + export fn pkg_exec_temp( ctx: ?*PkgContext, package_spec: [*:0]const u8, @@ -2776,14 +2888,16 @@ export fn pkg_exec_temp( var resolved_iter = res.resolved.valueIterator(); while (resolved_iter.next()) |pkg_ptr| { const pkg = pkg_ptr.*; - if (db.hasIntegrity(&pkg.integrity)) { - const pkg_cache_path = db.getPackagePath(&pkg.integrity, arena_alloc) catch continue; + if (db.lookup(&pkg.integrity)) |cache_entry| { + var entry = cache_entry; + defer entry.deinit(); + const pkg_cache_path = arena_alloc.dupe(u8, entry.path) catch continue; pkg_linker.linkPackage(.{ .cache_path = pkg_cache_path, .node_modules_path = temp_nm_dir, .name = pkg.name.slice(), .parent_path = pkg.parent_path, - .file_count = 0, + .file_count = entry.file_count, .has_bin = pkg.has_bin, }) catch continue; } @@ -2798,6 +2912,10 @@ export fn pkg_exec_temp( } runTrustedPostinstall(c, &trusted, temp_nm_dir, arena_alloc); + const selected_bin = selectPackageBinName(c, arena_alloc, temp_nm_dir, pkg_name); + if (selected_bin.err != .ok) return selected_bin.err; + bin_name = selected_bin.name; + var bin_path_buf: [std.fs.max_path_bytes]u8 = undefined; const bin_link_path = std.fmt.bufPrint(&bin_path_buf, "{s}/.bin/{s}", .{ temp_nm_dir, bin_name }) catch return .io_error; diff --git a/src/silver/ast.c b/src/silver/ast.c index 671ef59..09f7d8b 100644 --- a/src/silver/ast.c +++ b/src/silver/ast.c @@ -76,6 +76,7 @@ typedef struct { static sv_ast_t *parse_stmt(P); static sv_ast_t *parse_expr(P); +static sv_ast_t *parse_paren_expr(P); static sv_ast_t *parse_assign(P); static sv_ast_t *parse_ternary(P); static sv_ast_t *parse_binary(P, int min_prec); @@ -409,7 +410,7 @@ static sv_ast_t *try_parse_async_arrow(P) { } sv_lexer_restore_state(&p->lx, &saved); NEXT(); CONSUME(); - sv_ast_t *expr = parse_expr(p); + sv_ast_t *expr = parse_paren_expr(p); expect(p, TOK_RPAREN); if (LA() == TOK_ARROW) { NEXT(); CONSUME(); @@ -534,7 +535,7 @@ static sv_ast_t *parse_primary(P) { n->src_off = paren_off; return n; } - sv_ast_t *expr = parse_expr(p); + sv_ast_t *expr = parse_paren_expr(p); expect(p, TOK_RPAREN); expr->flags |= FN_PAREN; expr->src_off = paren_off; @@ -1248,6 +1249,20 @@ static sv_ast_t *parse_expr(P) { return left; } +static sv_ast_t *parse_paren_expr(P) { + sv_ast_t *left = parse_assign(p); + while (NEXT() == TOK_COMMA) { + CONSUME(); + if (NEXT() == TOK_RPAREN && LA() == TOK_ARROW) break; + + sv_ast_t *n = mk(N_SEQUENCE); + n->left = left; + n->right = parse_assign(p); + left = n; + } + return left; +} + bool ast_references_arguments(const sv_ast_t *node) { if (!node) return false; if (node->type == N_IDENT && node->len == 9 && memcmp(node->str, "arguments", 9) == 0) @@ -1336,7 +1351,13 @@ static sv_ast_t *parse_class(P) { uint8_t flags = 0; - if (TOK == TOK_STATIC) { + if ( + TOK == TOK_STATIC && + LA() != TOK_LPAREN && + LA() != TOK_ASSIGN && + LA() != TOK_SEMICOLON && + LA() != TOK_RBRACE + ) { flags |= FN_STATIC; CONSUME(); NEXT(); diff --git a/src/silver/compiler.c b/src/silver/compiler.c index bcb5399..6c2c124 100644 --- a/src/silver/compiler.c +++ b/src/silver/compiler.c @@ -2158,8 +2158,13 @@ static void compile_call_args_array(sv_compiler_t *c, sv_ast_t *call_node) { typedef enum { SV_CALL_DIRECT = 0, SV_CALL_METHOD = 1, + SV_CALL_SUPER = 2, } sv_call_kind_t; +static inline bool sv_call_kind_has_receiver(sv_call_kind_t kind) { + return kind == SV_CALL_METHOD || kind == SV_CALL_SUPER; +} + static void compile_receiver_property_get(sv_compiler_t *c, sv_ast_t *node) { emit_op(c, OP_DUP); if (node->flags & 1) { @@ -2177,17 +2182,17 @@ static void compile_call_emit_invoke( ) { int argc = node->args.count; if (has_spread) { - if (kind == SV_CALL_METHOD) emit_op(c, OP_SWAP); + if (sv_call_kind_has_receiver(kind)) emit_op(c, OP_SWAP); else emit_op(c, OP_GLOBAL); compile_call_args_array(c, node); - emit_op(c, OP_APPLY); + emit_op(c, kind == SV_CALL_SUPER ? OP_SUPER_APPLY : OP_APPLY); emit_u16(c, 1); return; } for (int i = 0; i < argc; i++) compile_expr(c, node->args.items[i]); - emit_op(c, kind == SV_CALL_METHOD ? OP_CALL_METHOD : OP_CALL); + emit_op(c, sv_call_kind_has_receiver(kind) ? OP_CALL_METHOD : OP_CALL); emit_u16(c, (uint16_t)argc); } @@ -2195,7 +2200,7 @@ static sv_call_kind_t compile_call_setup_non_optional(sv_compiler_t *c, sv_ast_t if (is_ident_name(callee, "super")) { emit_op(c, OP_THIS); emit_get_var(c, "super", 5); - return SV_CALL_METHOD; + return SV_CALL_SUPER; } if (callee->type == N_MEMBER && is_ident_name(callee->left, "super")) { @@ -2247,7 +2252,7 @@ static void compile_optional_call_after_setup( emit_op(c, OP_IS_UNDEF_OR_NULL); int j_do_call = emit_jump(c, OP_JMP_FALSE); emit_op(c, OP_POP); - if (kind == SV_CALL_METHOD) + if (sv_call_kind_has_receiver(kind)) emit_op(c, OP_POP); emit_op(c, OP_UNDEF); int j_end = emit_jump(c, OP_JMP); @@ -3649,14 +3654,14 @@ static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { emit_loop(c, loop_start); patch_jump(c, exit_jump); - int skip_break_cleanup = -1; - if (break_close_slot >= 0) - skip_break_cleanup = emit_jump(c, OP_JMP); - if (is_for_of) { emit_op(c, OP_POP); emit_op(c, OP_TRY_POP); + int skip_break_cleanup = -1; + if (break_close_slot >= 0) + skip_break_cleanup = emit_jump(c, OP_JMP); + pop_loop(c); if (break_close_slot >= 0) { emit_op(c, OP_CLOSE_UPVAL); @@ -3678,6 +3683,10 @@ static void compile_for_each(sv_compiler_t *c, sv_ast_t *node, bool is_for_of) { patch_jump(c, end_jump); c->try_depth--; } else { + int skip_break_cleanup = -1; + if (break_close_slot >= 0) + skip_break_cleanup = emit_jump(c, OP_JMP); + pop_loop(c); if (break_close_slot >= 0) { emit_op(c, OP_CLOSE_UPVAL); diff --git a/src/silver/engine.c b/src/silver/engine.c index 45d93fd..26585ab 100644 --- a/src/silver/engine.c +++ b/src/silver/engine.c @@ -1229,9 +1229,10 @@ ant_value_t sv_execute_frame(sv_vm_t *vm, sv_func_t *func, ant_value_t this, ant } L_NEW: { VM_CHECK(sv_op_new(vm, js, ip)); NEXT(3); } - L_APPLY: { VM_CHECK(sv_op_apply(vm, js, ip)); NEXT(3); } - L_NEW_APPLY: { VM_CHECK(sv_op_new_apply(vm, js, ip)); NEXT(3); } - L_EVAL: { VM_CHECK(sv_op_eval(vm, js, frame, ip)); NEXT(5); } + L_APPLY: { VM_CHECK(sv_op_apply(vm, js, ip)); NEXT(3); } + L_SUPER_APPLY: { VM_CHECK(sv_op_super_apply(vm, js, frame, ip)); NEXT(3); } + L_NEW_APPLY: { VM_CHECK(sv_op_new_apply(vm, js, ip)); NEXT(3); } + L_EVAL: { VM_CHECK(sv_op_eval(vm, js, frame, ip)); NEXT(5); } // TODO: make the methods below DRY L_RETURN: { diff --git a/src/silver/ops/calls.h b/src/silver/ops/calls.h index f3e0536..9f785b7 100644 --- a/src/silver/ops/calls.h +++ b/src/silver/ops/calls.h @@ -119,6 +119,29 @@ static inline ant_value_t sv_op_apply(sv_vm_t *vm, ant_t *js, uint8_t *ip) { return result; } +static inline ant_value_t sv_op_super_apply(sv_vm_t *vm, ant_t *js, sv_frame_t *frame, uint8_t *ip) { + uint16_t argc = sv_get_u16(ip + 1); + ant_value_t *args = &vm->stack[vm->sp - argc]; + ant_value_t this = vm->stack[vm->sp - argc - 1]; + ant_value_t func = vm->stack[vm->sp - argc - 2]; + sv_call_args_t call; + + sv_call_args_reset(&call, args, (int)argc); + ant_value_t norm = sv_apply_normalize_args(js, &call); + if (is_err(norm)) return norm; + + if (frame) js->new_target = frame->new_target; + ant_value_t super_this = this; + ant_value_t result = sv_vm_call( + vm, js, func, this, call.args, call.argc, &super_this, true); + sv_call_args_release(&call); + vm->sp -= argc + 2; + if (frame && !is_err(result)) + frame->this = is_object_type(result) ? result : super_this; + if (!is_err(result)) vm->stack[vm->sp++] = result; + return result; +} + static inline ant_value_t sv_op_new_apply(sv_vm_t *vm, ant_t *js, uint8_t *ip) { uint16_t argc = sv_get_u16(ip + 1); ant_value_t *args = &vm->stack[vm->sp - argc]; diff --git a/src/silver/ops/coercion.h b/src/silver/ops/coercion.h index 1b41ff4..44aefdc 100644 --- a/src/silver/ops/coercion.h +++ b/src/silver/ops/coercion.h @@ -101,12 +101,15 @@ static inline bool sv_module_namespace_has_export( return lookup_string_prop_meta(js, as_obj, name, len, &meta); } -static inline const char *sv_module_namespace_filename(ant_t *js, ant_value_t ns) { +static inline const char *sv_module_namespace_display_name(ant_t *js, ant_value_t ns) { if (!is_object_type(ns)) return NULL; ant_value_t module_ctx = js_get_slot(ns, SLOT_MODULE_CTX); if (!is_object_type(module_ctx)) return NULL; + ant_value_t display_name = js_get(js, module_ctx, "displayName"); + if (vtype(display_name) == T_STR) return js_getstr(js, display_name, NULL); + ant_value_t filename = js_get(js, module_ctx, "filename"); if (vtype(filename) != T_STR) return NULL; @@ -119,12 +122,12 @@ static inline ant_value_t sv_missing_named_export_error( const char *name, size_t len ) { - const char *filename = sv_module_namespace_filename(js, ns); - if (!filename) filename = ""; + const char *display_name = sv_module_namespace_display_name(js, ns); + if (!display_name) display_name = ""; return js_mkerr_typed( js, JS_ERR_SYNTAX, "The requested module '%s' does not provide an export named '%.*s'", - filename, (int)len, name + display_name, (int)len, name ); } diff --git a/src/types/buffer.d.ts b/src/types/buffer.d.ts index 1212e8d..d2f15a4 100644 --- a/src/types/buffer.d.ts +++ b/src/types/buffer.d.ts @@ -11,3 +11,20 @@ interface BufferMethods { } declare const Buffer: BufferStatic; + +declare module 'buffer' { + const Buffer: BufferStatic; + const constants: { + MAX_LENGTH: number; + MAX_STRING_LENGTH: number; + }; + const kMaxLength: number; + const kStringMaxLength: number; + const INSPECT_MAX_BYTES: number; + function atob(data: string): string; + function btoa(data: string): string; +} + +declare module 'node:buffer' { + export * from 'buffer'; +} diff --git a/src/types/modules/child_process.d.ts b/src/types/modules/child_process.d.ts index f84a0bd..1f0b11d 100644 --- a/src/types/modules/child_process.d.ts +++ b/src/types/modules/child_process.d.ts @@ -1,4 +1,6 @@ declare module 'child_process' { + import { EventEmitter } from 'events'; + interface SpawnResult { stdout: string; stderr: string; @@ -7,7 +9,7 @@ declare module 'child_process' { pid: number; } - interface ChildProcess { + class ChildProcess extends EventEmitter { stdout: string; stderr: string; exitCode: number | null; diff --git a/src/types/modules/constants.d.ts b/src/types/modules/constants.d.ts new file mode 100644 index 0000000..054d8dd --- /dev/null +++ b/src/types/modules/constants.d.ts @@ -0,0 +1,12 @@ +declare module 'constants' { + const constants: Record; + export = constants; +} + +declare module 'ant:constants' { + export * from 'constants'; +} + +declare module 'node:constants' { + export * from 'constants'; +} diff --git a/src/types/modules/fs.d.ts b/src/types/modules/fs.d.ts index 3c7324d..d01385f 100644 --- a/src/types/modules/fs.d.ts +++ b/src/types/modules/fs.d.ts @@ -134,10 +134,14 @@ declare module 'fs' { function existsSync(path: string): boolean; function access(path: string, mode?: number): Promise; function accessSync(path: string, mode?: number): void; + function chmod(path: string, mode: number | string): Promise; + function chmodSync(path: string, mode: number | string): void; function readdir(path: string): Promise; function readdirSync(path: string): string[]; function realpath(path: string): Promise; function realpathSync(path: string): string; + function readlink(path: string): Promise; + function readlinkSync(path: string): string; namespace realpathSync { function native(path: string): string; } @@ -235,8 +239,10 @@ declare module 'fs/promises' { function stat(path: string): Promise; function exists(path: string): Promise; function access(path: string, mode?: number): Promise; + function chmod(path: string, mode: number | string): Promise; function readdir(path: string): Promise; function realpath(path: string): Promise; + function readlink(path: string): Promise; } declare module 'ant:fs/promises' { diff --git a/src/types/modules/stream.d.ts b/src/types/modules/stream.d.ts index b34985f..353b6db 100644 --- a/src/types/modules/stream.d.ts +++ b/src/types/modules/stream.d.ts @@ -56,6 +56,8 @@ declare module 'stream' { function pipeline(...streams: Array void)>): Stream; function finished(stream: Stream, callback?: (error?: Error | null) => void): Stream; + function getDefaultHighWaterMark(objectMode: boolean): number; + function setDefaultHighWaterMark(objectMode: boolean, value: number): void; const promises: { pipeline(...streams: Stream[]): Promise; @@ -63,7 +65,19 @@ declare module 'stream' { }; export default Stream; - export { Stream, Readable, Writable, Duplex, Transform, PassThrough, pipeline, finished, promises }; + export { + Stream, + Readable, + Writable, + Duplex, + Transform, + PassThrough, + pipeline, + finished, + getDefaultHighWaterMark, + setDefaultHighWaterMark, + promises + }; } declare module 'ant:stream' { diff --git a/src/types/modules/util.d.ts b/src/types/modules/util.d.ts index 9ca4376..7acf4ae 100644 --- a/src/types/modules/util.d.ts +++ b/src/types/modules/util.d.ts @@ -55,6 +55,8 @@ declare module 'util' { function inherits(ctor: (...args: unknown[]) => unknown, superCtor: (...args: unknown[]) => unknown): void; function parseEnv(content: string): Record; function promisify(fn: (...args: unknown[]) => unknown): (...args: unknown[]) => Promise; + function callbackify(fn: (...args: unknown[]) => Promise): (...args: unknown[]) => void; + function aborted(signal: AbortSignal, resource: object): Promise; function stripVTControlCharacters(str: string): string; function styleText(format: StyleTextFormat, text: string, options?: StyleTextOptions): string; diff --git a/tests/builtin_missing_named_export_importer.mjs b/tests/builtin_missing_named_export_importer.mjs new file mode 100644 index 0000000..6becb94 --- /dev/null +++ b/tests/builtin_missing_named_export_importer.mjs @@ -0,0 +1,3 @@ +import { __antMissingExportForDisplayNameTest } from 'node:url'; + +void __antMissingExportForDisplayNameTest; diff --git a/tests/test_builtin_missing_named_export.mjs b/tests/test_builtin_missing_named_export.mjs new file mode 100644 index 0000000..ec9c7fd --- /dev/null +++ b/tests/test_builtin_missing_named_export.mjs @@ -0,0 +1,21 @@ +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const expectedMessage = + "The requested module 'node:url' does not provide an export named '__antMissingExportForDisplayNameTest'"; + +let caught = null; + +try { + await import('./builtin_missing_named_export_importer.mjs'); +} catch (err) { + caught = err; +} + +assert(caught, 'expected import to fail'); +assert(caught.name === 'SyntaxError', `expected SyntaxError, got ${caught && caught.name}`); +assert(caught.message === expectedMessage, + `unexpected message:\n${caught && caught.message}\n!=\n${expectedMessage}`); + +console.log('builtin missing named export test passed'); diff --git a/tests/test_fs_chmod_sync.mjs b/tests/test_fs_chmod_sync.mjs new file mode 100644 index 0000000..3f4151d --- /dev/null +++ b/tests/test_fs_chmod_sync.mjs @@ -0,0 +1,40 @@ +import fsDefault, { chmod as chmodFromFs, chmodSync as chmodSyncFromFs, statSync, writeFileSync, unlinkSync } from 'fs'; +import { chmod as chmodFromNodeFs, chmodSync as chmodSyncFromNodeFs } from 'node:fs'; +import { chmod as chmodFromPromises } from 'node:fs/promises'; + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const testFile = 'tests/.fs_chmod_sync_tmp'; + +try { + writeFileSync(testFile, 'chmod sync test'); + + chmodSyncFromFs(testFile, 0o600); + assert((statSync(testFile).mode & 0o777) === 0o600, 'fs named chmodSync should set numeric mode'); + + chmodSyncFromNodeFs(testFile, '644'); + assert((statSync(testFile).mode & 0o777) === 0o644, 'node:fs named chmodSync should set string mode'); + + fsDefault.chmodSync(testFile, '0o600'); + assert((fsDefault.statSync(testFile).mode & 0o777) === 0o600, 'default chmodSync should set prefixed string mode'); + + await chmodFromPromises(testFile, 0o644); + assert((statSync(testFile).mode & 0o777) === 0o644, 'fs/promises chmod should set numeric mode'); + + await chmodFromNodeFs(testFile, '600'); + assert((statSync(testFile).mode & 0o777) === 0o600, 'node:fs named chmod should set string mode'); + + await new Promise((resolve, reject) => { + chmodFromFs(testFile, 0o644, error => error ? reject(error) : resolve()); + }); + assert((statSync(testFile).mode & 0o777) === 0o644, 'fs callback chmod should set numeric mode'); +} finally { + try { + fsDefault.chmodSync(testFile, 0o600); + unlinkSync(testFile); + } catch {} +} + +console.log('fs chmodSync test passed'); diff --git a/tests/test_process_emit_warning.cjs b/tests/test_process_emit_warning.cjs new file mode 100644 index 0000000..674da47 --- /dev/null +++ b/tests/test_process_emit_warning.cjs @@ -0,0 +1,66 @@ +const { spawnSync } = require('child_process'); + +function assertIncludes(haystack, needle, label) { + if (!haystack.includes(needle)) { + throw new Error(`${label}: expected ${JSON.stringify(haystack)} to include ${JSON.stringify(needle)}`); + } +} + +function assertNotIncludes(haystack, needle, label) { + if (haystack.includes(needle)) { + throw new Error(`${label}: expected ${JSON.stringify(haystack)} not to include ${JSON.stringify(needle)}`); + } +} + +function assertMatch(haystack, pattern, label) { + if (!pattern.test(haystack)) { + throw new Error(`${label}: expected ${JSON.stringify(haystack)} to match ${pattern}`); + } +} + +function run(source) { + const result = spawnSync(process.execPath, ['-e', source]); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`child exited ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`); + } + return { + stdout: String(result.stdout), + stderr: String(result.stderr), + }; +} + +const withDetail = run(` + process.emitWarning('Something happened!', { + code: 'Custom_Warning', + detail: 'Additional information about warning' + }); +`); + +assertMatch( + withDetail.stderr, + /\\(ant:\\d+\\) \\[Custom_Warning\\] Warning: Something happened!\\n/, + 'emitWarning formats code, type, and message' +); +assertIncludes( + withDetail.stderr, + 'Additional information about warning\n', + 'emitWarning prints detail' +); +assertNotIncludes( + withDetail.stderr, + '[Custom_Warning] : Something happened!', + 'emitWarning avoids the old empty type formatting' +); + +const listenerPayload = run(` + process.on('warning', warning => { + console.log([warning.name, warning.message, warning.code, warning.detail].join('|')); + }); + process.emitWarning('msg', { code: 'C', detail: 'd' }); +`); + +assertIncludes(listenerPayload.stderr, '[C] Warning: msg\n', 'emitWarning still writes default warning output'); +assertIncludes(listenerPayload.stdout, 'Warning|msg|C|d\n', 'emitWarning emits a warning object'); + +console.log('process.emitWarning formatting ok'); -- 2.51.2