Merge TodoMVC-React with TodoMVC-React-Window

Reuse TodoMVC-React source and generate TodoMVC-React-Window files in
resources/architecture-examples/react/dist.

Change-Id: I67e37442f594cf17b13b350fc9fa5ec1bf4d6b68

TodoMVC-React windowed variation

Add a variation of the TodoMVC-React workload that renders the app in a
new Window but controls it through scripts loaded in the parent window.

Change-Id: I8b84cf7d9f0e80672808b5bf28959a034e139e0c


Add signpost support

Change-Id: I199b12c21fa25c2daab806988b0b1812a492a3d4


Add suiteForceGC helper

Force run a gc() (expose with `--js-flags=--expose-gc`) before and
after preparing a suite to limit potential ripple-effects from
previous tests.

Change-Id: Ia397b9e0aa8f5dabfb1dc4a69cfad9a7cf63ce60


[speedometer][v3.1-custom] Add params.leakyIframes to main-custom

DevTools can't handle sources from detached iframes.
To simplify local development we add an an option that just hides
the iframes away but never detaches them from the DOM.

Change-Id: Icac6d69018e7151cc82b406973a9bdd6b7b67629


Merge with upstream/release/3.1

b475f2df Perf-Dashboard.Render sync phase sets location.hash, but the actual DOM (#488) (#496)
b270564a (origin/3.1, 3.1) Release/3.1 428 - add id to input (NEW - with correct base to merge into) (#494)
aba1d5e4 (thor/release/3.1, flashd/release/3.1) Deconfuse Geomean Metric and geomean aggregate value (#408) (#480)
ce03cfbb Don't display invalid results (#406) (#479)
6f253451 Update Version Text from 3.0 to 3.1 (#485)
cc9ee085  Release/3.1 - merge 400 (rename className -> class) (#483)
e12e84c3 Release/3.1 - merge 448 (news site article ids nuxt) (#482)
8a4a5660 Release/3.1 - merge 447 (news site article ids next) (#481)

Change-Id: If0b19391128b82389eda1a61d33dc4e2be2cd180


Remove duplicated word. (#402) (#477)

Co-authored-by: Zev Eisenberg <zev@zeveisenberg.com>

Release/3.1 - merge 471 (news site remove useEffect)  (#475)

Change-Id: Ie25d06e297c9145b9af942fb4abd9d9d3b5ec7b7
This commit is contained in:
Camillo Bruni 2025-04-24 19:15:37 +02:00
parent 8d67f28d02
commit 19b7ca9c64
51 changed files with 713 additions and 94 deletions

View file

@ -23,6 +23,8 @@ package-lock.json
!/resources/todomvc/architecture-examples/vue
!/resources/todomvc/architecture-examples/vue-complex
/resources/todomvc/architecture-examples/react/public/window/*
/resources/todomvc/vanilla-examples/*
!/resources/todomvc/vanilla-examples/javascript-es5
!/resources/todomvc/vanilla-examples/javascript-es5-complex

View file

@ -23,6 +23,8 @@ package-lock.json
!/resources/todomvc/architecture-examples/vue
!/resources/todomvc/architecture-examples/vue-complex
/resources/todomvc/architecture-examples/react/public/window/*
/resources/todomvc/vanilla-examples/*
!/resources/todomvc/vanilla-examples/javascript-es5
!/resources/todomvc/vanilla-examples/javascript-es5-complex

View file

@ -41,6 +41,7 @@
<div class="version">3.0</div>
</a>
<div id="testContainer"></div>
<div id="leakedIframes"></div>
<div id="progress">
<progress aria-label="Progress" id="progress-completed"></progress>
</div>
@ -80,6 +81,7 @@
<div class="aggregated-metric-result">
<h2>Geomean</h2>
<div id="geomean-chart"></div>
<div id="toplevel-chart"></div>
<h2>Tests</h2>
<div id="tests-chart"></div>
</div>

View file

@ -22,6 +22,9 @@ function getParent(lookupStartNode, path) {
class Page {
constructor(frame) {
this._frame = frame;
// Downstream-only. this._lookupStartNode is used by the React-Window workload to run the
// querySelector* methods on a child window.
this._lookupStartNode = null;
}
layout() {
@ -58,7 +61,7 @@ class Page {
* @returns PageElement | null
*/
querySelector(selector, path = []) {
const lookupStartNode = this._frame.contentDocument;
const lookupStartNode = this._lookupStartNode ?? this._frame.contentDocument;
const element = getParent(lookupStartNode, path).querySelector(selector);
if (element === null)
@ -82,7 +85,7 @@ class Page {
* @returns array
*/
querySelectorAll(selector, path = []) {
const lookupStartNode = this._frame.contentDocument;
const lookupStartNode = this._lookupStartNode ?? this._frame.contentDocument;
const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector));
for (let i = 0; i < elements.length; i++)
elements[i] = this._wrapElement(elements[i]);
@ -114,6 +117,24 @@ class Page {
_wrapElement(element) {
return new PageElement(element);
}
// Downstream only.
async targetChildWindow() {
return new Promise((resolve) => {
const resolveIfReady = () => {
// Expects the workload to set a global childWindow object.
const maybeChildWindow = this._frame.contentWindow.globalThis.childWindow;
let callback = resolveIfReady;
if (maybeChildWindow) {
this._lookupStartNode = maybeChildWindow.document;
resolve();
return;
}
window.requestAnimationFrame(callback);
};
resolveIfReady();
});
}
}
const NATIVE_OPTIONS = {
@ -330,8 +351,40 @@ export class BenchmarkRunner {
this._page = null;
this._metrics = null;
this._iterationCount = params.iterationCount;
this._currentIteration = 0;
if (params.shuffleSeed !== "off")
this._suiteOrderRandomNumberGenerator = seededHashRandomNumberGenerator(params.shuffleSeed);
this._maybeForceGC = this._initForceGC();
this._useSignPost = !!globalThis.__signpostStart;
}
markStart(groupLabel, startLabel) {
performance.mark(startLabel);
if (this._useSignPost)
globalThis.__signpostStart(groupLabel);
}
markEnd(groupLabel, startLabel, endLabel) {
performance.mark(endLabel);
if (this._useSignPost)
globalThis.__signpostStop(groupLabel);
performance.measure(groupLabel, startLabel, endLabel);
}
_initForceGC() {
if (!params.suiteForceGC)
return () => {};
try {
let gc_extension = eval("gc");
return () => gc_extension({ type: "major", execution: "async" });
} catch (e) {
console.error("suiteForceGC was enabled but --expose-gc was not set", e);
}
return () => {};
}
get currentIteration() {
return this._currentIteration;
}
async runMultipleIterations(iterationCount) {
@ -341,11 +394,11 @@ export class BenchmarkRunner {
const iterationStartLabel = "iteration-start";
const iterationEndLabel = "iteration-end";
for (let i = 0; i < iterationCount; i++) {
performance.mark(iterationStartLabel);
for (; this._currentIteration < this._iterationCount; this._currentIteration++) {
const iterationLabel = `iteration-${this._currentIteration}`;
this.markStart(iterationLabel, iterationStartLabel);
await this._runAllSuites();
performance.mark(iterationEndLabel);
performance.measure(`iteration-${i}`, iterationStartLabel, iterationEndLabel);
this.markEnd(iterationLabel, iterationStartLabel, iterationEndLabel);
}
if (this._client?.didFinishLastIteration)
@ -354,7 +407,14 @@ export class BenchmarkRunner {
_removeFrame() {
if (this._frame) {
this._frame.parentNode.removeChild(this._frame);
if (params.leakyIframes) {
document.getElementById("leakedIframes").append(this._frame);
} else {
// Downstream only. The React-Window workload sets a global childWindow object.
if (this._frame.contentWindow?.globalThis?.childWindow)
this._frame.contentWindow.globalThis.childWindow.close();
this._frame.parentNode.removeChild(this._frame);
}
this._frame = null;
}
}
@ -383,10 +443,11 @@ export class BenchmarkRunner {
async _runAllSuites() {
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
const prepareLabel = "runner-prepare";
const prepareStartLabel = "runner-prepare-start";
const prepareEndLabel = "runner-prepare-end";
performance.mark(prepareStartLabel);
this.markStart(prepareLabel, prepareStartLabel);
this._removeFrame();
await this._appendFrame();
this._page = new Page(this._frame);
@ -402,43 +463,59 @@ export class BenchmarkRunner {
suites[j] = tmp;
}
}
this.markEnd(prepareLabel, prepareStartLabel, prepareEndLabel);
performance.mark(prepareEndLabel);
performance.measure("runner-prepare", prepareStartLabel, prepareEndLabel);
for (const suite of suites) {
if (!suite.disabled)
await this._runSuite(suite);
try {
for (const suite of suites) {
if (!suite.disabled)
await this._runSuite(suite);
}
} finally {
await this._finishRunAllSuites();
}
}
async _finishRunAllSuites() {
const finalizeLabel = "runner-finalize";
const finalizeStartLabel = "runner-finalize-start";
const finalizeEndLabel = "runner-finalize-end";
performance.mark(finalizeStartLabel);
this.markStart(finalizeLabel, finalizeStartLabel);
// Remove frame to clear the view for displaying the results.
this._removeFrame();
await this._finalize();
performance.mark(finalizeEndLabel);
performance.measure("runner-finalize", finalizeStartLabel, finalizeEndLabel);
this.markEnd(finalizeLabel, finalizeStartLabel, finalizeEndLabel);
}
async _runSuite(suite) {
const suitePrepareStartLabel = `suite-${suite.name}-prepare-start`;
const suitePrepareEndLabel = `suite-${suite.name}-prepare-end`;
const suiteStartLabel = `suite-${suite.name}-start`;
const suiteEndLabel = `suite-${suite.name}-end`;
const suiteName = suite.name;
const suitePrepareLabel = `suite-${suite.name}-prepare`;
const suitePrepareStartLabel = `suite-${suiteName}-prepare-start`;
const suitePrepareEndLabel = `suite-${suiteName}-prepare-end`;
const suiteLabel = `suite-${suite.name}`;
const suiteStartLabel = `suite-${suiteName}-start`;
const suiteEndLabel = `suite-${suiteName}-end`;
performance.mark(suitePrepareStartLabel);
this._prepareMetrics(suite);
await this._maybeForceGC();
this.markStart(suitePrepareLabel, suitePrepareStartLabel);
const prepareStartTime = performance.now();
await this._prepareSuite(suite);
performance.mark(suitePrepareEndLabel);
const prepareEndTime = performance.now();
this.markEnd(suitePrepareLabel, suitePrepareStartLabel, suitePrepareEndLabel);
performance.mark(suiteStartLabel);
await this._maybeForceGC();
if (params.measurePrepare) {
const prepareTime = prepareEndTime - prepareStartTime;
this._recordPrepareMetric(suite, prepareTime);
}
this.markStart(suiteLabel, suiteStartLabel);
for (const test of suite.tests)
await this._runTestAndRecordResults(suite, test);
performance.mark(suiteEndLabel);
performance.measure(`suite-${suite.name}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
performance.measure(`suite-${suite.name}`, suiteStartLabel, suiteEndLabel);
this.markEnd(suiteLabel, suiteStartLabel, suiteEndLabel);
}
async _prepareSuite(suite) {
@ -458,8 +535,10 @@ export class BenchmarkRunner {
await this._client.willRunTest(suite, test);
// Prepare all mark labels outside the measuring loop.
const startLabel = `${suite.name}.${test.name}-start`;
const syncLabel = `${suite.name}.${test.name}`;
const syncStartLabel = `${suite.name}.${test.name}-start`;
const syncEndLabel = `${suite.name}.${test.name}-sync-end`;
const asyncLabel = `${suite.name}.${test.name}-async`;
const asyncStartLabel = `${suite.name}.${test.name}-async-start`;
const asyncEndLabel = `${suite.name}.${test.name}-async-end`;
@ -468,22 +547,22 @@ export class BenchmarkRunner {
let asyncTime;
const runSync = () => {
if (params.warmupBeforeSync) {
performance.mark("warmup-start");
this.markStart("warmup", "warmup-start");
const startTime = performance.now();
// Infinite loop for the specified ms.
while (performance.now() - startTime < params.warmupBeforeSync)
continue;
performance.mark("warmup-end");
this.markEnd("warmup", "warmup-start", "warmup-end");
}
performance.mark(startLabel);
this.markStart(syncLabel, syncStartLabel);
const syncStartTime = performance.now();
test.run(this._page);
const syncEndTime = performance.now();
performance.mark(syncEndLabel);
this.markEnd(syncLabel, syncStartLabel, syncEndLabel);
syncTime = syncEndTime - syncStartTime;
performance.mark(asyncStartLabel);
this.markStart(asyncLabel, asyncStartLabel);
asyncStartTime = performance.now();
};
const measureAsync = () => {
@ -493,11 +572,7 @@ export class BenchmarkRunner {
const asyncEndTime = performance.now();
asyncTime = asyncEndTime - asyncStartTime;
this._frame.contentWindow._unusedHeightValue = height; // Prevent dead code elimination.
performance.mark(asyncEndLabel);
if (params.warmupBeforeSync)
performance.measure("warmup", "warmup-start", "warmup-end");
performance.measure(`${suite.name}.${test.name}-sync`, startLabel, syncEndLabel);
performance.measure(`${suite.name}.${test.name}-async`, asyncStartLabel, asyncEndLabel);
this.markEnd(asyncLabel, asyncStartLabel, asyncEndLabel);
};
const report = () => this._recordTestResults(suite, test, syncTime, asyncTime);
const invokerClass = params.measurementMethod === "raf" ? RAFTestInvoker : TimerTestInvoker;

View file

@ -22,6 +22,10 @@ export function createDeveloperModeContainer() {
settings.append(createUIForWarmupSuite());
settings.append(createUIForWarmupBeforeSync());
settings.append(createUIForSyncStepDelay());
settings.append(createUIForLeakyIframes());
settings.append(createUIForMeasurePrepare());
settings.append(createUIForDomainPerIteration());
settings.append(createUIForForceGC());
content.append(document.createElement("hr"));
content.append(settings);
@ -57,6 +61,23 @@ function createUIForMeasurementMethod() {
return label;
}
function createUIForLeakyIframes() {
let check = document.createElement("input");
check.type = "checkbox";
check.id = "leaky-iframes-checkbox";
check.checked = !!params.leakyIframes;
check.onchange = () => {
params.leakyIframes = check.checked;
updateURL();
};
let label = document.createElement("label");
label.append(check, " ", span("Leak IFrames"));
return label;
}
function createUIForWarmupSuite() {
let check = document.createElement("input");
check.type = "checkbox";
@ -74,6 +95,57 @@ function createUIForWarmupSuite() {
return label;
}
function createUIForMeasurePrepare() {
let check = document.createElement("input");
check.type = "checkbox";
check.id = "measure-prepare";
check.checked = params.measurePrepare;
check.onchange = () => {
params.measurePrepare = check.checked;
updateURL();
};
let label = document.createElement("label");
label.append(check, " ", "Measure Prepare");
return label;
}
function createUIForDomainPerIteration() {
let check = document.createElement("input");
check.type = "checkbox";
check.id = "domain-per-iteration";
check.checked = params.domainPerIteration;
check.onchange = () => {
params.domainPerIteration = check.checked;
updateURL();
};
let label = document.createElement("label");
label.append(check, " ", "Use Subdomain-Runner");
return label;
}
function createUIForForceGC() {
let check = document.createElement("input");
check.type = "checkbox";
check.id = "suite-force-gc";
check.checked = params.suiteForceGC;
check.onchange = () => {
params.suiteForceGC = check.checked;
updateURL();
};
let label = document.createElement("label");
label.append(check, " ", "Force GC before every Suite");
return label;
}
function createUIForIterationCount() {
const { range, label } = createTimeRangeUI("Iterations: ", params.iterationCount, "#", 1, 200);
range.onchange = () => {
@ -273,7 +345,7 @@ function updateURL() {
else
url.searchParams.delete("measurementMethod");
const boolParamKeys = ["iterationCount", "useWarmupSuite", "warmupBeforeSync", "waitBeforeSync"];
const boolParamKeys = ["iterationCount", "useWarmupSuite", "warmupBeforeSync", "waitBeforeSync", "domainPerIteration", "suiteForceGC", "leakyIframes"];
for (const paramKey of boolParamKeys) {
if (params[paramKey] !== defaultParams[paramKey])
url.searchParams.set(paramKey, params[paramKey]);

View file

@ -324,6 +324,10 @@ section > p {
height: var(--viewport-height);
}
#leakedIframes {
display: none;
}
section#home p {
margin: 0 auto;
width: 70%;
@ -423,6 +427,25 @@ section#summary > #confidence-number {
color: var(--inactive-color);
}
section#summary.invalid h1,
section#summary.invalid .gauge,
section#summary.invalid hr {
opacity: 0.2;
}
section#summary.invalid #result-number {
color: var(--highlight);
}
section#summary.invalid .buttons {
display: none;
}
#invalid-score-text {
display: none;
text-align: center;
}
section#summary.invalid #invalid-score-text {
display: block;
}
section#details {
--viewport-height: max(600px, 90vh);
--viewport-width: max(800px, 80vw);

View file

@ -107,19 +107,42 @@ class MainBenchmarkClient {
this._metrics = metrics;
const scoreResults = this._computeResults(this._measuredValuesList, "score");
this._updateGaugeNeedle(scoreResults.mean);
document.getElementById("result-number").textContent = scoreResults.formattedMean;
if (scoreResults.formattedDelta)
document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`;
if (scoreResults.isValid)
this._populateValidScore(scoreResults);
else
this._populateInvalidScore();
this._populateDetailedResults(metrics);
if (params.developerMode)
this.showResultsDetails();
else
this.showResultsSummary();
}
handleError(error) {
console.assert(this._isRunning);
this._isRunning = false;
this._hasResults = true;
this._metrics = Object.create(null);
this._populateInvalidScore();
this.showResultsSummary();
}
_populateValidScore(scoreResults) {
document.getElementById("summary").className = "valid";
this._updateGaugeNeedle(scoreResults.mean);
document.getElementById("result-number").textContent = scoreResults.formattedMean;
if (scoreResults.formattedDelta)
document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`;
}
_populateInvalidScore() {
document.getElementById("summary").className = "invalid";
document.getElementById("result-number").textContent = "Error";
document.getElementById("confidence-number").textContent = "";
}
_computeResults(measuredValuesList, displayUnit) {
function valueForUnit(measuredValues) {
if (displayUnit === "ms")
@ -137,9 +160,7 @@ class MainBenchmarkClient {
}
const values = measuredValuesList.map(valueForUnit);
const sum = values.reduce((a, b) => {
return a + b;
}, 0);
const sum = values.reduce((a, b) => a + b, 0);
const arithmeticMean = sum / values.length;
let meanSigFig = 4;
let formattedDelta;
@ -162,6 +183,7 @@ class MainBenchmarkClient {
formattedMean: formattedMean,
formattedDelta: formattedDelta,
formattedMeanAndDelta: formattedMean + (formattedDelta ? ` \xb1 ${formattedDelta} (${formattedPercentDelta})` : ""),
isValid: values.length > 0 && isFinite(sum) && sum > 0,
};
}

View file

@ -10,9 +10,15 @@ export class Metric {
throw new Error(`Invalid metric.name=${name}, expected string.`);
this.name = name;
this.unit = unit;
this.description = "";
this.mean = 0.0;
this.geomean = 0.0;
// Make "geomean" non-enumerable so we don't serialize it with JSON.stringify
// and avoid some confusion with the top-level Geomean metric.
Object.defineProperty(this, "geomean", {
writable: true,
value: 0,
});
this.delta = 0.0;
this.percentDelta = 0.0;
@ -21,7 +27,6 @@ export class Metric {
this.max = 0.0;
this.values = [];
this.children = [];
// Mark properties which refer to other Metric objects as
// non-enumerable to avoid issue with JSON.stringify due to circular
@ -31,6 +36,10 @@ export class Metric {
writable: true,
value: undefined,
},
children: {
writable: true,
value: [],
},
});
}

View file

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/pages/_error-54de1933a164a1ff.js" defer=""></script><script src="./_next/static/hVjtt3DRTxwTZ0w7LdK0j/_buildManifest.js" defer=""></script><script src="./_next/static/hVjtt3DRTxwTZ0w7LdK0j/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"hVjtt3DRTxwTZ0w7LdK0j","assetPrefix":".","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/pages/_error-54de1933a164a1ff.js" defer=""></script><script src="./_next/static/5v6xiLKzLrBzD4hPqXwOj/_buildManifest.js" defer=""></script><script src="./_next/static/5v6xiLKzLrBzD4hPqXwOj/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"5v6xiLKzLrBzD4hPqXwOj","assetPrefix":".","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>

View file

@ -1 +1 @@
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/743-fd706aeabb7828e3.js","static/css/2cf5163b53bb0adb.css","static/chunks/pages/index-a6d76b1b73bbfbe0.js"],"/_error":["static/chunks/pages/_error-54de1933a164a1ff.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/743-fd706aeabb7828e3.js","static/css/2cf5163b53bb0adb.css","static/chunks/pages/index-61e648f02ae686f6.js"],"/_error":["static/chunks/pages/_error-54de1933a164a1ff.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View file

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><link rel="preload" href="./_next/static/css/2cf5163b53bb0adb.css" as="style"/><link rel="stylesheet" href="./_next/static/css/2cf5163b53bb0adb.css" data-n-p=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/743-fd706aeabb7828e3.js" defer=""></script><script src="./_next/static/chunks/pages/index-a6d76b1b73bbfbe0.js" defer=""></script><script src="./_next/static/hVjtt3DRTxwTZ0w7LdK0j/_buildManifest.js" defer=""></script><script src="./_next/static/hVjtt3DRTxwTZ0w7LdK0j/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"hVjtt3DRTxwTZ0w7LdK0j","assetPrefix":".","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><link rel="preload" href="./_next/static/css/2cf5163b53bb0adb.css" as="style"/><link rel="stylesheet" href="./_next/static/css/2cf5163b53bb0adb.css" data-n-p=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/743-fd706aeabb7828e3.js" defer=""></script><script src="./_next/static/chunks/pages/index-61e648f02ae686f6.js" defer=""></script><script src="./_next/static/5v6xiLKzLrBzD4hPqXwOj/_buildManifest.js" defer=""></script><script src="./_next/static/5v6xiLKzLrBzD4hPqXwOj/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"5v6xiLKzLrBzD4hPqXwOj","assetPrefix":".","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>

View file

@ -1,4 +1,3 @@
import { v4 as uuidv4 } from "uuid";
import classNames from "classnames";
import ArticleImage from "./article-image";
@ -21,7 +20,7 @@ export default function ArticleContent({ type, content, display }) {
<div className={styles["article-content"]}>
<ul className={classNames(styles["article-list"], styles.vertical, { [styles[display]]: display })}>
{content.map((item) =>
<li key={uuidv4()} className={styles["article-list-item"]}>
<li key={item.id} className={styles["article-list-item"]}>
{item.url && !item.title
? <a href={item.url}>
<ArticleText text={item.content} />
@ -40,7 +39,7 @@ export default function ArticleContent({ type, content, display }) {
<div className={styles["article-list-content"]}>
<ul className={classNames(styles["article-list"], styles.vertical)}>
{content.map((item) =>
<li key={uuidv4()} className={styles["article-list-item"]}>
<li key={item.id} className={styles["article-list-item"]}>
<ArticleText textClass={classNames(styles["article-title"], "truncate-multiline", "truncate-multiline-3")} text={item.title} type="h3" />
{item.url && !item.title
? <a href={item.url}>
@ -59,7 +58,7 @@ export default function ArticleContent({ type, content, display }) {
return (
<ul className={classNames(styles["article-list"], styles.horizontal)}>
{content.map((item) =>
<li key={uuidv4()} className={styles["article-list-item"]}>
<li key={item.id} className={styles["article-list-item"]}>
<ArticleImage imageClass={styles["article-hero"]} image={item.image} />
<div className={styles["article-content"]}>
<ArticleText textClass="truncate-multiline truncate-multiline-3" text={item.text} type="div" />
@ -74,7 +73,7 @@ export default function ArticleContent({ type, content, display }) {
return (
<div className={classNames(layoutStyles["grid-container"], { [layoutStyles[display]]: display })}>
{content.map((item) =>
<div key={uuidv4()} className={layoutStyles["grid-item"]}>
<div key={item.id} className={layoutStyles["grid-item"]}>
<ArticleImage imageClass={styles["article-image-container"]} image={item.image} meta={item.meta} />
{item.url
@ -93,7 +92,7 @@ export default function ArticleContent({ type, content, display }) {
return (
<ul className={classNames(styles["article-list"], styles.vertical)}>
{content.map((item) =>
<li key={uuidv4()} className={styles["article-list-item"]}>
<li key={item.id} className={styles["article-list-item"]}>
<ArticleImage imageClass={styles["article-image-container"]} image={item.image} />
<ArticleText textClass={classNames(styles["article-title"], "truncate-multiline", "truncate-multiline-3")} text={item.title} type="h3" />
</li>

View file

@ -1,6 +1,8 @@
import { createContext, useContext } from "react";
import { dataSource } from "../data";
import { v4 as uuidv4 } from "uuid";
const RTL_LOCALES = ["ar", "he", "fa", "ps", "ur"];
const DEFAULT_LANG = "en";
const DEFAULT_DIR = "ltr";
@ -16,10 +18,41 @@ export const DataContextProvider = ({ children }) => {
document.documentElement.setAttribute("dir", dir);
document.documentElement.setAttribute("lang", lang);
const { content } = dataSource[lang];
// Generate unique IDs for all articles, and their content items where appropriate.
const contentWithIds = Object.create(null);
Object.keys(content).forEach((key) => {
const { sections } = content[key];
const currentSections = sections.map((section) => {
const currentSection = { ...section };
currentSection.articles = section.articles.map((article) => {
const currentArticle = { ...article };
currentArticle.id = uuidv4();
if (Array.isArray(article.content)) {
currentArticle.content = article.content.map((item) => {
const currentItem = { ...item };
currentItem.id = uuidv4();
return currentItem;
});
}
return currentArticle;
});
return currentSection;
});
contentWithIds[key] = {
...content[key],
sections: currentSections,
};
});
const value = {
lang,
dir,
...dataSource[lang],
content: contentWithIds,
};
return <DataContext.Provider value={value}>{children}</DataContext.Provider>;

View file

@ -14,14 +14,14 @@ export default function App() {
<DataContextProvider>
<Router>
<Routes>
<Route path="/business" element={<Page id="business" />} />
<Route path="/health" element={<Page id="health" />} />
<Route path="/opinion" element={<Page id="opinion" />} />
<Route path="/politics" element={<Page id="politics" />} />
<Route path="/us" element={<Page id="us" />} />
<Route path="/world" element={<Page id="world" />} />
<Route path="/home" element={<Page id="home" />} />
<Route path="/" element={<Page id="home" />} />
<Route path="/business" element={<Page id="business" key="business" />} />
<Route path="/health" element={<Page id="health" key="health" />} />
<Route path="/opinion" element={<Page id="opinion" key="opinion" />} />
<Route path="/politics" element={<Page id="politics" key="politics" />} />
<Route path="/us" element={<Page id="us" key="us" />} />
<Route path="/world" element={<Page id="world" key="world" />} />
<Route path="/home" element={<Page id="home" key="home" />} />
<Route path="/" element={<Page id="home" key="home" />} />
</Routes>
</Router>
</DataContextProvider>

View file

@ -13,12 +13,8 @@ import { useDataContext } from "@/context/data-context";
import styles from "news-site-css/dist/layout.module.css";
export default function Layout({ children, id }) {
const [showMessage, setShowMessage] = useState(false);
const { content, links } = useDataContext();
useEffect(() => {
setShowMessage(content[id].message);
}, [id]);
const [showMessage, setShowMessage] = useState(Boolean(content[id].message));
const pageRef = useRef(null);
const { pathname } = useLocation();

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import { createPortal } from "react-dom";
import Layout from "@/partials/layout/layout";
@ -8,12 +8,8 @@ import Toast from "@/components/toast/toast";
import { useDataContext } from "@/context/data-context";
export default function Page({ id }) {
const [showPortal, setShowPortal] = useState(false);
const { content } = useDataContext();
useEffect(() => {
setShowPortal(content[id].notification);
}, [id]);
const [showPortal, setShowPortal] = useState(Boolean(content[id].notification));
function closePortal() {
setShowPortal(false);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{a,b as n,o as r,e as d,f as e,t as s,h as l,w as c,i as p,j as f,p as h,k as x}from"./entry.30f4d106.js";const m=t=>(h("data-v-30d2164e"),t=t(),x(),t),u={class:"font-sans antialiased bg-white dark:bg-black text-black dark:text-white grid min-h-screen place-content-center overflow-hidden"},g=m(()=>e("div",{class:"fixed left-0 right-0 spotlight z-10"},null,-1)),_={class:"max-w-520px text-center z-20"},b=["textContent"],w=["textContent"],y={class:"w-full flex items-center justify-center"},S={__name:"error-404",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},statusCode:{type:Number,default:404},statusMessage:{type:String,default:"Not Found"},description:{type:String,default:"Sorry, the page you are looking for could not be found."},backHome:{type:String,default:"Go back home"}},setup(t){const o=t;return n({title:`${o.statusCode} - ${o.statusMessage} | ${o.appName}`,script:[],style:[{children:'*,:before,:after{-webkit-box-sizing:border-box;box-sizing:border-box;border-width:0;border-style:solid;border-color:#e0e0e0}*{--tw-ring-inset:var(--tw-empty, );--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(14, 165, 233, .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000}:root{-moz-tab-size:4;-o-tab-size:4;tab-size:4}a{color:inherit;text-decoration:inherit}body{margin:0;font-family:inherit;line-height:inherit}html{-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5}h1,p{margin:0}h1{font-size:inherit;font-weight:inherit}'}]}),(k,v)=>{const i=f;return r(),d("div",u,[g,e("div",_,[e("h1",{class:"text-8xl sm:text-10xl font-medium mb-8",textContent:s(t.statusCode)},null,8,b),e("p",{class:"text-xl px-8 sm:px-0 sm:text-4xl font-light mb-16 leading-tight",textContent:s(t.description)},null,8,w),e("div",y,[l(i,{to:"/",class:"gradient-border text-md sm:text-xl py-2 px-4 sm:py-3 sm:px-6 cursor-pointer"},{default:c(()=>[p(s(t.backHome),1)]),_:1})])])])}}},z=a(S,[["__scopeId","data-v-30d2164e"]]);export{z as default};

View file

@ -0,0 +1 @@
import{a as i,b as a,o as r,e as n,f as e,t as s,p as l,k as d}from"./entry.30f4d106.js";const c=t=>(l("data-v-32388612"),t=t(),d(),t),p={class:"font-sans antialiased bg-white dark:bg-black text-black dark:text-white grid min-h-screen place-content-center overflow-hidden"},h=c(()=>e("div",{class:"fixed -bottom-1/2 left-0 right-0 h-1/2 spotlight"},null,-1)),f={class:"max-w-520px text-center"},g=["textContent"],m=["textContent"],x={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},statusCode:{type:Number,default:500},statusMessage:{type:String,default:"Server error"},description:{type:String,default:"This page is temporarily unavailable."}},setup(t){const o=t;return a({title:`${o.statusCode} - ${o.statusMessage} | ${o.appName}`,script:[],style:[{children:'*,:before,:after{-webkit-box-sizing:border-box;box-sizing:border-box;border-width:0;border-style:solid;border-color:#e0e0e0}*{--tw-ring-inset:var(--tw-empty, );--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(14, 165, 233, .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000}:root{-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{margin:0;font-family:inherit;line-height:inherit}html{-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5}h1,p{margin:0}h1{font-size:inherit;font-weight:inherit}'}]}),(b,u)=>(r(),n("div",p,[h,e("div",f,[e("h1",{class:"text-8xl sm:text-10xl font-medium mb-8",textContent:s(t.statusCode)},null,8,g),e("p",{class:"text-xl px-8 sm:px-0 sm:text-4xl font-light mb-16 leading-tight",textContent:s(t.description)},null,8,m)])]))}},w=i(x,[["__scopeId","data-v-32388612"]]);export{w as default};

View file

@ -0,0 +1,3 @@
import{o as m,c as E,n as f,g,u as s,d as n,_ as a}from"./entry.30f4d106.js";const k={__name:"nuxt-error-page",props:{error:Object},setup(c){const u=c,{error:t}=u;(t.stack||"").split(`
`).splice(1).map(e=>({text:e.replace("webpack:/","").replace(".vue",".js").trim(),internal:e.includes("node_modules")&&!e.includes(".cache")||e.includes("internal")||e.includes("new Promise")})).map(e=>`<span class="stack${e.internal?" internal":""}">${e.text}</span>`).join(`
`);const r=Number(t.statusCode||500),o=r===404,i=t.statusMessage??(o?"Page Not Found":"Internal Server Error"),p=t.message||t.toString(),_=void 0,d=o?n(()=>a(()=>import("./error-404.f07efa12.js"),["./error-404.f07efa12.js","./entry.30f4d106.js","./entry.4163a698.css","./error-404.23f2309d.css"],import.meta.url).then(e=>e.default||e)):n(()=>a(()=>import("./error-500.f207c42f.js"),["./error-500.f207c42f.js","./entry.30f4d106.js","./entry.4163a698.css","./error-500.aa16ed4d.css"],import.meta.url).then(e=>e.default||e));return(e,l)=>(m(),E(s(d),f(g({statusCode:s(r),statusMessage:s(i),description:s(p),stack:s(_)})),null,16))}},h=k;export{h as default};

View file

@ -25,6 +25,16 @@ class Params {
// "generate": generate a random seed
// <integer>: use the provided integer as a seed
shuffleSeed = "off";
// Leak iframes to DevTools can display sources.
leakyIframes = false;
// Emit metrics for the prepare phase.
measurePrepare = false;
// Use a new subdomain for each iteration to avoid caching.
domainPerIteration = false;
// Force a gc before every suite (chrome specific)
suiteForceGC = false;
currentIteration = 0;
embedded = false;
constructor(searchParams = undefined) {
if (searchParams)
@ -81,6 +91,18 @@ class Params {
this.developerMode = searchParams.has("developerMode");
searchParams.delete("developerMode");
this.measurePrepare = searchParams.has("measurePrepare");
searchParams.delete("measurePrepare");
this.domainPerIteration = searchParams.has("domainPerIteration");
searchParams.delete("domainPerIteration");
this.suiteForceGC = searchParams.has("suiteForceGC");
searchParams.delete("suiteForceGC");
this.currentIteration = searchParams.get("currentIteration") | 0;
searchParams.delete("currentIteration");
if (searchParams.has("useWarmupSuite")) {
this.useWarmupSuite = true;
searchParams.delete("useWarmupSuite");
@ -122,6 +144,11 @@ class Params {
searchParams.delete("shuffleSeed");
}
if (searchParams.has("leakyIframes")) {
this.leakyIframes = true;
searchParams.delete("leakyIframes");
}
const unused = Array.from(searchParams.keys());
if (unused.length > 0)
console.error("Got unused search params", unused);

View file

@ -304,6 +304,39 @@ Suites.push({
],
});
// Downstream only.
Suites.push({
name: "TodoMVC-React-Window",
url: "todomvc/architecture-examples/react/dist/index-window.html#/home",
tags: ["todomvc"],
disabled: true,
async prepare(page) {
await page.targetChildWindow();
const element = await page.waitForElement(".new-todo");
element.focus();
},
tests: [
new BenchmarkTestStep(`Adding${numberOfItemsToAdd}Items`, (page) => {
const newTodo = page.querySelector(".new-todo");
for (let i = 0; i < numberOfItemsToAdd; i++) {
newTodo.setValue(getTodoText(defaultLanguage, i));
newTodo.dispatchEvent("input");
newTodo.enter("keydown");
}
}),
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();
}),
],
});
Suites.push({
name: "TodoMVC-React-Redux",
url: "todomvc/architecture-examples/react-redux/dist/index.html",
@ -1036,6 +1069,55 @@ Suites.push({
],
});
// The WarmupSuite is used to make sure all runner helper functions and
// classes are compiled, to avoid unnecessary pauses due to delayed
// compilation of runner methods in the middle of the measuring cycle.
export const WarmupSuite = {
name: "Warmup",
url: "warmup/index.html",
disabled: true,
async prepare(page) {
await page.waitForElement("#testItem");
},
tests: [
// Make sure to run ever page.method once at least
new BenchmarkTestStep("WarmingUpPageMethods", (page) => {
let results = [];
results.push(page.querySelector(".testItem"));
results.push(page.querySelectorAll(".item"));
results.push(page.getElementById("testItem"));
}),
new BenchmarkTestStep("WarmingUpPageElementMethods", (page) => {
const item = page.getElementById("testItem");
item.setValue("value");
item.click();
item.focus();
item.dispatchEvent("change");
item.enter("keypress");
item.dispatchEvent("input");
item.enter("keyup");
}),
new BenchmarkTestStep("WarmingUpPageElementMouseMethods", (page) => {
const item = page.getElementById("testItem");
const mouseEventOptions = { clientX: 100, clientY: 100, bubbles: true, cancelable: true };
const wheelEventOptions = {
clientX: 200,
clientY: 200,
deltaMode: 0,
delta: -10,
deltaY: -10,
bubbles: true,
cancelable: true,
};
item.dispatchEvent("mousedown", mouseEventOptions, MouseEvent);
item.dispatchEvent("mousemove", mouseEventOptions, MouseEvent);
item.dispatchEvent("mouseup", mouseEventOptions, MouseEvent);
item.dispatchEvent("wheel", wheelEventOptions, WheelEvent);
}),
],
};
Suites.push(WarmupSuite);
Object.freeze(Suites);
Suites.forEach((suite) => {
if (!suite.tags)

View file

@ -4,5 +4,5 @@
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
ie=ce,function(){var e={}.hasOwnProperty;function t(){for(var n=[],_=0;_<arguments.length;_++){var o=arguments[_];if(o){var l=typeof o;if("string"===l||"number"===l)n.push(o);else if(Array.isArray(o)){if(o.length){var r=t.apply(null,o);r&&n.push(r)}}else if("object"===l){if(o.toString!==Object.prototype.toString&&!o.toString.toString().includes("[native code]")){n.push(o.toString());continue}for(var i in o)e.call(o,i)&&o[i]&&n.push(i)}}}return n.join(" ")}ie.exports?(t.default=t,ie.exports=t):window.classNames=t}();var se=ue;function ae(e){let{onSave:t,onRemove:n,onToggle:_,todo:o,index:l}=e;const[r,i]=G(!1),u=J(null);function c(e){const _=e.target.value.trim();_?(t(o,_),i(!1)):n(o)}return z((()=>{u.current&&(u.current.focus(),u.current.setSelectionRange(u.current.value.length,u.current.value.length))}),[r]),f("li",{class:se({completed:o.completed,editing:r}),"data-priority":4-l%5},f("div",{class:"view"},f("input",{class:"toggle",type:"checkbox",checked:o.completed,onChange:function(e){_(o),e.preventDefault()}}),f("label",{onDblClick:function(){i(!0)}},o.title),f("button",{class:"destroy",onClick:function(){n(o)}})),r?f("div",{class:"input-container"},f("input",{class:"edit",id:"edit-todo-input",ref:u,onBlur:c,onKeyDown:function(e){"Escape"===e.key?i(!1):"Enter"===e.key&&c(e)},defaultValue:o.title}),f("label",{class:"visually-hidden",htmlFor:"edit-todo-input"},"Edit Todo Input"," ")):null)}const fe={all:e=>!0,active:e=>!e.completed,completed:e=>e.completed};function pe(e){let{onChange:t,onToggle:n,onRemove:_,onSave:o,todos:l,route:r}=e;const i=l.filter(fe[r]);return f("main",{class:"main"},f("div",{class:"toggle-all-container"},f("input",{class:"toggle-all",type:"checkbox",checked:0===l.filter(fe.active).length,onChange:t}),f("label",{class:"toggle-all-label",htmlFor:"toggle-all"},"Toggle All Input")),f("ul",{class:"todo-list show-priority"},i.map(((e,t)=>f(ae,{key:e.id,todo:e,onToggle:n,onRemove:_,onSave:o,index:t})))))}function de(e){let{todos:t,route:n,onClearCompleted:_}=e;const o=t.filter((e=>!e.completed)).length,l=t.length-o;return f("footer",{class:"footer"},f("span",{class:"todo-count"},`${o} ${1===o?"item":"items"} left!`),f("ul",{class:"filters"},f("li",null,f("a",{href:"#/",class:se({selected:"all"===n})},"All"))," ",f("li",null,f("a",{href:"#/active",class:se({selected:"active"===n})},"Active"))," ",f("li",null,f("a",{href:"#/completed",class:se({selected:"completed"===n})},"Completed"))),l>0&&f("button",{class:"clear-completed",onClick:_},"Clear completed"))}const he=()=>{let e=String(location.hash||"").split("/").pop();return fe[e]||(e="all"),e};!function(n,_,o){var l,r,u;t.__&&t.__(n,_),r=(l="function"==typeof o)?null:o&&o.__k||_.__k,u=[],T(_,n=(!l&&o||_).__k=f(d,null,[n]),r||i,i,void 0!==_.ownerSVGElement,!l&&o?[o]:r?null:_.firstChild?e.call(_.childNodes):null,u,!l&&o?o:r?r.__e:_.firstChild,l),N(u,n)}(f((function(){const[,e]=G(Date.now()),[t,n]=G("all"),_=function(e){const t=[e];function n(){t.forEach((e=>e()))}return{addItem:function(e){le=le.concat({id:oe(),title:e,completed:!1}),n()},toggleAll:function(e){le=le.map((t=>({...t,completed:e}))),n()},toggleItem:function(e){le=le.map((t=>t!==e?t:{...t,completed:!t.completed})),n()},removeItem:function(e){le=le.filter((t=>t!==e)),n()},updateItem:function(e,t){le=le.map((n=>n!==e?n:{...n,title:t})),n()},clearCompleted:function(){le=le.filter((e=>!e.completed)),n()},getTodos:function(){return[...le]}}}((function(){e(Date.now())}));return z((()=>{function e(){n(he())}addEventListener("hashchange",e),e()}),[]),f(d,null,f(re,{onKeyDown:function(e){if("Enter"===e.key){const t=e.target.value.trim();t&&(_.addItem(t),e.target.value="")}}}),_.getTodos().length>0?f(d,null,f(pe,{todos:_.getTodos(),route:t,onChange:function(e){_.toggleAll(e.target.checked)},onToggle:_.toggleItem,onRemove:_.removeItem,onSave:_.updateItem}),f(de,{todos:_.getTodos(),route:t,onClearCompleted:_.clearCompleted})):null)}),null),document.querySelector(".todoapp"))}();
ie=ce,function(){var e={}.hasOwnProperty;function t(){for(var n=[],_=0;_<arguments.length;_++){var o=arguments[_];if(o){var l=typeof o;if("string"===l||"number"===l)n.push(o);else if(Array.isArray(o)){if(o.length){var r=t.apply(null,o);r&&n.push(r)}}else if("object"===l){if(o.toString!==Object.prototype.toString&&!o.toString.toString().includes("[native code]")){n.push(o.toString());continue}for(var i in o)e.call(o,i)&&o[i]&&n.push(i)}}}return n.join(" ")}ie.exports?(t.default=t,ie.exports=t):window.classNames=t}();var se=ue;function ae(e){let{onSave:t,onRemove:n,onToggle:_,todo:o,index:l}=e;const[r,i]=G(!1),u=J(null);function c(e){const _=e.target.value.trim();_?(t(o,_),i(!1)):n(o)}return z((()=>{u.current&&(u.current.focus(),u.current.setSelectionRange(u.current.value.length,u.current.value.length))}),[r]),f("li",{class:se({completed:o.completed,editing:r}),"data-priority":4-l%5},f("div",{class:"view"},f("input",{class:"toggle",type:"checkbox",checked:o.completed,onChange:function(e){_(o),e.preventDefault()}}),f("label",{onDblClick:function(){i(!0)}},o.title),f("button",{class:"destroy",onClick:function(){n(o)}})),r?f("div",{class:"input-container"},f("input",{class:"edit",id:"edit-todo-input",ref:u,onBlur:c,onKeyDown:function(e){"Escape"===e.key?i(!1):"Enter"===e.key&&c(e)},defaultValue:o.title}),f("label",{class:"visually-hidden",htmlFor:"edit-todo-input"},"Edit Todo Input"," ")):null)}const fe={all:e=>!0,active:e=>!e.completed,completed:e=>e.completed};function pe(e){let{onChange:t,onToggle:n,onRemove:_,onSave:o,todos:l,route:r}=e;const i=l.filter(fe[r]);return f("main",{class:"main"},f("div",{class:"toggle-all-container"},f("input",{class:"toggle-all",type:"checkbox",id:"toggle-all",checked:0===l.filter(fe.active).length,onChange:t}),f("label",{class:"toggle-all-label",htmlFor:"toggle-all"},"Toggle All Input")),f("ul",{class:"todo-list show-priority"},i.map(((e,t)=>f(ae,{key:e.id,todo:e,onToggle:n,onRemove:_,onSave:o,index:t})))))}function de(e){let{todos:t,route:n,onClearCompleted:_}=e;const o=t.filter((e=>!e.completed)).length,l=t.length-o;return f("footer",{class:"footer"},f("span",{class:"todo-count"},`${o} ${1===o?"item":"items"} left!`),f("ul",{class:"filters"},f("li",null,f("a",{href:"#/",class:se({selected:"all"===n})},"All"))," ",f("li",null,f("a",{href:"#/active",class:se({selected:"active"===n})},"Active"))," ",f("li",null,f("a",{href:"#/completed",class:se({selected:"completed"===n})},"Completed"))),l>0&&f("button",{class:"clear-completed",onClick:_},"Clear completed"))}const he=()=>{let e=String(location.hash||"").split("/").pop();return fe[e]||(e="all"),e};!function(n,_,o){var l,r,u;t.__&&t.__(n,_),r=(l="function"==typeof o)?null:o&&o.__k||_.__k,u=[],T(_,n=(!l&&o||_).__k=f(d,null,[n]),r||i,i,void 0!==_.ownerSVGElement,!l&&o?[o]:r?null:_.firstChild?e.call(_.childNodes):null,u,!l&&o?o:r?r.__e:_.firstChild,l),N(u,n)}(f((function(){const[,e]=G(Date.now()),[t,n]=G("all"),_=function(e){const t=[e];function n(){t.forEach((e=>e()))}return{addItem:function(e){le=le.concat({id:oe(),title:e,completed:!1}),n()},toggleAll:function(e){le=le.map((t=>({...t,completed:e}))),n()},toggleItem:function(e){le=le.map((t=>t!==e?t:{...t,completed:!t.completed})),n()},removeItem:function(e){le=le.filter((t=>t!==e)),n()},updateItem:function(e,t){le=le.map((n=>n!==e?n:{...n,title:t})),n()},clearCompleted:function(){le=le.filter((e=>!e.completed)),n()},getTodos:function(){return[...le]}}}((function(){e(Date.now())}));return z((()=>{function e(){n(he())}addEventListener("hashchange",e),e()}),[]),f(d,null,f(re,{onKeyDown:function(e){if("Enter"===e.key){const t=e.target.value.trim();t&&(_.addItem(t),e.target.value="")}}}),_.getTodos().length>0?f(d,null,f(pe,{todos:_.getTodos(),route:t,onChange:function(e){_.toggleAll(e.target.checked)},onToggle:_.toggleItem,onRemove:_.removeItem,onSave:_.updateItem}),f(de,{todos:_.getTodos(),route:t,onClearCompleted:_.clearCompleted})):null)}),null),document.querySelector(".todoapp"))}();
//# sourceMappingURL=app.js.map

File diff suppressed because one or more lines are too long

View file

@ -4,5 +4,5 @@
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
ie=ce,function(){var e={}.hasOwnProperty;function t(){for(var n=[],_=0;_<arguments.length;_++){var o=arguments[_];if(o){var l=typeof o;if("string"===l||"number"===l)n.push(o);else if(Array.isArray(o)){if(o.length){var r=t.apply(null,o);r&&n.push(r)}}else if("object"===l){if(o.toString!==Object.prototype.toString&&!o.toString.toString().includes("[native code]")){n.push(o.toString());continue}for(var i in o)e.call(o,i)&&o[i]&&n.push(i)}}}return n.join(" ")}ie.exports?(t.default=t,ie.exports=t):window.classNames=t}();var se=ue;function ae(e){let{onSave:t,onRemove:n,onToggle:_,todo:o,index:l}=e;const[r,i]=G(!1),u=J(null);function c(e){const _=e.target.value.trim();_?(t(o,_),i(!1)):n(o)}return z((()=>{u.current&&(u.current.focus(),u.current.setSelectionRange(u.current.value.length,u.current.value.length))}),[r]),f("li",{class:se({completed:o.completed,editing:r}),"data-priority":4-l%5},f("div",{class:"view"},f("input",{class:"toggle",type:"checkbox",checked:o.completed,onChange:function(e){_(o),e.preventDefault()}}),f("label",{onDblClick:function(){i(!0)}},o.title),f("button",{class:"destroy",onClick:function(){n(o)}})),r?f("div",{class:"input-container"},f("input",{class:"edit",id:"edit-todo-input",ref:u,onBlur:c,onKeyDown:function(e){"Escape"===e.key?i(!1):"Enter"===e.key&&c(e)},defaultValue:o.title}),f("label",{class:"visually-hidden",htmlFor:"edit-todo-input"},"Edit Todo Input"," ")):null)}const fe={all:e=>!0,active:e=>!e.completed,completed:e=>e.completed};function pe(e){let{onChange:t,onToggle:n,onRemove:_,onSave:o,todos:l,route:r}=e;const i=l.filter(fe[r]);return f("main",{class:"main"},f("div",{class:"toggle-all-container"},f("input",{class:"toggle-all",type:"checkbox",checked:0===l.filter(fe.active).length,onChange:t}),f("label",{class:"toggle-all-label",htmlFor:"toggle-all"},"Toggle All Input")),f("ul",{class:"todo-list show-priority"},i.map(((e,t)=>f(ae,{key:e.id,todo:e,onToggle:n,onRemove:_,onSave:o,index:t})))))}function de(e){let{todos:t,route:n,onClearCompleted:_}=e;const o=t.filter((e=>!e.completed)).length,l=t.length-o;return f("footer",{class:"footer"},f("span",{class:"todo-count"},`${o} ${1===o?"item":"items"} left!`),f("ul",{class:"filters"},f("li",null,f("a",{href:"#/",class:se({selected:"all"===n})},"All"))," ",f("li",null,f("a",{href:"#/active",class:se({selected:"active"===n})},"Active"))," ",f("li",null,f("a",{href:"#/completed",class:se({selected:"completed"===n})},"Completed"))),l>0&&f("button",{class:"clear-completed",onClick:_},"Clear completed"))}const he=()=>{let e=String(location.hash||"").split("/").pop();return fe[e]||(e="all"),e};!function(n,_,o){var l,r,u;t.__&&t.__(n,_),r=(l="function"==typeof o)?null:o&&o.__k||_.__k,u=[],T(_,n=(!l&&o||_).__k=f(d,null,[n]),r||i,i,void 0!==_.ownerSVGElement,!l&&o?[o]:r?null:_.firstChild?e.call(_.childNodes):null,u,!l&&o?o:r?r.__e:_.firstChild,l),N(u,n)}(f((function(){const[,e]=G(Date.now()),[t,n]=G("all"),_=function(e){const t=[e];function n(){t.forEach((e=>e()))}return{addItem:function(e){le=le.concat({id:oe(),title:e,completed:!1}),n()},toggleAll:function(e){le=le.map((t=>({...t,completed:e}))),n()},toggleItem:function(e){le=le.map((t=>t!==e?t:{...t,completed:!t.completed})),n()},removeItem:function(e){le=le.filter((t=>t!==e)),n()},updateItem:function(e,t){le=le.map((n=>n!==e?n:{...n,title:t})),n()},clearCompleted:function(){le=le.filter((e=>!e.completed)),n()},getTodos:function(){return[...le]}}}((function(){e(Date.now())}));return z((()=>{function e(){n(he())}addEventListener("hashchange",e),e()}),[]),f(d,null,f(re,{onKeyDown:function(e){if("Enter"===e.key){const t=e.target.value.trim();t&&(_.addItem(t),e.target.value="")}}}),_.getTodos().length>0?f(d,null,f(pe,{todos:_.getTodos(),route:t,onChange:function(e){_.toggleAll(e.target.checked)},onToggle:_.toggleItem,onRemove:_.removeItem,onSave:_.updateItem}),f(de,{todos:_.getTodos(),route:t,onClearCompleted:_.clearCompleted})):null)}),null),document.querySelector(".todoapp"))}();
ie=ce,function(){var e={}.hasOwnProperty;function t(){for(var n=[],_=0;_<arguments.length;_++){var o=arguments[_];if(o){var l=typeof o;if("string"===l||"number"===l)n.push(o);else if(Array.isArray(o)){if(o.length){var r=t.apply(null,o);r&&n.push(r)}}else if("object"===l){if(o.toString!==Object.prototype.toString&&!o.toString.toString().includes("[native code]")){n.push(o.toString());continue}for(var i in o)e.call(o,i)&&o[i]&&n.push(i)}}}return n.join(" ")}ie.exports?(t.default=t,ie.exports=t):window.classNames=t}();var se=ue;function ae(e){let{onSave:t,onRemove:n,onToggle:_,todo:o,index:l}=e;const[r,i]=G(!1),u=J(null);function c(e){const _=e.target.value.trim();_?(t(o,_),i(!1)):n(o)}return z((()=>{u.current&&(u.current.focus(),u.current.setSelectionRange(u.current.value.length,u.current.value.length))}),[r]),f("li",{class:se({completed:o.completed,editing:r}),"data-priority":4-l%5},f("div",{class:"view"},f("input",{class:"toggle",type:"checkbox",checked:o.completed,onChange:function(e){_(o),e.preventDefault()}}),f("label",{onDblClick:function(){i(!0)}},o.title),f("button",{class:"destroy",onClick:function(){n(o)}})),r?f("div",{class:"input-container"},f("input",{class:"edit",id:"edit-todo-input",ref:u,onBlur:c,onKeyDown:function(e){"Escape"===e.key?i(!1):"Enter"===e.key&&c(e)},defaultValue:o.title}),f("label",{class:"visually-hidden",htmlFor:"edit-todo-input"},"Edit Todo Input"," ")):null)}const fe={all:e=>!0,active:e=>!e.completed,completed:e=>e.completed};function pe(e){let{onChange:t,onToggle:n,onRemove:_,onSave:o,todos:l,route:r}=e;const i=l.filter(fe[r]);return f("main",{class:"main"},f("div",{class:"toggle-all-container"},f("input",{class:"toggle-all",type:"checkbox",id:"toggle-all",checked:0===l.filter(fe.active).length,onChange:t}),f("label",{class:"toggle-all-label",htmlFor:"toggle-all"},"Toggle All Input")),f("ul",{class:"todo-list show-priority"},i.map(((e,t)=>f(ae,{key:e.id,todo:e,onToggle:n,onRemove:_,onSave:o,index:t})))))}function de(e){let{todos:t,route:n,onClearCompleted:_}=e;const o=t.filter((e=>!e.completed)).length,l=t.length-o;return f("footer",{class:"footer"},f("span",{class:"todo-count"},`${o} ${1===o?"item":"items"} left!`),f("ul",{class:"filters"},f("li",null,f("a",{href:"#/",class:se({selected:"all"===n})},"All"))," ",f("li",null,f("a",{href:"#/active",class:se({selected:"active"===n})},"Active"))," ",f("li",null,f("a",{href:"#/completed",class:se({selected:"completed"===n})},"Completed"))),l>0&&f("button",{class:"clear-completed",onClick:_},"Clear completed"))}const he=()=>{let e=String(location.hash||"").split("/").pop();return fe[e]||(e="all"),e};!function(n,_,o){var l,r,u;t.__&&t.__(n,_),r=(l="function"==typeof o)?null:o&&o.__k||_.__k,u=[],T(_,n=(!l&&o||_).__k=f(d,null,[n]),r||i,i,void 0!==_.ownerSVGElement,!l&&o?[o]:r?null:_.firstChild?e.call(_.childNodes):null,u,!l&&o?o:r?r.__e:_.firstChild,l),N(u,n)}(f((function(){const[,e]=G(Date.now()),[t,n]=G("all"),_=function(e){const t=[e];function n(){t.forEach((e=>e()))}return{addItem:function(e){le=le.concat({id:oe(),title:e,completed:!1}),n()},toggleAll:function(e){le=le.map((t=>({...t,completed:e}))),n()},toggleItem:function(e){le=le.map((t=>t!==e?t:{...t,completed:!t.completed})),n()},removeItem:function(e){le=le.filter((t=>t!==e)),n()},updateItem:function(e,t){le=le.map((n=>n!==e?n:{...n,title:t})),n()},clearCompleted:function(){le=le.filter((e=>!e.completed)),n()},getTodos:function(){return[...le]}}}((function(){e(Date.now())}));return z((()=>{function e(){n(he())}addEventListener("hashchange",e),e()}),[]),f(d,null,f(re,{onKeyDown:function(e){if("Enter"===e.key){const t=e.target.value.trim();t&&(_.addItem(t),e.target.value="")}}}),_.getTodos().length>0?f(d,null,f(pe,{todos:_.getTodos(),route:t,onChange:function(e){_.toggleAll(e.target.checked)},onToggle:_.toggleItem,onRemove:_.removeItem,onSave:_.updateItem}),f(de,{todos:_.getTodos(),route:t,onClearCompleted:_.clearCompleted})):null)}),null),document.querySelector(".todoapp"))}();
//# sourceMappingURL=app.js.map

File diff suppressed because one or more lines are too long

View file

@ -10,7 +10,7 @@ export default function TodoMain({ onChange, onToggle, onRemove, onSave, todos,
return (
<main class="main">
<div class="toggle-all-container">
<input class="toggle-all" type="checkbox" checked={activeTodoCount === 0} onChange={onChange} />
<input class="toggle-all" type="checkbox" id="toggle-all" checked={activeTodoCount === 0} onChange={onChange} />
<label class="toggle-all-label" htmlFor="toggle-all">
Toggle All Input
</label>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@ export default class Main extends Component {
return (
<main className="main" data-testid="main">
<div className="toggle-all-container">
<input className="toggle-all" type="checkbox" data-testid="toggle-all" checked={completedCount === todos.length} onChange={toggleAll} />
<input className="toggle-all" type="checkbox" id="toggle-all" data-testid="toggle-all" checked={completedCount === todos.length} onChange={toggleAll} />
<label className="toggle-all-label" htmlFor="toggle-all">
Toggle All Input
</label>

View file

@ -6,6 +6,8 @@ This application uses React 17.0.2 to implement a todo application.
- [React](https://reactjs.org/) is a JavaScript library for creating user interfaces.
The downstream version also generates the TodoMVC-React-Window workload. A variation of TodoMVC-React that renders and controls the app in a new window using the scripts loaded in the parent window.
## Implementation details
React:\
@ -22,6 +24,8 @@ Controller: serves as an interface between view & model components
To build the static files, this application utilizes webpack. It minifies and optimizes output files and copies all necessary files to a `dist` folder.
For TodoMVC-React-Window, the webpack scripts separate the JavaScript from the CSS. The JavaScript is loaded by index.html in the main window, while the css is loaded by todo.html in the child window.
## Requirements
The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<!doctype html><html lang="en" data-framework="react" data-version="17.0.2" data-features=""><head><meta charset="UTF-8"/><meta name="description" content="A TodoMVC workload app for Speedometer!"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta http-equiv="X-UA-Compatible" content="ie=edge"/><title>TodoMVC: React-Window</title><script defer="defer" src="windowed.bundle.js"></script></head></html>

View file

@ -0,0 +1 @@
<!doctype html><html lang="en" data-framework="react" data-version="17.0.2" data-features=""><head><meta charset="UTF-8"/><meta name="description" content="A TodoMVC workload app for Speedometer!"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta http-equiv="X-UA-Compatible" content="ie=edge"/><link href="windowed.css" rel="stylesheet"><title>TodoMVC: React-Window inner window</title></head><body><section class="todoapp" id="root"></section><footer class="info"><p>Click on input field to write your todo.</p><p>At least two characters are needed to be a valid entry.</p><p>Press 'enter' to add the todo.</p><p>Double-click to edit a todo</p></footer></body></html>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,69 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @remix-run/router v1.3.3
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.8.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en" data-framework="react" data-version="17.0.2" data-features="">
<head>
<meta charset="UTF-8" />
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TodoMVC: React-Window</title>
<script defer="defer" src=<%= htmlWebpackPlugin.options.script %> ></script>
</head>
</html>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en" data-framework="react" data-version="17.0.2" data-features="">
<head>
<meta charset="UTF-8" />
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link href=<%= htmlWebpackPlugin.options.styles %> rel="stylesheet">
<title>TodoMVC: React-Window inner window</title>
</head>
<body>
<section class="todoapp" id="root"></section>
<footer class="info">
<p>Click on input field to write your todo.</p>
<p>At least two characters are needed to be a valid entry.</p>
<p>Press 'enter' to add the todo.</p>
<p>Double-click to edit a todo</p>
</footer>
</body>
</html>

View file

@ -0,0 +1,26 @@
import React from "react";
import { render } from "react-dom";
import { HashRouter, Route, Routes } from "react-router-dom";
import { App } from "./todo/app";
import "todomvc-app-css/index.css";
let childWindow = window.open("todo.html", "_blank", "width=800,height=600");
let AsyncLoop = () => {
if (childWindow.document.getElementById("root")) {
render(
<HashRouter>
<Routes>
<Route path="*" element={<App />} />
</Routes>
</HashRouter>,
childWindow.document.getElementById("root")
);
globalThis.childWindow = childWindow;
} else {
setTimeout(AsyncLoop, 10);
}
};
AsyncLoop();

View file

@ -27,7 +27,7 @@ export function Main({ todos, dispatch }) {
<main className="main" data-testid="main">
{visibleTodos.length > 0 ? (
<div className="toggle-all-container">
<input className="toggle-all" type="checkbox" data-testid="toggle-all" checked={visibleTodos.every((todo) => todo.completed)} onChange={toggleAll} />
<input className="toggle-all" type="checkbox" id="toggle-all" data-testid="toggle-all" checked={visibleTodos.every((todo) => todo.completed)} onChange={toggleAll} />
<label className="toggle-all-label" htmlFor="toggle-all">
Toggle All Input
</label>

View file

@ -4,12 +4,28 @@ const path = require("path");
module.exports = {
entry: {
app: path.resolve(__dirname, "src", "index.js"),
windowed: path.resolve(__dirname, "src", "index-window.js"),
},
plugins: [
new HtmlWebpackPlugin({
title: "TodoMVC: React",
chunks: ["app"],
template: path.resolve(__dirname, "public", "index.html"),
}),
new HtmlWebpackPlugin({
title: "TodoMVC: React-Window",
inject: false,
template: path.resolve(__dirname, "public", "window", "index.html"),
filename: "index-window.html",
script: "windowed.bundle.js",
}),
new HtmlWebpackPlugin({
title: "TodoMVC: React-Window inner window",
inject: false,
template: path.resolve(__dirname, "public", "window", "todo.html"),
filename: "todo.html",
styles: "windowed.css",
}),
],
output: {
filename: "[name].bundle.js",