single-file-cli 2.3.0 → 2.4.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/lib/cdp-client.js CHANGED
@@ -25,6 +25,7 @@
25
25
 
26
26
  import {
27
27
  launchBrowser,
28
+ getBrowserOptions,
28
29
  closeBrowser,
29
30
  browserExited
30
31
  } from "./browser.js";
@@ -59,6 +60,7 @@ const SET_PDF_FUNCTION_NAME = "setPDF";
59
60
  const SET_PAGE_DATA_FUNCTION_NAME = "setPageData";
60
61
  const BINDING_CALLED_EVENT_TYPE = "bindingCalled";
61
62
  const LOCALHOST = "http://localhost:";
63
+ const HEADLESS_USER_AGENT_TOKEN = "Headless";
62
64
  const BROWSER_EXITED_MAX_DELAY = 2000;
63
65
 
64
66
  let browserOptions, relaunchBrowserPromise;
@@ -73,17 +75,7 @@ async function initialize(singleFileOptions) {
73
75
  if (singleFileOptions.browserServer) {
74
76
  options.apiUrl = singleFileOptions.browserServer;
75
77
  } else {
76
- browserOptions = {};
77
- browserOptions.args = singleFileOptions.browserArgs;
78
- browserOptions.headless = singleFileOptions.browserHeadless;
79
- browserOptions.executablePath = singleFileOptions.browserExecutablePath;
80
- browserOptions.debug = singleFileOptions.browserDebug;
81
- browserOptions.singleProcess = singleFileOptions.browserSingleProcess;
82
- browserOptions.disableWebSecurity = singleFileOptions.browserDisableWebSecurity;
83
- browserOptions.width = singleFileOptions.browserWidth;
84
- browserOptions.height = singleFileOptions.browserHeight;
85
- browserOptions.userAgent = singleFileOptions.userAgent;
86
- browserOptions.httpProxyServer = singleFileOptions.httpProxyServer;
78
+ browserOptions = getBrowserOptions(singleFileOptions);
87
79
  options.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
88
80
  }
89
81
  }
@@ -102,7 +94,10 @@ function relaunchBrowser() {
102
94
 
103
95
  async function getPageData(options) {
104
96
  const EMPTY_PAGE_URL = "about:blank";
105
- const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, fetchAbortController: new AbortController() };
97
+ // compiled here so that an invalid pattern is reported before the page is
98
+ // loaded, instead of throwing for every request that is intercepted
99
+ const blockedURLPatterns = (options.blockedURLPatterns || []).map(pattern => new RegExp(pattern));
100
+ const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, blockedURLPatterns, fetchAbortController: new AbortController() };
106
101
  let targetInfo, cdp;
107
102
  try {
108
103
  logData(["Loading page", EMPTY_PAGE_URL], pageContext);
@@ -205,23 +200,24 @@ async function setupBrowserWindow({ Browser }, targetId, { options, debugMessage
205
200
  }
206
201
  }
207
202
 
208
- async function setupSecurity({ Security }, { options, debugMessages }) {
203
+ async function setupSecurity({ Security }, { options, debugMessages }, sessionId) {
209
204
  if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
210
205
  logData(["Ignoring HTTPS errors"], { options, debugMessages });
211
- await Security.setIgnoreCertificateErrors({ ignore: true });
206
+ await Security.setIgnoreCertificateErrors({ ignore: true }, sessionId);
212
207
  }
213
208
  }
214
209
 
215
210
  async function setupDeviceEmulation({ Browser, Emulation, Runtime }, { options, debugMessages }) {
216
211
  const needsDeviceMetrics = options.browserMobileEmulation || options.browserDeviceWidth ||
217
212
  options.browserDeviceHeight || options.browserDeviceScaleFactor;
218
- const needsUserAgent = options.browserMobileEmulation || options.platform || options.acceptLanguage;
219
213
  if (needsDeviceMetrics) {
220
214
  await setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages });
221
215
  }
222
- if (needsUserAgent) {
223
- await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
224
- }
216
+ await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
217
+ }
218
+
219
+ function needsUserAgentOverride(options) {
220
+ return Boolean(options.browserMobileEmulation || options.platform || options.acceptLanguage);
225
221
  }
226
222
 
227
223
  async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages }) {
@@ -244,12 +240,12 @@ async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessag
244
240
  await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
245
241
  }
246
242
 
247
- async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }) {
243
+ async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }, sessionId) {
248
244
  const ANDROID_PLATFORM = "Android";
249
245
  const { userAgent, product } = await Browser.getVersion();
250
- const defaultMobileUA = `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) ${product} Mobile Safari/537.36`;
246
+ const defaultMobileUA = `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) ${removeHeadlessToken(product)} Mobile Safari/537.36`;
251
247
  const agentOptions = {
252
- userAgent: options.userAgent || (options.browserMobileEmulation ? defaultMobileUA : userAgent)
248
+ userAgent: options.userAgent || (options.browserMobileEmulation ? defaultMobileUA : removeHeadlessToken(userAgent))
253
249
  };
254
250
  if (options.acceptLanguage) {
255
251
  agentOptions.acceptLanguage = options.acceptLanguage;
@@ -257,23 +253,24 @@ async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }
257
253
  if (options.platform || options.browserMobileEmulation) {
258
254
  agentOptions.platform = options.platform || ANDROID_PLATFORM;
259
255
  }
260
- logData(["Emulating user agent", JSON.stringify(agentOptions)], { options, debugMessages });
261
- await Emulation.setUserAgentOverride(agentOptions);
256
+ if (needsUserAgentOverride(options) || agentOptions.userAgent !== userAgent) {
257
+ logData(["Emulating user agent", JSON.stringify(agentOptions)], { options, debugMessages });
258
+ await Emulation.setUserAgentOverride(agentOptions, sessionId);
259
+ }
262
260
  }
263
261
 
264
- async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo }) {
265
- const REQUEST_STAGE = "Request";
266
- const RESPONSE_STAGE = "Response";
262
+ function removeHeadlessToken(userAgent) {
263
+ return userAgent.replace(HEADLESS_USER_AGENT_TOKEN, "");
264
+ }
265
+
266
+ async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
267
267
  const DENY_BEHAVIOR = "deny";
268
- const handleAuthRequests = Boolean(options.httpProxyUsername);
269
- const patterns = handleAuthRequests ?
270
- [{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
271
- [{ requestStage: RESPONSE_STAGE }];
268
+ const { handleAuthRequests, patterns } = getInterceptionOptions(options);
272
269
  await Fetch.enable({ handleAuthRequests, patterns });
273
270
  if (handleAuthRequests) {
274
271
  setupProxyAuth({ Fetch }, { options, debugMessages });
275
272
  }
276
- setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo });
273
+ setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo, blockedURLPatterns });
277
274
  if (options.httpHeaders) {
278
275
  await setupHttpHeaders({ Network }, { options, debugMessages });
279
276
  }
@@ -286,10 +283,20 @@ async function setupNetworkInterception({ Browser, Emulation, Fetch, Network },
286
283
  await Browser.setDownloadBehavior({ behavior: DENY_BEHAVIOR });
287
284
  }
288
285
 
286
+ function getInterceptionOptions(options) {
287
+ const REQUEST_STAGE = "Request";
288
+ const RESPONSE_STAGE = "Response";
289
+ const handleAuthRequests = Boolean(options.httpProxyUsername);
290
+ const patterns = handleAuthRequests ?
291
+ [{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
292
+ [{ requestStage: RESPONSE_STAGE }];
293
+ return { handleAuthRequests, patterns };
294
+ }
295
+
289
296
  function setupProxyAuth({ Fetch }, { options, debugMessages }) {
290
297
  const AUTH_REQUIRED_EVENT_TYPE = "authRequired";
291
298
  const PROVIDE_CREDENTIALS_RESPONSE = "ProvideCredentials";
292
- Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
299
+ Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
293
300
  logData(["Authenticating"], { options, debugMessages });
294
301
  await Fetch.continueWithAuth({
295
302
  requestId: params.requestId,
@@ -298,38 +305,37 @@ function setupProxyAuth({ Fetch }, { options, debugMessages }) {
298
305
  username: options.httpProxyUsername,
299
306
  password: options.httpProxyPassword
300
307
  }
301
- });
308
+ }, sessionId);
302
309
  }, { options, debugMessages }));
303
310
  }
304
311
 
305
- function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo }) {
312
+ function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
306
313
  const REQUEST_PAUSED_EVENT_TYPE = "requestPaused";
307
314
  const ABORTED_ERROR_REASON = "Aborted";
308
315
  const urlState = { url: options.url, alternativeUrl: getAlternativeUrl(options.url) };
309
- // compiled here so that an invalid pattern is reported before the page is
310
- // loaded, instead of throwing for every request that is intercepted
311
- const blockedURLPatterns = (options.blockedURLPatterns || []).map(pattern => new RegExp(pattern));
312
- Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
316
+ Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
313
317
  const { requestId, request } = params;
314
318
  // the request is always resumed below, otherwise the page waits for it
315
319
  // until the load timeout expires
316
320
  let blocked = false;
317
321
  try {
318
- captureHttpInfo(params, urlState, { options, debugMessages, httpInfo });
322
+ if (!sessionId) {
323
+ captureHttpInfo(params, urlState, { options, debugMessages, httpInfo });
324
+ }
319
325
  blocked = shouldBlockRequest(request.url);
320
326
  } catch (error) {
321
327
  logData(["Ignoring request interception error", error.message], { options, debugMessages });
322
328
  }
323
329
  if (blocked) {
324
330
  try {
325
- await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON });
331
+ await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON }, sessionId);
326
332
  return;
327
333
  } catch {
328
334
  // ignored
329
335
  }
330
336
  }
331
337
  try {
332
- await Fetch.continueRequest({ requestId });
338
+ await Fetch.continueRequest({ requestId }, sessionId);
333
339
  } catch {
334
340
  // ignored
335
341
  }
@@ -381,13 +387,13 @@ function captureHttpInfo(params, urlState, { options, debugMessages, httpInfo })
381
387
  }
382
388
  }
383
389
 
384
- async function setupHttpHeaders({ Network }, { options, debugMessages }) {
390
+ async function setupHttpHeaders({ Network }, { options, debugMessages }, sessionId) {
385
391
  logData(["Setting HTTP headers", JSON.stringify(options.httpHeaders)], { options, debugMessages });
386
- await Network.enable();
387
- await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
392
+ await Network.enable({}, sessionId);
393
+ await Network.setExtraHTTPHeaders({ headers: options.httpHeaders }, sessionId);
388
394
  }
389
395
 
390
- async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
396
+ async function setupMediaFeatures({ Emulation }, { options, debugMessages }, sessionId) {
391
397
  const features = [];
392
398
  for (const mediaFeature of options.emulateMediaFeatures) {
393
399
  logData(["Emulating media feature", mediaFeature.name, mediaFeature.value], { options, debugMessages });
@@ -395,7 +401,7 @@ async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
395
401
  features.push({ name: mediaFeature.name, value: value.trim() });
396
402
  }
397
403
  }
398
- await Emulation.setEmulatedMedia({ features });
404
+ await Emulation.setEmulatedMedia({ features }, sessionId);
399
405
  }
400
406
 
401
407
  async function setupCookies({ Network }, { options, debugMessages }) {
@@ -403,16 +409,75 @@ async function setupCookies({ Network }, { options, debugMessages }) {
403
409
  await Network.setCookies({ cookies: options.browserCookies });
404
410
  }
405
411
 
406
- async function setupScriptInjection({ Page }, { options }) {
412
+ async function setupScriptInjection(cdp, { options, debugMessages }) {
413
+ const { Page } = cdp;
414
+ const scriptSource = await getScriptSource(options);
407
415
  await Page.addScriptToEvaluateOnNewDocument({
408
416
  source: getHookScriptSource(),
409
417
  runImmediately: true
410
418
  });
411
419
  await Page.addScriptToEvaluateOnNewDocument({
412
- source: await getScriptSource(options),
420
+ source: scriptSource,
413
421
  runImmediately: true,
414
422
  worldName: SINGLE_FILE_WORLD_NAME
415
423
  });
424
+ await setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages });
425
+ }
426
+
427
+ // out-of-process frames are separate targets, so the scripts registered above
428
+ // do not reach them; they are attached paused, injected and resumed instead
429
+ async function setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages }) {
430
+ const { Page, Runtime, Target } = cdp;
431
+ const ATTACHED_TO_TARGET_EVENT_TYPE = "attachedToTarget";
432
+ const IFRAME_TARGET_TYPE = "iframe";
433
+ const autoAttachOptions = { autoAttach: true, waitForDebuggerOnStart: true, flatten: true };
434
+ Target.addEventListener(ATTACHED_TO_TARGET_EVENT_TYPE, ignoringErrors(async ({ params }) => {
435
+ const { sessionId, targetInfo, waitingForDebugger } = params;
436
+ try {
437
+ if (targetInfo.type === IFRAME_TARGET_TYPE) {
438
+ logData(["Injecting scripts into frame", targetInfo.url], { options, debugMessages });
439
+ // the scripts registered below only run when the Page domain is
440
+ // enabled on the session
441
+ await Page.enable({}, sessionId);
442
+ await Target.setAutoAttach(autoAttachOptions, sessionId);
443
+ await Page.addScriptToEvaluateOnNewDocument({
444
+ source: getHookScriptSource(),
445
+ runImmediately: true
446
+ }, sessionId);
447
+ await Page.addScriptToEvaluateOnNewDocument({
448
+ source: scriptSource,
449
+ runImmediately: true,
450
+ worldName: SINGLE_FILE_WORLD_NAME
451
+ }, sessionId);
452
+ await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME }, sessionId);
453
+ await setupFrameSession(cdp, sessionId, { options, debugMessages });
454
+ }
455
+ } finally {
456
+ // the target is always resumed, even when a setup command fails,
457
+ // otherwise it would stay paused until the load timeout expires
458
+ if (waitingForDebugger) {
459
+ await Runtime.runIfWaitingForDebugger({}, sessionId);
460
+ }
461
+ }
462
+ }, { options, debugMessages }));
463
+ await Target.setAutoAttach(autoAttachOptions);
464
+ }
465
+
466
+ async function setupFrameSession({ Browser, Emulation, Fetch, Network, Security }, sessionId, { options, debugMessages }) {
467
+ const { handleAuthRequests, patterns } = getInterceptionOptions(options);
468
+ if (handleAuthRequests || (options.blockedURLPatterns && options.blockedURLPatterns.length)) {
469
+ // the requestPaused and authRequired listeners are registered once on
470
+ // the connection and receive the events of every session
471
+ await Fetch.enable({ handleAuthRequests, patterns }, sessionId);
472
+ }
473
+ await setupSecurity({ Security }, { options, debugMessages }, sessionId);
474
+ if (options.httpHeaders) {
475
+ await setupHttpHeaders({ Network }, { options, debugMessages }, sessionId);
476
+ }
477
+ if (options.emulateMediaFeatures) {
478
+ await setupMediaFeatures({ Emulation }, { options, debugMessages }, sessionId);
479
+ }
480
+ await setupUserAgent({ Browser, Emulation }, { options, debugMessages }, sessionId);
416
481
  }
417
482
 
418
483
  async function getContextId({ Debugger, Page, Runtime }, { options, debugMessages }) {
@@ -578,7 +643,7 @@ function setupPageDataCapture({ Runtime }, { options, debugMessages }) {
578
643
  });
579
644
  }
580
645
 
581
- async function setupBindings({ Page, Runtime }, { options, debugMessages, fetchAbortController }) {
646
+ async function setupBindings({ Page, Runtime }, { options, debugMessages, blockedURLPatterns, fetchAbortController }) {
582
647
  await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
583
648
  if (options.embedScreenshot && options.compressContent) {
584
649
  await setupScreenshotCapture({ Page, Runtime }, { options, debugMessages });
@@ -587,9 +652,9 @@ async function setupBindings({ Page, Runtime }, { options, debugMessages, fetchA
587
652
  await setupPdfCapture({ Page, Runtime }, { options, debugMessages });
588
653
  }
589
654
  await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
590
- Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
655
+ Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
591
656
  if (params.name === FETCH_FUNCTION_NAME) {
592
- await handleFetchRequest({ Runtime }, params, { options, debugMessages, fetchAbortController });
657
+ await handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId);
593
658
  }
594
659
  }, { options, debugMessages }));
595
660
  }
@@ -652,12 +717,23 @@ async function setupPdfCapture({ Page, Runtime }, { options, debugMessages }) {
652
717
  }
653
718
  }
654
719
 
655
- async function handleFetchRequest({ Runtime }, params, { options, debugMessages, fetchAbortController }) {
720
+ async function handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId) {
721
+ const BLOCKED_URL_ERROR_MESSAGE = "Blocked URL";
656
722
  const { executionContextId: contextId, payload } = params;
657
723
  const { requestId, url, options: fetchOptions } = JSON.parse(payload);
658
724
  logData(["Fetching URL", url], { options, debugMessages });
659
725
  try {
660
- const response = await fetch(url, Object.assign({}, fetchOptions, { signal: fetchAbortController.signal }));
726
+ // this fetch does not go through the browser, so the URL blocking and
727
+ // the extra HTTP headers must be applied here too
728
+ if (blockedURLPatterns.some(pattern => pattern.test(url))) {
729
+ logData(["Blocking request", url], { options, debugMessages });
730
+ throw new Error(BLOCKED_URL_ERROR_MESSAGE);
731
+ }
732
+ const headers = Object.assign({}, fetchOptions.headers, options.httpHeaders);
733
+ if (options.userAgent) {
734
+ headers["user-agent"] = options.userAgent;
735
+ }
736
+ const response = await fetch(url, Object.assign({}, fetchOptions, { headers, signal: fetchAbortController.signal }));
661
737
  const arrayBuffer = await response.arrayBuffer();
662
738
  const base64Data = arrayBufferToBase64(arrayBuffer);
663
739
  const result = {
@@ -665,22 +741,22 @@ async function handleFetchRequest({ Runtime }, params, { options, debugMessages,
665
741
  headers: Object.fromEntries(response.headers.entries()),
666
742
  data: base64Data
667
743
  };
668
- await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result]);
744
+ await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result], sessionId);
669
745
  } catch (error) {
670
746
  const errorResult = {
671
747
  error: error.message,
672
748
  code: error.code
673
749
  };
674
- await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult]);
750
+ await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult], sessionId);
675
751
  }
676
752
  }
677
753
 
678
- async function callBrowserFunction({ Runtime }, contextId, functionName, args) {
754
+ async function callBrowserFunction({ Runtime }, contextId, functionName, args, sessionId) {
679
755
  const serializedArgs = args.map(arg => JSON.stringify(arg)).join(", ");
680
756
  await Runtime.evaluate({
681
757
  expression: `globalThis.${functionName}(${serializedArgs})`,
682
758
  contextId
683
- });
759
+ }, sessionId);
684
760
  }
685
761
 
686
762
  async function capturePageData({ Runtime }, contextId, { options, debugMessages }) {
package/lib/constants.js CHANGED
@@ -43,7 +43,6 @@ const BROWSER_ARGS = [
43
43
  "--disable-renderer-backgrounding",
44
44
  "--force-color-profile=srgb",
45
45
  "--no-first-run",
46
- "--enable-automation",
47
46
  "--password-store=basic",
48
47
  "--use-mock-keychain",
49
48
  "--no-service-autorun",
@@ -101,8 +101,10 @@ const DenoAPI = {
101
101
  args,
102
102
  readFile,
103
103
  readTextFile,
104
+ readDir,
104
105
  writeTextFile,
105
106
  writeFile,
107
+ copyFile,
106
108
  mkdir,
107
109
  makeTempDir,
108
110
  stat,
@@ -118,6 +120,7 @@ const DenoAPI = {
118
120
 
119
121
  const pathAPI = {
120
122
  dirname,
123
+ join,
121
124
  toFileUrl,
122
125
  fromFileUrl,
123
126
  SEPARATOR: DENO_RUNTIME_DETECTED ? path.SEPARATOR : path.sep
@@ -154,6 +157,41 @@ async function readTextFile(path) {
154
157
  }
155
158
  }
156
159
 
160
+ async function readDir(path) {
161
+ if (DENO_RUNTIME_DETECTED) {
162
+ const entries = [];
163
+ try {
164
+ for await (const entry of Deno.readDir(path)) {
165
+ entries.push({ name: entry.name, isDirectory: entry.isDirectory, isSymlink: entry.isSymlink });
166
+ }
167
+ } catch (error) {
168
+ throw mapNotFoundError(error);
169
+ }
170
+ return entries;
171
+ } else {
172
+ const fsPromise = await import(NODE_MODULES["fs"]);
173
+ try {
174
+ const entries = await fsPromise.readdir(path, { withFileTypes: true });
175
+ return entries.map(entry => ({ name: entry.name, isDirectory: entry.isDirectory(), isSymlink: entry.isSymbolicLink() }));
176
+ } catch (error) {
177
+ throw mapNotFoundError(error);
178
+ }
179
+ }
180
+ }
181
+
182
+ async function copyFile(sourcePath, destinationPath) {
183
+ if (DENO_RUNTIME_DETECTED) {
184
+ return Deno.copyFile(sourcePath, destinationPath);
185
+ } else {
186
+ const fsPromise = await import(NODE_MODULES["fs"]);
187
+ return fsPromise.copyFile(sourcePath, destinationPath);
188
+ }
189
+ }
190
+
191
+ function mapNotFoundError(error) {
192
+ return error.code == "ENOENT" || error.code == "ENOTDIR" ? new errors.NotFound(error.message) : error;
193
+ }
194
+
157
195
  async function writeTextFile(path, data, options = {}) {
158
196
  if (DENO_RUNTIME_DETECTED) {
159
197
  return Deno.writeTextFile(path, data, options);
@@ -259,6 +297,10 @@ function dirname(filePath) {
259
297
  return path.dirname(filePath);
260
298
  }
261
299
 
300
+ function join(...filePaths) {
301
+ return path.join(...filePaths);
302
+ }
303
+
262
304
  async function toFileUrl(filePath) {
263
305
  if (DENO_RUNTIME_DETECTED) {
264
306
  return path.toFileUrl(filePath);