@gjsify/path 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ // Reference: Node.js lib/path.js
2
+ // Reimplemented for GJS
3
+ // Exports POSIX implementation by default (GJS runs on POSIX systems)
4
+
5
+ import * as posix from './posix.js';
6
+ import * as win32 from './win32.js';
7
+
8
+ export type { ParsedPath, FormatInputPathObject } from './posix.js';
9
+
10
+ // Re-export all POSIX functions as default path API
11
+ export const {
12
+ resolve,
13
+ normalize,
14
+ isAbsolute,
15
+ join,
16
+ relative,
17
+ toNamespacedPath,
18
+ dirname,
19
+ basename,
20
+ extname,
21
+ format,
22
+ parse,
23
+ sep,
24
+ delimiter,
25
+ } = posix;
26
+
27
+ // Export platform-specific implementations
28
+ export { posix, win32 };
29
+
30
+ // Default export is the posix module (matching Node.js behavior on POSIX systems)
31
+ export default {
32
+ resolve: posix.resolve,
33
+ normalize: posix.normalize,
34
+ isAbsolute: posix.isAbsolute,
35
+ join: posix.join,
36
+ relative: posix.relative,
37
+ toNamespacedPath: posix.toNamespacedPath,
38
+ dirname: posix.dirname,
39
+ basename: posix.basename,
40
+ extname: posix.extname,
41
+ format: posix.format,
42
+ parse: posix.parse,
43
+ sep: posix.sep,
44
+ delimiter: posix.delimiter,
45
+ posix,
46
+ win32,
47
+ };
package/src/posix.ts ADDED
@@ -0,0 +1,406 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from Deno (refs/deno/ext/node/polyfills/path/_posix.ts) and Node.js (refs/node/lib/path.js)
3
+ // Copyright (c) 2018-2026 the Deno authors. MIT license.
4
+ // Copyright (c) Node.js contributors. MIT license.
5
+ // Modifications: TypeScript types, no primordials, GJS-compatible
6
+
7
+ import { CHAR_DOT, CHAR_FORWARD_SLASH } from './constants.js';
8
+ import {
9
+ assertPath,
10
+ isPosixPathSeparator,
11
+ normalizeString,
12
+ _format,
13
+ } from './util.js';
14
+
15
+ export interface ParsedPath {
16
+ root: string;
17
+ dir: string;
18
+ base: string;
19
+ ext: string;
20
+ name: string;
21
+ }
22
+
23
+ export type FormatInputPathObject = Partial<ParsedPath>;
24
+
25
+ export const sep = '/';
26
+ export const delimiter = ':';
27
+
28
+ function posixCwd(): string {
29
+ // In GJS, try GLib.get_current_dir() at runtime
30
+ if (typeof globalThis.process?.cwd === 'function') {
31
+ return globalThis.process.cwd();
32
+ }
33
+ // Fallback: try GLib
34
+ try {
35
+ const GLib = (globalThis as any).imports?.gi?.GLib;
36
+ if (GLib?.get_current_dir) {
37
+ return GLib.get_current_dir();
38
+ }
39
+ } catch {
40
+ // ignore
41
+ }
42
+ return '/';
43
+ }
44
+
45
+ export function resolve(...pathSegments: string[]): string {
46
+ let resolvedPath = '';
47
+ let resolvedAbsolute = false;
48
+
49
+ for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
50
+ let path: string;
51
+ if (i >= 0) {
52
+ path = pathSegments[i];
53
+ assertPath(path);
54
+ if (path.length === 0) continue;
55
+ } else {
56
+ path = posixCwd();
57
+ }
58
+
59
+ resolvedPath = `${path}/${resolvedPath}`;
60
+ resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
61
+ }
62
+
63
+ resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/', isPosixPathSeparator);
64
+
65
+ if (resolvedAbsolute) {
66
+ if (resolvedPath.length > 0) {
67
+ return `/${resolvedPath}`;
68
+ }
69
+ return '/';
70
+ } else if (resolvedPath.length > 0) {
71
+ return resolvedPath;
72
+ }
73
+ return '.';
74
+ }
75
+
76
+ export function normalize(path: string): string {
77
+ assertPath(path);
78
+
79
+ if (path.length === 0) return '.';
80
+
81
+ const isAbsolutePath = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
82
+ const trailingSeparator = path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
83
+
84
+ let normalized = normalizeString(path, !isAbsolutePath, '/', isPosixPathSeparator);
85
+
86
+ if (normalized.length === 0 && !isAbsolutePath) {
87
+ normalized = '.';
88
+ }
89
+ if (normalized.length > 0 && trailingSeparator) {
90
+ normalized += '/';
91
+ }
92
+
93
+ if (isAbsolutePath) {
94
+ return `/${normalized}`;
95
+ }
96
+ return normalized;
97
+ }
98
+
99
+ export function isAbsolute(path: string): boolean {
100
+ assertPath(path);
101
+ return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH;
102
+ }
103
+
104
+ export function join(...paths: string[]): string {
105
+ if (paths.length === 0) return '.';
106
+
107
+ let joined: string | undefined;
108
+ for (let i = 0; i < paths.length; ++i) {
109
+ const arg = paths[i];
110
+ assertPath(arg);
111
+ if (arg.length > 0) {
112
+ if (joined === undefined) {
113
+ joined = arg;
114
+ } else {
115
+ joined += `/${arg}`;
116
+ }
117
+ }
118
+ }
119
+ if (joined === undefined) return '.';
120
+ return normalize(joined);
121
+ }
122
+
123
+ export function relative(from: string, to: string): string {
124
+ assertPath(from);
125
+ assertPath(to);
126
+
127
+ if (from === to) return '';
128
+
129
+ from = resolve(from);
130
+ to = resolve(to);
131
+
132
+ if (from === to) return '';
133
+
134
+ // Find common prefix
135
+ let fromStart = 1;
136
+ const fromEnd = from.length;
137
+ const fromLen = fromEnd - fromStart;
138
+
139
+ let toStart = 1;
140
+ const toLen = to.length - toStart;
141
+
142
+ const length = fromLen < toLen ? fromLen : toLen;
143
+ let lastCommonSep = -1;
144
+ let i = 0;
145
+
146
+ for (; i <= length; ++i) {
147
+ if (i === length) {
148
+ if (toLen > length) {
149
+ if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) {
150
+ return to.slice(toStart + i + 1);
151
+ } else if (i === 0) {
152
+ return to.slice(toStart + i);
153
+ }
154
+ } else if (fromLen > length) {
155
+ if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) {
156
+ lastCommonSep = i;
157
+ } else if (i === 0) {
158
+ lastCommonSep = 0;
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ const fromCode = from.charCodeAt(fromStart + i);
164
+ const toCode = to.charCodeAt(toStart + i);
165
+ if (fromCode !== toCode) break;
166
+ if (fromCode === CHAR_FORWARD_SLASH) lastCommonSep = i;
167
+ }
168
+
169
+ let out = '';
170
+ for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
171
+ if (i === fromEnd || from.charCodeAt(i) === CHAR_FORWARD_SLASH) {
172
+ if (out.length === 0) {
173
+ out += '..';
174
+ } else {
175
+ out += '/..';
176
+ }
177
+ }
178
+ }
179
+
180
+ if (out.length > 0) {
181
+ return out + to.slice(toStart + lastCommonSep);
182
+ }
183
+
184
+ toStart += lastCommonSep;
185
+ if (to.charCodeAt(toStart) === CHAR_FORWARD_SLASH) {
186
+ ++toStart;
187
+ }
188
+ return to.slice(toStart);
189
+ }
190
+
191
+ export function toNamespacedPath(path: string): string {
192
+ // On POSIX, this is a no-op
193
+ return path;
194
+ }
195
+
196
+ export function dirname(path: string): string {
197
+ assertPath(path);
198
+ if (path.length === 0) return '.';
199
+
200
+ const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
201
+ let end = -1;
202
+ let matchedSlash = true;
203
+
204
+ for (let i = path.length - 1; i >= 1; --i) {
205
+ if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
206
+ if (!matchedSlash) {
207
+ end = i;
208
+ break;
209
+ }
210
+ } else {
211
+ matchedSlash = false;
212
+ }
213
+ }
214
+
215
+ if (end === -1) return hasRoot ? '/' : '.';
216
+ if (hasRoot && end === 1) return '//';
217
+ return path.slice(0, end);
218
+ }
219
+
220
+ export function basename(path: string, ext?: string): string {
221
+ if (ext !== undefined) assertPath(ext);
222
+ assertPath(path);
223
+
224
+ let start = 0;
225
+ let end = -1;
226
+ let matchedSlash = true;
227
+
228
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
229
+ if (ext.length === path.length && ext === path) return '';
230
+ let extIdx = ext.length - 1;
231
+ let firstNonSlashEnd = -1;
232
+ for (let i = path.length - 1; i >= 0; --i) {
233
+ const code = path.charCodeAt(i);
234
+ if (code === CHAR_FORWARD_SLASH) {
235
+ if (!matchedSlash) {
236
+ start = i + 1;
237
+ break;
238
+ }
239
+ } else {
240
+ if (firstNonSlashEnd === -1) {
241
+ matchedSlash = false;
242
+ firstNonSlashEnd = i + 1;
243
+ }
244
+ if (extIdx >= 0) {
245
+ if (code === ext.charCodeAt(extIdx)) {
246
+ if (--extIdx === -1) {
247
+ end = i;
248
+ }
249
+ } else {
250
+ extIdx = -1;
251
+ end = firstNonSlashEnd;
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ if (start === end) end = firstNonSlashEnd;
258
+ else if (end === -1) end = path.length;
259
+ return path.slice(start, end);
260
+ } else {
261
+ for (let i = path.length - 1; i >= 0; --i) {
262
+ if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
263
+ if (!matchedSlash) {
264
+ start = i + 1;
265
+ break;
266
+ }
267
+ } else if (end === -1) {
268
+ matchedSlash = false;
269
+ end = i + 1;
270
+ }
271
+ }
272
+
273
+ if (end === -1) return '';
274
+ return path.slice(start, end);
275
+ }
276
+ }
277
+
278
+ export function extname(path: string): string {
279
+ assertPath(path);
280
+
281
+ let startDot = -1;
282
+ let startPart = 0;
283
+ let end = -1;
284
+ let matchedSlash = true;
285
+ let preDotState = 0;
286
+
287
+ for (let i = path.length - 1; i >= 0; --i) {
288
+ const code = path.charCodeAt(i);
289
+ if (code === CHAR_FORWARD_SLASH) {
290
+ if (!matchedSlash) {
291
+ startPart = i + 1;
292
+ break;
293
+ }
294
+ continue;
295
+ }
296
+ if (end === -1) {
297
+ matchedSlash = false;
298
+ end = i + 1;
299
+ }
300
+ if (code === CHAR_DOT) {
301
+ if (startDot === -1) {
302
+ startDot = i;
303
+ } else if (preDotState !== 1) {
304
+ preDotState = 1;
305
+ }
306
+ } else if (startDot !== -1) {
307
+ preDotState = -1;
308
+ }
309
+ }
310
+
311
+ if (
312
+ startDot === -1 ||
313
+ end === -1 ||
314
+ preDotState === 0 ||
315
+ (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
316
+ ) {
317
+ return '';
318
+ }
319
+ return path.slice(startDot, end);
320
+ }
321
+
322
+ export function format(pathObject: FormatInputPathObject): string {
323
+ if (pathObject === null || typeof pathObject !== 'object') {
324
+ throw new TypeError(
325
+ 'The "pathObject" argument must be of type Object. Received type ' + typeof pathObject
326
+ );
327
+ }
328
+ return _format('/', pathObject);
329
+ }
330
+
331
+ export function parse(path: string): ParsedPath {
332
+ assertPath(path);
333
+
334
+ const ret: ParsedPath = { root: '', dir: '', base: '', ext: '', name: '' };
335
+ if (path.length === 0) return ret;
336
+
337
+ const isAbsolutePath = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
338
+ let start: number;
339
+
340
+ if (isAbsolutePath) {
341
+ ret.root = '/';
342
+ start = 1;
343
+ } else {
344
+ start = 0;
345
+ }
346
+
347
+ let startDot = -1;
348
+ let startPart = 0;
349
+ let end = -1;
350
+ let matchedSlash = true;
351
+ let i = path.length - 1;
352
+ let preDotState = 0;
353
+
354
+ for (; i >= start; --i) {
355
+ const code = path.charCodeAt(i);
356
+ if (code === CHAR_FORWARD_SLASH) {
357
+ if (!matchedSlash) {
358
+ startPart = i + 1;
359
+ break;
360
+ }
361
+ continue;
362
+ }
363
+ if (end === -1) {
364
+ matchedSlash = false;
365
+ end = i + 1;
366
+ }
367
+ if (code === CHAR_DOT) {
368
+ if (startDot === -1) startDot = i;
369
+ else if (preDotState !== 1) preDotState = 1;
370
+ } else if (startDot !== -1) {
371
+ preDotState = -1;
372
+ }
373
+ }
374
+
375
+ if (
376
+ startDot === -1 ||
377
+ end === -1 ||
378
+ preDotState === 0 ||
379
+ (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
380
+ ) {
381
+ if (end !== -1) {
382
+ if (startPart === 0 && isAbsolutePath) {
383
+ ret.base = ret.name = path.slice(1, end);
384
+ } else {
385
+ ret.base = ret.name = path.slice(startPart, end);
386
+ }
387
+ }
388
+ } else {
389
+ if (startPart === 0 && isAbsolutePath) {
390
+ ret.name = path.slice(1, startDot);
391
+ ret.base = path.slice(1, end);
392
+ } else {
393
+ ret.name = path.slice(startPart, startDot);
394
+ ret.base = path.slice(startPart, end);
395
+ }
396
+ ret.ext = path.slice(startDot, end);
397
+ }
398
+
399
+ if (startPart > 0) {
400
+ ret.dir = path.slice(0, startPart - 1);
401
+ } else if (isAbsolutePath) {
402
+ ret.dir = '/';
403
+ }
404
+
405
+ return ret;
406
+ }
package/src/test.mts ADDED
@@ -0,0 +1,6 @@
1
+
2
+ import { run } from '@gjsify/unit';
3
+
4
+ import pathTestSuite from './index.spec.js';
5
+
6
+ run({ pathTestSuite });
package/src/util.ts ADDED
@@ -0,0 +1,145 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from Deno (refs/deno/ext/node/polyfills/path/) and Node.js (refs/node/lib/path.js)
3
+ // Copyright (c) 2018-2026 the Deno authors. MIT license.
4
+ // Copyright (c) Node.js contributors. MIT license.
5
+ // Modifications: TypeScript types, no primordials
6
+
7
+ import {
8
+ CHAR_DOT,
9
+ CHAR_FORWARD_SLASH,
10
+ CHAR_BACKWARD_SLASH,
11
+ } from './constants.js';
12
+
13
+ export function assertPath(path: unknown): asserts path is string {
14
+ if (typeof path !== 'string') {
15
+ throw new TypeError(
16
+ 'The "path" argument must be of type string. Received type ' + typeof path
17
+ );
18
+ }
19
+ }
20
+
21
+ export function isPosixPathSeparator(code: number): boolean {
22
+ return code === CHAR_FORWARD_SLASH;
23
+ }
24
+
25
+ export function isPathSeparator(code: number): boolean {
26
+ return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
27
+ }
28
+
29
+ export function isWindowsDeviceRoot(code: number): boolean {
30
+ return (
31
+ (code >= 65 && code <= 90) || // A-Z
32
+ (code >= 97 && code <= 122) // a-z
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Resolves `.` and `..` segments in a path string.
38
+ */
39
+ export function normalizeString(
40
+ path: string,
41
+ allowAboveRoot: boolean,
42
+ separator: string,
43
+ isPathSep: (code: number) => boolean
44
+ ): string {
45
+ let res = '';
46
+ let lastSegmentLength = 0;
47
+ let lastSlash = -1;
48
+ let dots = 0;
49
+ let code: number;
50
+
51
+ for (let i = 0; i <= path.length; ++i) {
52
+ if (i < path.length) {
53
+ code = path.charCodeAt(i);
54
+ } else if (isPathSep(code!)) {
55
+ break;
56
+ } else {
57
+ code = CHAR_FORWARD_SLASH;
58
+ }
59
+
60
+ if (isPathSep(code)) {
61
+ if (lastSlash === i - 1 || dots === 1) {
62
+ // NOOP — skip consecutive separators or single dot
63
+ } else if (lastSlash !== i - 1 && dots === 2) {
64
+ if (
65
+ res.length < 2 ||
66
+ lastSegmentLength !== 2 ||
67
+ res.charCodeAt(res.length - 1) !== CHAR_DOT ||
68
+ res.charCodeAt(res.length - 2) !== CHAR_DOT
69
+ ) {
70
+ if (res.length > 2) {
71
+ const lastSlashIndex = res.lastIndexOf(separator);
72
+ if (lastSlashIndex === -1) {
73
+ res = '';
74
+ lastSegmentLength = 0;
75
+ } else {
76
+ res = res.slice(0, lastSlashIndex);
77
+ lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
78
+ }
79
+ lastSlash = i;
80
+ dots = 0;
81
+ continue;
82
+ } else if (res.length === 2 || res.length === 1) {
83
+ res = '';
84
+ lastSegmentLength = 0;
85
+ lastSlash = i;
86
+ dots = 0;
87
+ continue;
88
+ }
89
+ }
90
+ if (allowAboveRoot) {
91
+ if (res.length > 0) {
92
+ res += `${separator}..`;
93
+ } else {
94
+ res = '..';
95
+ }
96
+ lastSegmentLength = 2;
97
+ }
98
+ } else {
99
+ if (res.length > 0) {
100
+ res += separator + path.slice(lastSlash + 1, i);
101
+ } else {
102
+ res = path.slice(lastSlash + 1, i);
103
+ }
104
+ lastSegmentLength = i - lastSlash - 1;
105
+ }
106
+ lastSlash = i;
107
+ dots = 0;
108
+ } else if (code === CHAR_DOT && dots !== -1) {
109
+ ++dots;
110
+ } else {
111
+ dots = -1;
112
+ }
113
+ }
114
+
115
+ return res;
116
+ }
117
+
118
+ /**
119
+ * Format a parsed path object into a path string.
120
+ */
121
+ export function _format(sep: string, pathObject: Record<string, any>): string {
122
+ if (pathObject === null || typeof pathObject !== 'object') {
123
+ throw new TypeError(
124
+ 'The "pathObject" argument must be of type Object. Received type ' + typeof pathObject
125
+ );
126
+ }
127
+
128
+ const dir = pathObject.dir || pathObject.root;
129
+ const base =
130
+ pathObject.base || (pathObject.name || '') + formatExt(pathObject.ext);
131
+
132
+ if (!dir) {
133
+ return base;
134
+ }
135
+
136
+ if (dir === pathObject.root) {
137
+ return dir + base;
138
+ }
139
+
140
+ return dir + sep + base;
141
+ }
142
+
143
+ function formatExt(ext?: string): string {
144
+ return ext ? `${ext[0] === '.' ? '' : '.'}${ext}` : '';
145
+ }