@slicemachine/manager 0.15.1-dev-next-release.12 → 0.15.1-dev-next-release.14

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.
@@ -9,6 +9,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
9
  const t = require("io-ts");
10
10
  require("readable-web-to-node-stream");
11
11
  const core = require('./../../_node_modules/file-type/core.cjs');
12
+ const index = require('./../../_node_modules/p-limit/index.cjs');
12
13
  const fetch = require("../../lib/fetch.cjs");
13
14
  const checkIsURLAccessible = require("../../lib/checkIsURLAccessible.cjs");
14
15
  const createContentDigest = require("../../lib/createContentDigest.cjs");
@@ -25,7 +26,7 @@ require("node:buffer");
25
26
  require("node:fs");
26
27
  require("node:path");
27
28
  require('./../../_node_modules/node-domexception/index.cjs');
28
- const index = require('./../../_node_modules/fetch-blob/index.cjs');
29
+ const index$1 = require('./../../_node_modules/fetch-blob/index.cjs');
29
30
  function _interopNamespaceDefault(e) {
30
31
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
31
32
  if (e) {
@@ -60,6 +61,7 @@ function assertBrowserContextInitialized(browserContext) {
60
61
  throw new Error("A browser context has not been initialized. Run `SliceMachineManager.screenshots.prototype.initBrowserContext()` before re-calling this method.");
61
62
  }
62
63
  }
64
+ const uploadScreenshotLimit = index(10);
63
65
  const encodeSliceSimulatorURLPart = (urlPart) => {
64
66
  return urlPart.replace(/\//g, "--");
65
67
  };
@@ -169,11 +171,12 @@ class ScreenshotsManager extends BaseManager.BaseManager {
169
171
  if (fileType) {
170
172
  formData.set("Content-Type", fileType.mime);
171
173
  }
172
- formData.set("file", new index.default([args.data], { type: fileType == null ? void 0 : fileType.mime }));
173
- const res = await fetch.default(this._s3ACL.uploadEndpoint, {
174
+ formData.set("file", new index$1.default([args.data], { type: fileType == null ? void 0 : fileType.mime }));
175
+ const s3ACLEndpoint = this._s3ACL.uploadEndpoint;
176
+ const res = await uploadScreenshotLimit(() => fetch.default(s3ACLEndpoint, {
174
177
  method: "POST",
175
178
  body: formData
176
- });
179
+ }));
177
180
  if (res.ok) {
178
181
  const url = new URL(key, this._s3ACL.imgixEndpoint);
179
182
  url.searchParams.set("auto", "compress,format");
@@ -1 +1 @@
1
- {"version":3,"file":"ScreenshotsManager.cjs","sources":["../../../../src/managers/screenshots/ScreenshotsManager.ts"],"sourcesContent":["import * as t from \"io-ts\";\nimport { fileTypeFromBuffer } from \"file-type\";\nimport fetch, { FormData, Blob, Response } from \"../../lib/fetch\";\n\nimport { checkIsURLAccessible } from \"../../lib/checkIsURLAccessible\";\nimport { createContentDigest } from \"../../lib/createContentDigest\";\nimport { decode } from \"../../lib/decode\";\n\nimport { S3ACL } from \"../../types\";\nimport { SLICE_MACHINE_USER_AGENT } from \"../../constants/SLICE_MACHINE_USER_AGENT\";\nimport { API_ENDPOINTS } from \"../../constants/API_ENDPOINTS\";\n\nimport { BaseManager } from \"../BaseManager\";\n\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR = \"#__iframe-ready\";\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT = 10_000; // ms\nconst SLICE_SIMULATOR_SCREENSHOT_SELECTOR = \"#__iframe-renderer\";\n\n// TODO(DT-1534): Use Puppeteer types if we want reactive Puppeteer screenshots\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Viewport = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BrowserContext = any;\n\nconst DEFAULT_SCREENSHOT_VIEWPORT: Viewport = {\n\twidth: 1200,\n\theight: 800,\n};\n\nfunction assertS3ACLInitialized(\n\ts3ACL: S3ACL | undefined,\n): asserts s3ACL is NonNullable<typeof s3ACL> {\n\tif (s3ACL == undefined) {\n\t\tthrow new Error(\n\t\t\t\"An S3 ACL has not been initialized. Run `SliceMachineManager.screenshots.prototype.initS3ACL()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\nfunction assertBrowserContextInitialized(\n\tbrowserContext: BrowserContext | undefined,\n): asserts browserContext is NonNullable<typeof browserContext> {\n\tif (browserContext == undefined) {\n\t\tthrow new Error(\n\t\t\t\"A browser context has not been initialized. Run `SliceMachineManager.screenshots.prototype.initBrowserContext()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\n/**\n * Encodes a part of a Slice Simulator URL to ensure it can be added to a URL\n * safely.\n *\n * The encoding logic must match Slice Machine UI's URL encoding practices.\n * Today, that requires the following:\n *\n * - Replace \"/\" with \"--\" (e.g. a Slice Library ID of \"./slices\" should turn into\n * \".--slices\")\n *\n * @param urlPart - A part of the URL.\n *\n * @returns `urlPart` encoded for use in a URL.\n */\nconst encodeSliceSimulatorURLPart = (urlPart: string): string => {\n\treturn urlPart.replace(/\\//g, \"--\");\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs = {\n\tsliceMachineUIOrigin: string;\n\tlibraryID: string;\n\tsliceID: string;\n\tvariationID: string;\n\tviewport?: Viewport;\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType = {\n\tdata: Buffer;\n};\n\ntype ScreenshotsManagerUploadScreenshotArgs = {\n\tdata: Buffer;\n\tkeyPrefix?: string;\n};\n\ntype ScreenshotsManagerUploadScreenshotReturnType = {\n\turl: string;\n};\n\ntype ScreenshotsManagerDeleteScreenshotFolderArgs = {\n\tsliceID: string;\n};\n\nexport class ScreenshotsManager extends BaseManager {\n\tprivate _browserContext: BrowserContext | undefined;\n\tprivate _s3ACL: S3ACL | undefined;\n\n\tasync initBrowserContext(): Promise<void> {\n\t\t// TODO(DT-1534): Uncomment to enable Puppeteer screenshots or delete if we decide to remove Puppeteer\n\t\t//\n\t\t// if (this._browserContext) {\n\t\t// \treturn;\n\t\t// }\n\t\t//\n\t\t// let puppeteer: typeof import(\"puppeteer\");\n\t\t// try {\n\t\t// \t// Lazy-load Puppeteer only once it is needed.\n\t\t// \tpuppeteer = await import(\"puppeteer\");\n\t\t// } catch {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Screenshots require Puppeteer but Puppeteer was not found. Check that the `puppeteer` package is installed before trying again.\",\n\t\t// \t);\n\t\t// }\n\t\t// try {\n\t\t// \tconst browser = await puppeteer.launch({ headless: \"new\" });\n\t\t// \tthis._browserContext = await browser.createIncognitoBrowserContext();\n\t\t// } catch (error) {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Error launching browser. If you're using an Apple Silicon Mac, check if Rosetta is installed.\",\n\t\t// \t);\n\t\t// }\n\t}\n\n\tasync initS3ACL(): Promise<void> {\n\t\t// TODO: we need to find a way to create a new AWS ACL only when necessary (e.g., when it has expired).\n\t\t// if (this._s3ACL) {\n\t\t// \treturn;\n\t\t// }\n\n\t\tconst awsACLURL = new URL(\"create\", API_ENDPOINTS.AwsAclProvider);\n\t\tconst awsACLRes = await this._fetch({ url: awsACLURL });\n\n\t\tconst awsACLText = await awsACLRes.text();\n\t\tlet awsACLJSON: unknown;\n\t\ttry {\n\t\t\tawsACLJSON = JSON.parse(awsACLText);\n\t\t} catch (error) {\n\t\t\t// Response is not JSON\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid AWS ACL response from ${awsACLURL}: ${awsACLText}`,\n\t\t\t);\n\t\t}\n\n\t\tconst { value: awsACL, error } = decode(\n\t\t\tt.intersection([\n\t\t\t\tt.type({\n\t\t\t\t\tvalues: t.type({\n\t\t\t\t\t\turl: t.string,\n\t\t\t\t\t\tfields: t.record(t.string, t.string),\n\t\t\t\t\t}),\n\t\t\t\t\timgixEndpoint: t.string,\n\t\t\t\t}),\n\t\t\t\tt.partial({\n\t\t\t\t\tmessage: t.string,\n\t\t\t\t\tMessage: t.string,\n\t\t\t\t\terror: t.string,\n\t\t\t\t}),\n\t\t\t]),\n\t\t\tawsACLJSON,\n\t\t);\n\n\t\tif (error) {\n\t\t\tthrow new Error(`Invalid AWS ACL response from ${awsACLURL}`);\n\t\t}\n\n\t\tconst errorMessage = awsACL.error || awsACL.message || awsACL.Message;\n\t\tif (errorMessage) {\n\t\t\tthrow new Error(`Failed to create an AWS ACL: ${errorMessage}`);\n\t\t}\n\n\t\tthis._s3ACL = {\n\t\t\tuploadEndpoint: awsACL.values.url,\n\t\t\trequiredFormDataFields: awsACL.values.fields,\n\t\t\timgixEndpoint: awsACL.imgixEndpoint,\n\t\t};\n\t}\n\n\t// TODO: Abstract to a generic `captureScreenshot()` method that is\n\t// used within a Slice-specific method in SliceManager.\n\tasync captureSliceSimulatorScreenshot(\n\t\targs: ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs,\n\t): Promise<ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType> {\n\t\tassertBrowserContextInitialized(this._browserContext);\n\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\tif (!sliceMachineConfig.localSliceSimulatorURL) {\n\t\t\t// TODO: Provide a more helpful error message.\n\t\t\tthrow new Error(\n\t\t\t\t\"A local Slice Simulator URL must be configured in your Slice Machine configuration file.\",\n\t\t\t);\n\t\t}\n\n\t\tconst { model } = await this.slices.readSlice({\n\t\t\tlibraryID: args.libraryID,\n\t\t\tsliceID: args.sliceID,\n\t\t});\n\t\tif (!model) {\n\t\t\tthrow new Error(\n\t\t\t\t`Did not find a Slice in library \"${args.libraryID}\" with ID \"${args.sliceID}\".`,\n\t\t\t);\n\t\t}\n\n\t\tconst viewport = args.viewport || DEFAULT_SCREENSHOT_VIEWPORT;\n\n\t\t// TODO: Change `model.name` to `args.sliceID`?\n\t\t// Making that change would require changing the screenshot\n\t\t// page path in Slice Machine UI.\n\t\tconst url = new URL(\n\t\t\t`./${encodeSliceSimulatorURLPart(args.libraryID)}/${model.name}/${\n\t\t\t\targs.variationID\n\t\t\t}/screenshot`,\n\t\t\targs.sliceMachineUIOrigin,\n\t\t);\n\t\turl.searchParams.set(\"screenWidth\", viewport.width.toString());\n\t\turl.searchParams.set(\"screenHeight\", viewport.height.toString());\n\n\t\tconst isURLAccessible = await checkIsURLAccessible(url.toString());\n\n\t\tif (!isURLAccessible) {\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator screenshot URL is not accessible: ${url}`,\n\t\t\t);\n\t\t}\n\n\t\tconst page = await this._browserContext.newPage();\n\t\tpage.setViewport(viewport);\n\n\t\tawait page.goto(url.toString(), { waitUntil: [\"load\", \"networkidle0\"] });\n\t\tawait page.waitForSelector(SLICE_SIMULATOR_WAIT_FOR_SELECTOR, {\n\t\t\ttimeout: SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT,\n\t\t});\n\n\t\tconst element = await page.$(SLICE_SIMULATOR_SCREENSHOT_SELECTOR);\n\t\tif (!element) {\n\t\t\tconst baseURL = new URL(url.pathname, url.origin);\n\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator did not find ${SLICE_SIMULATOR_WAIT_FOR_SELECTOR} on the page. Verify the URL is correct: ${baseURL}`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = (await element.screenshot({\n\t\t\tencoding: \"binary\",\n\t\t\tclip: {\n\t\t\t\twidth: viewport.width,\n\t\t\t\theight: viewport.height,\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t},\n\t\t})) as Buffer;\n\n\t\treturn {\n\t\t\tdata,\n\t\t};\n\t}\n\n\tasync uploadScreenshot(\n\t\targs: ScreenshotsManagerUploadScreenshotArgs,\n\t): Promise<ScreenshotsManagerUploadScreenshotReturnType> {\n\t\tassertS3ACLInitialized(this._s3ACL);\n\n\t\tconst formData = new FormData();\n\n\t\tfor (const requiredFormDataFieldKey in this._s3ACL.requiredFormDataFields) {\n\t\t\tformData.append(\n\t\t\t\trequiredFormDataFieldKey,\n\t\t\t\tthis._s3ACL.requiredFormDataFields[requiredFormDataFieldKey],\n\t\t\t);\n\t\t}\n\n\t\tconst contentDigest = createContentDigest(args.data);\n\t\tconst fileType = await fileTypeFromBuffer(args.data);\n\t\tconst fileName = fileType\n\t\t\t? `${contentDigest}.${fileType.ext}`\n\t\t\t: contentDigest;\n\t\tconst key = args.keyPrefix ? `${args.keyPrefix}/${fileName}` : fileName;\n\n\t\tformData.set(\"key\", key);\n\n\t\tif (fileType) {\n\t\t\tformData.set(\"Content-Type\", fileType.mime);\n\t\t}\n\n\t\tformData.set(\"file\", new Blob([args.data], { type: fileType?.mime }));\n\n\t\tconst res = await fetch(this._s3ACL.uploadEndpoint, {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: formData,\n\t\t});\n\n\t\tif (res.ok) {\n\t\t\tconst url = new URL(key, this._s3ACL.imgixEndpoint);\n\t\t\turl.searchParams.set(\"auto\", \"compress,format\");\n\n\t\t\treturn {\n\t\t\t\turl: url.toString(),\n\t\t\t};\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to upload screenshot with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync deleteScreenshotFolder(\n\t\targs: ScreenshotsManagerDeleteScreenshotFolderArgs,\n\t): Promise<void> {\n\t\tconst res = await this._fetch({\n\t\t\t// We're sending `args.sliceID` as `sliceName` because it's inconsistently\n\t\t\t// named in the ACL Provider API.\n\t\t\tbody: { sliceName: args.sliceID },\n\t\t\tmethod: \"POST\",\n\t\t\turl: new URL(\"delete-folder\", API_ENDPOINTS.AwsAclProvider),\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to delete screenshot folder with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async _fetch(args: {\n\t\turl: URL;\n\t\tmethod?: \"GET\" | \"POST\";\n\t\tbody?: unknown;\n\t}): Promise<Response> {\n\t\tconst authenticationToken = await this.user.getAuthenticationToken();\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\treturn await fetch(args.url, {\n\t\t\tbody: args.body ? JSON.stringify(args.body) : undefined,\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${authenticationToken}`,\n\t\t\t\tRepository: sliceMachineConfig.repositoryName,\n\t\t\t\t\"User-Agent\": SLICE_MACHINE_USER_AGENT,\n\t\t\t\t...(args.body ? { \"Content-Type\": \"application/json\" } : {}),\n\t\t\t},\n\t\t\tmethod: args.method,\n\t\t});\n\t}\n}\n"],"names":["BaseManager","API_ENDPOINTS","error","decode","t","checkIsURLAccessible","FormData","createContentDigest","fileTypeFromBuffer","Blob","fetch","SLICE_MACHINE_USER_AGENT"],"mappings":"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"}
1
+ {"version":3,"file":"ScreenshotsManager.cjs","sources":["../../../../src/managers/screenshots/ScreenshotsManager.ts"],"sourcesContent":["import * as t from \"io-ts\";\nimport { fileTypeFromBuffer } from \"file-type\";\nimport pLimit from \"p-limit\";\nimport fetch, { FormData, Blob, Response } from \"../../lib/fetch\";\n\nimport { checkIsURLAccessible } from \"../../lib/checkIsURLAccessible\";\nimport { createContentDigest } from \"../../lib/createContentDigest\";\nimport { decode } from \"../../lib/decode\";\n\nimport { S3ACL } from \"../../types\";\nimport { SLICE_MACHINE_USER_AGENT } from \"../../constants/SLICE_MACHINE_USER_AGENT\";\nimport { API_ENDPOINTS } from \"../../constants/API_ENDPOINTS\";\n\nimport { BaseManager } from \"../BaseManager\";\n\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR = \"#__iframe-ready\";\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT = 10_000; // ms\nconst SLICE_SIMULATOR_SCREENSHOT_SELECTOR = \"#__iframe-renderer\";\n\n// TODO(DT-1534): Use Puppeteer types if we want reactive Puppeteer screenshots\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Viewport = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BrowserContext = any;\n\nconst DEFAULT_SCREENSHOT_VIEWPORT: Viewport = {\n\twidth: 1200,\n\theight: 800,\n};\n\nfunction assertS3ACLInitialized(\n\ts3ACL: S3ACL | undefined,\n): asserts s3ACL is NonNullable<typeof s3ACL> {\n\tif (s3ACL == undefined) {\n\t\tthrow new Error(\n\t\t\t\"An S3 ACL has not been initialized. Run `SliceMachineManager.screenshots.prototype.initS3ACL()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\nfunction assertBrowserContextInitialized(\n\tbrowserContext: BrowserContext | undefined,\n): asserts browserContext is NonNullable<typeof browserContext> {\n\tif (browserContext == undefined) {\n\t\tthrow new Error(\n\t\t\t\"A browser context has not been initialized. Run `SliceMachineManager.screenshots.prototype.initBrowserContext()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\nconst uploadScreenshotLimit = pLimit(10);\n\n/**\n * Encodes a part of a Slice Simulator URL to ensure it can be added to a URL\n * safely.\n *\n * The encoding logic must match Slice Machine UI's URL encoding practices.\n * Today, that requires the following:\n *\n * - Replace \"/\" with \"--\" (e.g. a Slice Library ID of \"./slices\" should turn into\n * \".--slices\")\n *\n * @param urlPart - A part of the URL.\n *\n * @returns `urlPart` encoded for use in a URL.\n */\nconst encodeSliceSimulatorURLPart = (urlPart: string): string => {\n\treturn urlPart.replace(/\\//g, \"--\");\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs = {\n\tsliceMachineUIOrigin: string;\n\tlibraryID: string;\n\tsliceID: string;\n\tvariationID: string;\n\tviewport?: Viewport;\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType = {\n\tdata: Buffer;\n};\n\ntype ScreenshotsManagerUploadScreenshotArgs = {\n\tdata: Buffer;\n\tkeyPrefix?: string;\n};\n\ntype ScreenshotsManagerUploadScreenshotReturnType = {\n\turl: string;\n};\n\ntype ScreenshotsManagerDeleteScreenshotFolderArgs = {\n\tsliceID: string;\n};\n\nexport class ScreenshotsManager extends BaseManager {\n\tprivate _browserContext: BrowserContext | undefined;\n\tprivate _s3ACL: S3ACL | undefined;\n\n\tasync initBrowserContext(): Promise<void> {\n\t\t// TODO(DT-1534): Uncomment to enable Puppeteer screenshots or delete if we decide to remove Puppeteer\n\t\t//\n\t\t// if (this._browserContext) {\n\t\t// \treturn;\n\t\t// }\n\t\t//\n\t\t// let puppeteer: typeof import(\"puppeteer\");\n\t\t// try {\n\t\t// \t// Lazy-load Puppeteer only once it is needed.\n\t\t// \tpuppeteer = await import(\"puppeteer\");\n\t\t// } catch {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Screenshots require Puppeteer but Puppeteer was not found. Check that the `puppeteer` package is installed before trying again.\",\n\t\t// \t);\n\t\t// }\n\t\t// try {\n\t\t// \tconst browser = await puppeteer.launch({ headless: \"new\" });\n\t\t// \tthis._browserContext = await browser.createIncognitoBrowserContext();\n\t\t// } catch (error) {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Error launching browser. If you're using an Apple Silicon Mac, check if Rosetta is installed.\",\n\t\t// \t);\n\t\t// }\n\t}\n\n\tasync initS3ACL(): Promise<void> {\n\t\t// TODO: we need to find a way to create a new AWS ACL only when necessary (e.g., when it has expired).\n\t\t// if (this._s3ACL) {\n\t\t// \treturn;\n\t\t// }\n\n\t\tconst awsACLURL = new URL(\"create\", API_ENDPOINTS.AwsAclProvider);\n\t\tconst awsACLRes = await this._fetch({ url: awsACLURL });\n\n\t\tconst awsACLText = await awsACLRes.text();\n\t\tlet awsACLJSON: unknown;\n\t\ttry {\n\t\t\tawsACLJSON = JSON.parse(awsACLText);\n\t\t} catch (error) {\n\t\t\t// Response is not JSON\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid AWS ACL response from ${awsACLURL}: ${awsACLText}`,\n\t\t\t);\n\t\t}\n\n\t\tconst { value: awsACL, error } = decode(\n\t\t\tt.intersection([\n\t\t\t\tt.type({\n\t\t\t\t\tvalues: t.type({\n\t\t\t\t\t\turl: t.string,\n\t\t\t\t\t\tfields: t.record(t.string, t.string),\n\t\t\t\t\t}),\n\t\t\t\t\timgixEndpoint: t.string,\n\t\t\t\t}),\n\t\t\t\tt.partial({\n\t\t\t\t\tmessage: t.string,\n\t\t\t\t\tMessage: t.string,\n\t\t\t\t\terror: t.string,\n\t\t\t\t}),\n\t\t\t]),\n\t\t\tawsACLJSON,\n\t\t);\n\n\t\tif (error) {\n\t\t\tthrow new Error(`Invalid AWS ACL response from ${awsACLURL}`);\n\t\t}\n\n\t\tconst errorMessage = awsACL.error || awsACL.message || awsACL.Message;\n\t\tif (errorMessage) {\n\t\t\tthrow new Error(`Failed to create an AWS ACL: ${errorMessage}`);\n\t\t}\n\n\t\tthis._s3ACL = {\n\t\t\tuploadEndpoint: awsACL.values.url,\n\t\t\trequiredFormDataFields: awsACL.values.fields,\n\t\t\timgixEndpoint: awsACL.imgixEndpoint,\n\t\t};\n\t}\n\n\t// TODO: Abstract to a generic `captureScreenshot()` method that is\n\t// used within a Slice-specific method in SliceManager.\n\tasync captureSliceSimulatorScreenshot(\n\t\targs: ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs,\n\t): Promise<ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType> {\n\t\tassertBrowserContextInitialized(this._browserContext);\n\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\tif (!sliceMachineConfig.localSliceSimulatorURL) {\n\t\t\t// TODO: Provide a more helpful error message.\n\t\t\tthrow new Error(\n\t\t\t\t\"A local Slice Simulator URL must be configured in your Slice Machine configuration file.\",\n\t\t\t);\n\t\t}\n\n\t\tconst { model } = await this.slices.readSlice({\n\t\t\tlibraryID: args.libraryID,\n\t\t\tsliceID: args.sliceID,\n\t\t});\n\t\tif (!model) {\n\t\t\tthrow new Error(\n\t\t\t\t`Did not find a Slice in library \"${args.libraryID}\" with ID \"${args.sliceID}\".`,\n\t\t\t);\n\t\t}\n\n\t\tconst viewport = args.viewport || DEFAULT_SCREENSHOT_VIEWPORT;\n\n\t\t// TODO: Change `model.name` to `args.sliceID`?\n\t\t// Making that change would require changing the screenshot\n\t\t// page path in Slice Machine UI.\n\t\tconst url = new URL(\n\t\t\t`./${encodeSliceSimulatorURLPart(args.libraryID)}/${model.name}/${\n\t\t\t\targs.variationID\n\t\t\t}/screenshot`,\n\t\t\targs.sliceMachineUIOrigin,\n\t\t);\n\t\turl.searchParams.set(\"screenWidth\", viewport.width.toString());\n\t\turl.searchParams.set(\"screenHeight\", viewport.height.toString());\n\n\t\tconst isURLAccessible = await checkIsURLAccessible(url.toString());\n\n\t\tif (!isURLAccessible) {\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator screenshot URL is not accessible: ${url}`,\n\t\t\t);\n\t\t}\n\n\t\tconst page = await this._browserContext.newPage();\n\t\tpage.setViewport(viewport);\n\n\t\tawait page.goto(url.toString(), { waitUntil: [\"load\", \"networkidle0\"] });\n\t\tawait page.waitForSelector(SLICE_SIMULATOR_WAIT_FOR_SELECTOR, {\n\t\t\ttimeout: SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT,\n\t\t});\n\n\t\tconst element = await page.$(SLICE_SIMULATOR_SCREENSHOT_SELECTOR);\n\t\tif (!element) {\n\t\t\tconst baseURL = new URL(url.pathname, url.origin);\n\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator did not find ${SLICE_SIMULATOR_WAIT_FOR_SELECTOR} on the page. Verify the URL is correct: ${baseURL}`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = (await element.screenshot({\n\t\t\tencoding: \"binary\",\n\t\t\tclip: {\n\t\t\t\twidth: viewport.width,\n\t\t\t\theight: viewport.height,\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t},\n\t\t})) as Buffer;\n\n\t\treturn {\n\t\t\tdata,\n\t\t};\n\t}\n\n\tasync uploadScreenshot(\n\t\targs: ScreenshotsManagerUploadScreenshotArgs,\n\t): Promise<ScreenshotsManagerUploadScreenshotReturnType> {\n\t\tassertS3ACLInitialized(this._s3ACL);\n\n\t\tconst formData = new FormData();\n\n\t\tfor (const requiredFormDataFieldKey in this._s3ACL.requiredFormDataFields) {\n\t\t\tformData.append(\n\t\t\t\trequiredFormDataFieldKey,\n\t\t\t\tthis._s3ACL.requiredFormDataFields[requiredFormDataFieldKey],\n\t\t\t);\n\t\t}\n\n\t\tconst contentDigest = createContentDigest(args.data);\n\t\tconst fileType = await fileTypeFromBuffer(args.data);\n\t\tconst fileName = fileType\n\t\t\t? `${contentDigest}.${fileType.ext}`\n\t\t\t: contentDigest;\n\t\tconst key = args.keyPrefix ? `${args.keyPrefix}/${fileName}` : fileName;\n\n\t\tformData.set(\"key\", key);\n\n\t\tif (fileType) {\n\t\t\tformData.set(\"Content-Type\", fileType.mime);\n\t\t}\n\n\t\tformData.set(\"file\", new Blob([args.data], { type: fileType?.mime }));\n\n\t\tconst s3ACLEndpoint = this._s3ACL.uploadEndpoint;\n\t\tconst res = await uploadScreenshotLimit(() =>\n\t\t\tfetch(s3ACLEndpoint, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: formData,\n\t\t\t}),\n\t\t);\n\n\t\tif (res.ok) {\n\t\t\tconst url = new URL(key, this._s3ACL.imgixEndpoint);\n\t\t\turl.searchParams.set(\"auto\", \"compress,format\");\n\n\t\t\treturn {\n\t\t\t\turl: url.toString(),\n\t\t\t};\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to upload screenshot with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync deleteScreenshotFolder(\n\t\targs: ScreenshotsManagerDeleteScreenshotFolderArgs,\n\t): Promise<void> {\n\t\tconst res = await this._fetch({\n\t\t\t// We're sending `args.sliceID` as `sliceName` because it's inconsistently\n\t\t\t// named in the ACL Provider API.\n\t\t\tbody: { sliceName: args.sliceID },\n\t\t\tmethod: \"POST\",\n\t\t\turl: new URL(\"delete-folder\", API_ENDPOINTS.AwsAclProvider),\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to delete screenshot folder with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async _fetch(args: {\n\t\turl: URL;\n\t\tmethod?: \"GET\" | \"POST\";\n\t\tbody?: unknown;\n\t}): Promise<Response> {\n\t\tconst authenticationToken = await this.user.getAuthenticationToken();\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\treturn await fetch(args.url, {\n\t\t\tbody: args.body ? JSON.stringify(args.body) : undefined,\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${authenticationToken}`,\n\t\t\t\tRepository: sliceMachineConfig.repositoryName,\n\t\t\t\t\"User-Agent\": SLICE_MACHINE_USER_AGENT,\n\t\t\t\t...(args.body ? { \"Content-Type\": \"application/json\" } : {}),\n\t\t\t},\n\t\t\tmethod: args.method,\n\t\t});\n\t}\n}\n"],"names":["pLimit","BaseManager","API_ENDPOINTS","error","decode","t","checkIsURLAccessible","FormData","createContentDigest","fileTypeFromBuffer","Blob","fetch","SLICE_MACHINE_USER_AGENT"],"mappings":"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"}
@@ -7,6 +7,7 @@ var __publicField = (obj, key, value) => {
7
7
  import * as t from "io-ts";
8
8
  import "readable-web-to-node-stream";
9
9
  import { fileTypeFromBuffer } from './../../_node_modules/file-type/core.js';
10
+ import pLimit from './../../_node_modules/p-limit/index.js';
10
11
  import fetch from "../../lib/fetch.js";
11
12
  import { checkIsURLAccessible } from "../../lib/checkIsURLAccessible.js";
12
13
  import { createContentDigest } from "../../lib/createContentDigest.js";
@@ -41,6 +42,7 @@ function assertBrowserContextInitialized(browserContext) {
41
42
  throw new Error("A browser context has not been initialized. Run `SliceMachineManager.screenshots.prototype.initBrowserContext()` before re-calling this method.");
42
43
  }
43
44
  }
45
+ const uploadScreenshotLimit = pLimit(10);
44
46
  const encodeSliceSimulatorURLPart = (urlPart) => {
45
47
  return urlPart.replace(/\//g, "--");
46
48
  };
@@ -151,10 +153,11 @@ class ScreenshotsManager extends BaseManager {
151
153
  formData.set("Content-Type", fileType.mime);
152
154
  }
153
155
  formData.set("file", new Blob([args.data], { type: fileType == null ? void 0 : fileType.mime }));
154
- const res = await fetch(this._s3ACL.uploadEndpoint, {
156
+ const s3ACLEndpoint = this._s3ACL.uploadEndpoint;
157
+ const res = await uploadScreenshotLimit(() => fetch(s3ACLEndpoint, {
155
158
  method: "POST",
156
159
  body: formData
157
- });
160
+ }));
158
161
  if (res.ok) {
159
162
  const url = new URL(key, this._s3ACL.imgixEndpoint);
160
163
  url.searchParams.set("auto", "compress,format");
@@ -1 +1 @@
1
- {"version":3,"file":"ScreenshotsManager.js","sources":["../../../../src/managers/screenshots/ScreenshotsManager.ts"],"sourcesContent":["import * as t from \"io-ts\";\nimport { fileTypeFromBuffer } from \"file-type\";\nimport fetch, { FormData, Blob, Response } from \"../../lib/fetch\";\n\nimport { checkIsURLAccessible } from \"../../lib/checkIsURLAccessible\";\nimport { createContentDigest } from \"../../lib/createContentDigest\";\nimport { decode } from \"../../lib/decode\";\n\nimport { S3ACL } from \"../../types\";\nimport { SLICE_MACHINE_USER_AGENT } from \"../../constants/SLICE_MACHINE_USER_AGENT\";\nimport { API_ENDPOINTS } from \"../../constants/API_ENDPOINTS\";\n\nimport { BaseManager } from \"../BaseManager\";\n\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR = \"#__iframe-ready\";\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT = 10_000; // ms\nconst SLICE_SIMULATOR_SCREENSHOT_SELECTOR = \"#__iframe-renderer\";\n\n// TODO(DT-1534): Use Puppeteer types if we want reactive Puppeteer screenshots\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Viewport = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BrowserContext = any;\n\nconst DEFAULT_SCREENSHOT_VIEWPORT: Viewport = {\n\twidth: 1200,\n\theight: 800,\n};\n\nfunction assertS3ACLInitialized(\n\ts3ACL: S3ACL | undefined,\n): asserts s3ACL is NonNullable<typeof s3ACL> {\n\tif (s3ACL == undefined) {\n\t\tthrow new Error(\n\t\t\t\"An S3 ACL has not been initialized. Run `SliceMachineManager.screenshots.prototype.initS3ACL()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\nfunction assertBrowserContextInitialized(\n\tbrowserContext: BrowserContext | undefined,\n): asserts browserContext is NonNullable<typeof browserContext> {\n\tif (browserContext == undefined) {\n\t\tthrow new Error(\n\t\t\t\"A browser context has not been initialized. Run `SliceMachineManager.screenshots.prototype.initBrowserContext()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\n/**\n * Encodes a part of a Slice Simulator URL to ensure it can be added to a URL\n * safely.\n *\n * The encoding logic must match Slice Machine UI's URL encoding practices.\n * Today, that requires the following:\n *\n * - Replace \"/\" with \"--\" (e.g. a Slice Library ID of \"./slices\" should turn into\n * \".--slices\")\n *\n * @param urlPart - A part of the URL.\n *\n * @returns `urlPart` encoded for use in a URL.\n */\nconst encodeSliceSimulatorURLPart = (urlPart: string): string => {\n\treturn urlPart.replace(/\\//g, \"--\");\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs = {\n\tsliceMachineUIOrigin: string;\n\tlibraryID: string;\n\tsliceID: string;\n\tvariationID: string;\n\tviewport?: Viewport;\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType = {\n\tdata: Buffer;\n};\n\ntype ScreenshotsManagerUploadScreenshotArgs = {\n\tdata: Buffer;\n\tkeyPrefix?: string;\n};\n\ntype ScreenshotsManagerUploadScreenshotReturnType = {\n\turl: string;\n};\n\ntype ScreenshotsManagerDeleteScreenshotFolderArgs = {\n\tsliceID: string;\n};\n\nexport class ScreenshotsManager extends BaseManager {\n\tprivate _browserContext: BrowserContext | undefined;\n\tprivate _s3ACL: S3ACL | undefined;\n\n\tasync initBrowserContext(): Promise<void> {\n\t\t// TODO(DT-1534): Uncomment to enable Puppeteer screenshots or delete if we decide to remove Puppeteer\n\t\t//\n\t\t// if (this._browserContext) {\n\t\t// \treturn;\n\t\t// }\n\t\t//\n\t\t// let puppeteer: typeof import(\"puppeteer\");\n\t\t// try {\n\t\t// \t// Lazy-load Puppeteer only once it is needed.\n\t\t// \tpuppeteer = await import(\"puppeteer\");\n\t\t// } catch {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Screenshots require Puppeteer but Puppeteer was not found. Check that the `puppeteer` package is installed before trying again.\",\n\t\t// \t);\n\t\t// }\n\t\t// try {\n\t\t// \tconst browser = await puppeteer.launch({ headless: \"new\" });\n\t\t// \tthis._browserContext = await browser.createIncognitoBrowserContext();\n\t\t// } catch (error) {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Error launching browser. If you're using an Apple Silicon Mac, check if Rosetta is installed.\",\n\t\t// \t);\n\t\t// }\n\t}\n\n\tasync initS3ACL(): Promise<void> {\n\t\t// TODO: we need to find a way to create a new AWS ACL only when necessary (e.g., when it has expired).\n\t\t// if (this._s3ACL) {\n\t\t// \treturn;\n\t\t// }\n\n\t\tconst awsACLURL = new URL(\"create\", API_ENDPOINTS.AwsAclProvider);\n\t\tconst awsACLRes = await this._fetch({ url: awsACLURL });\n\n\t\tconst awsACLText = await awsACLRes.text();\n\t\tlet awsACLJSON: unknown;\n\t\ttry {\n\t\t\tawsACLJSON = JSON.parse(awsACLText);\n\t\t} catch (error) {\n\t\t\t// Response is not JSON\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid AWS ACL response from ${awsACLURL}: ${awsACLText}`,\n\t\t\t);\n\t\t}\n\n\t\tconst { value: awsACL, error } = decode(\n\t\t\tt.intersection([\n\t\t\t\tt.type({\n\t\t\t\t\tvalues: t.type({\n\t\t\t\t\t\turl: t.string,\n\t\t\t\t\t\tfields: t.record(t.string, t.string),\n\t\t\t\t\t}),\n\t\t\t\t\timgixEndpoint: t.string,\n\t\t\t\t}),\n\t\t\t\tt.partial({\n\t\t\t\t\tmessage: t.string,\n\t\t\t\t\tMessage: t.string,\n\t\t\t\t\terror: t.string,\n\t\t\t\t}),\n\t\t\t]),\n\t\t\tawsACLJSON,\n\t\t);\n\n\t\tif (error) {\n\t\t\tthrow new Error(`Invalid AWS ACL response from ${awsACLURL}`);\n\t\t}\n\n\t\tconst errorMessage = awsACL.error || awsACL.message || awsACL.Message;\n\t\tif (errorMessage) {\n\t\t\tthrow new Error(`Failed to create an AWS ACL: ${errorMessage}`);\n\t\t}\n\n\t\tthis._s3ACL = {\n\t\t\tuploadEndpoint: awsACL.values.url,\n\t\t\trequiredFormDataFields: awsACL.values.fields,\n\t\t\timgixEndpoint: awsACL.imgixEndpoint,\n\t\t};\n\t}\n\n\t// TODO: Abstract to a generic `captureScreenshot()` method that is\n\t// used within a Slice-specific method in SliceManager.\n\tasync captureSliceSimulatorScreenshot(\n\t\targs: ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs,\n\t): Promise<ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType> {\n\t\tassertBrowserContextInitialized(this._browserContext);\n\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\tif (!sliceMachineConfig.localSliceSimulatorURL) {\n\t\t\t// TODO: Provide a more helpful error message.\n\t\t\tthrow new Error(\n\t\t\t\t\"A local Slice Simulator URL must be configured in your Slice Machine configuration file.\",\n\t\t\t);\n\t\t}\n\n\t\tconst { model } = await this.slices.readSlice({\n\t\t\tlibraryID: args.libraryID,\n\t\t\tsliceID: args.sliceID,\n\t\t});\n\t\tif (!model) {\n\t\t\tthrow new Error(\n\t\t\t\t`Did not find a Slice in library \"${args.libraryID}\" with ID \"${args.sliceID}\".`,\n\t\t\t);\n\t\t}\n\n\t\tconst viewport = args.viewport || DEFAULT_SCREENSHOT_VIEWPORT;\n\n\t\t// TODO: Change `model.name` to `args.sliceID`?\n\t\t// Making that change would require changing the screenshot\n\t\t// page path in Slice Machine UI.\n\t\tconst url = new URL(\n\t\t\t`./${encodeSliceSimulatorURLPart(args.libraryID)}/${model.name}/${\n\t\t\t\targs.variationID\n\t\t\t}/screenshot`,\n\t\t\targs.sliceMachineUIOrigin,\n\t\t);\n\t\turl.searchParams.set(\"screenWidth\", viewport.width.toString());\n\t\turl.searchParams.set(\"screenHeight\", viewport.height.toString());\n\n\t\tconst isURLAccessible = await checkIsURLAccessible(url.toString());\n\n\t\tif (!isURLAccessible) {\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator screenshot URL is not accessible: ${url}`,\n\t\t\t);\n\t\t}\n\n\t\tconst page = await this._browserContext.newPage();\n\t\tpage.setViewport(viewport);\n\n\t\tawait page.goto(url.toString(), { waitUntil: [\"load\", \"networkidle0\"] });\n\t\tawait page.waitForSelector(SLICE_SIMULATOR_WAIT_FOR_SELECTOR, {\n\t\t\ttimeout: SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT,\n\t\t});\n\n\t\tconst element = await page.$(SLICE_SIMULATOR_SCREENSHOT_SELECTOR);\n\t\tif (!element) {\n\t\t\tconst baseURL = new URL(url.pathname, url.origin);\n\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator did not find ${SLICE_SIMULATOR_WAIT_FOR_SELECTOR} on the page. Verify the URL is correct: ${baseURL}`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = (await element.screenshot({\n\t\t\tencoding: \"binary\",\n\t\t\tclip: {\n\t\t\t\twidth: viewport.width,\n\t\t\t\theight: viewport.height,\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t},\n\t\t})) as Buffer;\n\n\t\treturn {\n\t\t\tdata,\n\t\t};\n\t}\n\n\tasync uploadScreenshot(\n\t\targs: ScreenshotsManagerUploadScreenshotArgs,\n\t): Promise<ScreenshotsManagerUploadScreenshotReturnType> {\n\t\tassertS3ACLInitialized(this._s3ACL);\n\n\t\tconst formData = new FormData();\n\n\t\tfor (const requiredFormDataFieldKey in this._s3ACL.requiredFormDataFields) {\n\t\t\tformData.append(\n\t\t\t\trequiredFormDataFieldKey,\n\t\t\t\tthis._s3ACL.requiredFormDataFields[requiredFormDataFieldKey],\n\t\t\t);\n\t\t}\n\n\t\tconst contentDigest = createContentDigest(args.data);\n\t\tconst fileType = await fileTypeFromBuffer(args.data);\n\t\tconst fileName = fileType\n\t\t\t? `${contentDigest}.${fileType.ext}`\n\t\t\t: contentDigest;\n\t\tconst key = args.keyPrefix ? `${args.keyPrefix}/${fileName}` : fileName;\n\n\t\tformData.set(\"key\", key);\n\n\t\tif (fileType) {\n\t\t\tformData.set(\"Content-Type\", fileType.mime);\n\t\t}\n\n\t\tformData.set(\"file\", new Blob([args.data], { type: fileType?.mime }));\n\n\t\tconst res = await fetch(this._s3ACL.uploadEndpoint, {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: formData,\n\t\t});\n\n\t\tif (res.ok) {\n\t\t\tconst url = new URL(key, this._s3ACL.imgixEndpoint);\n\t\t\turl.searchParams.set(\"auto\", \"compress,format\");\n\n\t\t\treturn {\n\t\t\t\turl: url.toString(),\n\t\t\t};\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to upload screenshot with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync deleteScreenshotFolder(\n\t\targs: ScreenshotsManagerDeleteScreenshotFolderArgs,\n\t): Promise<void> {\n\t\tconst res = await this._fetch({\n\t\t\t// We're sending `args.sliceID` as `sliceName` because it's inconsistently\n\t\t\t// named in the ACL Provider API.\n\t\t\tbody: { sliceName: args.sliceID },\n\t\t\tmethod: \"POST\",\n\t\t\turl: new URL(\"delete-folder\", API_ENDPOINTS.AwsAclProvider),\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to delete screenshot folder with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async _fetch(args: {\n\t\turl: URL;\n\t\tmethod?: \"GET\" | \"POST\";\n\t\tbody?: unknown;\n\t}): Promise<Response> {\n\t\tconst authenticationToken = await this.user.getAuthenticationToken();\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\treturn await fetch(args.url, {\n\t\t\tbody: args.body ? JSON.stringify(args.body) : undefined,\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${authenticationToken}`,\n\t\t\t\tRepository: sliceMachineConfig.repositoryName,\n\t\t\t\t\"User-Agent\": SLICE_MACHINE_USER_AGENT,\n\t\t\t\t...(args.body ? { \"Content-Type\": \"application/json\" } : {}),\n\t\t\t},\n\t\t\tmethod: args.method,\n\t\t});\n\t}\n}\n"],"names":["error"],"mappings":"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"}
1
+ {"version":3,"file":"ScreenshotsManager.js","sources":["../../../../src/managers/screenshots/ScreenshotsManager.ts"],"sourcesContent":["import * as t from \"io-ts\";\nimport { fileTypeFromBuffer } from \"file-type\";\nimport pLimit from \"p-limit\";\nimport fetch, { FormData, Blob, Response } from \"../../lib/fetch\";\n\nimport { checkIsURLAccessible } from \"../../lib/checkIsURLAccessible\";\nimport { createContentDigest } from \"../../lib/createContentDigest\";\nimport { decode } from \"../../lib/decode\";\n\nimport { S3ACL } from \"../../types\";\nimport { SLICE_MACHINE_USER_AGENT } from \"../../constants/SLICE_MACHINE_USER_AGENT\";\nimport { API_ENDPOINTS } from \"../../constants/API_ENDPOINTS\";\n\nimport { BaseManager } from \"../BaseManager\";\n\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR = \"#__iframe-ready\";\nconst SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT = 10_000; // ms\nconst SLICE_SIMULATOR_SCREENSHOT_SELECTOR = \"#__iframe-renderer\";\n\n// TODO(DT-1534): Use Puppeteer types if we want reactive Puppeteer screenshots\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Viewport = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BrowserContext = any;\n\nconst DEFAULT_SCREENSHOT_VIEWPORT: Viewport = {\n\twidth: 1200,\n\theight: 800,\n};\n\nfunction assertS3ACLInitialized(\n\ts3ACL: S3ACL | undefined,\n): asserts s3ACL is NonNullable<typeof s3ACL> {\n\tif (s3ACL == undefined) {\n\t\tthrow new Error(\n\t\t\t\"An S3 ACL has not been initialized. Run `SliceMachineManager.screenshots.prototype.initS3ACL()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\nfunction assertBrowserContextInitialized(\n\tbrowserContext: BrowserContext | undefined,\n): asserts browserContext is NonNullable<typeof browserContext> {\n\tif (browserContext == undefined) {\n\t\tthrow new Error(\n\t\t\t\"A browser context has not been initialized. Run `SliceMachineManager.screenshots.prototype.initBrowserContext()` before re-calling this method.\",\n\t\t);\n\t}\n}\n\nconst uploadScreenshotLimit = pLimit(10);\n\n/**\n * Encodes a part of a Slice Simulator URL to ensure it can be added to a URL\n * safely.\n *\n * The encoding logic must match Slice Machine UI's URL encoding practices.\n * Today, that requires the following:\n *\n * - Replace \"/\" with \"--\" (e.g. a Slice Library ID of \"./slices\" should turn into\n * \".--slices\")\n *\n * @param urlPart - A part of the URL.\n *\n * @returns `urlPart` encoded for use in a URL.\n */\nconst encodeSliceSimulatorURLPart = (urlPart: string): string => {\n\treturn urlPart.replace(/\\//g, \"--\");\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs = {\n\tsliceMachineUIOrigin: string;\n\tlibraryID: string;\n\tsliceID: string;\n\tvariationID: string;\n\tviewport?: Viewport;\n};\n\ntype ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType = {\n\tdata: Buffer;\n};\n\ntype ScreenshotsManagerUploadScreenshotArgs = {\n\tdata: Buffer;\n\tkeyPrefix?: string;\n};\n\ntype ScreenshotsManagerUploadScreenshotReturnType = {\n\turl: string;\n};\n\ntype ScreenshotsManagerDeleteScreenshotFolderArgs = {\n\tsliceID: string;\n};\n\nexport class ScreenshotsManager extends BaseManager {\n\tprivate _browserContext: BrowserContext | undefined;\n\tprivate _s3ACL: S3ACL | undefined;\n\n\tasync initBrowserContext(): Promise<void> {\n\t\t// TODO(DT-1534): Uncomment to enable Puppeteer screenshots or delete if we decide to remove Puppeteer\n\t\t//\n\t\t// if (this._browserContext) {\n\t\t// \treturn;\n\t\t// }\n\t\t//\n\t\t// let puppeteer: typeof import(\"puppeteer\");\n\t\t// try {\n\t\t// \t// Lazy-load Puppeteer only once it is needed.\n\t\t// \tpuppeteer = await import(\"puppeteer\");\n\t\t// } catch {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Screenshots require Puppeteer but Puppeteer was not found. Check that the `puppeteer` package is installed before trying again.\",\n\t\t// \t);\n\t\t// }\n\t\t// try {\n\t\t// \tconst browser = await puppeteer.launch({ headless: \"new\" });\n\t\t// \tthis._browserContext = await browser.createIncognitoBrowserContext();\n\t\t// } catch (error) {\n\t\t// \tthrow new InternalError(\n\t\t// \t\t\"Error launching browser. If you're using an Apple Silicon Mac, check if Rosetta is installed.\",\n\t\t// \t);\n\t\t// }\n\t}\n\n\tasync initS3ACL(): Promise<void> {\n\t\t// TODO: we need to find a way to create a new AWS ACL only when necessary (e.g., when it has expired).\n\t\t// if (this._s3ACL) {\n\t\t// \treturn;\n\t\t// }\n\n\t\tconst awsACLURL = new URL(\"create\", API_ENDPOINTS.AwsAclProvider);\n\t\tconst awsACLRes = await this._fetch({ url: awsACLURL });\n\n\t\tconst awsACLText = await awsACLRes.text();\n\t\tlet awsACLJSON: unknown;\n\t\ttry {\n\t\t\tawsACLJSON = JSON.parse(awsACLText);\n\t\t} catch (error) {\n\t\t\t// Response is not JSON\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid AWS ACL response from ${awsACLURL}: ${awsACLText}`,\n\t\t\t);\n\t\t}\n\n\t\tconst { value: awsACL, error } = decode(\n\t\t\tt.intersection([\n\t\t\t\tt.type({\n\t\t\t\t\tvalues: t.type({\n\t\t\t\t\t\turl: t.string,\n\t\t\t\t\t\tfields: t.record(t.string, t.string),\n\t\t\t\t\t}),\n\t\t\t\t\timgixEndpoint: t.string,\n\t\t\t\t}),\n\t\t\t\tt.partial({\n\t\t\t\t\tmessage: t.string,\n\t\t\t\t\tMessage: t.string,\n\t\t\t\t\terror: t.string,\n\t\t\t\t}),\n\t\t\t]),\n\t\t\tawsACLJSON,\n\t\t);\n\n\t\tif (error) {\n\t\t\tthrow new Error(`Invalid AWS ACL response from ${awsACLURL}`);\n\t\t}\n\n\t\tconst errorMessage = awsACL.error || awsACL.message || awsACL.Message;\n\t\tif (errorMessage) {\n\t\t\tthrow new Error(`Failed to create an AWS ACL: ${errorMessage}`);\n\t\t}\n\n\t\tthis._s3ACL = {\n\t\t\tuploadEndpoint: awsACL.values.url,\n\t\t\trequiredFormDataFields: awsACL.values.fields,\n\t\t\timgixEndpoint: awsACL.imgixEndpoint,\n\t\t};\n\t}\n\n\t// TODO: Abstract to a generic `captureScreenshot()` method that is\n\t// used within a Slice-specific method in SliceManager.\n\tasync captureSliceSimulatorScreenshot(\n\t\targs: ScreenshotsManagerCaptureSliceSimulatorScreenshotArgs,\n\t): Promise<ScreenshotsManagerCaptureSliceSimulatorScreenshotReturnType> {\n\t\tassertBrowserContextInitialized(this._browserContext);\n\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\tif (!sliceMachineConfig.localSliceSimulatorURL) {\n\t\t\t// TODO: Provide a more helpful error message.\n\t\t\tthrow new Error(\n\t\t\t\t\"A local Slice Simulator URL must be configured in your Slice Machine configuration file.\",\n\t\t\t);\n\t\t}\n\n\t\tconst { model } = await this.slices.readSlice({\n\t\t\tlibraryID: args.libraryID,\n\t\t\tsliceID: args.sliceID,\n\t\t});\n\t\tif (!model) {\n\t\t\tthrow new Error(\n\t\t\t\t`Did not find a Slice in library \"${args.libraryID}\" with ID \"${args.sliceID}\".`,\n\t\t\t);\n\t\t}\n\n\t\tconst viewport = args.viewport || DEFAULT_SCREENSHOT_VIEWPORT;\n\n\t\t// TODO: Change `model.name` to `args.sliceID`?\n\t\t// Making that change would require changing the screenshot\n\t\t// page path in Slice Machine UI.\n\t\tconst url = new URL(\n\t\t\t`./${encodeSliceSimulatorURLPart(args.libraryID)}/${model.name}/${\n\t\t\t\targs.variationID\n\t\t\t}/screenshot`,\n\t\t\targs.sliceMachineUIOrigin,\n\t\t);\n\t\turl.searchParams.set(\"screenWidth\", viewport.width.toString());\n\t\turl.searchParams.set(\"screenHeight\", viewport.height.toString());\n\n\t\tconst isURLAccessible = await checkIsURLAccessible(url.toString());\n\n\t\tif (!isURLAccessible) {\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator screenshot URL is not accessible: ${url}`,\n\t\t\t);\n\t\t}\n\n\t\tconst page = await this._browserContext.newPage();\n\t\tpage.setViewport(viewport);\n\n\t\tawait page.goto(url.toString(), { waitUntil: [\"load\", \"networkidle0\"] });\n\t\tawait page.waitForSelector(SLICE_SIMULATOR_WAIT_FOR_SELECTOR, {\n\t\t\ttimeout: SLICE_SIMULATOR_WAIT_FOR_SELECTOR_TIMEOUT,\n\t\t});\n\n\t\tconst element = await page.$(SLICE_SIMULATOR_SCREENSHOT_SELECTOR);\n\t\tif (!element) {\n\t\t\tconst baseURL = new URL(url.pathname, url.origin);\n\n\t\t\tthrow new Error(\n\t\t\t\t`Slice Simulator did not find ${SLICE_SIMULATOR_WAIT_FOR_SELECTOR} on the page. Verify the URL is correct: ${baseURL}`,\n\t\t\t);\n\t\t}\n\n\t\tconst data = (await element.screenshot({\n\t\t\tencoding: \"binary\",\n\t\t\tclip: {\n\t\t\t\twidth: viewport.width,\n\t\t\t\theight: viewport.height,\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t},\n\t\t})) as Buffer;\n\n\t\treturn {\n\t\t\tdata,\n\t\t};\n\t}\n\n\tasync uploadScreenshot(\n\t\targs: ScreenshotsManagerUploadScreenshotArgs,\n\t): Promise<ScreenshotsManagerUploadScreenshotReturnType> {\n\t\tassertS3ACLInitialized(this._s3ACL);\n\n\t\tconst formData = new FormData();\n\n\t\tfor (const requiredFormDataFieldKey in this._s3ACL.requiredFormDataFields) {\n\t\t\tformData.append(\n\t\t\t\trequiredFormDataFieldKey,\n\t\t\t\tthis._s3ACL.requiredFormDataFields[requiredFormDataFieldKey],\n\t\t\t);\n\t\t}\n\n\t\tconst contentDigest = createContentDigest(args.data);\n\t\tconst fileType = await fileTypeFromBuffer(args.data);\n\t\tconst fileName = fileType\n\t\t\t? `${contentDigest}.${fileType.ext}`\n\t\t\t: contentDigest;\n\t\tconst key = args.keyPrefix ? `${args.keyPrefix}/${fileName}` : fileName;\n\n\t\tformData.set(\"key\", key);\n\n\t\tif (fileType) {\n\t\t\tformData.set(\"Content-Type\", fileType.mime);\n\t\t}\n\n\t\tformData.set(\"file\", new Blob([args.data], { type: fileType?.mime }));\n\n\t\tconst s3ACLEndpoint = this._s3ACL.uploadEndpoint;\n\t\tconst res = await uploadScreenshotLimit(() =>\n\t\t\tfetch(s3ACLEndpoint, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: formData,\n\t\t\t}),\n\t\t);\n\n\t\tif (res.ok) {\n\t\t\tconst url = new URL(key, this._s3ACL.imgixEndpoint);\n\t\t\turl.searchParams.set(\"auto\", \"compress,format\");\n\n\t\t\treturn {\n\t\t\t\turl: url.toString(),\n\t\t\t};\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to upload screenshot with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync deleteScreenshotFolder(\n\t\targs: ScreenshotsManagerDeleteScreenshotFolderArgs,\n\t): Promise<void> {\n\t\tconst res = await this._fetch({\n\t\t\t// We're sending `args.sliceID` as `sliceName` because it's inconsistently\n\t\t\t// named in the ACL Provider API.\n\t\t\tbody: { sliceName: args.sliceID },\n\t\t\tmethod: \"POST\",\n\t\t\turl: new URL(\"delete-folder\", API_ENDPOINTS.AwsAclProvider),\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Unable to delete screenshot folder with status code: ${res.status}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async _fetch(args: {\n\t\turl: URL;\n\t\tmethod?: \"GET\" | \"POST\";\n\t\tbody?: unknown;\n\t}): Promise<Response> {\n\t\tconst authenticationToken = await this.user.getAuthenticationToken();\n\t\tconst sliceMachineConfig = await this.project.getSliceMachineConfig();\n\n\t\treturn await fetch(args.url, {\n\t\t\tbody: args.body ? JSON.stringify(args.body) : undefined,\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${authenticationToken}`,\n\t\t\t\tRepository: sliceMachineConfig.repositoryName,\n\t\t\t\t\"User-Agent\": SLICE_MACHINE_USER_AGENT,\n\t\t\t\t...(args.body ? { \"Content-Type\": \"application/json\" } : {}),\n\t\t\t},\n\t\t\tmethod: args.method,\n\t\t});\n\t}\n}\n"],"names":["error"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slicemachine/manager",
3
- "version": "0.15.1-dev-next-release.12",
3
+ "version": "0.15.1-dev-next-release.14",
4
4
  "description": "Manage all aspects of a Slice Machine project.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -68,7 +68,7 @@
68
68
  "@prismicio/custom-types-client": "^1.2.0-alpha.0",
69
69
  "@prismicio/mocks": "^2.0.0-alpha.2",
70
70
  "@prismicio/types-internal": "^2.2.0",
71
- "@slicemachine/plugin-kit": "^0.4.22-dev-next-release.12",
71
+ "@slicemachine/plugin-kit": "^0.4.22-dev-next-release.14",
72
72
  "@wooorm/starry-night": "^1.6.0",
73
73
  "analytics-node": "^6.2.0",
74
74
  "cookie": "^0.5.0",
@@ -134,5 +134,5 @@
134
134
  "engines": {
135
135
  "node": ">=14.15.0"
136
136
  },
137
- "gitHead": "3552f448630cec02a298f75d2be91e4c9c9ad18a"
137
+ "gitHead": "7258dc043e802a1e79e346c5a7f708a2fb3ec202"
138
138
  }
@@ -1,5 +1,6 @@
1
1
  import * as t from "io-ts";
2
2
  import { fileTypeFromBuffer } from "file-type";
3
+ import pLimit from "p-limit";
3
4
  import fetch, { FormData, Blob, Response } from "../../lib/fetch";
4
5
 
5
6
  import { checkIsURLAccessible } from "../../lib/checkIsURLAccessible";
@@ -47,6 +48,8 @@ function assertBrowserContextInitialized(
47
48
  }
48
49
  }
49
50
 
51
+ const uploadScreenshotLimit = pLimit(10);
52
+
50
53
  /**
51
54
  * Encodes a part of a Slice Simulator URL to ensure it can be added to a URL
52
55
  * safely.
@@ -283,10 +286,13 @@ export class ScreenshotsManager extends BaseManager {
283
286
 
284
287
  formData.set("file", new Blob([args.data], { type: fileType?.mime }));
285
288
 
286
- const res = await fetch(this._s3ACL.uploadEndpoint, {
287
- method: "POST",
288
- body: formData,
289
- });
289
+ const s3ACLEndpoint = this._s3ACL.uploadEndpoint;
290
+ const res = await uploadScreenshotLimit(() =>
291
+ fetch(s3ACLEndpoint, {
292
+ method: "POST",
293
+ body: formData,
294
+ }),
295
+ );
290
296
 
291
297
  if (res.ok) {
292
298
  const url = new URL(key, this._s3ACL.imgixEndpoint);