Skip to content

Commit 5038b5e

Browse files
mshanemcshetzel
andauthored
W-19183524 fix: web bundleability (#1597)
* fix: defer pipeline promisify * test: adjust test for streams changes * test: deploy/retreive mocks * feat: remove isBinaryFile dep, simplify * test: missing files * refactor: immutable * refactor: fs-handler simplification * refactor: just use mkdir * chore: copyright license reference * chore: copyright commit * chore: lint fix and bump core dep --------- Co-authored-by: Steve Hetzel <[email protected]>
1 parent 2154eea commit 5038b5e

17 files changed

+635
-245
lines changed

.cursor/check-your-work.mdc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
alwaysApply: true
3+
---
4+
After making changes, check your changes
5+
`yarn compile`
6+
`yarn lint` // linter can have warnings
7+
`yarn test:only` // unit tests
8+
`yarn test:snapshot` // snapshot tests, these take a bit longer
9+
10+
You can run a single unit test like `yarn mocha test/convert/streams.ts`

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,5 @@ test_session*
128128

129129
# --
130130
# put files here you don't want cleaned with sf-clean
131+
132+
.sfdx

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"node": ">=18.0.0"
2626
},
2727
"dependencies": {
28-
"@salesforce/core": "^8.18.1",
28+
"@salesforce/core": "^8.18.7",
2929
"@salesforce/kit": "^3.2.3",
3030
"@salesforce/ts-types": "^2.0.12",
3131
"@salesforce/types": "^1.3.0",
@@ -34,7 +34,6 @@
3434
"got": "^11.8.6",
3535
"graceful-fs": "^4.2.11",
3636
"ignore": "^5.3.2",
37-
"isbinaryfile": "^5.0.2",
3837
"jszip": "^3.10.1",
3938
"mime": "2.6.0",
4039
"minimatch": "^9.0.5",

src/convert/isBinaryFile.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* Copyright (c) 2024, 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+
// simplified version of https://github.com/gjtorikian/isBinaryFile
9+
10+
/*
11+
original copyright notice from https://github.com/gjtorikian/isBinaryFile/commit/123cce905590766d092c47e034163b03e47d5044#diff-d0ed4cc3fb70489fe51c7e0ac180cba2a7472124f9f9e9ae67b01a37fbd580b7
12+
Copyright (c) 2019 Garen J. Torikian
13+
14+
MIT License
15+
16+
Permission is hereby granted, free of charge, to any person obtaining
17+
a copy of this software and associated documentation files (the
18+
"Software"), to deal in the Software without restriction, including
19+
without limitation the rights to use, copy, modify, merge, publish,
20+
distribute, sublicense, and/or sell copies of the Software, and to
21+
permit persons to whom the Software is furnished to do so, subject to
22+
the following conditions:
23+
24+
The above copyright notice and this permission notice shall be
25+
included in all copies or substantial portions of the Software.
26+
27+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
29+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34+
35+
*/
36+
37+
import * as fs from 'node:fs';
38+
39+
const MAX_BYTES = 512;
40+
41+
// A very basic non-exception raising reader. Read bytes and
42+
// at the end use hasError() to check whether this worked.
43+
class Reader {
44+
public offset: number;
45+
public error: boolean;
46+
47+
public constructor(public fileBuffer: Buffer, public size: number) {
48+
this.offset = 0;
49+
this.error = false;
50+
}
51+
52+
public hasError(): boolean {
53+
return this.error;
54+
}
55+
56+
public nextByte(): number {
57+
if (this.offset === this.size || this.hasError()) {
58+
this.error = true;
59+
return 0xff;
60+
}
61+
return this.fileBuffer[this.offset++];
62+
}
63+
64+
public next(len: number): number[] {
65+
const n = [];
66+
for (let i = 0; i < len; i++) {
67+
n[i] = this.nextByte();
68+
}
69+
return n;
70+
}
71+
}
72+
73+
// Read a Google Protobuf var(iable)int from the buffer.
74+
function readProtoVarInt(reader: Reader): number {
75+
let idx = 0;
76+
let varInt = 0;
77+
78+
while (!reader.hasError()) {
79+
const b = reader.nextByte();
80+
varInt = varInt | ((b & 0x7f) << (7 * idx));
81+
if ((b & 0x80) === 0) {
82+
break;
83+
}
84+
idx++;
85+
}
86+
87+
return varInt;
88+
}
89+
90+
// Attempt to taste a full Google Protobuf message.
91+
function readProtoMessage(reader: Reader): boolean {
92+
const varInt = readProtoVarInt(reader);
93+
const wireType = varInt & 0x7;
94+
95+
switch (wireType) {
96+
case 0:
97+
readProtoVarInt(reader);
98+
return true;
99+
case 1:
100+
reader.next(8);
101+
return true;
102+
case 2: {
103+
const len = readProtoVarInt(reader);
104+
reader.next(len);
105+
return true;
106+
}
107+
case 5:
108+
reader.next(4);
109+
return true;
110+
}
111+
return false;
112+
}
113+
114+
// Check whether this seems to be a valid protobuf file.
115+
function isBinaryProto(fileBuffer: Buffer, totalBytes: number): boolean {
116+
const reader = new Reader(fileBuffer, totalBytes);
117+
let numMessages = 0;
118+
119+
// eslint-disable-next-line no-constant-condition
120+
while (true) {
121+
// Definitely not a valid protobuf
122+
if (!readProtoMessage(reader) && !reader.hasError()) {
123+
return false;
124+
}
125+
// Short read?
126+
if (reader.hasError()) {
127+
break;
128+
}
129+
numMessages++;
130+
}
131+
132+
return numMessages > 0;
133+
}
134+
135+
export function isBinaryFileSync(file: string): boolean {
136+
const stat = fs.statSync(file);
137+
138+
isStatFile(stat);
139+
140+
const fileDescriptor = fs.openSync(file, 'r');
141+
142+
const allocBuffer = Buffer.alloc(MAX_BYTES);
143+
144+
const bytesRead = fs.readSync(fileDescriptor, allocBuffer, 0, MAX_BYTES, 0);
145+
fs.closeSync(fileDescriptor);
146+
147+
return isBinaryCheck(allocBuffer, bytesRead);
148+
}
149+
150+
// eslint-disable-next-line complexity
151+
function isBinaryCheck(fileBuffer: Buffer, bytesRead: number): boolean {
152+
// empty file. no clue what it is.
153+
if (bytesRead === 0) {
154+
return false;
155+
}
156+
157+
let suspiciousBytes = 0;
158+
const totalBytes = Math.min(bytesRead, MAX_BYTES);
159+
160+
// UTF-8 BOM
161+
if (bytesRead >= 3 && fileBuffer[0] === 0xef && fileBuffer[1] === 0xbb && fileBuffer[2] === 0xbf) {
162+
return false;
163+
}
164+
165+
// UTF-32 BOM
166+
if (
167+
bytesRead >= 4 &&
168+
fileBuffer[0] === 0x00 &&
169+
fileBuffer[1] === 0x00 &&
170+
fileBuffer[2] === 0xfe &&
171+
fileBuffer[3] === 0xff
172+
) {
173+
return false;
174+
}
175+
176+
// UTF-32 LE BOM
177+
if (
178+
bytesRead >= 4 &&
179+
fileBuffer[0] === 0xff &&
180+
fileBuffer[1] === 0xfe &&
181+
fileBuffer[2] === 0x00 &&
182+
fileBuffer[3] === 0x00
183+
) {
184+
return false;
185+
}
186+
187+
// GB BOM
188+
if (
189+
bytesRead >= 4 &&
190+
fileBuffer[0] === 0x84 &&
191+
fileBuffer[1] === 0x31 &&
192+
fileBuffer[2] === 0x95 &&
193+
fileBuffer[3] === 0x33
194+
) {
195+
return false;
196+
}
197+
198+
if (totalBytes >= 5 && fileBuffer.slice(0, 5).toString() === '%PDF-') {
199+
/* PDF. This is binary.*/
200+
return true;
201+
}
202+
203+
// UTF-16 BE BOM
204+
if (bytesRead >= 2 && fileBuffer[0] === 0xfe && fileBuffer[1] === 0xff) {
205+
return false;
206+
}
207+
208+
// UTF-16 LE BOM
209+
if (bytesRead >= 2 && fileBuffer[0] === 0xff && fileBuffer[1] === 0xfe) {
210+
return false;
211+
}
212+
213+
for (let i = 0; i < totalBytes; i++) {
214+
if (fileBuffer[i] === 0) {
215+
// NULL byte--it's binary!
216+
return true;
217+
} else if ((fileBuffer[i] < 7 || fileBuffer[i] > 14) && (fileBuffer[i] < 32 || fileBuffer[i] > 127)) {
218+
// UTF-8 detection
219+
if (fileBuffer[i] >= 0xc0 && fileBuffer[i] <= 0xdf && i + 1 < totalBytes) {
220+
i++;
221+
if (fileBuffer[i] >= 0x80 && fileBuffer[i] <= 0xbf) {
222+
continue;
223+
}
224+
} else if (fileBuffer[i] >= 0xe0 && fileBuffer[i] <= 0xef && i + 2 < totalBytes) {
225+
i++;
226+
if (fileBuffer[i] >= 0x80 && fileBuffer[i] <= 0xbf && fileBuffer[i + 1] >= 0x80 && fileBuffer[i + 1] <= 0xbf) {
227+
i++;
228+
continue;
229+
}
230+
} else if (fileBuffer[i] >= 0xf0 && fileBuffer[i] <= 0xf7 && i + 3 < totalBytes) {
231+
i++;
232+
if (
233+
fileBuffer[i] >= 0x80 &&
234+
fileBuffer[i] <= 0xbf &&
235+
fileBuffer[i + 1] >= 0x80 &&
236+
fileBuffer[i + 1] <= 0xbf &&
237+
fileBuffer[i + 2] >= 0x80 &&
238+
fileBuffer[i + 2] <= 0xbf
239+
) {
240+
i += 2;
241+
continue;
242+
}
243+
}
244+
245+
suspiciousBytes++;
246+
// Read at least 32 fileBuffer before making a decision
247+
if (i >= 32 && (suspiciousBytes * 100) / totalBytes > 10) {
248+
return true;
249+
}
250+
}
251+
}
252+
253+
if ((suspiciousBytes * 100) / totalBytes > 10) {
254+
return true;
255+
}
256+
257+
if (suspiciousBytes > 1 && isBinaryProto(fileBuffer, totalBytes)) {
258+
return true;
259+
}
260+
261+
return false;
262+
}
263+
264+
function isStatFile(stat: fs.Stats): void {
265+
if (!stat.isFile()) {
266+
throw new Error('Path provided was not a file!');
267+
}
268+
}

src/convert/metadataConverter.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@
77
import { Readable, PassThrough } from 'node:stream';
88
import { dirname, join, normalize } from 'node:path';
99
import { Messages, SfError } from '@salesforce/core';
10-
import { promises } from 'graceful-fs';
10+
import { promises, mkdirSync } from 'graceful-fs';
1111
import { isString } from '@salesforce/ts-types';
1212
import { SourceComponent } from '../resolve/sourceComponent';
1313
import { MetadataResolver } from '../resolve/metadataResolver';
14-
import { ensureDirectoryExists } from '../utils/fileSystemHandler';
1514
import { SourcePath } from '../common/types';
1615
import { ComponentSet } from '../collections/componentSet';
1716
import { DestructiveChangesType } from '../collections/types';
1817
import { RegistryAccess } from '../registry/registryAccess';
19-
import { ComponentConverter, pipeline, StandardWriter, ZipWriter } from './streams';
18+
import { ComponentConverter, getPipeline, StandardWriter, ZipWriter } from './streams';
2019
import { ConvertOutputConfig, ConvertResult, DirectoryConfig, SfdxFileFormat, ZipConfig, MergeConfig } from './types';
2120
import { getReplacementMarkingStream } from './replacements';
2221

@@ -58,7 +57,7 @@ export class MetadataConverter {
5857
tasks = [],
5958
} = await getConvertIngredients(output, cs, targetFormatIsSource, this.registry);
6059

61-
const conversionPipeline = pipeline(
60+
const conversionPipeline = getPipeline()(
6261
Readable.from(components),
6362
!targetFormatIsSource && (process.env.SF_APPLY_REPLACEMENTS_ON_CONVERT === 'true' || output.type === 'zip')
6463
? (await getReplacementMarkingStream(cs.projectDirectory)) ?? new PassThrough({ objectMode: true })
@@ -126,9 +125,9 @@ function getPackagePath(outputConfig: DirectoryConfig | ZipConfig): SourcePath |
126125

127126
if (type === 'zip') {
128127
packagePath += '.zip';
129-
ensureDirectoryExists(dirname(packagePath));
128+
mkdirSync(dirname(packagePath), { recursive: true });
130129
} else {
131-
ensureDirectoryExists(packagePath);
130+
mkdirSync(packagePath, { recursive: true });
132131
}
133132
}
134133
return packagePath;

src/convert/replacements.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { Lifecycle, Messages, SfError, SfProject } from '@salesforce/core';
1111
import { minimatch } from 'minimatch';
1212
import { Env } from '@salesforce/kit';
1313
import { ensureString, isString } from '@salesforce/ts-types';
14-
import { isBinaryFileSync } from 'isbinaryfile';
1514
import { SourcePath } from '../common/types';
1615
import { SourceComponent } from '../resolve/sourceComponent';
16+
import { isBinaryFileSync } from './isBinaryFile';
1717
import { MarkedReplacement, ReplacementConfig, ReplacementEvent } from './types';
1818

1919
Messages.importMessagesDirectory(__dirname);

src/convert/streams.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { isAbsolute, join } from 'node:path';
8-
import { pipeline as cbPipeline, Readable, Stream, Transform, Writable } from 'node:stream';
8+
import { pipeline as cbPipeline, Readable, Transform, Writable, Stream } from 'node:stream';
99
import { promisify } from 'node:util';
1010
import { Messages, SfError } from '@salesforce/core';
1111
import JSZip from 'jszip';
@@ -28,7 +28,17 @@ import { SfdxFileFormat, WriteInfo, WriterFormat } from './types';
2828
Messages.importMessagesDirectory(__dirname);
2929
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
3030

31-
export const pipeline = promisify(cbPipeline);
31+
export type PromisifiedPipeline = <T extends NodeJS.ReadableStream>(
32+
source: T,
33+
...destinations: NodeJS.WritableStream[]
34+
) => Promise<void>;
35+
36+
let promisifiedPipeline: PromisifiedPipeline | undefined; // store it so we don't have to promisify every time
37+
38+
export const getPipeline = (): PromisifiedPipeline => {
39+
promisifiedPipeline ??= promisify(cbPipeline);
40+
return promisifiedPipeline;
41+
};
3242

3343
export const stream2buffer = async (stream: Stream): Promise<Buffer> =>
3444
new Promise<Buffer>((resolve, reject) => {
@@ -178,7 +188,7 @@ export class StandardWriter extends ComponentWriter {
178188
}
179189

180190
ensureFileExists(info.output);
181-
return pipeline(info.source, createWriteStream(info.output));
191+
return getPipeline()(info.source, createWriteStream(info.output));
182192
})
183193
);
184194

0 commit comments

Comments
 (0)