Skip to content

Commit 7b8d203

Browse files
authored
Merge pull request #1221 from forcedotcom/sm/org-browser-web-bundling
W-19183667 feat: web bundling, virtual fs
2 parents 720bfa4 + 2668399 commit 7b8d203

48 files changed

Lines changed: 2438 additions & 4315 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1229 additions & 4117 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"./config": "./lib/config/config.js",
1414
"./configAggregator": "./lib/config/configAggregator.js",
1515
"./envVars": "./lib/config/envVars.js",
16+
"./fs": "./lib/fs/fs.js",
1617
"./lifecycle": "./lib/lifecycleEvents.js",
1718
"./logger": "./lib/logger/logger.js",
1819
"./messages": "./lib/messages.js",
@@ -56,7 +57,7 @@
5657
"@jsforce/jsforce-node": "^3.10.0",
5758
"@salesforce/kit": "^3.2.2",
5859
"@salesforce/schemas": "^1.9.1",
59-
"@salesforce/ts-types": "^2.0.10",
60+
"@salesforce/ts-types": "^2.0.11",
6061
"ajv": "^8.17.1",
6162
"change-case": "^4.1.2",
6263
"fast-levenshtein": "^3.0.0",
@@ -65,6 +66,7 @@
6566
"js2xmlparser": "^4.0.1",
6667
"jsonwebtoken": "9.0.2",
6768
"jszip": "3.10.1",
69+
"memfs": "^4.30.1",
6870
"pino": "^9.7.0",
6971
"pino-abstract-transport": "^1.2.0",
7072
"pino-pretty": "^11.3.0",

src/config/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/*
2-
* Copyright (c) 2020, salesforce.com, inc.
2+
* Copyright (c) 2025, salesforce.com, inc.
33
* All rights reserved.
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

88
import { dirname as pathDirname, join as pathJoin } from 'node:path';
9-
import * as fs from 'node:fs';
10-
import { keyBy, parseJsonMap } from '@salesforce/kit';
119
import { Dictionary, ensure, isString, Nullable } from '@salesforce/ts-types';
10+
import { keyBy, parseJsonMap } from '@salesforce/kit';
11+
import { fs } from '../fs/fs';
1212
import { Global } from '../global';
1313
import { Logger } from '../logger/logger';
1414
import { Messages } from '../messages';

src/config/configFile.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import * as fs from 'node:fs';
9-
import { constants as fsConstants, Stats as fsStats } from 'node:fs';
108
import { homedir as osHomedir } from 'node:os';
119
import { join as pathJoin } from 'node:path';
10+
import { constants as fsConstants, Stats as fsStats } from 'node:fs';
1211
import { parseJsonMap } from '@salesforce/kit';
12+
import { fs } from '../fs/fs';
1313
import { Global } from '../global';
1414
import { Logger } from '../logger/logger';
1515
import { SfError } from '../sfError';
@@ -34,7 +34,7 @@ import { stateFromContents } from './lwwMap';
3434
* const myConfig = await MyConfig.create({
3535
* isGlobal: true
3636
* });
37-
* myConfig.set('mykey', 'myvalue');
37+
* myConfig.set('myKey', 'myValue');
3838
* await myConfig.write();
3939
* ```
4040
*/
@@ -446,5 +446,5 @@ const getNsTimeStampSync = (filePath: string): bigint =>
446446
getNsTimeStampFromStatus(fs.statSync(filePath, { bigint: true }));
447447

448448
/** in browser environment, memfs is missing the bigInt ns timestamp, so we generate it from the ms */
449-
const getNsTimeStampFromStatus = (stats: fs.BigIntStats): bigint =>
450-
stats.mtimeNs ?? BigInt(stats.mtimeMs) * BigInt(1_000_000);
449+
const getNsTimeStampFromStatus = (stats: Awaited<ReturnType<typeof fs.promises.stat>>): bigint =>
450+
'mtimeNs' in stats ? stats.mtimeNs : BigInt(stats.mtimeMs) * BigInt(1_000_000);

src/crypto/crypto.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,9 +423,7 @@ export class Crypto extends AsyncOptionalCreatable<CryptoOptions> {
423423
}
424424

425425
private async getKeyChain(platform: string): Promise<KeyChain> {
426-
if (!this.options.keychain) {
427-
this.options.keychain = await retrieveKeychain(platform);
428-
}
426+
this.options.keychain ??= await retrieveKeychain(platform);
429427
return this.options.keychain;
430428
}
431429
}

src/crypto/keyChainImpl.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,20 @@
66
*/
77

88
import * as childProcess from 'node:child_process';
9-
import * as nodeFs from 'node:fs';
10-
import * as fs from 'node:fs';
119
import * as os from 'node:os';
1210
import { homedir } from 'node:os';
1311
import * as path from 'node:path';
1412
import { asString, ensureString, Nullable } from '@salesforce/ts-types';
1513
import { parseJsonMap } from '@salesforce/kit';
14+
import { fs } from '../fs/fs';
1615
import { Global } from '../global';
1716
import { SfError } from '../sfError';
1817
import { Messages } from '../messages';
1918

2019
Messages.importMessagesDirectory(__dirname);
2120
const messages = Messages.loadMessages('@salesforce/core', 'encryption');
2221

23-
export type FsIfc = Pick<typeof nodeFs, 'statSync'>;
22+
export type FsIfc = Pick<typeof fs, 'statSync'>;
2423

2524
const GET_PASSWORD_RETRY_COUNT = 3;
2625

@@ -329,7 +328,7 @@ const linuxImpl: OsImpl = {
329328
setCommandFunc(opts, fn) {
330329
const secretTool = fn(linuxImpl.getProgram(), linuxImpl.setProgramOptions(opts));
331330
if (secretTool.stdin) {
332-
secretTool.stdin.write(`${opts.password}\n`);
331+
secretTool.stdin.write(`${opts.password ?? ''}\n`);
333332
}
334333
return secretTool;
335334
},
@@ -592,8 +591,8 @@ export const keyChainImpl = {
592591
generic_unix: new GenericUnixKeychainAccess(),
593592
// eslint-disable-next-line camelcase
594593
generic_windows: new GenericWindowsKeychainAccess(),
595-
darwin: new KeychainAccess(darwinImpl, nodeFs),
596-
linux: new KeychainAccess(linuxImpl, nodeFs),
594+
darwin: new KeychainAccess(darwinImpl, fs),
595+
linux: new KeychainAccess(linuxImpl, fs),
597596
validateProgram: _validateProgram,
598597
};
599598

src/deviceOauthService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ export class DeviceOauthService extends AsyncCreatable<OAuth2Config> {
143143

144144
protected async init(): Promise<void> {
145145
this.logger = await Logger.child(this.constructor.name);
146-
this.logger.debug(`this.options.clientId: ${this.options.clientId}`);
147-
this.logger.debug(`this.options.loginUrl: ${this.options.loginUrl}`);
146+
this.logger.debug(`this.options.clientId: ${this.options.clientId ?? '<undefined>'}`);
147+
this.logger.debug(`this.options.loginUrl: ${this.options.loginUrl ?? '<undefined>'}`);
148148
}
149149

150150
private getLoginOptions(url: string): HttpRequest {

src/fs/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Isomorphic filesystem support
2+
3+
We want to support web and nodejs use of this library.
4+
5+
fs.ts is meant to work in both scenarios.
6+
7+
memfs is a **nearly** drop-in replacement for node:fs, with some inconsistencies.
8+
9+
VirtualFS type should be an intersection type (the commonality between node:fs and memfs).
10+
11+
- it can be more restrictive or more open as needed
12+
13+
Then we can provide any overrides necessary to deal with the inconsistencies.
14+
15+
That way, consumers can use import our `fs` module instead of `node:fs` and use it comfortably. So other libraries can then become isomorphic without a lot of work
16+
17+
We, as a library, should handle the type complexity so that it's solid and reliable for consumers. minimal complexity is OK in the library, especially in types, since they are dev-time only.
18+
19+
## ruled out
20+
21+
Any kind of use of Proxy

src/fs/fs.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2023, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import * as nodeFs from 'node:fs';
9+
// yes, we're going to import it even though it might not be used.
10+
// the alternatives were all worse without top-level await (iife, runtime errors from something trying to use it before it's initialized)
11+
import * as memfs from 'memfs';
12+
import type { VirtualFs } from './types';
13+
14+
export let fs: VirtualFs;
15+
16+
const isWeb = (): boolean => process.env.FORCE_MEMFS === 'true' || 'window' in globalThis || 'self' in globalThis;
17+
18+
export const getVirtualFs = (memfsVolume?: memfs.Volume): VirtualFs => {
19+
if (isWeb()) {
20+
const memfsInstance = memfs.createFsFromVolume(memfsVolume ?? new memfs.Volume());
21+
22+
// Start with memfs instance and only override problematic methods
23+
const webFs = {
24+
...memfsInstance,
25+
26+
// Override only the methods that have incompatible signatures
27+
promises: {
28+
...memfsInstance.promises,
29+
writeFile: async (
30+
file: string,
31+
data: string | Buffer,
32+
options?: BufferEncoding | { encoding?: BufferEncoding; mode?: string | number }
33+
): Promise<void> => {
34+
const finalOptions = typeof options === 'string' ? { encoding: options } : options;
35+
await memfsInstance.promises.writeFile(file, data, finalOptions);
36+
},
37+
readFile: async (path: string, encoding?: BufferEncoding): Promise<string | Buffer> => {
38+
const result = await memfsInstance.promises.readFile(path, encoding);
39+
return encoding === 'utf8' ? String(result) : Buffer.from(result);
40+
},
41+
},
42+
43+
readFileSync: (path: string, encoding?: BufferEncoding): string | Buffer => {
44+
const result = memfsInstance.readFileSync(path, encoding);
45+
return encoding === 'utf8' ? String(result) : Buffer.from(result);
46+
},
47+
48+
writeFileSync: (file: string, data: string | Buffer, encoding?: BufferEncoding): void => {
49+
memfsInstance.writeFileSync(file, data, { encoding });
50+
},
51+
} as unknown as VirtualFs;
52+
53+
return webFs;
54+
}
55+
56+
return nodeFs as unknown as VirtualFs;
57+
};
58+
59+
export const setFs = (providedFs: VirtualFs): void => {
60+
fs = providedFs;
61+
};
62+
63+
export const resetFs = (): void => {
64+
fs = getVirtualFs();
65+
};
66+
67+
// Initialize fs at module load time
68+
fs = getVirtualFs();

src/fs/types.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) 2023, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import type * as nodeFs from 'node:fs';
8+
import type { IFs as MemFs } from 'memfs';
9+
10+
// Get the types for both node:fs and memfs
11+
type NodeFs = typeof nodeFs;
12+
13+
// Find keys that exist in both types
14+
type CommonKeys<T, U> = keyof T & keyof U;
15+
16+
// Create intersection type - be more specific for compatible methods
17+
type IntersectionType<T, U> = {
18+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
19+
[K in CommonKeys<T, U>]: T[K] extends (...args: any[]) => any
20+
? T[K] extends U[K]
21+
? T[K] // If signatures are compatible, use the more specific type
22+
: U[K] extends T[K]
23+
? U[K] // If memfs signature is more specific, use that
24+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
25+
any // Only use any for genuinely incompatible signatures
26+
: T[K] extends U[K]
27+
? T[K]
28+
: U[K] extends T[K]
29+
? U[K]
30+
: T[K]; // Default to node:fs for non-functions
31+
};
32+
33+
// Base intersection type
34+
type BaseVirtualFs = IntersectionType<NodeFs, MemFs>;
35+
36+
// VirtualFs with specific overrides for methods we know the behavior of
37+
export type VirtualFs = Omit<
38+
BaseVirtualFs,
39+
'writeFileSync' | 'readFileSync' | 'statSync' | 'promises' | 'mkdtempSync' | 'createWriteStream' | 'mkdirSync'
40+
> & {
41+
// Override promises object with specific types for methods we override
42+
promises: Omit<BaseVirtualFs['promises'], 'writeFile' | 'readFile'> & {
43+
writeFile: (
44+
file: string,
45+
data: string | Buffer,
46+
options?: BufferEncoding | { encoding?: BufferEncoding; mode?: string | number }
47+
) => Promise<void>;
48+
readFile: {
49+
(path: string): Promise<Buffer>;
50+
(path: string, encoding: BufferEncoding): Promise<string>;
51+
};
52+
};
53+
54+
// Override sync methods with specific types
55+
readFileSync: {
56+
(path: string): Buffer;
57+
(path: string, encoding: BufferEncoding): string;
58+
};
59+
writeFileSync: (file: string, data: string | Buffer, encoding?: BufferEncoding) => void;
60+
/** there are some differences between node:fs and memfs for statSync around bigint stats. Be careful if using those */
61+
statSync: typeof nodeFs.statSync;
62+
mkdtempSync: typeof nodeFs.mkdtempSync;
63+
createWriteStream: typeof nodeFs.createWriteStream;
64+
mkdirSync: typeof nodeFs.mkdirSync;
65+
};

0 commit comments

Comments
 (0)