@gjsify/url 0.0.4 → 0.1.0

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 CHANGED
@@ -1,3 +1,555 @@
1
- export * from '@gjsify/deno_std/node/url';
2
- import _default from '@gjsify/deno_std/node/url';
3
- export default _default;
1
+ // Node.js url module for GJS
2
+ // Uses GLib.Uri for WHATWG URL parsing since globalThis.URL is not available in GJS 1.86
3
+ // See refs/deno/ext/node/polyfills/url.ts, refs/bun/src/js/node/url.ts, refs/node/lib/url.js
4
+
5
+ import GLib from '@girs/glib-2.0';
6
+
7
+ // ---- URLSearchParams ----
8
+
9
+ const PARSE_FLAGS = GLib.UriFlags.HAS_PASSWORD | GLib.UriFlags.ENCODED | GLib.UriFlags.SCHEME_NORMALIZE;
10
+
11
+ export class URLSearchParams {
12
+ _entries: [string, string][] = [];
13
+
14
+ constructor(init?: string | Record<string, string> | [string, string][] | URLSearchParams) {
15
+ if (!init) return;
16
+ if (typeof init === 'string') {
17
+ const s = init.startsWith('?') ? init.slice(1) : init;
18
+ if (s) {
19
+ for (const pair of s.split('&')) {
20
+ const eqIdx = pair.indexOf('=');
21
+ if (eqIdx === -1) {
22
+ this._entries.push([decodeComponent(pair), '']);
23
+ } else {
24
+ this._entries.push([decodeComponent(pair.slice(0, eqIdx)), decodeComponent(pair.slice(eqIdx + 1))]);
25
+ }
26
+ }
27
+ }
28
+ } else if (Array.isArray(init)) {
29
+ for (const [k, v] of init) {
30
+ this._entries.push([String(k), String(v)]);
31
+ }
32
+ } else if (init instanceof URLSearchParams) {
33
+ this._entries = init._entries.map(([k, v]) => [k, v] as [string, string]);
34
+ } else {
35
+ for (const key of Object.keys(init)) {
36
+ this._entries.push([key, String(init[key])]);
37
+ }
38
+ }
39
+ }
40
+
41
+ get(name: string): string | null {
42
+ for (const [k, v] of this._entries) {
43
+ if (k === name) return v;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ getAll(name: string): string[] {
49
+ return this._entries.filter(([k]) => k === name).map(([, v]) => v);
50
+ }
51
+
52
+ set(name: string, value: string): void {
53
+ let found = false;
54
+ this._entries = this._entries.filter(([k]) => {
55
+ if (k === name) {
56
+ if (!found) { found = true; return true; }
57
+ return false;
58
+ }
59
+ return true;
60
+ });
61
+ if (found) {
62
+ for (let i = 0; i < this._entries.length; i++) {
63
+ if (this._entries[i][0] === name) { this._entries[i][1] = value; break; }
64
+ }
65
+ } else {
66
+ this._entries.push([name, value]);
67
+ }
68
+ }
69
+
70
+ has(name: string): boolean {
71
+ return this._entries.some(([k]) => k === name);
72
+ }
73
+
74
+ delete(name: string): void {
75
+ this._entries = this._entries.filter(([k]) => k !== name);
76
+ }
77
+
78
+ append(name: string, value: string): void {
79
+ this._entries.push([name, value]);
80
+ }
81
+
82
+ sort(): void {
83
+ this._entries.sort((a, b) => {
84
+ if (a[0] < b[0]) return -1;
85
+ if (a[0] > b[0]) return 1;
86
+ return 0;
87
+ });
88
+ }
89
+
90
+ toString(): string {
91
+ return this._entries.map(([k, v]) => encodeComponent(k) + '=' + encodeComponent(v)).join('&');
92
+ }
93
+
94
+ forEach(callback: (value: string, key: string, parent: URLSearchParams) => void): void {
95
+ for (const [k, v] of this._entries) {
96
+ callback(v, k, this);
97
+ }
98
+ }
99
+
100
+ *entries(): IterableIterator<[string, string]> {
101
+ yield* this._entries;
102
+ }
103
+
104
+ *keys(): IterableIterator<string> {
105
+ for (const [k] of this._entries) yield k;
106
+ }
107
+
108
+ *values(): IterableIterator<string> {
109
+ for (const [, v] of this._entries) yield v;
110
+ }
111
+
112
+ [Symbol.iterator](): IterableIterator<[string, string]> {
113
+ return this.entries();
114
+ }
115
+
116
+ get size(): number {
117
+ return this._entries.length;
118
+ }
119
+ }
120
+
121
+ function decodeComponent(s: string): string {
122
+ try { return decodeURIComponent(s.replace(/\+/g, ' ')); } catch { return s; }
123
+ }
124
+
125
+ function encodeComponent(s: string): string {
126
+ return encodeURIComponent(s).replace(/%20/g, '+');
127
+ }
128
+
129
+ // ---- URL class using GLib.Uri ----
130
+
131
+ export class URL {
132
+ #uri: any; // GLib.Uri
133
+ #searchParams: URLSearchParams;
134
+
135
+ constructor(url: string | URL, base?: string | URL) {
136
+ const urlStr = url instanceof URL ? url.href : String(url);
137
+
138
+ try {
139
+ if (base !== undefined) {
140
+ const baseStr = base instanceof URL ? base.href : String(base);
141
+ const baseUri = GLib.Uri.parse(baseStr, PARSE_FLAGS);
142
+ this.#uri = baseUri.parse_relative(urlStr, PARSE_FLAGS);
143
+ } else {
144
+ this.#uri = GLib.Uri.parse(urlStr, PARSE_FLAGS);
145
+ }
146
+ } catch (e: any) {
147
+ throw new TypeError(`Invalid URL: ${urlStr}`);
148
+ }
149
+
150
+ if (!this.#uri) {
151
+ throw new TypeError(`Invalid URL: ${urlStr}`);
152
+ }
153
+
154
+ this.#searchParams = new URLSearchParams(this.#uri.get_query() || '');
155
+ }
156
+
157
+ get protocol(): string {
158
+ return this.#uri.get_scheme() + ':';
159
+ }
160
+
161
+ get hostname(): string {
162
+ return (this.#uri.get_host() || '').toLowerCase();
163
+ }
164
+
165
+ get port(): string {
166
+ const p = this.#uri.get_port();
167
+ if (p === -1) return '';
168
+ // WHATWG URL spec: port should be empty string for default ports
169
+ const scheme = this.#uri.get_scheme();
170
+ if ((scheme === 'http' || scheme === 'ws') && p === 80) return '';
171
+ if ((scheme === 'https' || scheme === 'wss') && p === 443) return '';
172
+ if (scheme === 'ftp' && p === 21) return '';
173
+ return String(p);
174
+ }
175
+
176
+ get host(): string {
177
+ const hostname = this.hostname;
178
+ const port = this.port;
179
+ return port ? `${hostname}:${port}` : hostname;
180
+ }
181
+
182
+ get pathname(): string {
183
+ return this.#uri.get_path() || '/';
184
+ }
185
+
186
+ get search(): string {
187
+ const q = this.#uri.get_query();
188
+ return q ? '?' + q : '';
189
+ }
190
+
191
+ get hash(): string {
192
+ const f = this.#uri.get_fragment();
193
+ return f ? '#' + f : '';
194
+ }
195
+
196
+ get origin(): string {
197
+ const p = this.protocol;
198
+ if (p === 'http:' || p === 'https:' || p === 'ftp:') {
199
+ return `${p}//${this.host}`;
200
+ }
201
+ return 'null';
202
+ }
203
+
204
+ get username(): string {
205
+ return this.#uri.get_user() || '';
206
+ }
207
+
208
+ get password(): string {
209
+ return this.#uri.get_password() || '';
210
+ }
211
+
212
+ get href(): string {
213
+ let result = this.protocol;
214
+ const scheme = this.#uri.get_scheme();
215
+ const isSpecial = scheme === 'http' || scheme === 'https' || scheme === 'ftp' || scheme === 'file' || scheme === 'ws' || scheme === 'wss';
216
+
217
+ if (isSpecial || this.hostname) {
218
+ result += '//';
219
+ }
220
+
221
+ const user = this.username;
222
+ const pass = this.password;
223
+ if (user) {
224
+ result += user;
225
+ if (pass) result += ':' + pass;
226
+ result += '@';
227
+ }
228
+
229
+ result += this.hostname;
230
+ if (this.port) result += ':' + this.port;
231
+
232
+ const pathname = this.pathname;
233
+ result += pathname;
234
+
235
+ result += this.search;
236
+ result += this.hash;
237
+
238
+ return result;
239
+ }
240
+
241
+ get searchParams(): URLSearchParams {
242
+ return this.#searchParams;
243
+ }
244
+
245
+ toString(): string {
246
+ return this.href;
247
+ }
248
+
249
+ toJSON(): string {
250
+ return this.href;
251
+ }
252
+ }
253
+
254
+ // ---- Legacy url.parse / url.format / url.resolve ----
255
+
256
+ export interface UrlObject {
257
+ protocol?: string | null;
258
+ slashes?: boolean | null;
259
+ auth?: string | null;
260
+ host?: string | null;
261
+ port?: string | null;
262
+ hostname?: string | null;
263
+ hash?: string | null;
264
+ search?: string | null;
265
+ query?: string | Record<string, string> | null;
266
+ pathname?: string | null;
267
+ path?: string | null;
268
+ href?: string;
269
+ }
270
+
271
+ export interface Url extends UrlObject {
272
+ href: string;
273
+ }
274
+
275
+ export function parse(urlString: string, parseQueryString?: boolean, slashesDenoteHost?: boolean): Url {
276
+ if (typeof urlString !== 'string') {
277
+ throw new TypeError('The "url" argument must be of type string. Received type ' + typeof urlString);
278
+ }
279
+
280
+ const result: Url = {
281
+ protocol: null,
282
+ slashes: null,
283
+ auth: null,
284
+ host: null,
285
+ port: null,
286
+ hostname: null,
287
+ hash: null,
288
+ search: null,
289
+ query: null,
290
+ pathname: null,
291
+ path: null,
292
+ href: urlString,
293
+ };
294
+
295
+ let rest = urlString.trim();
296
+
297
+ // Extract hash
298
+ const hashIdx = rest.indexOf('#');
299
+ if (hashIdx !== -1) {
300
+ result.hash = rest.slice(hashIdx);
301
+ rest = rest.slice(0, hashIdx);
302
+ }
303
+
304
+ // Extract search/query
305
+ const qIdx = rest.indexOf('?');
306
+ if (qIdx !== -1) {
307
+ result.search = rest.slice(qIdx);
308
+ result.query = parseQueryString
309
+ ? Object.fromEntries(new URLSearchParams(rest.slice(qIdx + 1)))
310
+ : rest.slice(qIdx + 1);
311
+ rest = rest.slice(0, qIdx);
312
+ }
313
+
314
+ // Extract protocol
315
+ const protoMatch = /^([a-z][a-z0-9.+-]*:)/i.exec(rest);
316
+ if (protoMatch) {
317
+ result.protocol = protoMatch[1].toLowerCase();
318
+ rest = rest.slice(result.protocol.length);
319
+ }
320
+
321
+ // Check for slashes
322
+ if (slashesDenoteHost || result.protocol) {
323
+ const hasSlashes = rest.startsWith('//');
324
+ if (hasSlashes) {
325
+ result.slashes = true;
326
+ rest = rest.slice(2);
327
+ }
328
+ }
329
+
330
+ // Extract host portion (only if we had slashes or protocol)
331
+ if (result.slashes || (result.protocol && !['javascript:', 'data:', 'mailto:'].includes(result.protocol))) {
332
+ let hostEnd = -1;
333
+ for (let i = 0; i < rest.length; i++) {
334
+ const ch = rest[i];
335
+ if (ch === '/' || ch === '\\') {
336
+ hostEnd = i;
337
+ break;
338
+ }
339
+ }
340
+
341
+ const hostPart = hostEnd === -1 ? rest : rest.slice(0, hostEnd);
342
+ rest = hostEnd === -1 ? '' : rest.slice(hostEnd);
343
+
344
+ const atIdx = hostPart.lastIndexOf('@');
345
+ if (atIdx !== -1) {
346
+ result.auth = decodeURIComponent(hostPart.slice(0, atIdx));
347
+ const hostWithPort = hostPart.slice(atIdx + 1);
348
+ parseHostPort(hostWithPort, result);
349
+ } else {
350
+ parseHostPort(hostPart, result);
351
+ }
352
+ }
353
+
354
+ result.pathname = rest || (result.slashes ? '/' : null);
355
+
356
+ if (result.pathname !== null || result.search !== null) {
357
+ result.path = (result.pathname || '') + (result.search || '');
358
+ }
359
+
360
+ result.href = format(result);
361
+
362
+ return result;
363
+ }
364
+
365
+ function parseHostPort(hostPart: string, result: Url): void {
366
+ if (!hostPart) return;
367
+
368
+ const bracketIdx = hostPart.indexOf('[');
369
+ if (bracketIdx !== -1) {
370
+ const bracketEnd = hostPart.indexOf(']', bracketIdx);
371
+ if (bracketEnd !== -1) {
372
+ const portStr = hostPart.slice(bracketEnd + 1);
373
+ if (portStr.startsWith(':')) {
374
+ result.port = portStr.slice(1);
375
+ }
376
+ result.hostname = hostPart.slice(bracketIdx, bracketEnd + 1);
377
+ result.host = result.hostname + (result.port ? ':' + result.port : '');
378
+ return;
379
+ }
380
+ }
381
+
382
+ const colonIdx = hostPart.lastIndexOf(':');
383
+ if (colonIdx !== -1) {
384
+ const portCandidate = hostPart.slice(colonIdx + 1);
385
+ if (/^\d*$/.test(portCandidate)) {
386
+ result.port = portCandidate || null;
387
+ result.hostname = hostPart.slice(0, colonIdx).toLowerCase();
388
+ } else {
389
+ result.hostname = hostPart.toLowerCase();
390
+ }
391
+ } else {
392
+ result.hostname = hostPart.toLowerCase();
393
+ }
394
+
395
+ result.host = result.hostname + (result.port ? ':' + result.port : '');
396
+ }
397
+
398
+ export function format(urlObject: UrlObject | string | URL): string {
399
+ if (typeof urlObject === 'string') {
400
+ return urlObject;
401
+ }
402
+
403
+ if (urlObject instanceof URL) {
404
+ return urlObject.href;
405
+ }
406
+
407
+ const obj = urlObject as UrlObject;
408
+ let result = '';
409
+
410
+ if (obj.protocol) {
411
+ result += obj.protocol;
412
+ }
413
+
414
+ if (obj.slashes || (obj.protocol && !['javascript:', 'data:', 'mailto:'].includes(obj.protocol || ''))) {
415
+ result += '//';
416
+ }
417
+
418
+ if (obj.auth) {
419
+ result += encodeURIComponent(obj.auth) + '@';
420
+ }
421
+
422
+ if (obj.host) {
423
+ result += obj.host;
424
+ } else {
425
+ if (obj.hostname) {
426
+ result += obj.hostname;
427
+ }
428
+ if (obj.port) {
429
+ result += ':' + obj.port;
430
+ }
431
+ }
432
+
433
+ if (obj.pathname) {
434
+ result += obj.pathname;
435
+ }
436
+
437
+ if (obj.search) {
438
+ result += obj.search;
439
+ } else if (obj.query && typeof obj.query === 'object') {
440
+ const qs = new URLSearchParams(obj.query as Record<string, string>).toString();
441
+ if (qs) result += '?' + qs;
442
+ }
443
+
444
+ if (obj.hash) {
445
+ result += obj.hash;
446
+ }
447
+
448
+ return result;
449
+ }
450
+
451
+ export function resolve(from: string, to: string): string {
452
+ return new URL(to, new URL(from, 'resolve://')).href.replace(/^resolve:\/\//, '');
453
+ }
454
+
455
+ // ---- File URL helpers ----
456
+
457
+ export function fileURLToPath(url: string | URL): string {
458
+ if (typeof url === 'string') {
459
+ url = new URL(url);
460
+ }
461
+
462
+ if (!(url instanceof URL)) {
463
+ throw new TypeError('The "url" argument must be of type string or URL. Received type ' + typeof url);
464
+ }
465
+
466
+ if (url.protocol !== 'file:') {
467
+ throw new TypeError('The URL must be of scheme file');
468
+ }
469
+
470
+ if (url.hostname !== '' && url.hostname !== 'localhost') {
471
+ throw new TypeError(
472
+ `File URL host must be "localhost" or empty on linux`
473
+ );
474
+ }
475
+
476
+ const pathname = url.pathname;
477
+ for (let i = 0; i < pathname.length; i++) {
478
+ if (pathname[i] === '%') {
479
+ const third = pathname.codePointAt(i + 2)! | 0x20;
480
+ if (pathname[i + 1] === '2' && third === 102) {
481
+ throw new TypeError('File URL path must not include encoded / characters');
482
+ }
483
+ }
484
+ }
485
+
486
+ return decodeURIComponent(pathname);
487
+ }
488
+
489
+ export function pathToFileURL(filepath: string): URL {
490
+ let resolved = filepath;
491
+
492
+ if (filepath[0] !== '/') {
493
+ if (typeof globalThis.process?.cwd === 'function') {
494
+ resolved = globalThis.process.cwd() + '/' + filepath;
495
+ } else {
496
+ try {
497
+ if (GLib?.get_current_dir) {
498
+ resolved = GLib.get_current_dir() + '/' + filepath;
499
+ }
500
+ } catch {
501
+ // Fall through
502
+ }
503
+ }
504
+ }
505
+
506
+ return new URL('file://' + encodePathForURL(resolved));
507
+ }
508
+
509
+ function encodePathForURL(filepath: string): string {
510
+ let result = '';
511
+ for (let i = 0; i < filepath.length; i++) {
512
+ const ch = filepath[i];
513
+ if (
514
+ (ch >= 'a' && ch <= 'z') ||
515
+ (ch >= 'A' && ch <= 'Z') ||
516
+ (ch >= '0' && ch <= '9') ||
517
+ ch === '/' || ch === '-' || ch === '_' || ch === '.' || ch === '~' ||
518
+ ch === ':' || ch === '@' || ch === '!'
519
+ ) {
520
+ result += ch;
521
+ } else {
522
+ result += encodeURIComponent(ch);
523
+ }
524
+ }
525
+ return result;
526
+ }
527
+
528
+ export function domainToASCII(domain: string): string {
529
+ try {
530
+ return new URL(`http://${domain}`).hostname;
531
+ } catch {
532
+ return '';
533
+ }
534
+ }
535
+
536
+ export function domainToUnicode(domain: string): string {
537
+ try {
538
+ return new URL(`http://${domain}`).hostname;
539
+ } catch {
540
+ return '';
541
+ }
542
+ }
543
+
544
+ // Default export
545
+ export default {
546
+ URL,
547
+ URLSearchParams,
548
+ parse,
549
+ format,
550
+ resolve,
551
+ fileURLToPath,
552
+ pathToFileURL,
553
+ domainToASCII,
554
+ domainToUnicode,
555
+ };
package/tsconfig.json CHANGED
@@ -1,18 +1,31 @@
1
1
  {
2
- "compilerOptions": {
3
- "module": "ESNext",
4
- "types": ["node"],
5
- "target": "ESNext",
6
- "experimentalDecorators": true,
7
- "moduleResolution": "bundler",
8
- "allowImportingTsExtensions": true,
9
- "outDir": "lib",
10
- "rootDir": "src",
11
- "composite": true
12
- },
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "target": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "types": [
7
+ "node"
8
+ ],
9
+ "experimentalDecorators": true,
10
+ "emitDeclarationOnly": true,
11
+ "declaration": true,
12
+ "allowImportingTsExtensions": true,
13
+ "outDir": "lib",
14
+ "rootDir": "src",
15
+ "declarationDir": "lib/types",
16
+ "composite": true,
13
17
  "skipLibCheck": true,
14
18
  "allowJs": true,
15
19
  "checkJs": false,
16
- "reflection": false,
17
- "include": ["src/**/*.ts"]
18
- }
20
+ "strict": false
21
+ },
22
+ "include": [
23
+ "src/**/*.ts"
24
+ ],
25
+ "exclude": [
26
+ "src/test.ts",
27
+ "src/test.mts",
28
+ "src/**/*.spec.ts",
29
+ "src/**/*.spec.mts"
30
+ ]
31
+ }