Skip to content

Commit 726127a

Browse files
chore(internal): improve node 18 shims
1 parent dbd4446 commit 726127a

18 files changed

+136
-74
lines changed

Diff for: package.json

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
"resolutions": {
5454
"synckit": "0.8.8"
5555
},
56+
"browser": {
57+
"./internal/shims/getBuiltinModule.mjs": "./internal/shims/nullGetBuiltinModule.mjs",
58+
"./internal/shims/getBuiltinModule.js": "./internal/shims/nullGetBuiltinModule.js"
59+
},
5660
"imports": {
5761
"@gitpod/sdk": ".",
5862
"@gitpod/sdk/*": "./src/*"

Diff for: scripts/build

-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ node scripts/utils/fix-index-exports.cjs
3535
cp tsconfig.dist-src.json dist/src/tsconfig.json
3636
cp src/internal/shim-types.d.ts dist/internal/shim-types.d.ts
3737
cp src/internal/shim-types.d.ts dist/internal/shim-types.d.mts
38-
mkdir -p dist/internal/shims
39-
cp src/internal/shims/*.{mjs,js,d.ts,d.mts} dist/internal/shims
4038

4139
node scripts/utils/postprocess-files.cjs
4240

Diff for: src/internal/shims/crypto.node.d.mts

-1
This file was deleted.

Diff for: src/internal/shims/crypto.node.d.ts

-10
This file was deleted.

Diff for: src/internal/shims/crypto.node.js

-11
This file was deleted.

Diff for: src/internal/shims/crypto.node.mjs

-2
This file was deleted.

Diff for: src/internal/shims/crypto.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getBuiltinModule } from './getBuiltinModule';
2+
3+
type Crypto = {
4+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues) */
5+
getRandomValues<T extends ArrayBufferView | null>(array: T): T;
6+
/**
7+
* Available only in secure contexts.
8+
*
9+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)
10+
*/
11+
randomUUID?: () => string;
12+
};
13+
export let getCrypto: () => Crypto | undefined = function lazyGetCrypto() {
14+
if (getCrypto !== lazyGetCrypto) return getCrypto();
15+
const crypto: Crypto = (globalThis as any).crypto || (getBuiltinModule?.('node:crypto') as any)?.webcrypto;
16+
getCrypto = () => crypto;
17+
return crypto;
18+
};

Diff for: src/internal/shims/file.node.d.mts

-1
This file was deleted.

Diff for: src/internal/shims/file.node.d.ts

-20
This file was deleted.

Diff for: src/internal/shims/file.node.js

-11
This file was deleted.

Diff for: src/internal/shims/file.node.mjs

-2
This file was deleted.

Diff for: src/internal/shims/file.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { getBuiltinModule } from './getBuiltinModule';
2+
3+
export let getFile = function lazyGetFile(): FileConstructor {
4+
if (getFile !== lazyGetFile) return getFile();
5+
// We can drop getBuiltinModule once we no longer support Node < 20.0.0
6+
const File = (globalThis as any).File ?? (getBuiltinModule?.('node:buffer') as any)?.File;
7+
if (!File) throw new Error('`File` is not defined as a global, which is required for file uploads.');
8+
getFile = () => File;
9+
return File;
10+
};
11+
12+
type FileConstructor =
13+
typeof globalThis extends { File: infer fileConstructor } ? fileConstructor : typeof FallbackFile;
14+
export type File = InstanceType<FileConstructor>;
15+
16+
// The infer is to make TS show it as a nice union type,
17+
// instead of literally `ConstructorParameters<typeof Blob>[0]`
18+
type FallbackBlobSource = ConstructorParameters<typeof Blob>[0] extends infer T ? T : never;
19+
/**
20+
* A [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) provides information about files.
21+
*/
22+
declare class FallbackFile extends Blob {
23+
constructor(sources: FallbackBlobSource, fileName: string, options?: any);
24+
/**
25+
* The name of the `File`.
26+
*/
27+
readonly name: string;
28+
/**
29+
* The last modified date of the `File`.
30+
*/
31+
readonly lastModified: number;
32+
}

Diff for: src/internal/shims/getBuiltinModule.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Load a Node built-in module. ID may or may not be prefixed by `node:` and
3+
* will be normalized. If we used static imports then our bundle size would be bloated by
4+
* injected polyfills, and if we used dynamic require then in addition to bundlers logging warnings,
5+
* our code would not work when bundled to ESM and run in Node 18.
6+
* @param {string} id ID of the built-in to be loaded.
7+
* @returns {object|undefined} exports of the built-in. Undefined if the built-in
8+
* does not exist.
9+
*/
10+
export let getBuiltinModule: null | ((id: string) => object | undefined) = function getBuiltinModuleLazy(
11+
id: string,
12+
): object | undefined {
13+
try {
14+
if (getBuiltinModule !== getBuiltinModuleLazy) return getBuiltinModule!(id);
15+
if ((process as any).getBuiltinModule) {
16+
getBuiltinModule = (process as any).getBuiltinModule;
17+
} else {
18+
/* Fallback implementation for Node 18 */
19+
function createFallbackGetBuiltinModule(BuiltinModule: any) {
20+
return function getBuiltinModule(id: string): object | undefined {
21+
id = BuiltinModule.normalizeRequirableId(String(id));
22+
if (!BuiltinModule.canBeRequiredByUsers(id)) {
23+
return;
24+
}
25+
const mod = BuiltinModule.map.get(id);
26+
mod.compileForPublicLoader();
27+
return mod.exports;
28+
};
29+
}
30+
const magicKey = Math.random() + '';
31+
let module: { BuiltinModule: any } | undefined;
32+
try {
33+
const kClone = Object.getOwnPropertySymbols(Blob.prototype).find(
34+
(e) => e.description?.includes('clone'),
35+
)!;
36+
Object.defineProperty(Object.prototype, magicKey, {
37+
get() {
38+
module = this;
39+
throw null;
40+
},
41+
configurable: true,
42+
});
43+
structuredClone(
44+
new (class extends Blob {
45+
[kClone]() {
46+
return {
47+
deserializeInfo: 'internal/bootstrap/realm:' + magicKey,
48+
};
49+
}
50+
})([]),
51+
);
52+
} catch {}
53+
delete (Object.prototype as any)[magicKey];
54+
if (module) {
55+
getBuiltinModule = createFallbackGetBuiltinModule(module.BuiltinModule);
56+
} else {
57+
getBuiltinModule = () => undefined;
58+
}
59+
}
60+
return getBuiltinModule!(id);
61+
} catch {
62+
return undefined;
63+
}
64+
};

Diff for: src/internal/shims/nullGetBuiltinModule.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const getBuiltinModule = null;

Diff for: src/internal/to-file.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { File } from './shims/file.node.js';
1+
import { type File, getFile } from './shims/file';
22
import { BlobPart, getName, makeFile, isAsyncIterable } from './uploads';
33
import type { FilePropertyBag } from './builtin-types';
44

@@ -90,7 +90,7 @@ export async function toFile(
9090

9191
// If we've been given a `File` we don't need to do anything
9292
if (isFileLike(value)) {
93-
if (File && value instanceof File) {
93+
if (value instanceof getFile()) {
9494
return value;
9595
}
9696
return makeFile([await value.arrayBuffer()], value.name);

Diff for: src/internal/uploads.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type RequestOptions } from './request-options';
22
import type { FilePropertyBag, Fetch } from './builtin-types';
33
import type { Gitpod } from '../client';
4-
import { File } from './shims/file.node.js';
4+
import { type File, getFile } from './shims/file';
55
import { ReadableStreamFrom } from './shims';
66

77
export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView;
@@ -32,10 +32,7 @@ export function makeFile(
3232
fileName: string | undefined,
3333
options?: FilePropertyBag,
3434
): File {
35-
if (typeof File === 'undefined') {
36-
throw new Error('`File` is not defined as a global which is required for file uploads');
37-
}
38-
35+
const File = getFile();
3936
return new File(fileBits as any, fileName ?? 'unknown_file', options);
4037
}
4138

@@ -129,7 +126,7 @@ export const createForm = async <T = Record<string, unknown>>(
129126
// We check for Blob not File because Bun.File doesn't inherit from File,
130127
// but they both inherit from Blob and have a `name` property at runtime.
131128
const isNamedBlob = (value: object) =>
132-
(File && value instanceof File) || (value instanceof Blob && 'name' in value);
129+
value instanceof getFile() || (value instanceof Blob && 'name' in value);
133130

134131
const isUploadable = (value: unknown) =>
135132
typeof value === 'object' &&

Diff for: src/internal/utils/uuid.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3-
import { crypto } from '../shims/crypto.node.js';
3+
import { getCrypto } from '../shims/crypto';
44

55
/**
66
* https://stackoverflow.com/a/2117523
77
*/
8-
export function uuid4() {
9-
if (crypto.randomUUID) return crypto.randomUUID();
8+
export let uuid4 = function () {
9+
const crypto = getCrypto();
10+
if (crypto?.randomUUID) {
11+
uuid4 = crypto.randomUUID.bind(crypto);
12+
return crypto.randomUUID();
13+
}
14+
const u8 = new Uint8Array(1);
15+
const randomByte = crypto ? () => crypto.getRandomValues(u8)[0]! : () => (Math.random() * 0xff) & 0xff;
1016
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>
11-
(+c ^ (crypto.getRandomValues(new Uint8Array(1))[0]! & (15 >> (+c / 4)))).toString(16),
17+
(+c ^ (randomByte() & (15 >> (+c / 4)))).toString(16),
1218
);
13-
}
19+
};

Diff for: tests/uploads.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('missing File error message', () => {
101101
await expect(
102102
uploads.toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })),
103103
).rejects.toMatchInlineSnapshot(
104-
`[Error: \`File\` is not defined as a global which is required for file uploads]`,
104+
`[Error: \`File\` is not defined as a global, which is required for file uploads.]`,
105105
);
106106
});
107107
});

0 commit comments

Comments
 (0)