diff --git a/index.html b/index.html
index 31ba85ee4..6a26fc563 100644
--- a/index.html
+++ b/index.html
@@ -26,13 +26,20 @@
+
+
diff --git a/resources/benchmark-configurator.mjs b/resources/benchmark-configurator.mjs
index caac91775..531f5fd03 100644
--- a/resources/benchmark-configurator.mjs
+++ b/resources/benchmark-configurator.mjs
@@ -56,6 +56,8 @@ export class BenchmarkConfigurator {
this.#suites.forEach((suite) => {
if (!suite.tags)
suite.tags = [];
+ if (!("measurePrepare" in suite))
+ suite.measurePrepare = false;
if (suite.url.startsWith("experimental/"))
suite.tags.unshift("all", "experimental");
else
@@ -115,8 +117,10 @@ export class BenchmarkConfigurator {
}
_loadSuite(suite) {
- suite.tags.forEach((tag) => this.#tags.add(tag));
- this.#suites.push(suite);
+ if (suite.tags)
+ suite.tags.forEach((tag) => this.#tags.add(tag));
+
+ this.#suites.push({ ...suite, tags: suite.tags ? [...suite.tags] : [] });
}
enableSuites(names, tags) {
diff --git a/resources/developer-mode.mjs b/resources/developer-mode.mjs
index fdd254775..5dd02eca6 100644
--- a/resources/developer-mode.mjs
+++ b/resources/developer-mode.mjs
@@ -26,6 +26,7 @@ export function createDeveloperModeContainer() {
settings.append(createUIForSyncStepDelay());
settings.append(createUIForAsyncSteps());
settings.append(createUIForLayoutMode());
+ settings.append(createUIForPreload());
content.append(document.createElement("hr"));
content.append(settings);
@@ -50,6 +51,12 @@ function createUIForWarmupSuite() {
});
}
+function createUIForPreload() {
+ return createCheckboxUI("Use service worker for resource preloading", params.preload, (isChecked) => {
+ params.preload = isChecked;
+ });
+}
+
function createUIForMeasurePrepare() {
return createCheckboxUI("Measure Prepare", params.measurePrepare, (isChecked) => {
params.measurePrepare = isChecked;
diff --git a/resources/main.css b/resources/main.css
index 86c8207ed..674ce4ecc 100644
--- a/resources/main.css
+++ b/resources/main.css
@@ -419,7 +419,17 @@ button.show-about {
display: none;
}
-#progress {
+#preload-progress {
+ display: none;
+}
+
+:root[data-benchmark-state="PRELOADING"] #preload-progress,
+:root[data-benchmark-state="PRELOADING"] #preload-info {
+ display: block;
+}
+
+#progress,
+#preload-progress {
position: absolute;
bottom: -6px;
left: 60px;
@@ -429,7 +439,8 @@ button.show-about {
border-right: 6px solid var(--background);
}
-#progress-completed {
+#progress-completed,
+#preload-progress-completed {
position: absolute;
top: 0;
left: 0;
@@ -440,11 +451,13 @@ button.show-about {
background-color: var(--inactive-color);
}
-#progress-completed::-webkit-progress-value {
+#progress-completed::-webkit-progress-value,
+#preload-progress-completed::-webkit-progress-value {
background-color: var(--foreground);
}
-#progress-completed::-moz-progress-bar {
+#progress-completed::-moz-progress-bar,
+#preload-progress-completed::-moz-progress-bar {
background-color: var(--foreground);
}
@@ -455,7 +468,9 @@ button.show-about {
background-color: var(--background);
}
-#info {
+#info,
+#preload-info {
+ display: none;
position: absolute;
bottom: -25px;
left: 60px;
@@ -465,11 +480,13 @@ button.show-about {
text-align: center;
font-size: 12px;
}
-#info-label {
+#info-label,
+#preload-info-label {
position: absolute;
left: 6px;
}
-#info-progress {
+#info-progress,
+#preload-info-progress {
position: absolute;
right: 6px;
text-align: right;
@@ -945,3 +962,45 @@ section#about .note {
color: #fff;
stroke: #fff;
}
+
+#preload-progress,
+#preload-info {
+ display: none;
+}
+
+[data-benchmark-state="PRELOADING"] {
+ cursor: wait;
+}
+
+[data-benchmark-state="PRELOADING"] #preload-progress,
+[data-benchmark-state="PRELOADING"] #preload-info {
+ display: block;
+}
+
+[data-benchmark-state="RUNNING"] #info {
+ display: block;
+}
+
+[data-benchmark-state="PRELOADING"] .start-tests-button {
+ color: var(--foreground);
+ background-image: linear-gradient(-45deg, var(--foreground) 3px, transparent 3px, transparent 50%, var(--foreground) 50%, var(--foreground) calc(50% + 3px), transparent calc(50% + 3px), transparent 100%);
+ background-size: 20px 20px;
+ animation: barber-pole 1s linear infinite;
+ pointer-events: none;
+}
+
+[data-benchmark-state="PRELOADING"] .start-tests-button > div {
+ background-color: var(--background);
+ border-radius: 10px;
+ padding: 0 0.3em;
+ display: inline-block;
+}
+
+@keyframes barber-pole {
+ 0% {
+ background-position: 0 0, 0 0;
+ }
+ 100% {
+ background-position: 20px 0, 0 0;
+ }
+}
diff --git a/resources/main.mjs b/resources/main.mjs
index 67b1c2f7c..52382f49c 100644
--- a/resources/main.mjs
+++ b/resources/main.mjs
@@ -1,9 +1,142 @@
import { BenchmarkRunner } from "./benchmark-runner.mjs";
import * as Statistics from "./statistics.mjs";
+import { SW_MESSAGES } from "./shared/sw-messages.mjs";
import { renderMetricView } from "./metric-ui.mjs";
import { defaultParams, params } from "./shared/params.mjs";
import { createDeveloperModeContainer } from "./developer-mode.mjs";
+const PRELOAD_TIMEOUT_MS = 60_000;
+
+export class ResourcePreloader {
+ constructor() {
+ this._registration = null;
+ this._sw = null;
+ this._preloadParams = "";
+ }
+
+ isCached() {
+ return params.toSearchParams() === this._preloadParams;
+ }
+
+ async setup() {
+ await this._unregisterOldServiceWorkers();
+
+ if (!params.preload) {
+ this._registration = null;
+ this._sw = null;
+ return false;
+ }
+
+ await this._registerServiceWorker();
+ this._setupMessageListener();
+
+ return true;
+ }
+
+ async _unregisterOldServiceWorkers() {
+ const existingRegistrations = await navigator.serviceWorker.getRegistrations();
+ for (const existing of existingRegistrations)
+ await existing.unregister();
+ }
+
+ async _registerServiceWorker() {
+ this._registration = await navigator.serviceWorker.register("./sw.mjs", { type: "module" });
+ await navigator.serviceWorker.ready;
+ this._sw = navigator.serviceWorker.controller || this._registration.active;
+ }
+
+ _setupMessageListener() {
+ navigator.serviceWorker.addEventListener("message", (event) => {
+ if (event.data?.type === SW_MESSAGES.FATAL_ERROR)
+ globalThis.benchmarkClient?.handleError(new Error(event.data.message));
+ });
+ }
+
+ _sendMessageWithReply(messageData, onProgress, timeoutMs = 0) {
+ if (!this._sw)
+ return Promise.resolve();
+
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ const port = channel.port1;
+ let timeoutId = null;
+
+ const cleanup = () => {
+ port.onmessage = null;
+ if (timeoutId)
+ clearTimeout(timeoutId);
+ };
+
+ if (timeoutMs > 0) {
+ timeoutId = setTimeout(() => {
+ cleanup();
+ console.error(`Service worker message timed out: ${messageData.type}`);
+ resolve(); // Resolve to not block execution
+ }, timeoutMs);
+ }
+
+ port.onmessage = (event) => {
+ const data = event.data || {};
+ if (data.type === SW_MESSAGES.PRELOAD_PROGRESS) {
+ onProgress?.(data);
+ return;
+ }
+ cleanup();
+ if (data.type === "ERROR") {
+ reject(new Error(data.message || "Unknown SW Error"));
+ return;
+ }
+
+ resolve(data);
+ };
+
+ this._sw.postMessage(messageData, [channel.port2]);
+ });
+ }
+
+ async clearSw() {
+ if (!this._sw)
+ return;
+ await this._sendMessageWithReply({ type: SW_MESSAGES.CLEAR_CACHE });
+ }
+
+ async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) {
+ if (!this._sw || suites.length === 0)
+ return;
+
+ const suitesData = suites
+ .filter((s) => s.resources)
+ .map((s) => ({
+ name: s.name,
+ url: new URL(s.url, window.location.href).href,
+ resources: new URL(s.resources, window.location.href).href,
+ }));
+
+ if (suitesData.length === 0)
+ return;
+
+ const startTime = performance.now();
+ const response = await this._sendMessageWithReply({ type: SW_MESSAGES.PRELOAD_SUITES, suites: suitesData, delay: resourceLoadDelay, clearCache }, onProgress, PRELOAD_TIMEOUT_MS);
+
+ if (response?.type === SW_MESSAGES.PRELOAD_DONE) {
+ const timeTakenMs = performance.now() - startTime;
+ const sizeMB = (response.transferredSize / (1024 * 1024)).toFixed(2);
+ const timeSec = (timeTakenMs / 1000).toFixed(2);
+ console.log(`Preloaded ${response.count} files (${sizeMB} MB transferred) in ${timeSec}s`);
+ }
+ this._preloadParams = params.toSearchParams();
+ }
+}
+
+const BENCHMARK_STATE = Object.freeze({
+ IDLE: "IDLE",
+ PRELOADING: "PRELOADING",
+ READY: "READY",
+ RUNNING: "RUNNING",
+ DONE: "DONE",
+ ERROR: "ERROR",
+});
+
// FIXME(camillobruni): Add base class
class MainBenchmarkClient {
developerMode = false;
@@ -14,8 +147,8 @@ class MainBenchmarkClient {
_progressCompleted = null;
_isRunning = false;
_hasResults = false;
- _developerModeContainer = null;
_metrics = Object.create(null);
+ _resourcePreloader = new ResourcePreloader();
_steppingPromise = null;
_steppingResolver = null;
_benchmarkConfiguratorPromise = null;
@@ -31,21 +164,21 @@ class MainBenchmarkClient {
});
}
- start() {
+ async start() {
if (this._isStepping())
this._clearStepping();
- else if (this._startBenchmark())
+ else if (await this._startBenchmark())
this._showSection("#running");
}
- step() {
+ async step() {
const currentSteppingResolver = this._steppingResolver;
this._steppingPromise = new Promise((resolve) => {
this._steppingResolver = resolve;
});
currentSteppingResolver?.();
if (!this._isRunning) {
- this._startBenchmark();
+ await this._startBenchmark();
this._showSection("#running");
}
}
@@ -88,6 +221,16 @@ class MainBenchmarkClient {
}
if (!this._isStepping())
this._developerModeContainer?.remove();
+
+ await this._setupResourcePreloader(benchmarkConfigurator);
+
+ try {
+ await this._setBenchmarkState(BENCHMARK_STATE.RUNNING);
+ } catch (error) {
+ alert(error.message);
+ return false;
+ }
+
this._progressCompleted = document.getElementById("progress-completed");
if (params.iterationCount < 50) {
const progressNode = document.getElementById("progress");
@@ -147,6 +290,7 @@ class MainBenchmarkClient {
this._isRunning = false;
this._hasResults = true;
this._metrics = metrics;
+ this._setBenchmarkState(BENCHMARK_STATE.DONE);
const scoreResults = this._computeResults(this._measuredValuesList, "score");
if (scoreResults.isValid)
@@ -167,7 +311,8 @@ class MainBenchmarkClient {
this._isRunning = false;
this._hasResults = true;
this._metrics = Object.create(null);
- this._populateInvalidScore();
+ this._setBenchmarkState(BENCHMARK_STATE.ERROR);
+ this._populateInvalidScore(error.message);
this.showResultsSummary();
throw error;
}
@@ -181,10 +326,20 @@ class MainBenchmarkClient {
document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`;
}
- _populateInvalidScore() {
+ _populateInvalidScore(errorText) {
document.getElementById("summary").className = "invalid";
document.getElementById("result-number").textContent = "Error";
document.getElementById("confidence-number").textContent = "";
+
+ const invalidScoreText = document.getElementById("invalid-score-text");
+ if (errorText) {
+ invalidScoreText.textContent = errorText;
+ } else {
+ invalidScoreText.innerHTML = `
+ One or more subtests produced no duration.
+ Please check your browser settings and re-run the benchmark.
+ `;
+ }
}
_computeResults(measuredValuesList, displayUnit) {
@@ -289,15 +444,15 @@ class MainBenchmarkClient {
document.getElementById("metrics-results").innerHTML = html;
const filePrefix = `speedometer-3-${new Date().toISOString()}`;
- let jsonData = this._formattedJSONResult({ modern: false });
- let jsonLink = document.getElementById("download-classic-json");
- jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
- jsonLink.setAttribute("download", `${filePrefix}.json`);
+ const classicJsonData = this._formattedJSONResult({ modern: false });
+ const classicJsonLink = document.getElementById("download-classic-json");
+ classicJsonLink.href = URL.createObjectURL(new Blob([classicJsonData], { type: "application/json" }));
+ classicJsonLink.setAttribute("download", `${filePrefix}.json`);
- jsonLink = document.getElementById("download-full-json");
- jsonData = this._formattedJSONResult({ modern: true });
- jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
- jsonLink.setAttribute("download", `${filePrefix}.json`);
+ const fullJsonLink = document.getElementById("download-full-json");
+ const fullJsonData = this._formattedJSONResult({ modern: true });
+ fullJsonLink.href = URL.createObjectURL(new Blob([fullJsonData], { type: "application/json" }));
+ fullJsonLink.setAttribute("download", `${filePrefix}.json`);
const csvData = this._formattedCSVResult();
const csvLink = document.getElementById("download-csv");
@@ -344,6 +499,7 @@ class MainBenchmarkClient {
document.getElementById("copy-csv").onclick = this.copyCSVResults.bind(this);
document.querySelectorAll(".start-tests-button").forEach((button) => {
button.onclick = this._startBenchmarkHandler.bind(this);
+ button.disabled = true;
});
}
@@ -358,10 +514,72 @@ class MainBenchmarkClient {
document.body.append(this._developerModeContainer);
}
+ await this._setupResourcePreloader(benchmarkConfigurator);
+
if (params.startAutomatically)
this.start();
}
+ async _setupResourcePreloader(benchmarkConfigurator) {
+ await this._resourcePreloader.setup();
+ if (!this._resourcePreloader.isCached())
+ await this._cacheResources(benchmarkConfigurator);
+ }
+
+ async _cacheResources(benchmarkConfigurator) {
+ const enabledSuites = benchmarkConfigurator.suites.filter((suite) => suite.enabled);
+ const clearCache = !params.isDefault();
+ this._setBenchmarkState(BENCHMARK_STATE.PRELOADING);
+
+ try {
+ await this._resourcePreloader.preloadSuites(enabledSuites, params.resourceLoadDelay, clearCache, this._updateCacheProgress.bind(this));
+ this._enableStartButtons();
+ } catch (error) {
+ console.error("Service Worker preload failed:", error);
+ this._setBenchmarkState(BENCHMARK_STATE.ERROR);
+ this._populateInvalidScore(error?.message);
+ this.showResultsSummary();
+ throw error;
+ }
+ }
+
+ _updateCacheProgress(progressData) {
+ const { loaded, total, url, suiteName } = progressData;
+ document.body.style.setProperty("--preload-progress", `${total > 0 ? (loaded / total) * 100 : 100}%`);
+ const progress = document.getElementById("preload-progress-completed");
+ progress.max = total;
+ progress.value = loaded;
+ const filename = url ? url.substring(url.lastIndexOf("/") + 1) : "";
+ const labelText = suiteName ? `${suiteName}: ${filename}` : filename;
+ document.getElementById("preload-info-label").textContent = labelText;
+ document.getElementById("preload-info-progress").textContent = `${loaded} / ${total}`;
+ }
+
+ _enableStartButtons() {
+ this._setBenchmarkState(BENCHMARK_STATE.READY);
+ document.querySelectorAll(".start-tests-button").forEach((button) => {
+ button.disabled = false;
+ });
+ }
+
+ async _setBenchmarkState(state) {
+ document.body.setAttribute("data-benchmark-state", state);
+
+ const startButton = document.querySelector(".start-tests-button");
+ if (state === BENCHMARK_STATE.PRELOADING) {
+ document.getElementById("preload-progress-completed").value = 0;
+ document.getElementById("preload-info-label").textContent = "";
+ document.getElementById("preload-info-progress").textContent = "";
+ document.body.style.setProperty("--preload-progress", "0%");
+ startButton.innerHTML = "Preloading
";
+ } else if (state !== BENCHMARK_STATE.RUNNING) {
+ document.body.style.removeProperty("--preload-progress");
+ startButton.innerHTML = "Start Test
";
+ if (state !== BENCHMARK_STATE.READY)
+ await this._resourcePreloader?.clearSw();
+ }
+ }
+
_hashChangeHandler() {
this._showSection(window.location.hash);
}
diff --git a/resources/shared/generate-resources.mjs b/resources/shared/generate-resources.mjs
new file mode 100644
index 000000000..8fb54f656
--- /dev/null
+++ b/resources/shared/generate-resources.mjs
@@ -0,0 +1,26 @@
+import fs from "fs";
+import path from "path";
+
+function walkDir(dir, fileList = []) {
+ const files = fs.readdirSync(dir);
+ for (const file of files) {
+ const filePath = path.join(dir, file);
+ if (fs.statSync(filePath).isDirectory())
+ walkDir(filePath, fileList);
+ else
+ fileList.push(filePath);
+ }
+ return fileList;
+}
+
+export function generateResourcesFile(distPath) {
+ if (!fs.existsSync(distPath)) {
+ console.warn(`Directory ${distPath} does not exist, skipping resources.txt generation.`);
+ return;
+ }
+ const absoluteDist = path.resolve(distPath);
+ const files = walkDir(absoluteDist);
+ const relativePaths = files.map((f) => path.relative(absoluteDist, f)).filter((f) => f !== "resources.txt");
+ fs.writeFileSync(path.join(absoluteDist, "resources.txt"), `${relativePaths.join("\n")}\n`, "utf8");
+ console.log(`Generated resources.txt at ${distPath}`);
+}
diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs
index d8830b0d6..208dcb335 100644
--- a/resources/shared/params.mjs
+++ b/resources/shared/params.mjs
@@ -35,6 +35,10 @@ export class Params {
measurePrepare = false;
// External config url to override internal tests.
config = "";
+ // Resource load delay in ms for the service worker pre-caching.
+ resourceLoadDelay = 0;
+ // Use service worker for resource preloading.
+ preload = false;
constructor(searchParams = undefined, warnUnused = false) {
if (searchParams)
@@ -68,6 +72,8 @@ export class Params {
this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES);
this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare");
this.config = this._parseConfig(searchParams);
+ this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0);
+ this.preload = this._parseBooleanParam(searchParams, "preload");
if (warnUnused) {
const unused = Array.from(searchParams.keys());
@@ -205,6 +211,10 @@ export class Params {
toSearchParams() {
return this.toSearchParamsObject().toString();
}
+
+ isDefault() {
+ return this === defaultParams;
+ }
}
function isValidJsonUrl(url) {
diff --git a/resources/shared/sw-messages.mjs b/resources/shared/sw-messages.mjs
new file mode 100644
index 000000000..c07c3b7ad
--- /dev/null
+++ b/resources/shared/sw-messages.mjs
@@ -0,0 +1,7 @@
+export const SW_MESSAGES = Object.freeze({
+ PRELOAD_SUITES: "PRELOAD_SUITES",
+ PRELOAD_PROGRESS: "PRELOAD_PROGRESS",
+ PRELOAD_DONE: "PRELOAD_DONE",
+ FATAL_ERROR: "FATAL_ERROR",
+ CLEAR_CACHE: "CLEAR_CACHE",
+});
diff --git a/resources/suite-runner.mjs b/resources/suite-runner.mjs
index 55b8f8a43..e4a28b3ae 100644
--- a/resources/suite-runner.mjs
+++ b/resources/suite-runner.mjs
@@ -96,7 +96,7 @@ export class SuiteRunner {
const { suiteTotal, suitePrepare } = this.#suiteResults.total;
if (suiteTotal === 0)
throw new Error(`Got invalid 0-time total for suite ${this.#suite.name}: ${suiteTotal}`);
- if (this.#params.measurePrepare && suitePrepare === 0)
+ if ((this.#params.measurePrepare || this.#suite.measurePrepare) && suitePrepare === 0)
throw new Error(`Got invalid 0-time prepare time for suite ${this.#suite.name}: ${suitePrepare}`);
}
@@ -174,7 +174,6 @@ export class RemoteSuiteRunner extends SuiteRunner {
this.appId = response?.appId;
performance.mark(suitePrepareEndLabel);
-
const entry = performance.measure(`suite-${suiteName}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
this.#prepareTime = entry.duration;
}
diff --git a/suites-experimental/javascript-wc-indexeddb/dist/resources.txt b/suites-experimental/javascript-wc-indexeddb/dist/resources.txt
new file mode 100644
index 000000000..601810cf1
--- /dev/null
+++ b/suites-experimental/javascript-wc-indexeddb/dist/resources.txt
@@ -0,0 +1,39 @@
+index.html
+libs/dexie.mjs
+src/components/todo-app/todo-app.component.js
+src/components/todo-app/todo-app.template.js
+src/components/todo-bottombar/todo-bottombar.component.js
+src/components/todo-bottombar/todo-bottombar.template.js
+src/components/todo-item/todo-item.component.js
+src/components/todo-item/todo-item.template.js
+src/components/todo-list/todo-list.component.js
+src/components/todo-list/todo-list.template.js
+src/components/todo-topbar/todo-topbar.component.js
+src/components/todo-topbar/todo-topbar.template.js
+src/hooks/useDoubleClick.js
+src/hooks/useKeyListener.js
+src/hooks/useRouter.js
+src/index.mjs
+src/speedometer-utils/benchmark.mjs
+src/speedometer-utils/helpers.mjs
+src/speedometer-utils/params.mjs
+src/speedometer-utils/step-runner.mjs
+src/speedometer-utils/step-scheduler.mjs
+src/speedometer-utils/todomvc-utils.mjs
+src/speedometer-utils/translations.mjs
+src/storage/base-storage-manager.js
+src/storage/dexieDB-manager.js
+src/storage/indexedDB-manager.js
+src/storage/storage-factory.js
+src/utils/nanoid.js
+src/workload-test.mjs
+styles/app.constructable.js
+styles/bottombar.constructable.js
+styles/footer.css
+styles/global.constructable.js
+styles/global.css
+styles/header.css
+styles/main.constructable.js
+styles/todo-item.constructable.js
+styles/todo-list.constructable.js
+styles/topbar.constructable.js
diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs
index 264145159..2ef5c7ff4 100644
--- a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs
+++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs
@@ -1,5 +1,5 @@
/* eslint-disable no-case-declarations */
-import { TestRunner, AsyncTestRunner } from "./test-runner.mjs";
+import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
import { Params } from "./params.mjs";
/**
@@ -15,10 +15,10 @@ export class BenchmarkStep {
}
async runAndRecord(params, suite, test, callback) {
- const TestRunnerClass = params.useAsyncSteps ? AsyncTestRunner : TestRunner;
+ const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner;
const type = params.useAsyncSteps ? "async" : "sync";
- const testRunner = new TestRunnerClass(null, null, params, suite, test, callback, type);
- const result = await testRunner.runTest();
+ const stepRunner = new StepRunnerClass(null, null, params, suite, test, callback, type);
+ const result = await stepRunner.runStep();
return result;
}
}
diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs
index 520679d24..b54bf1277 100644
--- a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs
+++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs
@@ -35,6 +35,10 @@ export class Params {
measurePrepare = false;
// External config url to override internal tests.
config = "";
+ // Resource load delay in ms for the service worker pre-caching.
+ resourceLoadDelay = 0;
+ // Use service worker for resource preloading.
+ preload = false;
constructor(searchParams = undefined) {
if (searchParams)
@@ -68,6 +72,8 @@ export class Params {
this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES);
this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare");
this.config = this._parseConfig(searchParams);
+ this.resourceLoadDelay = this._parseIntParam(searchParams, "resourceLoadDelay", 0);
+ this.preload = this._parseBooleanParam(searchParams, "preload");
const unused = Array.from(searchParams.keys());
if (unused.length > 0)
@@ -203,6 +209,10 @@ export class Params {
toSearchParams() {
return this.toSearchParamsObject().toString();
}
+
+ isDefault() {
+ return this === defaultParams;
+ }
}
function isValidJsonUrl(url) {
diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs
new file mode 100644
index 000000000..46187a6ba
--- /dev/null
+++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-runner.mjs
@@ -0,0 +1,116 @@
+import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs";
+import { forceLayout } from "./helpers.mjs";
+
+export class StepRunner {
+ #frame;
+ #page;
+ #params;
+ #suite;
+ #step;
+ #callback;
+ #type;
+
+ constructor(frame, page, params, suite, step, callback, type) {
+ this.#suite = suite;
+ this.#step = step;
+ this.#params = params;
+ this.#callback = callback;
+ this.#page = page;
+ this.#frame = frame;
+ this.#type = type;
+ }
+
+ get page() {
+ return this.#page;
+ }
+
+ get step() {
+ return this.#step;
+ }
+
+ _runSyncStep(step, page) {
+ step.run(page);
+ }
+
+ async runStep() {
+ // Prepare all mark labels outside the measuring loop.
+ const suiteName = this.#suite.name;
+ const stepName = this.#step.name;
+ const syncStartLabel = `${suiteName}.${stepName}-start`;
+ const syncEndLabel = `${suiteName}.${stepName}-sync-end`;
+ const asyncEndLabel = `${suiteName}.${stepName}-async-end`;
+
+ let syncTime;
+ let asyncStartTime;
+ let asyncTime;
+
+ const runSync = async () => {
+ if (this.#params.warmupBeforeSync) {
+ performance.mark("warmup-start");
+ const startTime = performance.now();
+ // Infinite loop for the specified ms.
+ while (performance.now() - startTime < this.#params.warmupBeforeSync)
+ continue;
+ performance.mark("warmup-end");
+ }
+ performance.mark(syncStartLabel);
+ const syncStartTime = performance.now();
+
+ if (this.#type === "async")
+ await this._runSyncStep(this.step, this.page);
+ else
+ this._runSyncStep(this.step, this.page);
+
+ const mark = performance.mark(syncEndLabel);
+ const syncEndTime = mark.startTime;
+
+ syncTime = syncEndTime - syncStartTime;
+ asyncStartTime = syncEndTime;
+ };
+ const measureAsync = () => {
+ // Some browsers don't immediately update the layout for paint.
+ // Force the layout here to ensure we're measuring the layout time.
+ //
+ // Note: This matches the behavior of Page.layout() in benchmark-runner.mjs.
+ // Since shared code cannot depend on Page, we duplicate the logic here,
+ // falling back to document.body for remote workloads that don't have a frame.
+ const body = this.#frame?.contentDocument?.body ?? document.body;
+ const value = forceLayout(body, this.#params.layoutMode);
+ body._leakedLayoutValue = value; // Prevent dead code elimination.
+
+ const asyncEndTime = performance.now();
+ performance.mark(asyncEndLabel);
+
+ asyncTime = asyncEndTime - asyncStartTime;
+
+ if (this.#params.warmupBeforeSync)
+ performance.measure("warmup", "warmup-start", "warmup-end");
+ performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel);
+ performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel);
+ };
+
+ const report = () => this.#callback(this.#step, syncTime, asyncTime);
+ const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
+ const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType];
+ const scheduler = new schedulerClass(runSync, measureAsync, report, this.#params);
+
+ return scheduler.start();
+ }
+}
+
+export class AsyncStepRunner extends StepRunner {
+ constructor(frame, page, params, suite, step, callback, type) {
+ super(frame, page, params, suite, step, callback, type);
+ }
+
+ async _runSyncStep(step, page) {
+ await step.run(page);
+ }
+}
+
+export const STEP_RUNNER_LOOKUP = {
+ __proto__: null,
+ default: StepRunner,
+ async: AsyncStepRunner,
+ remote: StepRunner,
+};
diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs
new file mode 100644
index 000000000..df7d96a21
--- /dev/null
+++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/step-scheduler.mjs
@@ -0,0 +1,84 @@
+class StepScheduler {
+ constructor(syncCallback, asyncCallback, reportCallback, params) {
+ this._syncCallback = syncCallback;
+ this._asyncCallback = asyncCallback;
+ this._reportCallback = reportCallback;
+ this._params = params;
+ }
+
+ start() {
+ return new Promise((resolve) => {
+ if (this._params.waitBeforeSync)
+ setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync);
+ else
+ this._scheduleCallbacks(resolve);
+ });
+ }
+}
+
+class RAFStepScheduler extends StepScheduler {
+ _scheduleCallbacks(resolve) {
+ requestAnimationFrame(() => this._syncCallback());
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ this._asyncCallback();
+ setTimeout(async () => {
+ const result = await this._reportCallback();
+ resolve(result);
+ }, 0);
+ }, 0);
+ });
+ }
+}
+
+class AsyncRAFStepScheduler extends StepScheduler {
+ static mc = new MessageChannel();
+ _scheduleCallbacks(resolve) {
+ let gotTimer = false;
+ let gotMessage = false;
+ let gotPromise = false;
+
+ const tryTriggerAsyncCallback = () => {
+ if (!gotTimer || !gotMessage || !gotPromise)
+ return;
+
+ this._asyncCallback();
+ setTimeout(async () => {
+ const results = await this._reportCallback();
+ resolve(results);
+ }, 0);
+ };
+
+ requestAnimationFrame(async () => {
+ await this._syncCallback();
+ gotPromise = true;
+ tryTriggerAsyncCallback();
+ });
+
+ requestAnimationFrame(() => {
+ setTimeout(async () => {
+ await Promise.resolve();
+ gotTimer = true;
+ tryTriggerAsyncCallback();
+ });
+
+ AsyncRAFStepScheduler.mc.port1.addEventListener(
+ "message",
+ async function () {
+ await Promise.resolve();
+ gotMessage = true;
+ tryTriggerAsyncCallback();
+ },
+ { once: true }
+ );
+ AsyncRAFStepScheduler.mc.port1.start();
+ AsyncRAFStepScheduler.mc.port2.postMessage("speedometer");
+ });
+ }
+}
+
+export const STEP_SCHEDULER_LOOKUP = {
+ __proto__: null,
+ raf: RAFStepScheduler,
+ async: AsyncRAFStepScheduler,
+};
diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs
index cb63c3de3..df7d96a21 100644
--- a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs
+++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs
@@ -1,4 +1,4 @@
-class TestInvoker {
+class StepScheduler {
constructor(syncCallback, asyncCallback, reportCallback, params) {
this._syncCallback = syncCallback;
this._asyncCallback = asyncCallback;
@@ -16,7 +16,7 @@ class TestInvoker {
}
}
-class RAFTestInvoker extends TestInvoker {
+class RAFStepScheduler extends StepScheduler {
_scheduleCallbacks(resolve) {
requestAnimationFrame(() => this._syncCallback());
requestAnimationFrame(() => {
@@ -31,7 +31,7 @@ class RAFTestInvoker extends TestInvoker {
}
}
-class AsyncRAFTestInvoker extends TestInvoker {
+class AsyncRAFStepScheduler extends StepScheduler {
static mc = new MessageChannel();
_scheduleCallbacks(resolve) {
let gotTimer = false;
@@ -62,7 +62,7 @@ class AsyncRAFTestInvoker extends TestInvoker {
tryTriggerAsyncCallback();
});
- AsyncRAFTestInvoker.mc.port1.addEventListener(
+ AsyncRAFStepScheduler.mc.port1.addEventListener(
"message",
async function () {
await Promise.resolve();
@@ -71,14 +71,14 @@ class AsyncRAFTestInvoker extends TestInvoker {
},
{ once: true }
);
- AsyncRAFTestInvoker.mc.port1.start();
- AsyncRAFTestInvoker.mc.port2.postMessage("speedometer");
+ AsyncRAFStepScheduler.mc.port1.start();
+ AsyncRAFStepScheduler.mc.port2.postMessage("speedometer");
});
}
}
-export const TEST_INVOKER_LOOKUP = {
+export const STEP_SCHEDULER_LOOKUP = {
__proto__: null,
- raf: RAFTestInvoker,
- async: AsyncRAFTestInvoker,
+ raf: RAFStepScheduler,
+ async: AsyncRAFStepScheduler,
};
diff --git a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs
index 2d816e853..46187a6ba 100644
--- a/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs
+++ b/suites-experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs
@@ -1,18 +1,18 @@
-import { TEST_INVOKER_LOOKUP } from "./test-invoker.mjs";
+import { STEP_SCHEDULER_LOOKUP } from "./step-scheduler.mjs";
import { forceLayout } from "./helpers.mjs";
-export class TestRunner {
+export class StepRunner {
#frame;
#page;
#params;
#suite;
- #test;
+ #step;
#callback;
#type;
- constructor(frame, page, params, suite, test, callback, type) {
+ constructor(frame, page, params, suite, step, callback, type) {
this.#suite = suite;
- this.#test = test;
+ this.#step = step;
this.#params = params;
this.#callback = callback;
this.#page = page;
@@ -24,21 +24,21 @@ export class TestRunner {
return this.#page;
}
- get test() {
- return this.#test;
+ get step() {
+ return this.#step;
}
- _runSyncStep(test, page) {
- test.run(page);
+ _runSyncStep(step, page) {
+ step.run(page);
}
- async runTest() {
+ async runStep() {
// Prepare all mark labels outside the measuring loop.
const suiteName = this.#suite.name;
- const testName = this.#test.name;
- const syncStartLabel = `${suiteName}.${testName}-start`;
- const syncEndLabel = `${suiteName}.${testName}-sync-end`;
- const asyncEndLabel = `${suiteName}.${testName}-async-end`;
+ const stepName = this.#step.name;
+ const syncStartLabel = `${suiteName}.${stepName}-start`;
+ const syncEndLabel = `${suiteName}.${stepName}-sync-end`;
+ const asyncEndLabel = `${suiteName}.${stepName}-async-end`;
let syncTime;
let asyncStartTime;
@@ -57,9 +57,9 @@ export class TestRunner {
const syncStartTime = performance.now();
if (this.#type === "async")
- await this._runSyncStep(this.test, this.page);
+ await this._runSyncStep(this.step, this.page);
else
- this._runSyncStep(this.test, this.page);
+ this._runSyncStep(this.step, this.page);
const mark = performance.mark(syncEndLabel);
const syncEndTime = mark.startTime;
@@ -85,32 +85,32 @@ export class TestRunner {
if (this.#params.warmupBeforeSync)
performance.measure("warmup", "warmup-start", "warmup-end");
- performance.measure(`${suiteName}.${testName}-sync`, syncStartLabel, syncEndLabel);
- performance.measure(`${suiteName}.${testName}-async`, syncEndLabel, asyncEndLabel);
+ performance.measure(`${suiteName}.${stepName}-sync`, syncStartLabel, syncEndLabel);
+ performance.measure(`${suiteName}.${stepName}-async`, syncEndLabel, asyncEndLabel);
};
- const report = () => this.#callback(this.#test, syncTime, asyncTime);
- const invokerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
- const invokerClass = TEST_INVOKER_LOOKUP[invokerType];
- const invoker = new invokerClass(runSync, measureAsync, report, this.#params);
+ const report = () => this.#callback(this.#step, syncTime, asyncTime);
+ const schedulerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod;
+ const schedulerClass = STEP_SCHEDULER_LOOKUP[schedulerType];
+ const scheduler = new schedulerClass(runSync, measureAsync, report, this.#params);
- return invoker.start();
+ return scheduler.start();
}
}
-export class AsyncTestRunner extends TestRunner {
- constructor(frame, page, params, suite, test, callback, type) {
- super(frame, page, params, suite, test, callback, type);
+export class AsyncStepRunner extends StepRunner {
+ constructor(frame, page, params, suite, step, callback, type) {
+ super(frame, page, params, suite, step, callback, type);
}
- async _runSyncStep(test, page) {
- await test.run(page);
+ async _runSyncStep(step, page) {
+ await step.run(page);
}
}
-export const TEST_RUNNER_LOOKUP = {
+export const STEP_RUNNER_LOOKUP = {
__proto__: null,
- default: TestRunner,
- async: AsyncTestRunner,
- remote: TestRunner,
+ default: StepRunner,
+ async: AsyncStepRunner,
+ remote: StepRunner,
};
diff --git a/suites-experimental/javascript-wc-indexeddb/scripts/build.mjs b/suites-experimental/javascript-wc-indexeddb/scripts/build.mjs
index 9db636c3a..1345e23c9 100644
--- a/suites-experimental/javascript-wc-indexeddb/scripts/build.mjs
+++ b/suites-experimental/javascript-wc-indexeddb/scripts/build.mjs
@@ -1,3 +1,5 @@
+import path from "path";
+import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs";
import fs from "fs/promises";
import { dirname } from "path";
@@ -97,7 +99,7 @@ const filesToMove = [
{ src: "node_modules/todomvc-css/dist/todo-list.constructable.js", dest: "./dist/styles/todo-list.constructable.js" },
{ src: "node_modules/todomvc-css/dist/todo-item.constructable.js", dest: "./dist/styles/todo-item.constructable.js" },
{ src: "node_modules/dexie/dist/modern/dexie.mjs", dest: "./dist/libs/dexie.mjs" },
- { src: "node_modules/speedometer-utils/step-scheduler.mjs", dest: "./dist/src/speedometer-utils/test-invoker.mjs" },
+ { src: "node_modules/speedometer-utils/step-scheduler.mjs", dest: "./dist/src/speedometer-utils/step-scheduler.mjs" },
{ src: "node_modules/speedometer-utils/step-runner.mjs", dest: "./dist/src/speedometer-utils/step-runner.mjs" },
{ src: "node_modules/speedometer-utils/params.mjs", dest: "./dist/src/speedometer-utils/params.mjs" },
{ src: "src/speedometer-utils/benchmark.mjs", dest: "./dist/src/speedometer-utils/benchmark.mjs" },
@@ -164,4 +166,5 @@ const build = async () => {
console.log("Done with building!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites-experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs b/suites-experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs
index a16f0831f..87639c816 100644
--- a/suites-experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs
+++ b/suites-experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs
@@ -1,5 +1,5 @@
/* eslint-disable no-case-declarations */
-import { TestRunner, AsyncTestRunner } from "./test-runner.mjs";
+import { StepRunner, AsyncStepRunner } from "./step-runner.mjs";
import { Params } from "/node_modules/speedometer-utils/params.mjs";
/**
@@ -15,10 +15,10 @@ export class BenchmarkStep {
}
async runAndRecord(params, suite, test, callback) {
- const TestRunnerClass = params.useAsyncSteps ? AsyncTestRunner : TestRunner;
+ const StepRunnerClass = params.useAsyncSteps ? AsyncStepRunner : StepRunner;
const type = params.useAsyncSteps ? "async" : "sync";
- const testRunner = new TestRunnerClass(null, null, params, suite, test, callback, type);
- const result = await testRunner.runTest();
+ const stepRunner = new StepRunnerClass(null, null, params, suite, test, callback, type);
+ const result = await stepRunner.runStep();
return result;
}
}
diff --git a/suites-experimental/responsive-design/dist/resources.txt b/suites-experimental/responsive-design/dist/resources.txt
new file mode 100644
index 000000000..67c1ae38c
--- /dev/null
+++ b/suites-experimental/responsive-design/dist/resources.txt
@@ -0,0 +1,39 @@
+app.js
+index.html
+public/images/5-healthy-smoothie.webp
+public/images/beef-stroganoff.webp
+public/images/beef-tacos.webp
+public/images/best-comfort-food.webp
+public/images/bread-at-home.webp
+public/images/caesar-salad.webp
+public/images/chicken-alfredo.webp
+public/images/chocolate-cake.webp
+public/images/chocolate-chip-cookies.webp
+public/images/fruit-salad.webp
+public/images/garlic-bread.webp
+public/images/gluten-free-baking.webp
+public/images/greek-salad.webp
+public/images/homemade-pizza.webp
+public/images/icons-outline.webp
+public/images/lemon-drizzle-cake.webp
+public/images/low-carb-desserts.webp
+public/images/margherita-pizza.webp
+public/images/mastering-art-of-french-cooking.webp
+public/images/meal-prepping.webp
+public/images/mediterranean-cuisine.webp
+public/images/pancakes.webp
+public/images/plant-based-protein.webp
+public/images/quinoa-stuffed-peppers.webp
+public/images/ramen-noodles.webp
+public/images/seasonal-salads.webp
+public/images/shrimp-paella.webp
+public/images/spaghetti-carbonara.webp
+public/images/superfoods-you-should-include.webp
+public/images/sushi-platter.webp
+public/images/thai-green-curry.webp
+public/images/tomato-soup.webp
+public/images/vegan-burger.webp
+public/images/vegan-desserts.webp
+public/images/vegetable-stir-fry.webp
+public/images/vegetarian-stir-fry.webp
+public/images/week-night-dinners.webp
diff --git a/suites-experimental/responsive-design/package-lock.json b/suites-experimental/responsive-design/package-lock.json
index b78e3b8fb..b50e599b3 100644
--- a/suites-experimental/responsive-design/package-lock.json
+++ b/suites-experimental/responsive-design/package-lock.json
@@ -1015,6 +1015,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1219,6 +1220,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
@@ -3327,6 +3329,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
@@ -4132,6 +4135,7 @@
"integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
diff --git a/suites-experimental/suites.mjs b/suites-experimental/suites.mjs
index 3de20b4dc..85ac0dbd9 100644
--- a/suites-experimental/suites.mjs
+++ b/suites-experimental/suites.mjs
@@ -4,9 +4,39 @@ import { numberOfItemsToAdd } from "../resources/shared/todomvc-utils.mjs";
import { freezeSuites } from "../resources/suites-helper.mjs";
export const ExperimentalSuites = freezeSuites([
+ {
+ name: "TodoMVC-JavaScript-ES5-Cached",
+ url: "suites/todomvc/vanilla-examples/javascript-es5/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt",
+ tags: ["todomvc", "experimental", "cached"],
+ async prepare(page) {
+ (await page.waitForElement(".new-todo")).focus();
+ },
+ tests: [
+ new BenchmarkTestStep(`Adding${numberOfItemsToAdd}Items`, (page) => {
+ const newTodo = page.querySelector(".new-todo");
+ for (let i = 0; i < numberOfItemsToAdd; i++) {
+ newTodo.setValue(getTodoText("ja", i));
+ newTodo.dispatchEvent("change");
+ newTodo.enter("keypress");
+ }
+ }),
+ new BenchmarkTestStep("CompletingAllItems", (page) => {
+ const checkboxes = page.querySelectorAll(".toggle");
+ for (let i = 0; i < numberOfItemsToAdd; i++)
+ checkboxes[i].click();
+ }),
+ new BenchmarkTestStep("DeletingAllItems", (page) => {
+ const deleteButtons = page.querySelectorAll(".destroy");
+ for (let i = numberOfItemsToAdd - 1; i >= 0; i--)
+ deleteButtons[i].click();
+ }),
+ ],
+ },
{
name: "TodoMVC-LocalStorage",
url: "suites-experimental/todomvc-localstorage/dist/index.html",
+ resources: "suites-experimental/todomvc-localstorage/dist/resources.txt",
tags: ["todomvc", "experimental"],
async prepare(page) {
(await page.waitForElement(".new-todo")).focus();
@@ -36,6 +66,7 @@ export const ExperimentalSuites = freezeSuites([
{
name: "TodoMVC-Emoji",
url: "suites/todomvc/vanilla-examples/javascript-web-components/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt",
tags: ["todomvc", "experimental"],
async prepare(page) {
await page.waitForElement("todo-app");
@@ -68,6 +99,7 @@ export const ExperimentalSuites = freezeSuites([
{
name: "TodoMVC-WebComponents-PostMessage",
url: "suites/todomvc/vanilla-examples/javascript-web-components/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt",
tags: ["experimental", "todomvc", "webcomponents"],
async prepare() {},
type: "remote",
@@ -78,6 +110,7 @@ export const ExperimentalSuites = freezeSuites([
{
name: "TodoMVC-Jaspr-Dart2JS-O4",
url: "suites-experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/index.html",
+ resources: "suites-experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt",
tags: ["todomvc", "experimental"],
async prepare(page) {
(await page.waitForElement(".new-todo")).focus();
@@ -106,6 +139,7 @@ export const ExperimentalSuites = freezeSuites([
{
name: "TodoMVC-Jaspr-Dart2Wasm-O2",
url: "suites-experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/index.html",
+ resources: "suites-experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt",
tags: ["todomvc", "experimental"],
disabled: true,
async prepare(page) {
@@ -135,6 +169,7 @@ export const ExperimentalSuites = freezeSuites([
{
name: "NewsSite-PostMessage",
url: "suites/newssite/news-next/dist/index.html",
+ resources: "suites/newssite/news-next/dist/resources.txt",
tags: ["experimental", "newssite", "language"],
async prepare() {},
type: "remote",
@@ -165,6 +200,7 @@ export const ExperimentalSuites = freezeSuites([
{
name: "Responsive-Design",
url: "suites-experimental/responsive-design/dist/index.html",
+ resources: "suites-experimental/responsive-design/dist/resources.txt",
tags: ["responsive-design", "webcomponents", "experimental"],
type: "async",
async prepare(page) {
diff --git a/suites-experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt b/suites-experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt
new file mode 100644
index 000000000..85de5f3ba
--- /dev/null
+++ b/suites-experimental/todomvc-dart-jaspr/dist/out-dart2js-O4/resources.txt
@@ -0,0 +1,5 @@
+base.css
+favicon.ico
+index.css
+index.html
+main.dart.js
diff --git a/suites-experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt b/suites-experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt
new file mode 100644
index 000000000..f66e98e5e
--- /dev/null
+++ b/suites-experimental/todomvc-dart-jaspr/dist/out-dart2wasm-O2/resources.txt
@@ -0,0 +1,7 @@
+base.css
+favicon.ico
+index.css
+index.html
+main.dart.js
+main.mjs
+main.wasm
diff --git a/suites-experimental/todomvc-localstorage/dist/resources.txt b/suites-experimental/todomvc-localstorage/dist/resources.txt
new file mode 100644
index 000000000..abac8a4db
--- /dev/null
+++ b/suites-experimental/todomvc-localstorage/dist/resources.txt
@@ -0,0 +1,8 @@
+app.js
+controller.js
+helpers.js
+index.html
+model.js
+store.js
+template.js
+view.js
diff --git a/suites-experimental/todomvc-localstorage/package-lock.json b/suites-experimental/todomvc-localstorage/package-lock.json
index 140b1e004..22d21efe6 100644
--- a/suites-experimental/todomvc-localstorage/package-lock.json
+++ b/suites-experimental/todomvc-localstorage/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "todomvc-javascript-es5",
+ "name": "todomvc-localstorage",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "todomvc-javascript-es5",
+ "name": "todomvc-localstorage",
"version": "1.0.0",
"dependencies": {
"todomvc-app-css": "^2.4.2",
diff --git a/suites-experimental/todomvc-localstorage/scripts/build.mjs b/suites-experimental/todomvc-localstorage/scripts/build.mjs
index cc923230e..b49adb74f 100644
--- a/suites-experimental/todomvc-localstorage/scripts/build.mjs
+++ b/suites-experimental/todomvc-localstorage/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../resources/shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
@@ -53,4 +54,5 @@ const build = async () => {
console.log("done!!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites-experimental/todomvc-localstorage/src/resources.txt b/suites-experimental/todomvc-localstorage/src/resources.txt
new file mode 100644
index 000000000..bbe806d24
--- /dev/null
+++ b/suites-experimental/todomvc-localstorage/src/resources.txt
@@ -0,0 +1,12 @@
+app.js
+base.css
+big-dom-with-stacking-context-scrollable.css
+controller.js
+helpers.js
+index.css
+index.html
+logo.png
+model.js
+store.js
+template.js
+view.js
diff --git a/suites/charts/dist/resources.txt b/suites/charts/dist/resources.txt
new file mode 100644
index 000000000..0d970f580
--- /dev/null
+++ b/suites/charts/dist/resources.txt
@@ -0,0 +1,12 @@
+assets/chartjs-7fd89fd7.js
+assets/chartjs-7fd89fd7.js.map
+assets/developer-4ed993c7.js.map
+assets/flights-airports-9a9e6422.js
+assets/flights-airports-9a9e6422.js.map
+assets/index-4ed993c7.js.map
+assets/plot-562fbdb6.js
+assets/plot-562fbdb6.js.map
+chartjs.html
+developer.html
+index.html
+observable-plot.html
diff --git a/suites/default-suites.mjs b/suites/default-suites.mjs
index 0f3e78b75..e30b7203b 100644
--- a/suites/default-suites.mjs
+++ b/suites/default-suites.mjs
@@ -7,6 +7,8 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-JavaScript-ES5",
url: "suites/todomvc/vanilla-examples/javascript-es5/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt",
+ preload: true,
tags: ["default", "todomvc"],
async prepare(page) {
(await page.waitForElement(".new-todo")).focus();
@@ -35,6 +37,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-JavaScript-ES5-Complex-DOM",
url: "suites/todomvc/vanilla-examples/javascript-es5-complex/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt",
tags: ["todomvc", "complex"],
async prepare(page) {
(await page.waitForElement(".new-todo")).focus();
@@ -63,6 +66,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-JavaScript-ES6-Webpack",
url: "suites/todomvc/vanilla-examples/javascript-es6-webpack/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt",
tags: ["todomvc"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -92,6 +96,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-JavaScript-ES6-Webpack-Complex-DOM",
url: "suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt",
tags: ["default", "todomvc", "complex", "complex-default"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -121,6 +126,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-WebComponents",
url: "suites/todomvc/vanilla-examples/javascript-web-components/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt",
tags: ["default", "todomvc", "webcomponents"],
async prepare(page) {
await page.waitForElement("todo-app");
@@ -153,6 +159,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-WebComponents-Complex-DOM",
url: "suites/todomvc/vanilla-examples/javascript-web-components-complex/dist/index.html",
+ resources: "suites/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt",
tags: ["todomvc", "webcomponents", "complex"],
async prepare(page) {
await page.waitForElement("todo-app");
@@ -243,6 +250,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-React-Redux",
url: "suites/todomvc/architecture-examples/react-redux/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/react-redux/dist/resources.txt",
tags: ["default", "todomvc"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -271,6 +279,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-React-Redux-Complex-DOM",
url: "suites/todomvc/architecture-examples/react-redux-complex/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/react-redux-complex/dist/resources.txt",
tags: ["todomvc", "complex"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -299,6 +308,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Backbone",
url: "suites/todomvc/architecture-examples/backbone/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/backbone/dist/resources.txt",
tags: ["default", "todomvc"],
async prepare(page) {
await page.waitForElement("#appIsReady");
@@ -329,6 +339,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Backbone-Complex-DOM",
url: "suites/todomvc/architecture-examples/backbone-complex/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/backbone-complex/dist/resources.txt",
tags: ["todomvc", "complex"],
async prepare(page) {
await page.waitForElement("#appIsReady");
@@ -359,6 +370,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Angular",
url: "suites/todomvc/architecture-examples/angular/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/angular/dist/resources.txt",
tags: ["todomvc"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -388,6 +400,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Angular-Complex-DOM",
url: "suites/todomvc/architecture-examples/angular-complex/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/angular-complex/dist/resources.txt",
tags: ["default", "todomvc", "complex", "complex-default"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -417,6 +430,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Vue",
url: "suites/todomvc/architecture-examples/vue/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/vue/dist/resources.txt",
tags: ["default", "todomvc"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -446,6 +460,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Vue-Complex-DOM",
url: "suites/todomvc/architecture-examples/vue-complex/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/vue-complex/dist/resources.txt",
tags: ["todomvc", "complex", "complex-default"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -475,6 +490,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-jQuery",
url: "suites/todomvc/architecture-examples/jquery/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/jquery/dist/resources.txt",
tags: ["default", "todomvc"],
async prepare(page) {
await page.waitForElement("#appIsReady");
@@ -502,6 +518,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-jQuery-Complex-DOM",
url: "suites/todomvc/architecture-examples/jquery-complex/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/jquery-complex/dist/resources.txt",
tags: ["todomvc", "complex"],
async prepare(page) {
await page.waitForElement("#appIsReady");
@@ -585,6 +602,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Svelte",
url: "suites/todomvc/architecture-examples/svelte/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/svelte/dist/resources.txt",
tags: ["todomvc"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -613,6 +631,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Svelte-Complex-DOM",
url: "suites/todomvc/architecture-examples/svelte-complex/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/svelte-complex/dist/resources.txt",
tags: ["default", "todomvc", "complex", "complex-default"],
async prepare(page) {
const element = await page.waitForElement(".new-todo");
@@ -641,6 +660,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Lit",
url: "suites/todomvc/architecture-examples/lit/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/lit/dist/resources.txt",
tags: ["todomvc", "webcomponents"],
async prepare(page) {
await page.waitForElement("todo-app");
@@ -672,6 +692,7 @@ export const DefaultSuites = freezeSuites([
{
name: "TodoMVC-Lit-Complex-DOM",
url: "suites/todomvc/architecture-examples/lit-complex/dist/index.html",
+ resources: "suites/todomvc/architecture-examples/lit-complex/dist/resources.txt",
tags: ["default", "todomvc", "webcomponents", "complex", "complex-default"],
async prepare(page) {
await page.waitForElement("todo-app");
@@ -703,6 +724,7 @@ export const DefaultSuites = freezeSuites([
{
name: "NewsSite-Next",
url: "suites/newssite/news-next/dist/index.html",
+ resources: "suites/newssite/news-next/dist/resources.txt",
tags: ["default", "newssite", "language"],
async prepare(page) {
await page.waitForElement("#navbar-dropdown-toggle");
@@ -743,6 +765,7 @@ export const DefaultSuites = freezeSuites([
{
name: "NewsSite-Nuxt",
url: "suites/newssite/news-nuxt/dist/index.html",
+ resources: "suites/newssite/news-nuxt/dist/resources.txt",
tags: ["default", "newssite"],
async prepare(page) {
await page.waitForElement("#navbar-dropdown-toggle");
diff --git a/suites/editors/dist/resources.txt b/suites/editors/dist/resources.txt
new file mode 100644
index 000000000..9b870632a
--- /dev/null
+++ b/suites/editors/dist/resources.txt
@@ -0,0 +1,12 @@
+assets/codemirror-521de7ab.js
+assets/codemirror-521de7ab.js.map
+assets/index-2feebe42.css
+assets/index.es-02a92ebc.js
+assets/index.es-02a92ebc.js.map
+assets/main-4ed993c7.js.map
+assets/tiptap-95a40ba8.js
+assets/tiptap-95a40ba8.js.map
+codemirror.html
+index.html
+tiptap.html
+vite.svg
diff --git a/suites/newssite/news-next/dist/resources.txt b/suites/newssite/news-next/dist/resources.txt
new file mode 100644
index 000000000..5d8d79a5c
--- /dev/null
+++ b/suites/newssite/news-next/dist/resources.txt
@@ -0,0 +1,359 @@
+404.html
+_next/static/chunks/743-fd706aeabb7828e3.js
+_next/static/chunks/framework-2c79e2a64abdb08b.js
+_next/static/chunks/main-2ba37e62325cc71b.js
+_next/static/chunks/pages/_app-77983e68be50f72a.js
+_next/static/chunks/pages/_error-54de1933a164a1ff.js
+_next/static/chunks/pages/index-99c54b3ee91525b8.js
+_next/static/chunks/polyfills-c67a75d1b6f99dc8.js
+_next/static/chunks/webpack-e50e9853db18b759.js
+_next/static/css/2cf5163b53bb0adb.css
+_next/static/css/a0dca1379a01e5cf.css
+_next/static/tuwdCnX7HYK_fwpI0QvDm/_buildManifest.js
+_next/static/tuwdCnX7HYK_fwpI0QvDm/_ssgManifest.js
+assets/images/2h-media-lPcQhLP-b4I-unsplash_150.jpg
+assets/images/aaron-huber-G7sE2S4Lab4-unsplash_336.jpg
+assets/images/adam-whitlock-I9j8Rk-JYFM-unsplash_684.jpg
+assets/images/aditya-chinchure-ZhQCZjr9fHo-unsplash_336.jpg
+assets/images/adrian-dascal-myAz-buELXs-unsplash_448.jpg
+assets/images/ahmet-kurt-5BGg2L5nhlU-unsplash_150.jpg
+assets/images/ahmet-kurt-WviyUzOg4RU-unsplash_150.jpg
+assets/images/alan-rodriguez-qrD-g7oc9is-unsplash_684.jpg
+assets/images/albert-stoynov-fEdf0fig3os-unsplash_448.jpg
+assets/images/alec-favale-dLctr-PqFys-unsplash_150.jpg
+assets/images/alev-takil-fYyYz38bUkQ-unsplash_150.jpg
+assets/images/alex-hudson-7AgqAZbogOQ-unsplash_448.jpg
+assets/images/alexander-grey-8lnbXtxFGZw-unsplash_684.jpg
+assets/images/alexander-kirov-YhDJXJjmxUQ-unsplash_336.jpg
+assets/images/alexandre-debieve-FO7JIlwjOtU-unsplash_336.jpg
+assets/images/alice-pasqual-Olki5QpHxts-unsplash_336.jpg
+assets/images/allan-wadsworth-Lp78NT-mf9o-unsplash_150.jpg
+assets/images/anastassia-anufrieva-ecHGTPfjNfA-unsplash_150.jpg
+assets/images/anatol-rurac-NeSj0i6HLak-unsplash_150.jpg
+assets/images/anatol-rurac-b5t2lqeCGfA-unsplash_150.jpg
+assets/images/andrew-solok-LbckXdUVOlY-unsplash_448.jpg
+assets/images/angela-loria-hFc0JEKD4Cc-unsplash_150.jpg
+assets/images/ani-kolleshi-7jjnJ-QA9fY-unsplash_150.jpg
+assets/images/anne-nygard-tcJ6sJTtTWI-unsplash_448.jpg
+assets/images/annie-spratt-IT6aov1ScW0-unsplash_150.jpg
+assets/images/annie-spratt-KiOHnBkLQQU-unsplash_684.jpg
+assets/images/ant-rozetsky-SLIFI67jv5k-unsplash_150.jpg
+assets/images/arnaud-jaegers-IBWJsMObnnU-unsplash_336.jpg
+assets/images/arnel-hasanovic-MNd-Rka1o0Q-unsplash_336.jpg
+assets/images/artur-voznenko-rwPIQQPz1ew-unsplash_150.jpg
+assets/images/arturo-rey-5yP83RhaFGA-unsplash_336.jpg
+assets/images/ashim-d-silva-ZmgJiztRHXE-unsplash_336.jpg
+assets/images/astronaud23-ox3t0m3PUqA-unsplash_336.jpg
+assets/images/austin-distel-97HfVpyNR1M-unsplash_448.jpg
+assets/images/austin-distel-rxpThOwuVgE-unsplash_448.jpg
+assets/images/axp-photography-v6pAkO31d50-unsplash_448.jpg
+assets/images/azhar-j-t2hgHV1R7_g-unsplash_448.jpg
+assets/images/benjamin-child-rOn57CBgyMo-unsplash_448.jpg
+assets/images/bermix-studio-yUnSMBogWNI-unsplash_150.jpg
+assets/images/bernd-dittrich-Xk1IfNnEhRA-unsplash_448.jpg
+assets/images/bianca-ackermann-qr0-lKAOZSk-unsplash_150.jpg
+assets/images/boris-stefanik-q49CgyIrLes-unsplash_150.jpg
+assets/images/boxed-water-is-better-y-TpYAlcBYM-unsplash_150.jpg
+assets/images/braden-collum-9HI8UJMSdZA-unsplash_336.jpg
+assets/images/bram-naus-oqnVnI5ixHg-unsplash_448.jpg
+assets/images/brigitte-tohm-iIupxcq-yH4-unsplash_150.jpg
+assets/images/brooke-lark-GJMlSBS0FhU-unsplash_150.jpg
+assets/images/brooke-lark-HjWzkqW1dgI-unsplash_336.jpg
+assets/images/brooke-lark-IDTEXXXfS44-unsplash_150.jpg
+assets/images/brooke-lark-lcZ9NxhOSlo-unsplash_448.jpg
+assets/images/brooke-lark-oaz0raysASk-unsplash_150.jpg
+assets/images/bruce-mars-xj8qrWvuOEs-unsplash_336.jpg
+assets/images/bruno-nascimento-PHIgYUGQPvU-unsplash_448.jpg
+assets/images/c-j-1GHqOftzYo0-unsplash_150.jpg
+assets/images/caglar-oskay-d0Be8Vs9XRk-unsplash_336.jpg
+assets/images/campaign-creators-pypeCEaJeZY-unsplash_448.jpg
+assets/images/cardmapr-nl-s8F8yglbpjo-unsplash_336.jpg
+assets/images/carl-nenzen-loven-c-pc2mP7hTs-unsplash_448.jpg
+assets/images/chad-stembridge-sEHrIPpkKQY-unsplash_150.jpg
+assets/images/chloe-taranto-x2zyAOmVNtM-unsplash_448.jpg
+assets/images/chris-leboutillier-TUJud0AWAPI-unsplash_150.jpg
+assets/images/chris-leboutillier-c7RWVGL8lPA-unsplash_150.jpg
+assets/images/christian-lue-8Yw6tsB8tnc-unsplash_448.jpg
+assets/images/christian-tenguan-P3gfVKhz8d0-unsplash_336.jpg
+assets/images/christina-rumpf-gUU4MF87Ipw-unsplash_150.jpg
+assets/images/clark-street-mercantile-P3pI6xzovu0-unsplash_336.jpg
+assets/images/claudio-schwarz-3cWxxW2ggKE-unsplash_336.jpg
+assets/images/clay-banks-BY-R0UNRE7w-unsplash_150.jpg
+assets/images/clay-banks-cisdc-344vo-unsplash_150.jpg
+assets/images/clem-onojeghuo-DoA2duXyzRM-unsplash_684.jpg
+assets/images/clem-onojeghuo-RLJnH4Mt9A0-unsplash_336.jpg
+assets/images/colin-lloyd-2ULmNrj44QY-unsplash_448.jpg
+assets/images/colin-lloyd-NKS5gg7rWGw-unsplash_150.jpg
+assets/images/colin-lloyd-uaM_Ijy_joY-unsplash_448.jpg
+assets/images/craig-manners-LvJCFOW3Ma8-unsplash_448.jpg
+assets/images/craig-melville-_JKymnZ1Uc4-unsplash_150.jpg
+assets/images/crystal-kwok-xD5SWy7hMbw-unsplash_448.jpg
+assets/images/dani-navarro-6CnGzrLwM28-unsplash_336.jpg
+assets/images/daniel-staple-N320vzTBviA-unsplash_684.jpg
+assets/images/david-beale--lQR8yeDzek-unsplash_336.jpg
+assets/images/david-everett-strickler-igCBFrMd11I-unsplash_684.jpg
+assets/images/david-lezcano-NfZiOJzZgcg-unsplash_336.jpg
+assets/images/david-vives-Nzbkev7SQTg-unsplash_448.jpg
+assets/images/declan-sun-misAHv6YWkI-unsplash_336.jpg
+assets/images/denys-nevozhai-7nrsVjvALnA-unsplash_150.jpg
+assets/images/denys-nevozhai-z0nVqfrOqWA-unsplash_150.jpg
+assets/images/derick-mckinney-muhK4oeYJiU-unsplash_150.jpg
+assets/images/devi-puspita-amartha-yahya-7ln0pST_O8M-unsplash_448.jpg
+assets/images/dibakar-roy-K9JwokzSvrc-unsplash_150.jpg
+assets/images/diego-jimenez-A-NVHPka9Rk-unsplash_336.jpg
+assets/images/dillon-kydd-2keCPb73aQY-unsplash_336.jpg
+assets/images/dino-reichmuth-A5rCN8626Ck-unsplash_336.jpg
+assets/images/dom-hill-nimElTcTNyY-unsplash_336.jpg
+assets/images/dominic-bieri-vXRt4rFr4hI-unsplash_336.jpg
+assets/images/dominic-hampton-_8aRumOixtI-unsplash_684.jpg
+assets/images/domino-studio-164_6wVEHfI-unsplash_336.jpg
+assets/images/drew-hays-tGYrlchfObE-unsplash_336.jpg
+assets/images/dyana-wing-so-Og16Foo-pd8-unsplash_150.jpg
+assets/images/dylan-calluy-j9q18vvHitg-unsplash_150.jpg
+assets/images/ednilson-cardoso-dos-santos-haiooWA_weo-unsplash_336.jpg
+assets/images/egor-myznik-GFHKMW6KiJ0-unsplash_684.jpg
+assets/images/ehimetalor-akhere-unuabona-yS0uBoF4xDo-unsplash_448.jpg
+assets/images/elimende-inagella-7OxV_qDiGRI-unsplash_448.jpg
+assets/images/emmanuel-ikwuegbu-ceawFbpA-14-unsplash_448.jpg
+assets/images/emran-yousof-k8ZbMQWbx34-unsplash_336.jpg
+assets/images/erik-mclean-ByjIzFupcHo-unsplash_336.jpg
+assets/images/esther-ann-glpYh1cWf0o-unsplash_448.jpg
+assets/images/etienne-girardet-RqOyRtYGhLg-unsplash_448.jpg
+assets/images/evie-calder-97CO-A4P0GQ-unsplash_336.jpg
+assets/images/fanga-studio-bOfCOy3_4wU-unsplash_448.jpg
+assets/images/fons-heijnsbroek-vBfEZdpEr-E-unsplash_448.jpg
+assets/images/freestocks-nss2eRzQwgw-unsplash_150.jpg
+assets/images/gaku-suyama-VyiLZUcdJv0-unsplash_150.jpg
+assets/images/geert-pieters-NbpUM86Jo8Y-unsplash_150.jpg
+assets/images/geoff-scott-8lUTnkZXZSA-unsplash_336.jpg
+assets/images/glenn-carstens-peters-npxXWgQ33ZQ-unsplash_336.jpg
+assets/images/greg-rakozy-oMpAz-DN-9I-unsplash_684.jpg
+assets/images/gregory-hayes-h5cd51KXmRQ-unsplash_684.jpg
+assets/images/guillaume-perigois-HL4LEIyGEYU-unsplash_150.jpg
+assets/images/hannah-busing-0V6DmTuJaIk-unsplash_448.jpg
+assets/images/hansjorg-keller-CQqyv5uldW4-unsplash_448.jpg
+assets/images/harley-davidson-fFbUdx80oCc-unsplash_336.jpg
+assets/images/harold-mendoza-6xafY_AE1LM-unsplash_150.jpg
+assets/images/harri-kuokkanen-SEtUeWL8bIQ-unsplash_336.jpg
+assets/images/hassan-kibwana-fmXLB_uHIh4-unsplash_448.jpg
+assets/images/heather-ford-5gkYsrH_ebY-unsplash_336.jpg
+assets/images/heidi-fin-2TLREZi7BUg-unsplash_336.jpg
+assets/images/heidi-kaden-L_U4jhwZ6hY-unsplash_448.jpg
+assets/images/hush-naidoo-jade-photography-ZCO_5Y29s8k-unsplash_150.jpg
+assets/images/hush-naidoo-jade-photography-Zp7ebyti3MU-unsplash_336.jpg
+assets/images/hyttalo-souza-a1p0Z7RSkL8-unsplash_684.jpg
+assets/images/ian-hutchinson-P8rgDtEFn7s-unsplash_150.jpg
+assets/images/ian-taylor-kAWTCt7p7rs-unsplash_448.jpg
+assets/images/ibrahim-rifath-OApHds2yEGQ-unsplash_150.jpg
+assets/images/icons8-team-k5fUTay0ghw-unsplash_336.jpg
+assets/images/ilaria-de-bona-RuFfpBsaRY0-unsplash_336.jpg
+assets/images/ilyass-seddoug-06w8RxgSzF0-unsplash_150.jpg
+assets/images/inaki-del-olmo-NIJuEQw0RKg-unsplash_336.jpg
+assets/images/irwan-rbDE93-0hHs-unsplash_684.jpg
+assets/images/isai-ramos-Sp70YIWtuM8-unsplash_336.jpg
+assets/images/isaw-company-Oqv_bQbZgS8-unsplash_336.jpg
+assets/images/ixography-05Q_XPF_YKs-unsplash_336.jpg
+assets/images/jacob-licht-8nA_iHrxHIo-unsplash_150.jpg
+assets/images/jakob-dalbjorn-cuKJre3nyYc-unsplash_448.jpg
+assets/images/jakub-zerdzicki-VnTR3XFwxWs-unsplash_150.jpg
+assets/images/jakub-zerdzicki-qcRGVZNZ5js-unsplash_336.jpg
+assets/images/jannis-brandt-mmsQUgMLqUo-unsplash_150.jpg
+assets/images/jaron-nix-7wWRXewYCH4-unsplash_684.jpg
+assets/images/jay-clark-P3sLerH3UmM-unsplash_336.jpg
+assets/images/jed-owen-ajZibDGpPew-unsplash_336.jpg
+assets/images/jessica-weiller-So4eFi-d1nc-unsplash_336.jpg
+assets/images/jon-tyson-0BLE1xp5HBQ-unsplash_150.jpg
+assets/images/jon-tyson-ne2mqMgER8Y-unsplash_150.jpg
+assets/images/jonathan-borba-lrQPTQs7nQQ-unsplash_150.jpg
+assets/images/jonathan-simcoe-S9J1HqoL9ns-unsplash_150.jpg
+assets/images/jordhan-madec-AD5ylD2T0UY-unsplash_336.jpg
+assets/images/joseph-gonzalez-QaGDmf5tMiE-unsplash_150.jpg
+assets/images/joshua-rawson-harris-YNaSz-E7Qss-unsplash_336.jpg
+assets/images/juli-kosolapova-4PE3X9eKsu4-unsplash_336.jpg
+assets/images/junko-nakase-Q-72wa9-7Dg-unsplash_336.jpg
+assets/images/k8-uYf_C34PAao-unsplash_336.jpg
+assets/images/keagan-henman-xPJYL0l5Ii8-unsplash_336.jpg
+assets/images/kelli-dougal-vbiQ_7vwfrs-unsplash_448.jpg
+assets/images/kelly-sikkema-A-lovieAmjA-unsplash_150.jpg
+assets/images/kelly-sikkema-WIYtZU3PxsI-unsplash_448.jpg
+assets/images/kenny-K72n3BHgHCg-unsplash_336.jpg
+assets/images/kenny-eliason-4N3iHYmqy_E-unsplash_336.jpg
+assets/images/kenny-eliason-5ddH9Y2accI-unsplash_150.jpg
+assets/images/kevin-rajaram-qhixFFO8EWQ-unsplash_336.jpg
+assets/images/kevin-wang-t7vEVxwGGm0-unsplash_336.jpg
+assets/images/kitthitorn-chaiyuthapoom-TOH_gw5dd20-unsplash_336.jpg
+assets/images/koshu-kunii-ADLj1cyFfV8-unsplash_150.jpg
+assets/images/koshu-kunii-cWEGNQqcImk-unsplash_336.jpg
+assets/images/koshu-kunii-v9ferChkC9A-unsplash_150.jpg
+assets/images/kyle-glenn-gcw_WWu_uBQ-unsplash_336.jpg
+assets/images/leah-hetteberg-kTVN2l0ZUv8-unsplash_684.jpg
+assets/images/leisara-studio-EzzW1oNek-I-unsplash_336.jpg
+assets/images/leon-seibert-Xs3al4NpIFQ-unsplash_336.jpg
+assets/images/libre-leung-9O0Sp22DF0I-unsplash_336.jpg
+assets/images/linkedin-sales-solutions--AXDunSs-n4-unsplash_150.jpg
+assets/images/little-plant-TZw891-oMio-unsplash_150.jpg
+assets/images/lorenzo-moschi-N7ypjB7HKIk-unsplash_150.jpg
+assets/images/louis-reed-pwcKF7L4-no-unsplash_448.jpg
+assets/images/luis-cortes-QrPDA15pRkM-unsplash_336.jpg
+assets/images/lukasz-radziejewski-cg4MzL_eSvU-unsplash_336.jpg
+assets/images/m-ZzOa5G8hSPI-unsplash_150.jpg
+assets/images/maarten-van-den-heuvel-gZXx8lKAb7Y-unsplash_150.jpg
+assets/images/mac-blades-jpgJSBQtw5U-unsplash_336.jpg
+assets/images/maddi-bazzocco-qKbHvzXb85A-unsplash_150.jpg
+assets/images/maja-kochanowska-EiJQdDI_t_Y-unsplash_150.jpg
+assets/images/maksim-larin-tecILYzVAzg-unsplash_336.jpg
+assets/images/maksym-harbar-okn8ZIjPMxI-unsplash_448.jpg
+assets/images/maksym-kaharlytskyi-Y0z9MyDsrU0-unsplash_150.jpg
+assets/images/marc-olivier-jodoin-_eclsGKsUdo-unsplash_448.jpg
+assets/images/marcelo-leal-6pcGTJDuf6M-unsplash_448.jpg
+assets/images/marco-oriolesi-wqLGlhjr6Og-unsplash_684.jpg
+assets/images/marek-studzinski-9U9I-eVx9nI-unsplash_150.jpg
+assets/images/maria-lysenko-tZvkSuBleso-unsplash_150.jpg
+assets/images/marius-oprea-ySA9uj7zSmw-unsplash_150.jpg
+assets/images/markus-spiske-7PMGUqYQpYc-unsplash_150.jpg
+assets/images/markus-spiske-Nph1oyRsHm4-unsplash_150.jpg
+assets/images/markus-spiske-WUehAgqO5hE-unsplash_684.jpg
+assets/images/markus-spiske-jgOkEjVw-KM-unsplash_150.jpg
+assets/images/markus-spiske-lUc5pRFB25s-unsplash_336.jpg
+assets/images/markus-spiske-p2Xor4Lbrrk-unsplash_150.jpg
+assets/images/markus-spiske-wIUxLHndcLw-unsplash_150.jpg
+assets/images/martha-dominguez-de-gouveia-k-NnVZ-z26w-unsplash_150.jpg
+assets/images/martin-jernberg-jVNWCFwdjZU-unsplash_448.jpg
+assets/images/martin-podsiad-wrdtA9lew9E-unsplash_150.jpg
+assets/images/mathias-reding-yfXhqAW5X0c-unsplash_150.jpg
+assets/images/mathurin-napoly-matnapo-ejWJ3a92FEs-unsplash_336.jpg
+assets/images/mathurin-napoly-matnapo-pIJ34ZrZEEw-unsplash_150.jpg
+assets/images/matt-popovich-7mqsZsE6FaU-unsplash_336.jpg
+assets/images/matthew-ball-3wW2fBjptQo-unsplash_448.jpg
+assets/images/matthew-henry-0Ol8Sa2n21c-unsplash_150.jpg
+assets/images/matthew-landers-v8UgmRa6UDg-unsplash_150.jpg
+assets/images/matthew-tenbruggencate-0HJWobhGhJs-unsplash_150.jpg
+assets/images/matthias-heil-lDOEwat_MPs-unsplash_336.jpg
+assets/images/mattia-bericchia-xkD79yf4tb8-unsplash_150.jpg
+assets/images/maxim-hopman-IayKLkmz6g0-unsplash_448.jpg
+assets/images/maximilian-bungart-nwqfl_HtJjk-unsplash_150.jpg
+assets/images/melissa-askew-8n00CqwnqO8-unsplash_448.jpg
+assets/images/memento-media-XhYq-5KnxSk-unsplash_448.jpg
+assets/images/mesut-kaya-eOcyhe5-9sQ-unsplash_336.jpg
+assets/images/metin-ozer-hShrr0WvrQs-unsplash_336.jpg
+assets/images/microsoft-365-f1zQuagWCTA-unsplash_336.jpg
+assets/images/miguel-bruna-TzVN0xQhWaQ-unsplash_150.jpg
+assets/images/mika-baumeister-Hm4zYX-BDxk-unsplash_150.jpg
+assets/images/mika-baumeister-bGZZBDvh8s4-unsplash_150.jpg
+assets/images/mika-baumeister-jXPQY1em3Ew-unsplash_150.jpg
+assets/images/mike-enerio-7ryPpZK1qV8-unsplash_448.jpg
+assets/images/milad-fakurian-58Z17lnVS4U-unsplash_684.jpg
+assets/images/mr-cup-fabien-barral-Mwuod2cm8g4-unsplash_448.jpg
+assets/images/mr-lee-f4RBYsY2hxA-unsplash_150.jpg
+assets/images/mufid-majnun-J12RfFH-2ZE-unsplash_684.jpg
+assets/images/mufid-majnun-tJJIGh703I4-unsplash_336.jpg
+assets/images/mulyadi-JeCNRxGLSp4-unsplash_150.jpg
+assets/images/myriam-zilles-KltoLK6Mk-g-unsplash_336.jpg
+assets/images/nasa-Q1p7bh3SHj8-unsplash_150.jpg
+assets/images/nasa-cIX5TlQ_FgM-unsplash_448.jpg
+assets/images/nasa-dCgbRAQmTQA-unsplash_684.jpg
+assets/images/nathan-cima-TQuq2OtLBNU-unsplash_150.jpg
+assets/images/nathan-dumlao-bRdRUUtbxO0-unsplash_150.jpg
+assets/images/nathan-dumlao-laCrvNG3F_I-unsplash_336.jpg
+assets/images/national-cancer-institute-A2CK97sS0ns-unsplash_684.jpg
+assets/images/national-cancer-institute-GcrSgHDrniY-unsplash_336.jpg
+assets/images/national-cancer-institute-KrsoedfRAf4-unsplash_336.jpg
+assets/images/national-cancer-institute-L7en7Lb-Ovc-unsplash_336.jpg
+assets/images/national-cancer-institute-NFvdKIhxYlU-unsplash_336.jpg
+assets/images/national-cancer-institute-SMxzEaidR20-unsplash_336.jpg
+assets/images/national-cancer-institute-YvvFRJgWShM-unsplash_336.jpg
+assets/images/national-cancer-institute-cw2Zn2ZQ9YQ-unsplash_336.jpg
+assets/images/neil-thomas-SIU1Glk6v5k-unsplash_150.jpg
+assets/images/nikita-karimov-lvJZhHOIJJ4-unsplash_448.jpg
+assets/images/no-revisions-UhpAf0ySwuk-unsplash_336.jpg
+assets/images/noaa--urO88VoCRE-unsplash_336.jpg
+assets/images/noaa-FY3vXNBl1v4-unsplash_336.jpg
+assets/images/noaa-I323ZqSkkn8-unsplash_448.jpg
+assets/images/noaa-Led9c1SSNFo-unsplash_684.jpg
+assets/images/nonresident-nizUHtSIrKM-unsplash_150.jpg
+assets/images/nuno-alberto-MykFFC5zolE-unsplash_448.jpg
+assets/images/oguzhan-edman-ZWPkHLRu3_4-unsplash_336.jpg
+assets/images/olga-guryanova-ft7vJxwl2RY-unsplash_336.jpg
+assets/images/olga-guryanova-tMFeatBSS4s-unsplash_336.jpg
+assets/images/paola-chaaya-QrbuLFT6ypw-unsplash_336.jpg
+assets/images/parker-johnson-v0OWc_skg0g-unsplash_336.jpg
+assets/images/pascal-bullan-M8sQPAfhPdk-unsplash_336.jpg
+assets/images/pat-taylor-12V36G17IbQ-unsplash_336.jpg
+assets/images/pat-whelen-68OkRwuOeyQ-unsplash_150.jpg
+assets/images/patti-black-FnV-PjAYHCI-unsplash_150.jpg
+assets/images/paul-bill-HLuPjCa6IYw-unsplash_150.jpg
+assets/images/paul-hanaoka-s0XabTAKvak-unsplash_684.jpg
+assets/images/peter-conlan-LEgwEaBVGMo-unsplash_448.jpg
+assets/images/peter-lawrence-rXZa4ufjoGw-unsplash_448.jpg
+assets/images/peter-thomas-17EJD0QdKFI-unsplash_150.jpg
+assets/images/phil-hearing-bu27Y0xg7dk-unsplash_150.jpg
+assets/images/pierre-bamin-lM4_Nmcj4Xk-unsplash_150.jpg
+assets/images/piron-guillaume-U4FyCp3-KzY-unsplash_150.jpg
+assets/images/prince-abban-0OUHhvNIbYc-unsplash_150.jpg
+assets/images/priscilla-du-preez-GgtxccOjIXE-unsplash_448.jpg
+assets/images/quick-ps-sW41y3lETZk-unsplash_150.jpg
+assets/images/quino-al-KydWCDJe9s0-unsplash_150.jpg
+assets/images/razvan-chisu-Ua-agENjmI4-unsplash_448.jpg
+assets/images/rebe-pascual-SACRQSof7Qw-unsplash_448.jpg
+assets/images/red-dot-Q98X_JVRGS0-unsplash_336.jpg
+assets/images/redd-f-N9CYH-H_gBE-unsplash_684.jpg
+assets/images/rio-lecatompessy-cfDURuQKABk-unsplash_336.jpg
+assets/images/robert-bye-EILw-nEK46k-unsplash_150.jpg
+assets/images/robert-bye-xHUZuSwVJg4-unsplash_336.jpg
+assets/images/roberto-junior-4fsCBcZt9H8-unsplash_150.jpg
+assets/images/robina-weermeijer-Pw9aFhc92P8-unsplash_448.jpg
+assets/images/ronda-darby-HbMLSB-uhQY-unsplash_336.jpg
+assets/images/ross-parmly-rf6ywHVkrlY-unsplash_336.jpg
+assets/images/ruben-valenzuela-JEp9cl5jfZA-unsplash_684.jpg
+assets/images/rufinochka-XonjCOZZN_w-unsplash_448.jpg
+assets/images/ryan-plomp-TT6Hep-JzrU-unsplash_336.jpg
+assets/images/sabri-tuzcu-kxR3hh0IRHU-unsplash_336.jpg
+assets/images/sam-mcghee-4siwRamtFAk-unsplash_684.jpg
+assets/images/samuel-schroth-hyPt63Df3Dw-unsplash_150.jpg
+assets/images/sandip-roy-4hgTlYb9jzg-unsplash_448.jpg
+assets/images/sapan-patel-gmgWd0CgWQI-unsplash_150.jpg
+assets/images/sara-cottle-bGjtWs8sXT0-unsplash_448.jpg
+assets/images/sarah-dorweiler-gUPiTDBdRe4-unsplash_336.jpg
+assets/images/scott-graham-OQMZwNd3ThU-unsplash_448.jpg
+assets/images/scott-webb-U5kQvbQWoG0-unsplash_150.jpg
+assets/images/sean-lee-hDqRQmcjM3s-unsplash_336.jpg
+assets/images/sheelah-brennan-UOfERQF_pr4-unsplash_336.jpg
+assets/images/shubham-dhage-tT6GNIFkZv4-unsplash_150.jpg
+assets/images/sj-objio-8hHxO3iYuU0-unsplash_448.jpg
+assets/images/stan-georgiev-pvNxRUq7O7U-unsplash_150.jpg
+assets/images/stefan-stefancik-pzA7QWNCIYg-unsplash_150.jpg
+assets/images/stephen-phillips-hostreviews-co-uk-em37kS8WJJQ-unsplash_150.jpg
+assets/images/steven-van-bTPP3jBnOb8-unsplash_684.jpg
+assets/images/sven-mieke-Lx_GDv7VA9M-unsplash_150.jpg
+assets/images/tachina-lee--wjk_SSqCE4-unsplash_448.jpg
+assets/images/tadeusz-lakota-Tb38UzCvKCY-unsplash_336.jpg
+assets/images/tamanna-rumee-lpGm415q9JA-unsplash_336.jpg
+assets/images/tamara-bellis-IwVRO3TLjLc-unsplash_336.jpg
+assets/images/tania-mousinho-YlpfE9uCakE-unsplash_150.jpg
+assets/images/tatjana-petkevica-iad-dMBDdoo-unsplash_150.jpg
+assets/images/taylor-grote-UiVe5QvOhao-unsplash_150.jpg
+assets/images/tech-daily-vxTWpu14zeM-unsplash_150.jpg
+assets/images/thisisengineering-ZPeXrWxOjRQ-unsplash_336.jpg
+assets/images/tierra-mallorca-rgJ1J8SDEAY-unsplash_150.jpg
+assets/images/tingey-injury-law-firm-9SKhDFnw4c4-unsplash_448.jpg
+assets/images/toa-heftiba--abWByT3yg4-unsplash_336.jpg
+assets/images/toa-heftiba-4xe-yVFJCvw-unsplash_336.jpg
+assets/images/todd-trapani-vS54KomBEJU-unsplash_684.jpg
+assets/images/truckrun-XBWF6_TEsFM-unsplash_684.jpg
+assets/images/trung-thanh-LgdDeuBcgIY-unsplash_684.jpg
+assets/images/unseen-histories-4kYkKW8v8rY-unsplash_150.jpg
+assets/images/valery-tenevoy-c0VbjkPEfmM-unsplash_336.jpg
+assets/images/valtteri-laukkanen-9u9Pc0t9vKM-unsplash_150.jpg
+assets/images/vanilla-bear-films-JEwNQerg3Hs-unsplash_336.jpg
+assets/images/venti-views-KElJx4R4Py8-unsplash_684.jpg
+assets/images/viktor-talashuk-05HLFQu8bFw-unsplash_150.jpg
+assets/images/vladislav-babienko-KTpSVEcU0XU-unsplash_448.jpg
+assets/images/wan-san-yip-ID1yWa1Wpx0-unsplash_336.jpg
+assets/images/wesley-tingey-7BkCRNwh_V0-unsplash_150.jpg
+assets/images/will-shirley-xRKcHoCOA4Y-unsplash_448.jpg
+assets/images/xan-griffin-QxNkzEjB180-unsplash_150.jpg
+assets/images/yucel-moran-4ndj0pATzeM-unsplash_150.jpg
+assets/images/zac-gudakov-wwqZ8CM21gg-unsplash_150.jpg
+favicon.ico
+index.html
+placeholder_light.jpg
diff --git a/suites/newssite/news-next/public/resources.txt b/suites/newssite/news-next/public/resources.txt
new file mode 100644
index 000000000..1129d04ea
--- /dev/null
+++ b/suites/newssite/news-next/public/resources.txt
@@ -0,0 +1,354 @@
+404.html
+_nuxt/entry.4163a698.css
+_nuxt/entry.61b8a168.js
+_nuxt/error-404.23f2309d.css
+_nuxt/error-404.2a577bdf.js
+_nuxt/error-500.aa16ed4d.css
+_nuxt/error-500.c1eac2b8.js
+_nuxt/error-component.2cb571d2.js
+assets/images/2h-media-lPcQhLP-b4I-unsplash_150.jpg
+assets/images/aaron-huber-G7sE2S4Lab4-unsplash_336.jpg
+assets/images/adam-whitlock-I9j8Rk-JYFM-unsplash_684.jpg
+assets/images/aditya-chinchure-ZhQCZjr9fHo-unsplash_336.jpg
+assets/images/adrian-dascal-myAz-buELXs-unsplash_448.jpg
+assets/images/ahmet-kurt-5BGg2L5nhlU-unsplash_150.jpg
+assets/images/ahmet-kurt-WviyUzOg4RU-unsplash_150.jpg
+assets/images/alan-rodriguez-qrD-g7oc9is-unsplash_684.jpg
+assets/images/albert-stoynov-fEdf0fig3os-unsplash_448.jpg
+assets/images/alec-favale-dLctr-PqFys-unsplash_150.jpg
+assets/images/alev-takil-fYyYz38bUkQ-unsplash_150.jpg
+assets/images/alex-hudson-7AgqAZbogOQ-unsplash_448.jpg
+assets/images/alexander-grey-8lnbXtxFGZw-unsplash_684.jpg
+assets/images/alexander-kirov-YhDJXJjmxUQ-unsplash_336.jpg
+assets/images/alexandre-debieve-FO7JIlwjOtU-unsplash_336.jpg
+assets/images/alice-pasqual-Olki5QpHxts-unsplash_336.jpg
+assets/images/allan-wadsworth-Lp78NT-mf9o-unsplash_150.jpg
+assets/images/anastassia-anufrieva-ecHGTPfjNfA-unsplash_150.jpg
+assets/images/anatol-rurac-NeSj0i6HLak-unsplash_150.jpg
+assets/images/anatol-rurac-b5t2lqeCGfA-unsplash_150.jpg
+assets/images/andrew-solok-LbckXdUVOlY-unsplash_448.jpg
+assets/images/angela-loria-hFc0JEKD4Cc-unsplash_150.jpg
+assets/images/ani-kolleshi-7jjnJ-QA9fY-unsplash_150.jpg
+assets/images/anne-nygard-tcJ6sJTtTWI-unsplash_448.jpg
+assets/images/annie-spratt-IT6aov1ScW0-unsplash_150.jpg
+assets/images/annie-spratt-KiOHnBkLQQU-unsplash_684.jpg
+assets/images/ant-rozetsky-SLIFI67jv5k-unsplash_150.jpg
+assets/images/arnaud-jaegers-IBWJsMObnnU-unsplash_336.jpg
+assets/images/arnel-hasanovic-MNd-Rka1o0Q-unsplash_336.jpg
+assets/images/artur-voznenko-rwPIQQPz1ew-unsplash_150.jpg
+assets/images/arturo-rey-5yP83RhaFGA-unsplash_336.jpg
+assets/images/ashim-d-silva-ZmgJiztRHXE-unsplash_336.jpg
+assets/images/astronaud23-ox3t0m3PUqA-unsplash_336.jpg
+assets/images/austin-distel-97HfVpyNR1M-unsplash_448.jpg
+assets/images/austin-distel-rxpThOwuVgE-unsplash_448.jpg
+assets/images/axp-photography-v6pAkO31d50-unsplash_448.jpg
+assets/images/azhar-j-t2hgHV1R7_g-unsplash_448.jpg
+assets/images/benjamin-child-rOn57CBgyMo-unsplash_448.jpg
+assets/images/bermix-studio-yUnSMBogWNI-unsplash_150.jpg
+assets/images/bernd-dittrich-Xk1IfNnEhRA-unsplash_448.jpg
+assets/images/bianca-ackermann-qr0-lKAOZSk-unsplash_150.jpg
+assets/images/boris-stefanik-q49CgyIrLes-unsplash_150.jpg
+assets/images/boxed-water-is-better-y-TpYAlcBYM-unsplash_150.jpg
+assets/images/braden-collum-9HI8UJMSdZA-unsplash_336.jpg
+assets/images/bram-naus-oqnVnI5ixHg-unsplash_448.jpg
+assets/images/brigitte-tohm-iIupxcq-yH4-unsplash_150.jpg
+assets/images/brooke-lark-GJMlSBS0FhU-unsplash_150.jpg
+assets/images/brooke-lark-HjWzkqW1dgI-unsplash_336.jpg
+assets/images/brooke-lark-IDTEXXXfS44-unsplash_150.jpg
+assets/images/brooke-lark-lcZ9NxhOSlo-unsplash_448.jpg
+assets/images/brooke-lark-oaz0raysASk-unsplash_150.jpg
+assets/images/bruce-mars-xj8qrWvuOEs-unsplash_336.jpg
+assets/images/bruno-nascimento-PHIgYUGQPvU-unsplash_448.jpg
+assets/images/c-j-1GHqOftzYo0-unsplash_150.jpg
+assets/images/caglar-oskay-d0Be8Vs9XRk-unsplash_336.jpg
+assets/images/campaign-creators-pypeCEaJeZY-unsplash_448.jpg
+assets/images/cardmapr-nl-s8F8yglbpjo-unsplash_336.jpg
+assets/images/carl-nenzen-loven-c-pc2mP7hTs-unsplash_448.jpg
+assets/images/chad-stembridge-sEHrIPpkKQY-unsplash_150.jpg
+assets/images/chloe-taranto-x2zyAOmVNtM-unsplash_448.jpg
+assets/images/chris-leboutillier-TUJud0AWAPI-unsplash_150.jpg
+assets/images/chris-leboutillier-c7RWVGL8lPA-unsplash_150.jpg
+assets/images/christian-lue-8Yw6tsB8tnc-unsplash_448.jpg
+assets/images/christian-tenguan-P3gfVKhz8d0-unsplash_336.jpg
+assets/images/christina-rumpf-gUU4MF87Ipw-unsplash_150.jpg
+assets/images/clark-street-mercantile-P3pI6xzovu0-unsplash_336.jpg
+assets/images/claudio-schwarz-3cWxxW2ggKE-unsplash_336.jpg
+assets/images/clay-banks-BY-R0UNRE7w-unsplash_150.jpg
+assets/images/clay-banks-cisdc-344vo-unsplash_150.jpg
+assets/images/clem-onojeghuo-DoA2duXyzRM-unsplash_684.jpg
+assets/images/clem-onojeghuo-RLJnH4Mt9A0-unsplash_336.jpg
+assets/images/colin-lloyd-2ULmNrj44QY-unsplash_448.jpg
+assets/images/colin-lloyd-NKS5gg7rWGw-unsplash_150.jpg
+assets/images/colin-lloyd-uaM_Ijy_joY-unsplash_448.jpg
+assets/images/craig-manners-LvJCFOW3Ma8-unsplash_448.jpg
+assets/images/craig-melville-_JKymnZ1Uc4-unsplash_150.jpg
+assets/images/crystal-kwok-xD5SWy7hMbw-unsplash_448.jpg
+assets/images/dani-navarro-6CnGzrLwM28-unsplash_336.jpg
+assets/images/daniel-staple-N320vzTBviA-unsplash_684.jpg
+assets/images/david-beale--lQR8yeDzek-unsplash_336.jpg
+assets/images/david-everett-strickler-igCBFrMd11I-unsplash_684.jpg
+assets/images/david-lezcano-NfZiOJzZgcg-unsplash_336.jpg
+assets/images/david-vives-Nzbkev7SQTg-unsplash_448.jpg
+assets/images/declan-sun-misAHv6YWkI-unsplash_336.jpg
+assets/images/denys-nevozhai-7nrsVjvALnA-unsplash_150.jpg
+assets/images/denys-nevozhai-z0nVqfrOqWA-unsplash_150.jpg
+assets/images/derick-mckinney-muhK4oeYJiU-unsplash_150.jpg
+assets/images/devi-puspita-amartha-yahya-7ln0pST_O8M-unsplash_448.jpg
+assets/images/dibakar-roy-K9JwokzSvrc-unsplash_150.jpg
+assets/images/diego-jimenez-A-NVHPka9Rk-unsplash_336.jpg
+assets/images/dillon-kydd-2keCPb73aQY-unsplash_336.jpg
+assets/images/dino-reichmuth-A5rCN8626Ck-unsplash_336.jpg
+assets/images/dom-hill-nimElTcTNyY-unsplash_336.jpg
+assets/images/dominic-bieri-vXRt4rFr4hI-unsplash_336.jpg
+assets/images/dominic-hampton-_8aRumOixtI-unsplash_684.jpg
+assets/images/domino-studio-164_6wVEHfI-unsplash_336.jpg
+assets/images/drew-hays-tGYrlchfObE-unsplash_336.jpg
+assets/images/dyana-wing-so-Og16Foo-pd8-unsplash_150.jpg
+assets/images/dylan-calluy-j9q18vvHitg-unsplash_150.jpg
+assets/images/ednilson-cardoso-dos-santos-haiooWA_weo-unsplash_336.jpg
+assets/images/egor-myznik-GFHKMW6KiJ0-unsplash_684.jpg
+assets/images/ehimetalor-akhere-unuabona-yS0uBoF4xDo-unsplash_448.jpg
+assets/images/elimende-inagella-7OxV_qDiGRI-unsplash_448.jpg
+assets/images/emmanuel-ikwuegbu-ceawFbpA-14-unsplash_448.jpg
+assets/images/emran-yousof-k8ZbMQWbx34-unsplash_336.jpg
+assets/images/erik-mclean-ByjIzFupcHo-unsplash_336.jpg
+assets/images/esther-ann-glpYh1cWf0o-unsplash_448.jpg
+assets/images/etienne-girardet-RqOyRtYGhLg-unsplash_448.jpg
+assets/images/evie-calder-97CO-A4P0GQ-unsplash_336.jpg
+assets/images/fanga-studio-bOfCOy3_4wU-unsplash_448.jpg
+assets/images/fons-heijnsbroek-vBfEZdpEr-E-unsplash_448.jpg
+assets/images/freestocks-nss2eRzQwgw-unsplash_150.jpg
+assets/images/gaku-suyama-VyiLZUcdJv0-unsplash_150.jpg
+assets/images/geert-pieters-NbpUM86Jo8Y-unsplash_150.jpg
+assets/images/geoff-scott-8lUTnkZXZSA-unsplash_336.jpg
+assets/images/glenn-carstens-peters-npxXWgQ33ZQ-unsplash_336.jpg
+assets/images/greg-rakozy-oMpAz-DN-9I-unsplash_684.jpg
+assets/images/gregory-hayes-h5cd51KXmRQ-unsplash_684.jpg
+assets/images/guillaume-perigois-HL4LEIyGEYU-unsplash_150.jpg
+assets/images/hannah-busing-0V6DmTuJaIk-unsplash_448.jpg
+assets/images/hansjorg-keller-CQqyv5uldW4-unsplash_448.jpg
+assets/images/harley-davidson-fFbUdx80oCc-unsplash_336.jpg
+assets/images/harold-mendoza-6xafY_AE1LM-unsplash_150.jpg
+assets/images/harri-kuokkanen-SEtUeWL8bIQ-unsplash_336.jpg
+assets/images/hassan-kibwana-fmXLB_uHIh4-unsplash_448.jpg
+assets/images/heather-ford-5gkYsrH_ebY-unsplash_336.jpg
+assets/images/heidi-fin-2TLREZi7BUg-unsplash_336.jpg
+assets/images/heidi-kaden-L_U4jhwZ6hY-unsplash_448.jpg
+assets/images/hush-naidoo-jade-photography-ZCO_5Y29s8k-unsplash_150.jpg
+assets/images/hush-naidoo-jade-photography-Zp7ebyti3MU-unsplash_336.jpg
+assets/images/hyttalo-souza-a1p0Z7RSkL8-unsplash_684.jpg
+assets/images/ian-hutchinson-P8rgDtEFn7s-unsplash_150.jpg
+assets/images/ian-taylor-kAWTCt7p7rs-unsplash_448.jpg
+assets/images/ibrahim-rifath-OApHds2yEGQ-unsplash_150.jpg
+assets/images/icons8-team-k5fUTay0ghw-unsplash_336.jpg
+assets/images/ilaria-de-bona-RuFfpBsaRY0-unsplash_336.jpg
+assets/images/ilyass-seddoug-06w8RxgSzF0-unsplash_150.jpg
+assets/images/inaki-del-olmo-NIJuEQw0RKg-unsplash_336.jpg
+assets/images/irwan-rbDE93-0hHs-unsplash_684.jpg
+assets/images/isai-ramos-Sp70YIWtuM8-unsplash_336.jpg
+assets/images/isaw-company-Oqv_bQbZgS8-unsplash_336.jpg
+assets/images/ixography-05Q_XPF_YKs-unsplash_336.jpg
+assets/images/jacob-licht-8nA_iHrxHIo-unsplash_150.jpg
+assets/images/jakob-dalbjorn-cuKJre3nyYc-unsplash_448.jpg
+assets/images/jakub-zerdzicki-VnTR3XFwxWs-unsplash_150.jpg
+assets/images/jakub-zerdzicki-qcRGVZNZ5js-unsplash_336.jpg
+assets/images/jannis-brandt-mmsQUgMLqUo-unsplash_150.jpg
+assets/images/jaron-nix-7wWRXewYCH4-unsplash_684.jpg
+assets/images/jay-clark-P3sLerH3UmM-unsplash_336.jpg
+assets/images/jed-owen-ajZibDGpPew-unsplash_336.jpg
+assets/images/jessica-weiller-So4eFi-d1nc-unsplash_336.jpg
+assets/images/jon-tyson-0BLE1xp5HBQ-unsplash_150.jpg
+assets/images/jon-tyson-ne2mqMgER8Y-unsplash_150.jpg
+assets/images/jonathan-borba-lrQPTQs7nQQ-unsplash_150.jpg
+assets/images/jonathan-simcoe-S9J1HqoL9ns-unsplash_150.jpg
+assets/images/jordhan-madec-AD5ylD2T0UY-unsplash_336.jpg
+assets/images/joseph-gonzalez-QaGDmf5tMiE-unsplash_150.jpg
+assets/images/joshua-rawson-harris-YNaSz-E7Qss-unsplash_336.jpg
+assets/images/juli-kosolapova-4PE3X9eKsu4-unsplash_336.jpg
+assets/images/junko-nakase-Q-72wa9-7Dg-unsplash_336.jpg
+assets/images/k8-uYf_C34PAao-unsplash_336.jpg
+assets/images/keagan-henman-xPJYL0l5Ii8-unsplash_336.jpg
+assets/images/kelli-dougal-vbiQ_7vwfrs-unsplash_448.jpg
+assets/images/kelly-sikkema-A-lovieAmjA-unsplash_150.jpg
+assets/images/kelly-sikkema-WIYtZU3PxsI-unsplash_448.jpg
+assets/images/kenny-K72n3BHgHCg-unsplash_336.jpg
+assets/images/kenny-eliason-4N3iHYmqy_E-unsplash_336.jpg
+assets/images/kenny-eliason-5ddH9Y2accI-unsplash_150.jpg
+assets/images/kevin-rajaram-qhixFFO8EWQ-unsplash_336.jpg
+assets/images/kevin-wang-t7vEVxwGGm0-unsplash_336.jpg
+assets/images/kitthitorn-chaiyuthapoom-TOH_gw5dd20-unsplash_336.jpg
+assets/images/koshu-kunii-ADLj1cyFfV8-unsplash_150.jpg
+assets/images/koshu-kunii-cWEGNQqcImk-unsplash_336.jpg
+assets/images/koshu-kunii-v9ferChkC9A-unsplash_150.jpg
+assets/images/kyle-glenn-gcw_WWu_uBQ-unsplash_336.jpg
+assets/images/leah-hetteberg-kTVN2l0ZUv8-unsplash_684.jpg
+assets/images/leisara-studio-EzzW1oNek-I-unsplash_336.jpg
+assets/images/leon-seibert-Xs3al4NpIFQ-unsplash_336.jpg
+assets/images/libre-leung-9O0Sp22DF0I-unsplash_336.jpg
+assets/images/linkedin-sales-solutions--AXDunSs-n4-unsplash_150.jpg
+assets/images/little-plant-TZw891-oMio-unsplash_150.jpg
+assets/images/lorenzo-moschi-N7ypjB7HKIk-unsplash_150.jpg
+assets/images/louis-reed-pwcKF7L4-no-unsplash_448.jpg
+assets/images/luis-cortes-QrPDA15pRkM-unsplash_336.jpg
+assets/images/lukasz-radziejewski-cg4MzL_eSvU-unsplash_336.jpg
+assets/images/m-ZzOa5G8hSPI-unsplash_150.jpg
+assets/images/maarten-van-den-heuvel-gZXx8lKAb7Y-unsplash_150.jpg
+assets/images/mac-blades-jpgJSBQtw5U-unsplash_336.jpg
+assets/images/maddi-bazzocco-qKbHvzXb85A-unsplash_150.jpg
+assets/images/maja-kochanowska-EiJQdDI_t_Y-unsplash_150.jpg
+assets/images/maksim-larin-tecILYzVAzg-unsplash_336.jpg
+assets/images/maksym-harbar-okn8ZIjPMxI-unsplash_448.jpg
+assets/images/maksym-kaharlytskyi-Y0z9MyDsrU0-unsplash_150.jpg
+assets/images/marc-olivier-jodoin-_eclsGKsUdo-unsplash_448.jpg
+assets/images/marcelo-leal-6pcGTJDuf6M-unsplash_448.jpg
+assets/images/marco-oriolesi-wqLGlhjr6Og-unsplash_684.jpg
+assets/images/marek-studzinski-9U9I-eVx9nI-unsplash_150.jpg
+assets/images/maria-lysenko-tZvkSuBleso-unsplash_150.jpg
+assets/images/marius-oprea-ySA9uj7zSmw-unsplash_150.jpg
+assets/images/markus-spiske-7PMGUqYQpYc-unsplash_150.jpg
+assets/images/markus-spiske-Nph1oyRsHm4-unsplash_150.jpg
+assets/images/markus-spiske-WUehAgqO5hE-unsplash_684.jpg
+assets/images/markus-spiske-jgOkEjVw-KM-unsplash_150.jpg
+assets/images/markus-spiske-lUc5pRFB25s-unsplash_336.jpg
+assets/images/markus-spiske-p2Xor4Lbrrk-unsplash_150.jpg
+assets/images/markus-spiske-wIUxLHndcLw-unsplash_150.jpg
+assets/images/martha-dominguez-de-gouveia-k-NnVZ-z26w-unsplash_150.jpg
+assets/images/martin-jernberg-jVNWCFwdjZU-unsplash_448.jpg
+assets/images/martin-podsiad-wrdtA9lew9E-unsplash_150.jpg
+assets/images/mathias-reding-yfXhqAW5X0c-unsplash_150.jpg
+assets/images/mathurin-napoly-matnapo-ejWJ3a92FEs-unsplash_336.jpg
+assets/images/mathurin-napoly-matnapo-pIJ34ZrZEEw-unsplash_150.jpg
+assets/images/matt-popovich-7mqsZsE6FaU-unsplash_336.jpg
+assets/images/matthew-ball-3wW2fBjptQo-unsplash_448.jpg
+assets/images/matthew-henry-0Ol8Sa2n21c-unsplash_150.jpg
+assets/images/matthew-landers-v8UgmRa6UDg-unsplash_150.jpg
+assets/images/matthew-tenbruggencate-0HJWobhGhJs-unsplash_150.jpg
+assets/images/matthias-heil-lDOEwat_MPs-unsplash_336.jpg
+assets/images/mattia-bericchia-xkD79yf4tb8-unsplash_150.jpg
+assets/images/maxim-hopman-IayKLkmz6g0-unsplash_448.jpg
+assets/images/maximilian-bungart-nwqfl_HtJjk-unsplash_150.jpg
+assets/images/melissa-askew-8n00CqwnqO8-unsplash_448.jpg
+assets/images/memento-media-XhYq-5KnxSk-unsplash_448.jpg
+assets/images/mesut-kaya-eOcyhe5-9sQ-unsplash_336.jpg
+assets/images/metin-ozer-hShrr0WvrQs-unsplash_336.jpg
+assets/images/microsoft-365-f1zQuagWCTA-unsplash_336.jpg
+assets/images/miguel-bruna-TzVN0xQhWaQ-unsplash_150.jpg
+assets/images/mika-baumeister-Hm4zYX-BDxk-unsplash_150.jpg
+assets/images/mika-baumeister-bGZZBDvh8s4-unsplash_150.jpg
+assets/images/mika-baumeister-jXPQY1em3Ew-unsplash_150.jpg
+assets/images/mike-enerio-7ryPpZK1qV8-unsplash_448.jpg
+assets/images/milad-fakurian-58Z17lnVS4U-unsplash_684.jpg
+assets/images/mr-cup-fabien-barral-Mwuod2cm8g4-unsplash_448.jpg
+assets/images/mr-lee-f4RBYsY2hxA-unsplash_150.jpg
+assets/images/mufid-majnun-J12RfFH-2ZE-unsplash_684.jpg
+assets/images/mufid-majnun-tJJIGh703I4-unsplash_336.jpg
+assets/images/mulyadi-JeCNRxGLSp4-unsplash_150.jpg
+assets/images/myriam-zilles-KltoLK6Mk-g-unsplash_336.jpg
+assets/images/nasa-Q1p7bh3SHj8-unsplash_150.jpg
+assets/images/nasa-cIX5TlQ_FgM-unsplash_448.jpg
+assets/images/nasa-dCgbRAQmTQA-unsplash_684.jpg
+assets/images/nathan-cima-TQuq2OtLBNU-unsplash_150.jpg
+assets/images/nathan-dumlao-bRdRUUtbxO0-unsplash_150.jpg
+assets/images/nathan-dumlao-laCrvNG3F_I-unsplash_336.jpg
+assets/images/national-cancer-institute-A2CK97sS0ns-unsplash_684.jpg
+assets/images/national-cancer-institute-GcrSgHDrniY-unsplash_336.jpg
+assets/images/national-cancer-institute-KrsoedfRAf4-unsplash_336.jpg
+assets/images/national-cancer-institute-L7en7Lb-Ovc-unsplash_336.jpg
+assets/images/national-cancer-institute-NFvdKIhxYlU-unsplash_336.jpg
+assets/images/national-cancer-institute-SMxzEaidR20-unsplash_336.jpg
+assets/images/national-cancer-institute-YvvFRJgWShM-unsplash_336.jpg
+assets/images/national-cancer-institute-cw2Zn2ZQ9YQ-unsplash_336.jpg
+assets/images/neil-thomas-SIU1Glk6v5k-unsplash_150.jpg
+assets/images/nikita-karimov-lvJZhHOIJJ4-unsplash_448.jpg
+assets/images/no-revisions-UhpAf0ySwuk-unsplash_336.jpg
+assets/images/noaa--urO88VoCRE-unsplash_336.jpg
+assets/images/noaa-FY3vXNBl1v4-unsplash_336.jpg
+assets/images/noaa-I323ZqSkkn8-unsplash_448.jpg
+assets/images/noaa-Led9c1SSNFo-unsplash_684.jpg
+assets/images/nonresident-nizUHtSIrKM-unsplash_150.jpg
+assets/images/nuno-alberto-MykFFC5zolE-unsplash_448.jpg
+assets/images/oguzhan-edman-ZWPkHLRu3_4-unsplash_336.jpg
+assets/images/olga-guryanova-ft7vJxwl2RY-unsplash_336.jpg
+assets/images/olga-guryanova-tMFeatBSS4s-unsplash_336.jpg
+assets/images/paola-chaaya-QrbuLFT6ypw-unsplash_336.jpg
+assets/images/parker-johnson-v0OWc_skg0g-unsplash_336.jpg
+assets/images/pascal-bullan-M8sQPAfhPdk-unsplash_336.jpg
+assets/images/pat-taylor-12V36G17IbQ-unsplash_336.jpg
+assets/images/pat-whelen-68OkRwuOeyQ-unsplash_150.jpg
+assets/images/patti-black-FnV-PjAYHCI-unsplash_150.jpg
+assets/images/paul-bill-HLuPjCa6IYw-unsplash_150.jpg
+assets/images/paul-hanaoka-s0XabTAKvak-unsplash_684.jpg
+assets/images/peter-conlan-LEgwEaBVGMo-unsplash_448.jpg
+assets/images/peter-lawrence-rXZa4ufjoGw-unsplash_448.jpg
+assets/images/peter-thomas-17EJD0QdKFI-unsplash_150.jpg
+assets/images/phil-hearing-bu27Y0xg7dk-unsplash_150.jpg
+assets/images/pierre-bamin-lM4_Nmcj4Xk-unsplash_150.jpg
+assets/images/piron-guillaume-U4FyCp3-KzY-unsplash_150.jpg
+assets/images/prince-abban-0OUHhvNIbYc-unsplash_150.jpg
+assets/images/priscilla-du-preez-GgtxccOjIXE-unsplash_448.jpg
+assets/images/quick-ps-sW41y3lETZk-unsplash_150.jpg
+assets/images/quino-al-KydWCDJe9s0-unsplash_150.jpg
+assets/images/razvan-chisu-Ua-agENjmI4-unsplash_448.jpg
+assets/images/rebe-pascual-SACRQSof7Qw-unsplash_448.jpg
+assets/images/red-dot-Q98X_JVRGS0-unsplash_336.jpg
+assets/images/redd-f-N9CYH-H_gBE-unsplash_684.jpg
+assets/images/rio-lecatompessy-cfDURuQKABk-unsplash_336.jpg
+assets/images/robert-bye-EILw-nEK46k-unsplash_150.jpg
+assets/images/robert-bye-xHUZuSwVJg4-unsplash_336.jpg
+assets/images/roberto-junior-4fsCBcZt9H8-unsplash_150.jpg
+assets/images/robina-weermeijer-Pw9aFhc92P8-unsplash_448.jpg
+assets/images/ronda-darby-HbMLSB-uhQY-unsplash_336.jpg
+assets/images/ross-parmly-rf6ywHVkrlY-unsplash_336.jpg
+assets/images/ruben-valenzuela-JEp9cl5jfZA-unsplash_684.jpg
+assets/images/rufinochka-XonjCOZZN_w-unsplash_448.jpg
+assets/images/ryan-plomp-TT6Hep-JzrU-unsplash_336.jpg
+assets/images/sabri-tuzcu-kxR3hh0IRHU-unsplash_336.jpg
+assets/images/sam-mcghee-4siwRamtFAk-unsplash_684.jpg
+assets/images/samuel-schroth-hyPt63Df3Dw-unsplash_150.jpg
+assets/images/sandip-roy-4hgTlYb9jzg-unsplash_448.jpg
+assets/images/sapan-patel-gmgWd0CgWQI-unsplash_150.jpg
+assets/images/sara-cottle-bGjtWs8sXT0-unsplash_448.jpg
+assets/images/sarah-dorweiler-gUPiTDBdRe4-unsplash_336.jpg
+assets/images/scott-graham-OQMZwNd3ThU-unsplash_448.jpg
+assets/images/scott-webb-U5kQvbQWoG0-unsplash_150.jpg
+assets/images/sean-lee-hDqRQmcjM3s-unsplash_336.jpg
+assets/images/sheelah-brennan-UOfERQF_pr4-unsplash_336.jpg
+assets/images/shubham-dhage-tT6GNIFkZv4-unsplash_150.jpg
+assets/images/sj-objio-8hHxO3iYuU0-unsplash_448.jpg
+assets/images/stan-georgiev-pvNxRUq7O7U-unsplash_150.jpg
+assets/images/stefan-stefancik-pzA7QWNCIYg-unsplash_150.jpg
+assets/images/stephen-phillips-hostreviews-co-uk-em37kS8WJJQ-unsplash_150.jpg
+assets/images/steven-van-bTPP3jBnOb8-unsplash_684.jpg
+assets/images/sven-mieke-Lx_GDv7VA9M-unsplash_150.jpg
+assets/images/tachina-lee--wjk_SSqCE4-unsplash_448.jpg
+assets/images/tadeusz-lakota-Tb38UzCvKCY-unsplash_336.jpg
+assets/images/tamanna-rumee-lpGm415q9JA-unsplash_336.jpg
+assets/images/tamara-bellis-IwVRO3TLjLc-unsplash_336.jpg
+assets/images/tania-mousinho-YlpfE9uCakE-unsplash_150.jpg
+assets/images/tatjana-petkevica-iad-dMBDdoo-unsplash_150.jpg
+assets/images/taylor-grote-UiVe5QvOhao-unsplash_150.jpg
+assets/images/tech-daily-vxTWpu14zeM-unsplash_150.jpg
+assets/images/thisisengineering-ZPeXrWxOjRQ-unsplash_336.jpg
+assets/images/tierra-mallorca-rgJ1J8SDEAY-unsplash_150.jpg
+assets/images/tingey-injury-law-firm-9SKhDFnw4c4-unsplash_448.jpg
+assets/images/toa-heftiba--abWByT3yg4-unsplash_336.jpg
+assets/images/toa-heftiba-4xe-yVFJCvw-unsplash_336.jpg
+assets/images/todd-trapani-vS54KomBEJU-unsplash_684.jpg
+assets/images/truckrun-XBWF6_TEsFM-unsplash_684.jpg
+assets/images/trung-thanh-LgdDeuBcgIY-unsplash_684.jpg
+assets/images/unseen-histories-4kYkKW8v8rY-unsplash_150.jpg
+assets/images/valery-tenevoy-c0VbjkPEfmM-unsplash_336.jpg
+assets/images/valtteri-laukkanen-9u9Pc0t9vKM-unsplash_150.jpg
+assets/images/vanilla-bear-films-JEwNQerg3Hs-unsplash_336.jpg
+assets/images/venti-views-KElJx4R4Py8-unsplash_684.jpg
+assets/images/viktor-talashuk-05HLFQu8bFw-unsplash_150.jpg
+assets/images/vladislav-babienko-KTpSVEcU0XU-unsplash_448.jpg
+assets/images/wan-san-yip-ID1yWa1Wpx0-unsplash_336.jpg
+assets/images/wesley-tingey-7BkCRNwh_V0-unsplash_150.jpg
+assets/images/will-shirley-xRKcHoCOA4Y-unsplash_448.jpg
+assets/images/xan-griffin-QxNkzEjB180-unsplash_150.jpg
+assets/images/yucel-moran-4ndj0pATzeM-unsplash_150.jpg
+assets/images/zac-gudakov-wwqZ8CM21gg-unsplash_150.jpg
+favicon.ico
+index.html
+placeholder_light.jpg
diff --git a/suites/newssite/news-nuxt/dist/resources.txt b/suites/newssite/news-nuxt/dist/resources.txt
new file mode 100644
index 000000000..80cf9f3b0
--- /dev/null
+++ b/suites/newssite/news-nuxt/dist/resources.txt
@@ -0,0 +1,355 @@
+favicon.ico
+index.html
+placeholder_light.jpg
+404.html
+_nuxt/error-500.c1eac2b8.js
+_nuxt/entry.61b8a168.js
+_nuxt/error-500.aa16ed4d.css
+_nuxt/error-component.2cb571d2.js
+_nuxt/error-404.2a577bdf.js
+_nuxt/error-404.23f2309d.css
+_nuxt/entry.4163a698.css
+assets/images/mika-baumeister-Hm4zYX-BDxk-unsplash_150.jpg
+assets/images/kelly-sikkema-WIYtZU3PxsI-unsplash_448.jpg
+assets/images/nathan-cima-TQuq2OtLBNU-unsplash_150.jpg
+assets/images/robert-bye-EILw-nEK46k-unsplash_150.jpg
+assets/images/jannis-brandt-mmsQUgMLqUo-unsplash_150.jpg
+assets/images/colin-lloyd-uaM_Ijy_joY-unsplash_448.jpg
+assets/images/mathurin-napoly-matnapo-ejWJ3a92FEs-unsplash_336.jpg
+assets/images/shubham-dhage-tT6GNIFkZv4-unsplash_150.jpg
+assets/images/jakub-zerdzicki-VnTR3XFwxWs-unsplash_150.jpg
+assets/images/mulyadi-JeCNRxGLSp4-unsplash_150.jpg
+assets/images/artur-voznenko-rwPIQQPz1ew-unsplash_150.jpg
+assets/images/matthew-henry-0Ol8Sa2n21c-unsplash_150.jpg
+assets/images/caglar-oskay-d0Be8Vs9XRk-unsplash_336.jpg
+assets/images/marc-olivier-jodoin-_eclsGKsUdo-unsplash_448.jpg
+assets/images/ilaria-de-bona-RuFfpBsaRY0-unsplash_336.jpg
+assets/images/juli-kosolapova-4PE3X9eKsu4-unsplash_336.jpg
+assets/images/mike-enerio-7ryPpZK1qV8-unsplash_448.jpg
+assets/images/national-cancer-institute-KrsoedfRAf4-unsplash_336.jpg
+assets/images/yucel-moran-4ndj0pATzeM-unsplash_150.jpg
+assets/images/guillaume-perigois-HL4LEIyGEYU-unsplash_150.jpg
+assets/images/anatol-rurac-b5t2lqeCGfA-unsplash_150.jpg
+assets/images/ehimetalor-akhere-unuabona-yS0uBoF4xDo-unsplash_448.jpg
+assets/images/bernd-dittrich-Xk1IfNnEhRA-unsplash_448.jpg
+assets/images/rio-lecatompessy-cfDURuQKABk-unsplash_336.jpg
+assets/images/alev-takil-fYyYz38bUkQ-unsplash_150.jpg
+assets/images/national-cancer-institute-L7en7Lb-Ovc-unsplash_336.jpg
+assets/images/azhar-j-t2hgHV1R7_g-unsplash_448.jpg
+assets/images/olga-guryanova-tMFeatBSS4s-unsplash_336.jpg
+assets/images/louis-reed-pwcKF7L4-no-unsplash_448.jpg
+assets/images/maximilian-bungart-nwqfl_HtJjk-unsplash_150.jpg
+assets/images/hannah-busing-0V6DmTuJaIk-unsplash_448.jpg
+assets/images/sheelah-brennan-UOfERQF_pr4-unsplash_336.jpg
+assets/images/tania-mousinho-YlpfE9uCakE-unsplash_150.jpg
+assets/images/brooke-lark-IDTEXXXfS44-unsplash_150.jpg
+assets/images/paul-hanaoka-s0XabTAKvak-unsplash_684.jpg
+assets/images/quino-al-KydWCDJe9s0-unsplash_150.jpg
+assets/images/christina-rumpf-gUU4MF87Ipw-unsplash_150.jpg
+assets/images/sean-lee-hDqRQmcjM3s-unsplash_336.jpg
+assets/images/albert-stoynov-fEdf0fig3os-unsplash_448.jpg
+assets/images/campaign-creators-pypeCEaJeZY-unsplash_448.jpg
+assets/images/kelli-dougal-vbiQ_7vwfrs-unsplash_448.jpg
+assets/images/mathias-reding-yfXhqAW5X0c-unsplash_150.jpg
+assets/images/valtteri-laukkanen-9u9Pc0t9vKM-unsplash_150.jpg
+assets/images/noaa--urO88VoCRE-unsplash_336.jpg
+assets/images/stefan-stefancik-pzA7QWNCIYg-unsplash_150.jpg
+assets/images/sabri-tuzcu-kxR3hh0IRHU-unsplash_336.jpg
+assets/images/nasa-cIX5TlQ_FgM-unsplash_448.jpg
+assets/images/nonresident-nizUHtSIrKM-unsplash_150.jpg
+assets/images/glenn-carstens-peters-npxXWgQ33ZQ-unsplash_336.jpg
+assets/images/noaa-I323ZqSkkn8-unsplash_448.jpg
+assets/images/melissa-askew-8n00CqwnqO8-unsplash_448.jpg
+assets/images/dyana-wing-so-Og16Foo-pd8-unsplash_150.jpg
+assets/images/crystal-kwok-xD5SWy7hMbw-unsplash_448.jpg
+assets/images/sven-mieke-Lx_GDv7VA9M-unsplash_150.jpg
+assets/images/national-cancer-institute-SMxzEaidR20-unsplash_336.jpg
+assets/images/maddi-bazzocco-qKbHvzXb85A-unsplash_150.jpg
+assets/images/junko-nakase-Q-72wa9-7Dg-unsplash_336.jpg
+assets/images/gregory-hayes-h5cd51KXmRQ-unsplash_684.jpg
+assets/images/clark-street-mercantile-P3pI6xzovu0-unsplash_336.jpg
+assets/images/milad-fakurian-58Z17lnVS4U-unsplash_684.jpg
+assets/images/2h-media-lPcQhLP-b4I-unsplash_150.jpg
+assets/images/jaron-nix-7wWRXewYCH4-unsplash_684.jpg
+assets/images/alexandre-debieve-FO7JIlwjOtU-unsplash_336.jpg
+assets/images/jay-clark-P3sLerH3UmM-unsplash_336.jpg
+assets/images/national-cancer-institute-GcrSgHDrniY-unsplash_336.jpg
+assets/images/axp-photography-v6pAkO31d50-unsplash_448.jpg
+assets/images/maja-kochanowska-EiJQdDI_t_Y-unsplash_150.jpg
+assets/images/pierre-bamin-lM4_Nmcj4Xk-unsplash_150.jpg
+assets/images/koshu-kunii-v9ferChkC9A-unsplash_150.jpg
+assets/images/clay-banks-BY-R0UNRE7w-unsplash_150.jpg
+assets/images/annie-spratt-IT6aov1ScW0-unsplash_150.jpg
+assets/images/mesut-kaya-eOcyhe5-9sQ-unsplash_336.jpg
+assets/images/tierra-mallorca-rgJ1J8SDEAY-unsplash_150.jpg
+assets/images/tatjana-petkevica-iad-dMBDdoo-unsplash_150.jpg
+assets/images/luis-cortes-QrPDA15pRkM-unsplash_336.jpg
+assets/images/martha-dominguez-de-gouveia-k-NnVZ-z26w-unsplash_150.jpg
+assets/images/lukasz-radziejewski-cg4MzL_eSvU-unsplash_336.jpg
+assets/images/koshu-kunii-ADLj1cyFfV8-unsplash_150.jpg
+assets/images/erik-mclean-ByjIzFupcHo-unsplash_336.jpg
+assets/images/maksym-harbar-okn8ZIjPMxI-unsplash_448.jpg
+assets/images/razvan-chisu-Ua-agENjmI4-unsplash_448.jpg
+assets/images/markus-spiske-p2Xor4Lbrrk-unsplash_150.jpg
+assets/images/stan-georgiev-pvNxRUq7O7U-unsplash_150.jpg
+assets/images/kevin-wang-t7vEVxwGGm0-unsplash_336.jpg
+assets/images/harley-davidson-fFbUdx80oCc-unsplash_336.jpg
+assets/images/xan-griffin-QxNkzEjB180-unsplash_150.jpg
+assets/images/jon-tyson-ne2mqMgER8Y-unsplash_150.jpg
+assets/images/maksim-larin-tecILYzVAzg-unsplash_336.jpg
+assets/images/linkedin-sales-solutions--AXDunSs-n4-unsplash_150.jpg
+assets/images/kitthitorn-chaiyuthapoom-TOH_gw5dd20-unsplash_336.jpg
+assets/images/patti-black-FnV-PjAYHCI-unsplash_150.jpg
+assets/images/red-dot-Q98X_JVRGS0-unsplash_336.jpg
+assets/images/mathurin-napoly-matnapo-pIJ34ZrZEEw-unsplash_150.jpg
+assets/images/martin-podsiad-wrdtA9lew9E-unsplash_150.jpg
+assets/images/martin-jernberg-jVNWCFwdjZU-unsplash_448.jpg
+assets/images/bruce-mars-xj8qrWvuOEs-unsplash_336.jpg
+assets/images/cardmapr-nl-s8F8yglbpjo-unsplash_336.jpg
+assets/images/mufid-majnun-tJJIGh703I4-unsplash_336.jpg
+assets/images/vladislav-babienko-KTpSVEcU0XU-unsplash_448.jpg
+assets/images/matt-popovich-7mqsZsE6FaU-unsplash_336.jpg
+assets/images/mac-blades-jpgJSBQtw5U-unsplash_336.jpg
+assets/images/carl-nenzen-loven-c-pc2mP7hTs-unsplash_448.jpg
+assets/images/arnel-hasanovic-MNd-Rka1o0Q-unsplash_336.jpg
+assets/images/dani-navarro-6CnGzrLwM28-unsplash_336.jpg
+assets/images/pat-taylor-12V36G17IbQ-unsplash_336.jpg
+assets/images/jonathan-simcoe-S9J1HqoL9ns-unsplash_150.jpg
+assets/images/leah-hetteberg-kTVN2l0ZUv8-unsplash_684.jpg
+assets/images/viktor-talashuk-05HLFQu8bFw-unsplash_150.jpg
+assets/images/kyle-glenn-gcw_WWu_uBQ-unsplash_336.jpg
+assets/images/will-shirley-xRKcHoCOA4Y-unsplash_448.jpg
+assets/images/k8-uYf_C34PAao-unsplash_336.jpg
+assets/images/david-vives-Nzbkev7SQTg-unsplash_448.jpg
+assets/images/anne-nygard-tcJ6sJTtTWI-unsplash_448.jpg
+assets/images/scott-webb-U5kQvbQWoG0-unsplash_150.jpg
+assets/images/m-ZzOa5G8hSPI-unsplash_150.jpg
+assets/images/adam-whitlock-I9j8Rk-JYFM-unsplash_684.jpg
+assets/images/alan-rodriguez-qrD-g7oc9is-unsplash_684.jpg
+assets/images/braden-collum-9HI8UJMSdZA-unsplash_336.jpg
+assets/images/hansjorg-keller-CQqyv5uldW4-unsplash_448.jpg
+assets/images/denys-nevozhai-7nrsVjvALnA-unsplash_150.jpg
+assets/images/samuel-schroth-hyPt63Df3Dw-unsplash_150.jpg
+assets/images/prince-abban-0OUHhvNIbYc-unsplash_150.jpg
+assets/images/chloe-taranto-x2zyAOmVNtM-unsplash_448.jpg
+assets/images/venti-views-KElJx4R4Py8-unsplash_684.jpg
+assets/images/peter-thomas-17EJD0QdKFI-unsplash_150.jpg
+assets/images/hush-naidoo-jade-photography-Zp7ebyti3MU-unsplash_336.jpg
+assets/images/pascal-bullan-M8sQPAfhPdk-unsplash_336.jpg
+assets/images/leisara-studio-EzzW1oNek-I-unsplash_336.jpg
+assets/images/arnaud-jaegers-IBWJsMObnnU-unsplash_336.jpg
+assets/images/aaron-huber-G7sE2S4Lab4-unsplash_336.jpg
+assets/images/todd-trapani-vS54KomBEJU-unsplash_684.jpg
+assets/images/ahmet-kurt-WviyUzOg4RU-unsplash_150.jpg
+assets/images/chris-leboutillier-TUJud0AWAPI-unsplash_150.jpg
+assets/images/clay-banks-cisdc-344vo-unsplash_150.jpg
+assets/images/marek-studzinski-9U9I-eVx9nI-unsplash_150.jpg
+assets/images/miguel-bruna-TzVN0xQhWaQ-unsplash_150.jpg
+assets/images/tamanna-rumee-lpGm415q9JA-unsplash_336.jpg
+assets/images/redd-f-N9CYH-H_gBE-unsplash_684.jpg
+assets/images/emmanuel-ikwuegbu-ceawFbpA-14-unsplash_448.jpg
+assets/images/priscilla-du-preez-GgtxccOjIXE-unsplash_448.jpg
+assets/images/austin-distel-97HfVpyNR1M-unsplash_448.jpg
+assets/images/craig-melville-_JKymnZ1Uc4-unsplash_150.jpg
+assets/images/truckrun-XBWF6_TEsFM-unsplash_684.jpg
+assets/images/hush-naidoo-jade-photography-ZCO_5Y29s8k-unsplash_150.jpg
+assets/images/allan-wadsworth-Lp78NT-mf9o-unsplash_150.jpg
+assets/images/nathan-dumlao-bRdRUUtbxO0-unsplash_150.jpg
+assets/images/hyttalo-souza-a1p0Z7RSkL8-unsplash_684.jpg
+assets/images/nasa-dCgbRAQmTQA-unsplash_684.jpg
+assets/images/alexander-grey-8lnbXtxFGZw-unsplash_684.jpg
+assets/images/alexander-kirov-YhDJXJjmxUQ-unsplash_336.jpg
+assets/images/no-revisions-UhpAf0ySwuk-unsplash_336.jpg
+assets/images/fons-heijnsbroek-vBfEZdpEr-E-unsplash_448.jpg
+assets/images/hassan-kibwana-fmXLB_uHIh4-unsplash_448.jpg
+assets/images/anatol-rurac-NeSj0i6HLak-unsplash_150.jpg
+assets/images/angela-loria-hFc0JEKD4Cc-unsplash_150.jpg
+assets/images/matthew-tenbruggencate-0HJWobhGhJs-unsplash_150.jpg
+assets/images/domino-studio-164_6wVEHfI-unsplash_336.jpg
+assets/images/noaa-Led9c1SSNFo-unsplash_684.jpg
+assets/images/tingey-injury-law-firm-9SKhDFnw4c4-unsplash_448.jpg
+assets/images/robert-bye-xHUZuSwVJg4-unsplash_336.jpg
+assets/images/emran-yousof-k8ZbMQWbx34-unsplash_336.jpg
+assets/images/matthew-ball-3wW2fBjptQo-unsplash_448.jpg
+assets/images/keagan-henman-xPJYL0l5Ii8-unsplash_336.jpg
+assets/images/memento-media-XhYq-5KnxSk-unsplash_448.jpg
+assets/images/brigitte-tohm-iIupxcq-yH4-unsplash_150.jpg
+assets/images/vanilla-bear-films-JEwNQerg3Hs-unsplash_336.jpg
+assets/images/david-beale--lQR8yeDzek-unsplash_336.jpg
+assets/images/ednilson-cardoso-dos-santos-haiooWA_weo-unsplash_336.jpg
+assets/images/mika-baumeister-bGZZBDvh8s4-unsplash_150.jpg
+assets/images/greg-rakozy-oMpAz-DN-9I-unsplash_684.jpg
+assets/images/egor-myznik-GFHKMW6KiJ0-unsplash_684.jpg
+assets/images/kenny-eliason-5ddH9Y2accI-unsplash_150.jpg
+assets/images/isaw-company-Oqv_bQbZgS8-unsplash_336.jpg
+assets/images/geert-pieters-NbpUM86Jo8Y-unsplash_150.jpg
+assets/images/boxed-water-is-better-y-TpYAlcBYM-unsplash_150.jpg
+assets/images/paola-chaaya-QrbuLFT6ypw-unsplash_336.jpg
+assets/images/devi-puspita-amartha-yahya-7ln0pST_O8M-unsplash_448.jpg
+assets/images/inaki-del-olmo-NIJuEQw0RKg-unsplash_336.jpg
+assets/images/dylan-calluy-j9q18vvHitg-unsplash_150.jpg
+assets/images/oguzhan-edman-ZWPkHLRu3_4-unsplash_336.jpg
+assets/images/david-lezcano-NfZiOJzZgcg-unsplash_336.jpg
+assets/images/alec-favale-dLctr-PqFys-unsplash_150.jpg
+assets/images/unseen-histories-4kYkKW8v8rY-unsplash_150.jpg
+assets/images/freestocks-nss2eRzQwgw-unsplash_150.jpg
+assets/images/steven-van-bTPP3jBnOb8-unsplash_684.jpg
+assets/images/colin-lloyd-2ULmNrj44QY-unsplash_448.jpg
+assets/images/libre-leung-9O0Sp22DF0I-unsplash_336.jpg
+assets/images/markus-spiske-lUc5pRFB25s-unsplash_336.jpg
+assets/images/olga-guryanova-ft7vJxwl2RY-unsplash_336.jpg
+assets/images/noaa-FY3vXNBl1v4-unsplash_336.jpg
+assets/images/kelly-sikkema-A-lovieAmjA-unsplash_150.jpg
+assets/images/markus-spiske-wIUxLHndcLw-unsplash_150.jpg
+assets/images/denys-nevozhai-z0nVqfrOqWA-unsplash_150.jpg
+assets/images/ian-taylor-kAWTCt7p7rs-unsplash_448.jpg
+assets/images/dillon-kydd-2keCPb73aQY-unsplash_336.jpg
+assets/images/ross-parmly-rf6ywHVkrlY-unsplash_336.jpg
+assets/images/arturo-rey-5yP83RhaFGA-unsplash_336.jpg
+assets/images/lorenzo-moschi-N7ypjB7HKIk-unsplash_150.jpg
+assets/images/etienne-girardet-RqOyRtYGhLg-unsplash_448.jpg
+assets/images/rufinochka-XonjCOZZN_w-unsplash_448.jpg
+assets/images/stephen-phillips-hostreviews-co-uk-em37kS8WJJQ-unsplash_150.jpg
+assets/images/kenny-K72n3BHgHCg-unsplash_336.jpg
+assets/images/trung-thanh-LgdDeuBcgIY-unsplash_684.jpg
+assets/images/maarten-van-den-heuvel-gZXx8lKAb7Y-unsplash_150.jpg
+assets/images/tadeusz-lakota-Tb38UzCvKCY-unsplash_336.jpg
+assets/images/valery-tenevoy-c0VbjkPEfmM-unsplash_336.jpg
+assets/images/dom-hill-nimElTcTNyY-unsplash_336.jpg
+assets/images/sara-cottle-bGjtWs8sXT0-unsplash_448.jpg
+assets/images/toa-heftiba-4xe-yVFJCvw-unsplash_336.jpg
+assets/images/nuno-alberto-MykFFC5zolE-unsplash_448.jpg
+assets/images/chad-stembridge-sEHrIPpkKQY-unsplash_150.jpg
+assets/images/quick-ps-sW41y3lETZk-unsplash_150.jpg
+assets/images/anastassia-anufrieva-ecHGTPfjNfA-unsplash_150.jpg
+assets/images/bram-naus-oqnVnI5ixHg-unsplash_448.jpg
+assets/images/nasa-Q1p7bh3SHj8-unsplash_150.jpg
+assets/images/ani-kolleshi-7jjnJ-QA9fY-unsplash_150.jpg
+assets/images/ibrahim-rifath-OApHds2yEGQ-unsplash_150.jpg
+assets/images/isai-ramos-Sp70YIWtuM8-unsplash_336.jpg
+assets/images/drew-hays-tGYrlchfObE-unsplash_336.jpg
+assets/images/c-j-1GHqOftzYo0-unsplash_150.jpg
+assets/images/brooke-lark-oaz0raysASk-unsplash_150.jpg
+assets/images/daniel-staple-N320vzTBviA-unsplash_684.jpg
+assets/images/heather-ford-5gkYsrH_ebY-unsplash_336.jpg
+assets/images/marco-oriolesi-wqLGlhjr6Og-unsplash_684.jpg
+assets/images/brooke-lark-HjWzkqW1dgI-unsplash_336.jpg
+assets/images/irwan-rbDE93-0hHs-unsplash_684.jpg
+assets/images/bermix-studio-yUnSMBogWNI-unsplash_150.jpg
+assets/images/geoff-scott-8lUTnkZXZSA-unsplash_336.jpg
+assets/images/astronaud23-ox3t0m3PUqA-unsplash_336.jpg
+assets/images/national-cancer-institute-A2CK97sS0ns-unsplash_684.jpg
+assets/images/clem-onojeghuo-DoA2duXyzRM-unsplash_684.jpg
+assets/images/leon-seibert-Xs3al4NpIFQ-unsplash_336.jpg
+assets/images/mr-lee-f4RBYsY2hxA-unsplash_150.jpg
+assets/images/derick-mckinney-muhK4oeYJiU-unsplash_150.jpg
+assets/images/thisisengineering-ZPeXrWxOjRQ-unsplash_336.jpg
+assets/images/heidi-kaden-L_U4jhwZ6hY-unsplash_448.jpg
+assets/images/jed-owen-ajZibDGpPew-unsplash_336.jpg
+assets/images/tamara-bellis-IwVRO3TLjLc-unsplash_336.jpg
+assets/images/maxim-hopman-IayKLkmz6g0-unsplash_448.jpg
+assets/images/joshua-rawson-harris-YNaSz-E7Qss-unsplash_336.jpg
+assets/images/clem-onojeghuo-RLJnH4Mt9A0-unsplash_336.jpg
+assets/images/esther-ann-glpYh1cWf0o-unsplash_448.jpg
+assets/images/colin-lloyd-NKS5gg7rWGw-unsplash_150.jpg
+assets/images/ronda-darby-HbMLSB-uhQY-unsplash_336.jpg
+assets/images/icons8-team-k5fUTay0ghw-unsplash_336.jpg
+assets/images/alex-hudson-7AgqAZbogOQ-unsplash_448.jpg
+assets/images/koshu-kunii-cWEGNQqcImk-unsplash_336.jpg
+assets/images/maksym-kaharlytskyi-Y0z9MyDsrU0-unsplash_150.jpg
+assets/images/kenny-eliason-4N3iHYmqy_E-unsplash_336.jpg
+assets/images/ian-hutchinson-P8rgDtEFn7s-unsplash_150.jpg
+assets/images/ahmet-kurt-5BGg2L5nhlU-unsplash_150.jpg
+assets/images/jordhan-madec-AD5ylD2T0UY-unsplash_336.jpg
+assets/images/dominic-hampton-_8aRumOixtI-unsplash_684.jpg
+assets/images/aditya-chinchure-ZhQCZjr9fHo-unsplash_336.jpg
+assets/images/robina-weermeijer-Pw9aFhc92P8-unsplash_448.jpg
+assets/images/piron-guillaume-U4FyCp3-KzY-unsplash_150.jpg
+assets/images/sj-objio-8hHxO3iYuU0-unsplash_448.jpg
+assets/images/national-cancer-institute-YvvFRJgWShM-unsplash_336.jpg
+assets/images/declan-sun-misAHv6YWkI-unsplash_336.jpg
+assets/images/mufid-majnun-J12RfFH-2ZE-unsplash_684.jpg
+assets/images/wan-san-yip-ID1yWa1Wpx0-unsplash_336.jpg
+assets/images/jakub-zerdzicki-qcRGVZNZ5js-unsplash_336.jpg
+assets/images/ryan-plomp-TT6Hep-JzrU-unsplash_336.jpg
+assets/images/ixography-05Q_XPF_YKs-unsplash_336.jpg
+assets/images/bianca-ackermann-qr0-lKAOZSk-unsplash_150.jpg
+assets/images/sandip-roy-4hgTlYb9jzg-unsplash_448.jpg
+assets/images/kevin-rajaram-qhixFFO8EWQ-unsplash_336.jpg
+assets/images/jacob-licht-8nA_iHrxHIo-unsplash_150.jpg
+assets/images/annie-spratt-KiOHnBkLQQU-unsplash_684.jpg
+assets/images/nikita-karimov-lvJZhHOIJJ4-unsplash_448.jpg
+assets/images/marius-oprea-ySA9uj7zSmw-unsplash_150.jpg
+assets/images/neil-thomas-SIU1Glk6v5k-unsplash_150.jpg
+assets/images/ashim-d-silva-ZmgJiztRHXE-unsplash_336.jpg
+assets/images/dino-reichmuth-A5rCN8626Ck-unsplash_336.jpg
+assets/images/paul-bill-HLuPjCa6IYw-unsplash_150.jpg
+assets/images/microsoft-365-f1zQuagWCTA-unsplash_336.jpg
+assets/images/jessica-weiller-So4eFi-d1nc-unsplash_336.jpg
+assets/images/dominic-bieri-vXRt4rFr4hI-unsplash_336.jpg
+assets/images/joseph-gonzalez-QaGDmf5tMiE-unsplash_150.jpg
+assets/images/jonathan-borba-lrQPTQs7nQQ-unsplash_150.jpg
+assets/images/christian-lue-8Yw6tsB8tnc-unsplash_448.jpg
+assets/images/christian-tenguan-P3gfVKhz8d0-unsplash_336.jpg
+assets/images/zac-gudakov-wwqZ8CM21gg-unsplash_150.jpg
+assets/images/claudio-schwarz-3cWxxW2ggKE-unsplash_336.jpg
+assets/images/adrian-dascal-myAz-buELXs-unsplash_448.jpg
+assets/images/little-plant-TZw891-oMio-unsplash_150.jpg
+assets/images/matthew-landers-v8UgmRa6UDg-unsplash_150.jpg
+assets/images/benjamin-child-rOn57CBgyMo-unsplash_448.jpg
+assets/images/phil-hearing-bu27Y0xg7dk-unsplash_150.jpg
+assets/images/pat-whelen-68OkRwuOeyQ-unsplash_150.jpg
+assets/images/jakob-dalbjorn-cuKJre3nyYc-unsplash_448.jpg
+assets/images/tachina-lee--wjk_SSqCE4-unsplash_448.jpg
+assets/images/toa-heftiba--abWByT3yg4-unsplash_336.jpg
+assets/images/matthias-heil-lDOEwat_MPs-unsplash_336.jpg
+assets/images/taylor-grote-UiVe5QvOhao-unsplash_150.jpg
+assets/images/peter-lawrence-rXZa4ufjoGw-unsplash_448.jpg
+assets/images/andrew-solok-LbckXdUVOlY-unsplash_448.jpg
+assets/images/scott-graham-OQMZwNd3ThU-unsplash_448.jpg
+assets/images/ant-rozetsky-SLIFI67jv5k-unsplash_150.jpg
+assets/images/maria-lysenko-tZvkSuBleso-unsplash_150.jpg
+assets/images/fanga-studio-bOfCOy3_4wU-unsplash_448.jpg
+assets/images/sarah-dorweiler-gUPiTDBdRe4-unsplash_336.jpg
+assets/images/markus-spiske-7PMGUqYQpYc-unsplash_150.jpg
+assets/images/alice-pasqual-Olki5QpHxts-unsplash_336.jpg
+assets/images/brooke-lark-lcZ9NxhOSlo-unsplash_448.jpg
+assets/images/chris-leboutillier-c7RWVGL8lPA-unsplash_150.jpg
+assets/images/jon-tyson-0BLE1xp5HBQ-unsplash_150.jpg
+assets/images/nathan-dumlao-laCrvNG3F_I-unsplash_336.jpg
+assets/images/diego-jimenez-A-NVHPka9Rk-unsplash_336.jpg
+assets/images/mika-baumeister-jXPQY1em3Ew-unsplash_150.jpg
+assets/images/rebe-pascual-SACRQSof7Qw-unsplash_448.jpg
+assets/images/bruno-nascimento-PHIgYUGQPvU-unsplash_448.jpg
+assets/images/myriam-zilles-KltoLK6Mk-g-unsplash_336.jpg
+assets/images/boris-stefanik-q49CgyIrLes-unsplash_150.jpg
+assets/images/national-cancer-institute-cw2Zn2ZQ9YQ-unsplash_336.jpg
+assets/images/david-everett-strickler-igCBFrMd11I-unsplash_684.jpg
+assets/images/markus-spiske-Nph1oyRsHm4-unsplash_150.jpg
+assets/images/ruben-valenzuela-JEp9cl5jfZA-unsplash_684.jpg
+assets/images/ilyass-seddoug-06w8RxgSzF0-unsplash_150.jpg
+assets/images/heidi-fin-2TLREZi7BUg-unsplash_336.jpg
+assets/images/mattia-bericchia-xkD79yf4tb8-unsplash_150.jpg
+assets/images/elimende-inagella-7OxV_qDiGRI-unsplash_448.jpg
+assets/images/harold-mendoza-6xafY_AE1LM-unsplash_150.jpg
+assets/images/tech-daily-vxTWpu14zeM-unsplash_150.jpg
+assets/images/dibakar-roy-K9JwokzSvrc-unsplash_150.jpg
+assets/images/marcelo-leal-6pcGTJDuf6M-unsplash_448.jpg
+assets/images/sapan-patel-gmgWd0CgWQI-unsplash_150.jpg
+assets/images/evie-calder-97CO-A4P0GQ-unsplash_336.jpg
+assets/images/brooke-lark-GJMlSBS0FhU-unsplash_150.jpg
+assets/images/harri-kuokkanen-SEtUeWL8bIQ-unsplash_336.jpg
+assets/images/roberto-junior-4fsCBcZt9H8-unsplash_150.jpg
+assets/images/markus-spiske-WUehAgqO5hE-unsplash_684.jpg
+assets/images/gaku-suyama-VyiLZUcdJv0-unsplash_150.jpg
+assets/images/mr-cup-fabien-barral-Mwuod2cm8g4-unsplash_448.jpg
+assets/images/austin-distel-rxpThOwuVgE-unsplash_448.jpg
+assets/images/national-cancer-institute-NFvdKIhxYlU-unsplash_336.jpg
+assets/images/sam-mcghee-4siwRamtFAk-unsplash_684.jpg
+assets/images/markus-spiske-jgOkEjVw-KM-unsplash_150.jpg
+assets/images/peter-conlan-LEgwEaBVGMo-unsplash_448.jpg
+assets/images/metin-ozer-hShrr0WvrQs-unsplash_336.jpg
+assets/images/craig-manners-LvJCFOW3Ma8-unsplash_448.jpg
+assets/images/parker-johnson-v0OWc_skg0g-unsplash_336.jpg
+assets/images/wesley-tingey-7BkCRNwh_V0-unsplash_150.jpg
+resources.txt
diff --git a/suites/react-stockcharts/build/resources.txt b/suites/react-stockcharts/build/resources.txt
new file mode 100644
index 000000000..318ab6b64
--- /dev/null
+++ b/suites/react-stockcharts/build/resources.txt
@@ -0,0 +1,9 @@
+asset-manifest.json
+index.html
+static/js/2.8e539c84.chunk.js
+static/js/2.8e539c84.chunk.js.LICENSE.txt
+static/js/2.8e539c84.chunk.js.map
+static/js/main.d5117271.chunk.js
+static/js/main.d5117271.chunk.js.map
+static/js/runtime-main.177cc4f8.js
+static/js/runtime-main.177cc4f8.js.map
diff --git a/suites/todomvc/architecture-examples/angular-complex/dist/resources.txt b/suites/todomvc/architecture-examples/angular-complex/dist/resources.txt
new file mode 100644
index 000000000..5d76449cd
--- /dev/null
+++ b/suites/todomvc/architecture-examples/angular-complex/dist/resources.txt
@@ -0,0 +1,13 @@
+3rdpartylicenses.txt
+big-dom-with-stacking-context-scrollable.css
+favicon.ico
+index.html
+logo.png
+main.db97198c6e808d8a.js
+main.db97198c6e808d8a.js.map
+polyfills.35f89f9aebee2e5c.js
+polyfills.35f89f9aebee2e5c.js.map
+runtime.557aa15b5f22cb0a.js
+runtime.557aa15b5f22cb0a.js.map
+styles.746a93d9fd4de753.css
+styles.746a93d9fd4de753.css.map
diff --git a/suites/todomvc/architecture-examples/angular-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/angular-complex/scripts/build.mjs
index 0967ca52d..4cdc47f81 100644
--- a/suites/todomvc/architecture-examples/angular-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/angular-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Angular Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/angular/dist/resources.txt b/suites/todomvc/architecture-examples/angular/dist/resources.txt
new file mode 100644
index 000000000..777969f77
--- /dev/null
+++ b/suites/todomvc/architecture-examples/angular/dist/resources.txt
@@ -0,0 +1,11 @@
+3rdpartylicenses.txt
+favicon.ico
+index.html
+main.db97198c6e808d8a.js
+main.db97198c6e808d8a.js.map
+polyfills.35f89f9aebee2e5c.js
+polyfills.35f89f9aebee2e5c.js.map
+runtime.557aa15b5f22cb0a.js
+runtime.557aa15b5f22cb0a.js.map
+styles.746a93d9fd4de753.css
+styles.746a93d9fd4de753.css.map
diff --git a/suites/todomvc/architecture-examples/backbone-complex/dist/resources.txt b/suites/todomvc/architecture-examples/backbone-complex/dist/resources.txt
new file mode 100644
index 000000000..33f3faff5
--- /dev/null
+++ b/suites/todomvc/architecture-examples/backbone-complex/dist/resources.txt
@@ -0,0 +1,16 @@
+app.js
+backbone-min.js
+backbone-min.js.map
+base.css
+big-dom-with-stacking-context-scrollable.css
+collections/todos.js
+index.css
+index.html
+jquery.min.js
+logo.png
+models/todo.js
+routers/router.js
+sync/backbone.sync.js
+underscore-min.js
+views/app-view.js
+views/todo-view.js
diff --git a/suites/todomvc/architecture-examples/backbone-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/backbone-complex/scripts/build.mjs
index 691f04619..2cc83bc87 100644
--- a/suites/todomvc/architecture-examples/backbone-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/backbone-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Backbone Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/backbone/dist/resources.txt b/suites/todomvc/architecture-examples/backbone/dist/resources.txt
new file mode 100644
index 000000000..706e1950b
--- /dev/null
+++ b/suites/todomvc/architecture-examples/backbone/dist/resources.txt
@@ -0,0 +1,14 @@
+app.js
+backbone-min.js
+backbone-min.js.map
+base.css
+collections/todos.js
+index.css
+index.html
+jquery.min.js
+models/todo.js
+routers/router.js
+sync/backbone.sync.js
+underscore-min.js
+views/app-view.js
+views/todo-view.js
diff --git a/suites/todomvc/architecture-examples/backbone/scripts/build.mjs b/suites/todomvc/architecture-examples/backbone/scripts/build.mjs
index c6ec2b793..402cecfac 100644
--- a/suites/todomvc/architecture-examples/backbone/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/backbone/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
@@ -58,4 +59,5 @@ const build = async () => {
console.log("done!!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/jquery-complex/dist/resources.txt b/suites/todomvc/architecture-examples/jquery-complex/dist/resources.txt
new file mode 100644
index 000000000..5d9142684
--- /dev/null
+++ b/suites/todomvc/architecture-examples/jquery-complex/dist/resources.txt
@@ -0,0 +1,10 @@
+app.css
+app.js
+base.css
+big-dom-with-stacking-context-scrollable.css
+director.min.js
+handlebars.min.js
+index.css
+index.html
+jquery.min.js
+logo.png
diff --git a/suites/todomvc/architecture-examples/jquery-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/jquery-complex/scripts/build.mjs
index 302d57cee..ddaccd1d9 100644
--- a/suites/todomvc/architecture-examples/jquery-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/jquery-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC jQuery Complex DOM.
*/
@@ -21,4 +22,5 @@ const options = {
cssFilesToAddLinksFor: ["big-dom-with-stacking-context-scrollable.css"],
};
-buildComplex(options);
\ No newline at end of file
+buildComplex(options);
+import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist")));
diff --git a/suites/todomvc/architecture-examples/jquery/dist/resources.txt b/suites/todomvc/architecture-examples/jquery/dist/resources.txt
new file mode 100644
index 000000000..326d36af5
--- /dev/null
+++ b/suites/todomvc/architecture-examples/jquery/dist/resources.txt
@@ -0,0 +1,8 @@
+app.css
+app.js
+base.css
+director.min.js
+handlebars.min.js
+index.css
+index.html
+jquery.min.js
diff --git a/suites/todomvc/architecture-examples/jquery/scripts/build.mjs b/suites/todomvc/architecture-examples/jquery/scripts/build.mjs
index e94df94b2..eef43065d 100644
--- a/suites/todomvc/architecture-examples/jquery/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/jquery/scripts/build.mjs
@@ -1,6 +1,9 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
+const __dirname = import.meta.dirname;
+
const rootDirectory = "./";
const sourceDirectory = "./src";
const targetDirectory = "./dist";
@@ -51,4 +54,4 @@ const build = async () => {
console.log("done!!");
};
-build();
+build().then(() => import("../../../../shared/generate-resources.mjs").then(m => m.generateResourcesFile(path.join(__dirname, "../dist"))));
diff --git a/suites/todomvc/architecture-examples/lit-complex/dist/resources.txt b/suites/todomvc/architecture-examples/lit-complex/dist/resources.txt
new file mode 100644
index 000000000..7f235eaf7
--- /dev/null
+++ b/suites/todomvc/architecture-examples/lit-complex/dist/resources.txt
@@ -0,0 +1,8 @@
+app.css
+big-dom-with-stacking-context-scrollable.css
+default-variables.css
+index.html
+index.js
+logo.png
+todo-item-extra-css.js
+todo-list-extra-css.js
diff --git a/suites/todomvc/architecture-examples/lit-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/lit-complex/scripts/build.mjs
index 894318a0c..0d7f34f09 100644
--- a/suites/todomvc/architecture-examples/lit-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/lit-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Lit Complex DOM.
*/
@@ -25,3 +26,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/lit/dist/resources.txt b/suites/todomvc/architecture-examples/lit/dist/resources.txt
new file mode 100644
index 000000000..bd88e6e22
--- /dev/null
+++ b/suites/todomvc/architecture-examples/lit/dist/resources.txt
@@ -0,0 +1,2 @@
+index.html
+index.js
diff --git a/suites/todomvc/architecture-examples/preact-complex/dist/resources.txt b/suites/todomvc/architecture-examples/preact-complex/dist/resources.txt
new file mode 100644
index 000000000..a75067fbb
--- /dev/null
+++ b/suites/todomvc/architecture-examples/preact-complex/dist/resources.txt
@@ -0,0 +1,6 @@
+app.css
+app.js
+app.js.map
+big-dom-with-stacking-context-scrollable.css
+index.html
+logo.png
diff --git a/suites/todomvc/architecture-examples/preact-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/preact-complex/scripts/build.mjs
index 133d2924a..3ac4ef8c2 100644
--- a/suites/todomvc/architecture-examples/preact-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/preact-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Build the TodoMVC Preact Complex DOM example.
*/
@@ -16,3 +17,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/preact/dist/resources.txt b/suites/todomvc/architecture-examples/preact/dist/resources.txt
new file mode 100644
index 000000000..ba4690c7b
--- /dev/null
+++ b/suites/todomvc/architecture-examples/preact/dist/resources.txt
@@ -0,0 +1,4 @@
+app.css
+app.js
+app.js.map
+index.html
diff --git a/suites/todomvc/architecture-examples/react-complex/dist/resources.txt b/suites/todomvc/architecture-examples/react-complex/dist/resources.txt
new file mode 100644
index 000000000..e22e65792
--- /dev/null
+++ b/suites/todomvc/architecture-examples/react-complex/dist/resources.txt
@@ -0,0 +1,8 @@
+app.bundle.js
+app.bundle.js.LICENSE.txt
+app.bundle.js.map
+app.css
+app.css.map
+big-dom.css
+index.html
+logo.png
diff --git a/suites/todomvc/architecture-examples/react-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/react-complex/scripts/build.mjs
index 49fdf8eaf..c6101cf1a 100644
--- a/suites/todomvc/architecture-examples/react-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/react-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Build the TodoMVC: React Complex DOM example.
*/
@@ -15,3 +16,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/react-redux-complex/dist/resources.txt b/suites/todomvc/architecture-examples/react-redux-complex/dist/resources.txt
new file mode 100644
index 000000000..3a3f68157
--- /dev/null
+++ b/suites/todomvc/architecture-examples/react-redux-complex/dist/resources.txt
@@ -0,0 +1,8 @@
+app.bundle.js
+app.bundle.js.LICENSE.txt
+app.bundle.js.map
+app.css
+app.css.map
+big-dom-with-stacking-context-scrollable.css
+index.html
+logo.png
diff --git a/suites/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs
index 870e615c8..5920511ec 100644
--- a/suites/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/react-redux-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC React-Redux Complex DOM.
*/
@@ -16,3 +17,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/react-redux/dist/resources.txt b/suites/todomvc/architecture-examples/react-redux/dist/resources.txt
new file mode 100644
index 000000000..1633b3d99
--- /dev/null
+++ b/suites/todomvc/architecture-examples/react-redux/dist/resources.txt
@@ -0,0 +1,6 @@
+app.bundle.js
+app.bundle.js.LICENSE.txt
+app.bundle.js.map
+app.css
+app.css.map
+index.html
diff --git a/suites/todomvc/architecture-examples/react/dist/resources.txt b/suites/todomvc/architecture-examples/react/dist/resources.txt
new file mode 100644
index 000000000..1633b3d99
--- /dev/null
+++ b/suites/todomvc/architecture-examples/react/dist/resources.txt
@@ -0,0 +1,6 @@
+app.bundle.js
+app.bundle.js.LICENSE.txt
+app.bundle.js.map
+app.css
+app.css.map
+index.html
diff --git a/suites/todomvc/architecture-examples/svelte-complex/dist/resources.txt b/suites/todomvc/architecture-examples/svelte-complex/dist/resources.txt
new file mode 100644
index 000000000..a75067fbb
--- /dev/null
+++ b/suites/todomvc/architecture-examples/svelte-complex/dist/resources.txt
@@ -0,0 +1,6 @@
+app.css
+app.js
+app.js.map
+big-dom-with-stacking-context-scrollable.css
+index.html
+logo.png
diff --git a/suites/todomvc/architecture-examples/svelte-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/svelte-complex/scripts/build.mjs
index ee7b201cd..128aa2666 100644
--- a/suites/todomvc/architecture-examples/svelte-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/svelte-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Svelte Complex DOM.
*/
@@ -16,3 +17,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/svelte/dist/resources.txt b/suites/todomvc/architecture-examples/svelte/dist/resources.txt
new file mode 100644
index 000000000..ba4690c7b
--- /dev/null
+++ b/suites/todomvc/architecture-examples/svelte/dist/resources.txt
@@ -0,0 +1,4 @@
+app.css
+app.js
+app.js.map
+index.html
diff --git a/suites/todomvc/architecture-examples/vue-complex/dist/resources.txt b/suites/todomvc/architecture-examples/vue-complex/dist/resources.txt
new file mode 100644
index 000000000..8a1e91343
--- /dev/null
+++ b/suites/todomvc/architecture-examples/vue-complex/dist/resources.txt
@@ -0,0 +1,8 @@
+big-dom.css
+css/app.319576e1.css
+index.html
+js/app.ad36df07.js
+js/app.ad36df07.js.map
+js/chunk-vendors.b4ac9361.js
+js/chunk-vendors.b4ac9361.js.map
+logo.png
diff --git a/suites/todomvc/architecture-examples/vue-complex/scripts/build.mjs b/suites/todomvc/architecture-examples/vue-complex/scripts/build.mjs
index b2a7e475c..b20c8b700 100644
--- a/suites/todomvc/architecture-examples/vue-complex/scripts/build.mjs
+++ b/suites/todomvc/architecture-examples/vue-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC Vue Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/architecture-examples/vue/dist/resources.txt b/suites/todomvc/architecture-examples/vue/dist/resources.txt
new file mode 100644
index 000000000..f71739756
--- /dev/null
+++ b/suites/todomvc/architecture-examples/vue/dist/resources.txt
@@ -0,0 +1,6 @@
+css/app.319576e1.css
+index.html
+js/app.ad36df07.js
+js/app.ad36df07.js.map
+js/chunk-vendors.b4ac9361.js
+js/chunk-vendors.b4ac9361.js.map
diff --git a/suites/todomvc/big-dom-generator/dist/resources.txt b/suites/todomvc/big-dom-generator/dist/resources.txt
new file mode 100644
index 000000000..7cbdfb6a3
--- /dev/null
+++ b/suites/todomvc/big-dom-generator/dist/resources.txt
@@ -0,0 +1,4 @@
+big-dom-with-stacking-context-scrollable.css
+big-dom.css
+index.html
+logo.png
diff --git a/suites/todomvc/todomvc-css/dist/resources.txt b/suites/todomvc/todomvc-css/dist/resources.txt
new file mode 100644
index 000000000..d75ef0f53
--- /dev/null
+++ b/suites/todomvc/todomvc-css/dist/resources.txt
@@ -0,0 +1,31 @@
+app.constructable.js
+app.css
+app.module.css
+bottombar.constructable.js
+bottombar.css
+bottombar.module.css
+footer.constructable.js
+footer.css
+footer.module.css
+global.constructable.js
+global.css
+header.constructable.js
+header.css
+header.module.css
+index.css
+index.html
+index.js
+index.min.css
+main.constructable.js
+main.css
+main.module.css
+partials.css
+todo-item.constructable.js
+todo-item.css
+todo-item.module.css
+todo-list.constructable.js
+todo-list.css
+todo-list.module.css
+topbar.constructable.js
+topbar.css
+topbar.module.css
diff --git a/suites/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt b/suites/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt
new file mode 100644
index 000000000..3a9a2b41d
--- /dev/null
+++ b/suites/todomvc/vanilla-examples/javascript-es5-complex/dist/resources.txt
@@ -0,0 +1,10 @@
+app.js
+base.css
+controller.js
+helpers.js
+index.css
+index.html
+model.js
+store.js
+template.js
+view.js
diff --git a/suites/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs b/suites/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs
index 2d75a1108..2a3bbfdad 100644
--- a/suites/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs
+++ b/suites/todomvc/vanilla-examples/javascript-es5-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC JavaScript Es5 Complex DOM.
*/
@@ -18,3 +19,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt b/suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt
new file mode 100644
index 000000000..3a9a2b41d
--- /dev/null
+++ b/suites/todomvc/vanilla-examples/javascript-es5/dist/resources.txt
@@ -0,0 +1,10 @@
+app.js
+base.css
+controller.js
+helpers.js
+index.css
+index.html
+model.js
+store.js
+template.js
+view.js
diff --git a/suites/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs b/suites/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs
index cc923230e..9c354a838 100644
--- a/suites/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs
+++ b/suites/todomvc/vanilla-examples/javascript-es5/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import path from "path";
@@ -53,4 +54,5 @@ const build = async () => {
console.log("done!!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt b/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt
new file mode 100644
index 000000000..c4d7a7c4b
--- /dev/null
+++ b/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/dist/resources.txt
@@ -0,0 +1,7 @@
+app.bundle.js
+app.bundle.js.map
+app.css
+app.css.map
+big-dom.css
+index.html
+logo.png
diff --git a/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs b/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs
index 08c7f2bd7..4e5ec8dbc 100644
--- a/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs
+++ b/suites/todomvc/vanilla-examples/javascript-es6-webpack-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC JavaScript Es6 Webpack Complex DOM.
*/
@@ -15,3 +16,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt b/suites/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt
new file mode 100644
index 000000000..bd159144e
--- /dev/null
+++ b/suites/todomvc/vanilla-examples/javascript-es6-webpack/dist/resources.txt
@@ -0,0 +1,5 @@
+app.bundle.js
+app.bundle.js.map
+app.css
+app.css.map
+index.html
diff --git a/suites/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt b/suites/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt
new file mode 100644
index 000000000..589e9482c
--- /dev/null
+++ b/suites/todomvc/vanilla-examples/javascript-web-components-complex/dist/resources.txt
@@ -0,0 +1,31 @@
+app.css
+big-dom.css
+components/todo-app/todo-app.component.js
+components/todo-app/todo-app.template.js
+components/todo-bottombar/todo-bottombar.component.js
+components/todo-bottombar/todo-bottombar.template.js
+components/todo-item/todo-item.component.js
+components/todo-item/todo-item.template.js
+components/todo-list/todo-list.component.js
+components/todo-list/todo-list.template.js
+components/todo-topbar/todo-topbar.component.js
+components/todo-topbar/todo-topbar.template.js
+default-variables.css
+hooks/useDoubleClick.js
+hooks/useKeyListener.js
+hooks/useRouter.js
+index.html
+logo.png
+styles/app.constructable.js
+styles/bottombar.constructable.js
+styles/footer.css
+styles/global.constructable.js
+styles/global.css
+styles/header.css
+styles/main.constructable.js
+styles/todo-item.constructable.js
+styles/todo-list.constructable.js
+styles/topbar.constructable.js
+todo-item-extra-css.js
+todo-list-extra-css.js
+utils/nanoid.js
diff --git a/suites/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs b/suites/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs
index 45dce8bd3..f5da135d8 100644
--- a/suites/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs
+++ b/suites/todomvc/vanilla-examples/javascript-web-components-complex/scripts/build.mjs
@@ -1,3 +1,4 @@
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
/**
* Builds the TodoMVC JavaScript Web Components Complex DOM.
*/
@@ -24,3 +25,4 @@ const options = {
};
buildComplex(options);
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/suites/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt b/suites/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt
new file mode 100644
index 000000000..d217710b6
--- /dev/null
+++ b/suites/todomvc/vanilla-examples/javascript-web-components/dist/resources.txt
@@ -0,0 +1,34 @@
+index.html
+src/components/todo-app/todo-app.component.js
+src/components/todo-app/todo-app.template.js
+src/components/todo-bottombar/todo-bottombar.component.js
+src/components/todo-bottombar/todo-bottombar.template.js
+src/components/todo-item/todo-item.component.js
+src/components/todo-item/todo-item.template.js
+src/components/todo-list/todo-list.component.js
+src/components/todo-list/todo-list.template.js
+src/components/todo-topbar/todo-topbar.component.js
+src/components/todo-topbar/todo-topbar.template.js
+src/hooks/useDoubleClick.js
+src/hooks/useKeyListener.js
+src/hooks/useRouter.js
+src/index.mjs
+src/speedometer-utils/benchmark.mjs
+src/speedometer-utils/helpers.mjs
+src/speedometer-utils/params.mjs
+src/speedometer-utils/test-invoker.mjs
+src/speedometer-utils/test-runner.mjs
+src/speedometer-utils/todomvc-utils.mjs
+src/speedometer-utils/translations.mjs
+src/utils/nanoid.js
+src/workload-test.mjs
+styles/app.constructable.js
+styles/bottombar.constructable.js
+styles/footer.css
+styles/global.constructable.js
+styles/global.css
+styles/header.css
+styles/main.constructable.js
+styles/todo-item.constructable.js
+styles/todo-list.constructable.js
+styles/topbar.constructable.js
diff --git a/suites/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs b/suites/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs
index 8980583e2..09865953b 100644
--- a/suites/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs
+++ b/suites/todomvc/vanilla-examples/javascript-web-components/scripts/build.mjs
@@ -1,3 +1,5 @@
+import path from "path";
+import { generateResourcesFile } from "../../../../shared/generate-resources.mjs";
import fs from "fs/promises";
import { dirname } from "path";
@@ -153,4 +155,5 @@ const build = async () => {
console.log("Done with building!");
};
-build();
+await build();
+await generateResourcesFile(path.join(import.meta.dirname, "../dist"));
diff --git a/sw.mjs b/sw.mjs
new file mode 100644
index 000000000..c1f935a90
--- /dev/null
+++ b/sw.mjs
@@ -0,0 +1,276 @@
+import { SW_MESSAGES } from "./resources/shared/sw-messages.mjs";
+
+const CACHE_NAME = "speedometer-cache-v4.0";
+const DB_NAME = "SpeedometerStateDB";
+const STORE_NAME = "SpeedometerSWState";
+const DB_VERSION = 2;
+
+class LockStore {
+ constructor() {
+ this.activeClientId = null;
+ this.dbPromise = null;
+ }
+
+ async _openDB() {
+ if (this.dbPromise)
+ return this.dbPromise;
+
+ this.dbPromise = new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
+ request.onupgradeneeded = (event) => {
+ const db = event.target.result;
+ if (!db.objectStoreNames.contains(STORE_NAME))
+ db.createObjectStore(STORE_NAME);
+ };
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => {
+ this.dbPromise = null;
+ reject(request.error);
+ };
+ });
+ return this.dbPromise;
+ }
+
+ async getOwner() {
+ if (this.activeClientId !== null)
+ return this.activeClientId;
+ try {
+ const db = await this._openDB();
+ const data = await new Promise((resolve, reject) => {
+ const tx = db.transaction(STORE_NAME, "readonly");
+ const req = tx.objectStore(STORE_NAME).get("stateData");
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
+ this.activeClientId = data?.clientId || null;
+ } catch (e) {
+ console.warn("IndexedDB read failed", e);
+ this.activeClientId = null;
+ }
+ return this.activeClientId;
+ }
+
+ async setOwner(clientId) {
+ this.activeClientId = clientId;
+ try {
+ const db = await this._openDB();
+ await new Promise((resolve, reject) => {
+ const tx = db.transaction(STORE_NAME, "readwrite");
+ const req = tx.objectStore(STORE_NAME).put({ clientId }, "stateData");
+ req.onsuccess = () => resolve();
+ req.onerror = () => reject(req.error);
+ });
+ } catch (e) {
+ console.warn("IndexedDB write failed", e);
+ }
+ }
+
+ async clear() {
+ this.activeClientId = null;
+ try {
+ const db = await this._openDB();
+ await new Promise((resolve, reject) => {
+ const tx = db.transaction(STORE_NAME, "readwrite");
+ const req = tx.objectStore(STORE_NAME).delete("stateData");
+ req.onsuccess = () => resolve();
+ req.onerror = () => reject(req.error);
+ });
+ } catch (e) {
+ console.warn("IndexedDB clear failed", e);
+ }
+ }
+
+ hasLock(clientId) {
+ if (!this.activeClientId)
+ return true;
+ return this.activeClientId === clientId;
+ }
+}
+
+const STORE = new LockStore();
+
+function replyToClient(event, type, msg = {}) {
+ event.ports?.[0]?.postMessage({ type, ...msg });
+}
+
+function replyError(event, message) {
+ replyToClient(event, "ERROR", { message });
+}
+
+function delayAsync(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+const MAX_CONCURRENT_REQUESTS = 20;
+
+class RequestLimiter {
+ constructor(limit = MAX_CONCURRENT_REQUESTS) {
+ this.limit = limit;
+ this.active = 0;
+ this.queue = [];
+ }
+
+ async _processQueue() {
+ while (this.queue.length > 0) {
+ const task = this.queue.shift();
+ try {
+ await task();
+ } catch (e) {
+ // Individual task errors are handled by their respective promises
+ }
+ }
+ this.active--;
+ }
+
+ schedule(fn) {
+ return new Promise((resolve, reject) => {
+ const task = () => fn().then(resolve, reject);
+ this.queue.push(task);
+
+ if (this.active < this.limit) {
+ this.active++;
+ this._processQueue();
+ }
+ });
+ }
+}
+
+const requestLimiter = new RequestLimiter(MAX_CONCURRENT_REQUESTS);
+
+async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) {
+ await STORE.getOwner();
+ await updateActiveClient(clientId);
+
+ if (clearCache)
+ await caches.delete(CACHE_NAME);
+
+ const cache = await caches.open(CACHE_NAME);
+ let loaded = 0;
+ let transferredSize = 0;
+ const urlsToCache = [];
+
+ for (const suite of suites) {
+ if (!suite.resources)
+ continue;
+ urlsToCache.push(...await parseSuiteResources(suite));
+ }
+
+ const total = urlsToCache.length;
+ const promises = urlsToCache.map(async (item, index) => {
+ const size = await fetchAndCache(cache, item.url, delay * index);
+ transferredSize += size;
+ loaded++;
+ replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName });
+ });
+
+ await Promise.all(promises);
+
+ if (!STORE.hasLock(clientId)) {
+ replyError(event, "Speedometer aborted: Another tab took over.");
+ return;
+ }
+ replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length });
+}
+
+async function parseSuiteResources(suite) {
+ const response = await fetch(suite.resources);
+ if (!response.ok)
+ return [];
+ const text = await response.text();
+ return text
+ .trim()
+ .split("\n")
+ .map((resourceUrl) => ({
+ url: new URL(resourceUrl.trim(), suite.url).href,
+ suiteName: suite.name,
+ }));
+}
+
+function getResponseSize(response) {
+ const contentLength = response.headers.get("content-length");
+ return contentLength ? parseInt(contentLength, 10) : 0;
+}
+
+async function fetchAndCache(cache, url, delayMs) {
+ if (delayMs)
+ await delayAsync(delayMs);
+ return requestLimiter.schedule(async () => {
+ const request = new Request(url, { cache: "no-cache" });
+ const existing = await cache.match(request, { ignoreSearch: true });
+ if (existing)
+ return getResponseSize(existing);
+
+ try {
+ const response = await fetch(request);
+ if (!response.ok)
+ return 0;
+ const size = getResponseSize(response);
+ await cache.put(request, response);
+ return size;
+ } catch (e) {
+ console.warn("Cache failed for", url, e);
+ return 0;
+ }
+ });
+}
+
+self.addEventListener("install", () => {
+ self.skipWaiting();
+});
+
+self.addEventListener("activate", (event) => {
+ event.waitUntil(self.clients.claim());
+});
+
+async function updateActiveClient(newClientId) {
+ const oldClientId = STORE.activeClientId;
+ if (oldClientId === newClientId)
+ return false;
+
+ await STORE.setOwner(newClientId);
+
+ if (oldClientId) {
+ const oldClient = await self.clients.get(oldClientId);
+ oldClient?.postMessage({ type: SW_MESSAGES.FATAL_ERROR, message: "Speedometer aborted: Another tab took over." });
+ }
+ return true;
+}
+
+async function handleClearCacheMessage(event, clientId) {
+ await STORE.getOwner();
+ if (!STORE.hasLock(clientId)) {
+ replyError(event, "Cannot clear SW: You do not own the lock.");
+ return;
+ }
+ await STORE.clear();
+ replyToClient(event, "SUCCESS");
+}
+
+const MESSAGE_HANDLERS = Object.freeze({
+ __proto__: null,
+ [SW_MESSAGES.PRELOAD_SUITES]: handlePreloadSuitesMessage,
+ [SW_MESSAGES.CLEAR_CACHE]: handleClearCacheMessage,
+});
+
+self.addEventListener("message", (event) => {
+ const { data } = event;
+ if (!data)
+ return;
+ const handler = MESSAGE_HANDLERS[data.type];
+ if (handler)
+ event.waitUntil(handler(event, event.source?.id, data));
+ else
+ console.error("Unknown service worker message type:", data.type);
+});
+
+self.addEventListener("fetch", (event) => {
+ event.respondWith(
+ (async () => {
+ const cachedResponse = await caches.match(event.request, { cacheName: CACHE_NAME, ignoreSearch: true });
+ if (cachedResponse)
+ return cachedResponse;
+
+ return fetch(event.request);
+ })()
+ );
+});
diff --git a/tests/index.html b/tests/index.html
index a6bc60a3b..821356444 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -28,6 +28,7 @@
});
await import("./unittests/benchmark-runner.mjs");
+ await import("./unittests/benchmark-configurator.mjs");
await import("./unittests/params.mjs");
await import("./unittests/suites.mjs");
diff --git a/tests/run-end2end.mjs b/tests/run-end2end.mjs
index cdb81ebb4..f10177ea4 100644
--- a/tests/run-end2end.mjs
+++ b/tests/run-end2end.mjs
@@ -97,6 +97,18 @@ async function testAll() {
assert(metrics.Score.values.length === 1);
}
+async function testPreloadEnabled() {
+ const metrics = await testPage("index.html?iterationCount=1&suites=Perf-Dashboard&preload=true");
+ assert("Perf-Dashboard" in metrics);
+ assert(metrics["Perf-Dashboard"].values.length === 1);
+}
+
+async function testPreloadDisabled() {
+ const metrics = await testPage("index.html?iterationCount=1&suites=Perf-Dashboard&preload=false");
+ assert("Perf-Dashboard" in metrics);
+ assert(metrics["Perf-Dashboard"].values.length === 1);
+}
+
async function testDeveloperMode() {
const params = ["developerMode", "iterationCount=1", "warmupBeforeSync=2", "waitBeforeSync=2", "shuffleSeed=123", "suites=Perf-Dashboard"];
const metrics = await testPage(`index.html?${params.join("&")}`);
@@ -140,6 +152,8 @@ async function tryTests() {
await driver.manage().setTimeouts({ script: 60000 });
await testIterations();
await testAll();
+ await testPreloadEnabled();
+ await testPreloadDisabled();
await testDeveloperMode();
console.log("\nTests complete!");
}
diff --git a/tests/unittests/benchmark-configurator.mjs b/tests/unittests/benchmark-configurator.mjs
new file mode 100644
index 000000000..546a74a47
--- /dev/null
+++ b/tests/unittests/benchmark-configurator.mjs
@@ -0,0 +1,42 @@
+import { BenchmarkConfigurator } from "../../resources/benchmark-configurator.mjs";
+
+describe("BenchmarkConfigurator", () => {
+ it("should set measurePrepare to false on processed suites if not present", async () => {
+ const configurator = new BenchmarkConfigurator();
+ await configurator.init();
+ const suites = configurator.suites;
+ expect(suites.length).to.be.greaterThan(0);
+ suites.forEach((suite) => {
+ expect(suite).to.have.property("measurePrepare");
+ expect(suite.measurePrepare).to.be.a("boolean");
+ });
+ });
+
+ it("should validate URLs correctly in _isValidUrl", () => {
+ const configurator = new BenchmarkConfigurator();
+ expect(configurator._isValidUrl("http://example.com")).to.be(true);
+ expect(configurator._isValidUrl("suites/todomvc/index.html")).to.be(true);
+ expect(configurator._isValidUrl("")).to.be(false);
+ expect(configurator._isValidUrl(null)).to.be(false);
+ });
+
+ it("should enable suites by names", async () => {
+ const configurator = new BenchmarkConfigurator();
+ await configurator.init();
+ const firstSuiteName = configurator.suites[0].name;
+ configurator.enableSuites([firstSuiteName], []);
+ expect(configurator.suites[0].enabled).to.be(true);
+ // Assuming there are multiple suites
+ expect(configurator.suites[1].enabled).to.be(false);
+ });
+
+ it("should enable suites by tags", async () => {
+ const configurator = new BenchmarkConfigurator();
+ await configurator.init();
+ configurator.enableSuites([], ["todomvc"]);
+ const todomvcSuites = configurator.suites.filter(suite => suite.tags.includes("todomvc"));
+ const otherSuites = configurator.suites.filter(suite => !suite.tags.includes("todomvc"));
+ todomvcSuites.forEach(suite => expect(suite.enabled).to.be(true));
+ otherSuites.forEach(suite => expect(suite.enabled).to.be(false));
+ });
+});
diff --git a/tests/unittests/params.mjs b/tests/unittests/params.mjs
index 4cffeb43f..549066bb2 100644
--- a/tests/unittests/params.mjs
+++ b/tests/unittests/params.mjs
@@ -76,6 +76,24 @@ describe("Params", () => {
});
});
+ describe("isDefault", () => {
+ it("should return true for defaultParams", () => {
+ expect(defaultParams.isDefault()).to.be(true);
+ });
+ it("should return false for newly instantiated empty Params", () => {
+ const params = new Params();
+ expect(params.isDefault()).to.be(false);
+ });
+ it("should return false for custom params", () => {
+ const params = new Params(
+ new URLSearchParams({
+ iterationCount: "100",
+ })
+ );
+ expect(params.isDefault()).to.be(false);
+ });
+ });
+
describe("parse input params", () => {
it("should parse custom viewport", () => {
const params = new Params(
diff --git a/tests/unittests/suites.mjs b/tests/unittests/suites.mjs
index 97252f569..af7f96700 100644
--- a/tests/unittests/suites.mjs
+++ b/tests/unittests/suites.mjs
@@ -64,6 +64,40 @@ for (const [name, suites] of Object.entries(Suites)) {
expect(suite.url.length).to.be.greaterThan(0);
});
});
+ it("should have resources.txt listing only valid files", async () => {
+ const baseUrl = `${window.location.origin}/`;
+ const brokenResourcesList = [];
+ for (const suite of suites) {
+ if (!suite.resources)
+ continue;
+ const resourcesUrl = new URL(suite.resources, baseUrl).href;
+ const res = await fetch(resourcesUrl);
+ if (!res.ok)
+ throw new Error(`Failed to load resources.txt for ${suite.name} at ${resourcesUrl}`);
+
+ const text = await res.text();
+ if (text.trim().length === 0)
+ throw new Error(`resources.txt for ${suite.name} is empty`);
+
+ const files = text.trim().split("\n");
+ for (const file of files)
+ expect(file.trim().length).to.be.greaterThan(0, `Found empty line in resources.txt for ${suite.name}`);
+
+ await Promise.all(
+ files.map(async (file) => {
+ const fileUrl = new URL(file, resourcesUrl).href;
+ const fileRes = await fetch(fileUrl, { method: "HEAD" });
+ if (!fileRes.ok)
+ brokenResourcesList.push(`${fileUrl} (listed in ${resourcesUrl})`);
+ else
+ expect(fileRes.ok).to.be(true);
+ })
+ );
+ }
+ if (brokenResourcesList.length > 0)
+ throw new Error(`Failed to load the following resources:\n${brokenResourcesList.join("\n")}`);
+
+ });
});
}