From 7051874f97f006d3f2156aceee6a7b21bccf8c7b Mon Sep 17 00:00:00 2001 From: Bit Cloud Labs Date: Sat, 27 Jun 2026 09:19:15 +0000 Subject: [PATCH] feat: convert Module 03 to interactive autograded starter workspace Transform the JavaScript Foundations module into a work-along workspace: self-contained labs//{README,src,tests} and assignments/capstone, with vitest (Node + jsdom) as the autograder. Tests are the spec; no answer keys. - Remove LMS-duplicated content (Lesson_*, guides, flat lab/assignment briefs). - Add root toolchain: package.json, vitest.config.ts, scripts/grade.mjs (per-exercise score, no tsc gate), .github/workflows/autograde.yml, .gitignore. - 12 labs + capstone: render-blocking analysis, login-form DOM wiring (jsdom), layout audit, runtime bugs, ES6+ modernization, event loop + async, Map/Set optimization, binary search/memoize/debounce, fetch error handling, curl timing attribution, layout-trigger/batched-render, and an integrated capstone. - 81 tests; verified stubs RED (16/81) and reference solution 100% GREEN. --- .devcontainer/devcontainer.json | 2 +- .github/workflows/autograde.yml | 60 + .gitignore | 4 + LEARNER_GUIDE.md | 38 - Lesson_00.md | 104 - Lesson_01.md | 103 - Lesson_02.md | 102 - Lesson_03.md | 101 - Lesson_04.md | 111 - Lesson_05.md | 122 - Lesson_06.md | 115 - Lesson_07.md | 109 - Lesson_08.md | 122 - Lesson_09.md | 118 - Lesson_10.md | 95 - Lesson_11.md | 99 - Lesson_12.md | 82 - MODULE_SYLLABUS.md | 55 - README.md | 118 +- assignments/README.md | 19 - assignments/capstone-brief.md | 65 - assignments/capstone/README.md | 45 + assignments/capstone/src/diagnostics.js | 68 + .../capstone/tests/diagnostics.test.js | 96 + labs/README.md | 42 - labs/lab-00-environment.md | 69 - labs/lab-00-environment/README.md | 36 + labs/lab-00-environment/src/devtools.js | 33 + .../lab-00-environment/tests/devtools.test.js | 64 + labs/lab-01-page-load.md | 57 - labs/lab-01-page-load/README.md | 29 + labs/lab-01-page-load/src/load.js | 30 + labs/lab-01-page-load/tests/load.test.js | 43 + labs/lab-02-login-form.md | 102 - labs/lab-02-login-form/README.md | 37 + labs/lab-02-login-form/src/login.js | 44 + labs/lab-02-login-form/tests/login.test.js | 69 + labs/lab-03-layout.md | 83 - labs/lab-03-layout/README.md | 52 + labs/lab-03-layout/src/layout.js | 41 + labs/lab-03-layout/tests/layout.test.js | 57 + labs/lab-04-js-runtime.md | 93 - labs/lab-04-js-runtime/README.md | 29 + labs/lab-04-js-runtime/src/runtime.js | 37 + labs/lab-04-js-runtime/tests/runtime.test.js | 31 + labs/lab-05-modernize.md | 83 - labs/lab-05-modernize/README.md | 34 + labs/lab-05-modernize/src/utils.js | 33 + labs/lab-05-modernize/tests/utils.test.js | 37 + labs/lab-06-async-eventloop.md | 88 - labs/lab-06-async-eventloop/README.md | 51 + labs/lab-06-async-eventloop/src/async.js | 59 + .../tests/async.test.js | 55 + labs/lab-07-data-structures.md | 88 - labs/lab-07-data-structures/README.md | 26 + labs/lab-07-data-structures/src/fast.js | 24 + labs/lab-07-data-structures/src/slow.js | 16 + .../lab-07-data-structures/tests/fast.test.js | 48 + labs/lab-08-search.md | 92 - labs/lab-08-search/README.md | 28 + labs/lab-08-search/src/fast.js | 36 + labs/lab-08-search/tests/fast.test.js | 74 + labs/lab-09-http-rest.md | 69 - labs/lab-09-http-rest/README.md | 33 + labs/lab-09-http-rest/src/client.js | 31 + labs/lab-09-http-rest/tests/client.test.js | 74 + labs/lab-10-dns-tcp.md | 49 - labs/lab-10-dns-tcp/README.md | 44 + labs/lab-10-dns-tcp/src/journey.js | 39 + labs/lab-10-dns-tcp/tests/journey.test.js | 36 + labs/lab-11-rendering.md | 72 - labs/lab-11-rendering/README.md | 31 + labs/lab-11-rendering/src/render.js | 40 + labs/lab-11-rendering/tests/render.test.js | 54 + package-lock.json | 2197 +++++++++++++++++ package.json | 16 + scripts/grade.mjs | 87 + vitest.config.ts | 11 + 78 files changed, 4173 insertions(+), 2613 deletions(-) create mode 100644 .github/workflows/autograde.yml delete mode 100644 LEARNER_GUIDE.md delete mode 100644 Lesson_00.md delete mode 100644 Lesson_01.md delete mode 100644 Lesson_02.md delete mode 100644 Lesson_03.md delete mode 100644 Lesson_04.md delete mode 100644 Lesson_05.md delete mode 100644 Lesson_06.md delete mode 100644 Lesson_07.md delete mode 100644 Lesson_08.md delete mode 100644 Lesson_09.md delete mode 100644 Lesson_10.md delete mode 100644 Lesson_11.md delete mode 100644 Lesson_12.md delete mode 100644 MODULE_SYLLABUS.md delete mode 100644 assignments/README.md delete mode 100644 assignments/capstone-brief.md create mode 100644 assignments/capstone/README.md create mode 100644 assignments/capstone/src/diagnostics.js create mode 100644 assignments/capstone/tests/diagnostics.test.js delete mode 100644 labs/README.md delete mode 100644 labs/lab-00-environment.md create mode 100644 labs/lab-00-environment/README.md create mode 100644 labs/lab-00-environment/src/devtools.js create mode 100644 labs/lab-00-environment/tests/devtools.test.js delete mode 100644 labs/lab-01-page-load.md create mode 100644 labs/lab-01-page-load/README.md create mode 100644 labs/lab-01-page-load/src/load.js create mode 100644 labs/lab-01-page-load/tests/load.test.js delete mode 100644 labs/lab-02-login-form.md create mode 100644 labs/lab-02-login-form/README.md create mode 100644 labs/lab-02-login-form/src/login.js create mode 100644 labs/lab-02-login-form/tests/login.test.js delete mode 100644 labs/lab-03-layout.md create mode 100644 labs/lab-03-layout/README.md create mode 100644 labs/lab-03-layout/src/layout.js create mode 100644 labs/lab-03-layout/tests/layout.test.js delete mode 100644 labs/lab-04-js-runtime.md create mode 100644 labs/lab-04-js-runtime/README.md create mode 100644 labs/lab-04-js-runtime/src/runtime.js create mode 100644 labs/lab-04-js-runtime/tests/runtime.test.js delete mode 100644 labs/lab-05-modernize.md create mode 100644 labs/lab-05-modernize/README.md create mode 100644 labs/lab-05-modernize/src/utils.js create mode 100644 labs/lab-05-modernize/tests/utils.test.js delete mode 100644 labs/lab-06-async-eventloop.md create mode 100644 labs/lab-06-async-eventloop/README.md create mode 100644 labs/lab-06-async-eventloop/src/async.js create mode 100644 labs/lab-06-async-eventloop/tests/async.test.js delete mode 100644 labs/lab-07-data-structures.md create mode 100644 labs/lab-07-data-structures/README.md create mode 100644 labs/lab-07-data-structures/src/fast.js create mode 100644 labs/lab-07-data-structures/src/slow.js create mode 100644 labs/lab-07-data-structures/tests/fast.test.js delete mode 100644 labs/lab-08-search.md create mode 100644 labs/lab-08-search/README.md create mode 100644 labs/lab-08-search/src/fast.js create mode 100644 labs/lab-08-search/tests/fast.test.js delete mode 100644 labs/lab-09-http-rest.md create mode 100644 labs/lab-09-http-rest/README.md create mode 100644 labs/lab-09-http-rest/src/client.js create mode 100644 labs/lab-09-http-rest/tests/client.test.js delete mode 100644 labs/lab-10-dns-tcp.md create mode 100644 labs/lab-10-dns-tcp/README.md create mode 100644 labs/lab-10-dns-tcp/src/journey.js create mode 100644 labs/lab-10-dns-tcp/tests/journey.test.js delete mode 100644 labs/lab-11-rendering.md create mode 100644 labs/lab-11-rendering/README.md create mode 100644 labs/lab-11-rendering/src/render.js create mode 100644 labs/lab-11-rendering/tests/render.test.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/grade.mjs create mode 100644 vitest.config.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ac86273..b823406 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,7 +11,7 @@ }, "ghcr.io/devcontainers/features/github-cli:1": {} }, - "postCreateCommand": "echo '\\n=== Forge SWEXP environment ready ==='; git --version; node --version 2>/dev/null; echo 'Open README.md, then Lesson_00.md to begin.'", + "postCreateCommand": "npm install && echo '\\n=== Forge SWEXP environment ready ==='; git --version; node --version 2>/dev/null; echo 'Open README.md, then pick an exercise under labs/ and run: npm test'", "customizations": { "vscode": { "extensions": [ diff --git a/.github/workflows/autograde.yml b/.github/workflows/autograde.yml new file mode 100644 index 0000000..6da497e --- /dev/null +++ b/.github/workflows/autograde.yml @@ -0,0 +1,60 @@ +name: Autograde + +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + grade: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run autograder + id: grade + continue-on-error: true + run: npm run grade + + - name: Publish score to job summary + if: always() + run: cat grade-report.md >> "$GITHUB_STEP_SUMMARY" || true + + - name: Comment score on pull request + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + let body = marker + '\n'; + try { body += fs.readFileSync('grade-report.md', 'utf8'); } + catch { body += 'Autograder did not produce a report.'; } + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + + - name: Fail the check if incomplete + if: steps.grade.outcome != 'success' + run: | + echo "Exercises are not yet complete — see the autograde summary above." + exit 1 diff --git a/.gitignore b/.gitignore index 646ac51..f9bd586 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .DS_Store node_modules/ +.grade/ +grade-report.md +dist/ +coverage/ diff --git a/LEARNER_GUIDE.md b/LEARNER_GUIDE.md deleted file mode 100644 index f8c4174..0000000 --- a/LEARNER_GUIDE.md +++ /dev/null @@ -1,38 +0,0 @@ -# Learner Guide — Web Platform & JavaScript Foundations - -## You are a frontend engineer, not a student doing exercises -Every lesson is an **engineering ticket** on *Project Forge*. Approach each one as real work: understand what's being asked, open the right instrument and gather evidence *before* changing anything, fix the cause, measure the result, and document your reasoning. The goal isn't the "right answer" — it's the judgment and habits of an engineer a team trusts with a production web app. - -## The one habit that matters most -**Investigate first, optimize second.** Before you touch code: what does the instrument show? The Network waterfall, the Performance trace, the Elements box model, the Console error, the actual HTTP status, the `curl -v` output. Then localize the problem to a layer, fix the cause, and prove the fix with before/after evidence. - -## How each lesson works -1. **Read the ticket and the deep dive** — understand the concepts and acceptance criteria. -2. **Do the lab.** Node labs: run the generator, predict before running, then fix. Browser labs: serve the page and investigate with DevTools. -3. **Investigate** — the Engineering Investigation pushes you from "it works" to "I have evidence for why." -4. **Run the AI exercise** — practice draft → verify → log deliberately. -5. **Submit the assignment** and **update your notebook.** -6. **Check the solution** to validate your reasoning — after you've done the work. - -Track your progress in `dashboard.html`. - -## What every assignment must include -- **Investigation:** the instrument, the evidence, the layer localized. -- **Root cause** (not the symptom) and the **fix**. -- **Verification:** measured before/after, a clean trace, passing tests, or correct status codes. -- **AI-usage log:** what you asked, what you verified it against, what you corrected. -- **Clean commits** (your Module 02 Git habits apply). - -## Using AI responsibly -AI is a fast, confident, sometimes-wrong assistant. On the web platform, "confidently wrong" usually means an explanation the DevTools evidence contradicts (blaming images for slow paint, mis-ordering the event loop, assuming `fetch` throws on 404). Use it, but always **draft → verify against the instrument → log.** When the AI and the instrument disagree, the instrument wins. `resources/ai-workflow-guide.md` maps where AI most often misleads here. - -## When you're stuck -- The Console and the Network/Performance panels almost always show where you are. -- `resources/debugging-playbook.md` has an evidence-first recipe for each common failure. -- Reproduce the problem, observe it in an instrument, localize it — then fix. You're rarely as stuck as you feel. - -## The golden rule -**Never claim a cause you haven't observed.** A measurement beats an intuition; a trace beats a guess; the instrument beats the confident answer. - -## How you're graded -Against `ASSESSMENT_RUBRIC.md` — on investigation, evidence-based debugging, performance reasoning, and documentation, **not** on memorizing APIs. Choosing the right instrument and proving your fix beats reciting syntax. diff --git a/Lesson_00.md b/Lesson_00.md deleted file mode 100644 index 7211cae..0000000 --- a/Lesson_00.md +++ /dev/null @@ -1,104 +0,0 @@ -# Lesson 00 — Welcome to the Frontend Engineering Team - -> **Role:** Frontend Engineer · **Competency:** Environment & Developer Tools · **Track:** WEB · **Est. time:** 2–3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: WEB-1000 -TITLE: Onboard to frontend engineering and the browser as a platform -PRIORITY: P1 — blocks all other work -TYPE: Onboarding -ASSIGNEE: You (Frontend Engineer) -DESCRIPTION: Welcome to the Project Forge web team. The browser is not a black - box that "runs your code" — it is the platform you build on, and a - professional frontend engineer can open it up and see exactly what - it is doing. Set up your environment and learn to drive the - browser's developer tools as investigation instruments. - -ACCEPTANCE CRITERIA: - - A modern browser with DevTools, Node.js (LTS or newer), and an editor are working - - You can serve a static page locally and open it in the browser - - You can navigate the core DevTools panels: Elements, Console, Network, Sources, Performance - - You can explain what each panel is for in your own words - - Your engineering notebook is created with a dated first entry -``` - -## 🏢 Business Context - -Project Forge's web app is used by thousands of customers daily. When it's slow, broken, or behaving strangely, the engineers who can *investigate* — open the browser's instruments and read what's actually happening — fix problems in minutes that others guess at for hours. This module's ethos, which you'll hear constantly: **investigate first, optimize second.** You cannot fix what you have not measured. - -## 🎯 Learning Objectives - -- Set up a frontend environment: browser + DevTools, Node.js, an editor, and a local static server -- Identify the purpose of each core DevTools panel -- Serve and open a local page -- Begin treating the browser as an inspectable platform, not magic -- Start an engineering notebook - -## 📚 Technical Deep Dive - -**The browser is a platform.** It parses HTML into a DOM, CSS into a CSSOM, runs JavaScript in an engine (V8 in Chrome/Node, SpiderMonkey in Firefox), makes network requests, and paints pixels. Every one of those stages is observable through DevTools. Learning to *look* is the whole job for the first half of this module. - -**The core DevTools panels:** - -| Panel | What it shows | You'll use it for | -|-------|---------------|-------------------| -| Elements | the live DOM tree + applied CSS | inspecting structure, debugging layout (L2, L3, L11) | -| Console | logs, errors, a JS REPL | reading errors, quick experiments (every lesson) | -| Network | every request, timing, headers | page-load and API investigation (L1, L9) | -| Sources | loaded scripts, breakpoints, debugger | stepping through JS (L2, L4, L6) | -| Performance | a recorded timeline of the main thread | finding jank, long tasks, reflow (L6, L11) | -| Application | storage, cache, service workers | inspecting client state | - -**Open DevTools:** `F12`, or `Ctrl/Cmd+Shift+I`, or right-click → Inspect. - -**Why a local server (not `file://`).** Opening `file://...` works for trivial pages but breaks modules, fetch, and many APIs due to the browser's security model. Serve over `http://localhost` instead: - -```bash -npx serve . # or: python3 -m http.server 8000 -``` - -**Node.js** lets you run JavaScript outside the browser — essential for the language and algorithm lessons (4–8), where we'll measure code directly without browser noise. - -### Common gotchas -- Treating the browser as a black box and `console.log`-guessing instead of using the debugger and panels. -- Using `file://` and hitting confusing CORS/module errors. -- Forgetting that the Console is a full REPL — you can inspect live objects there. - -## 🧪 Hands-on Labs - -Work through **`labs/lab-00-environment.md`**: install/verify the toolchain, serve a provided sample page, open each DevTools panel, and record what each reveals about the page. - -## 🔍 Engineering Investigation - -Open the sample page with DevTools. In **Elements**, find the page's `

`. In **Console**, type `document.title` and `document.querySelectorAll('a').length`. In **Network**, reload and count the requests and total transfer size. Record one concrete fact you learned about the page from *each* panel. - -## 🤖 AI Engineering Exercise - -Ask an AI: *"What does the browser do between receiving HTML and showing pixels?"* **Draft** its answer, then **verify** the stages against what you can actually observe in the Network and Performance panels, and **log** in your notebook what matched reality and what was vague. This is the loop you'll use all module: **draft → verify against the instruments → log.** The golden rule here: **never claim a cause you haven't observed** — measurement beats intuition. - -## 📝 Assignment - -1. Get the full toolchain working; paste `node --version` and a note on your browser/editor. -2. Complete the lab and record one finding from each DevTools panel. -3. Write a short "what the browser actually is" explainer (5–8 sentences) in your own words. -4. Commit your notebook with a clear message (your Module 02 Git skills apply here). - -## 🚀 Stretch Goal - -Install the Lighthouse panel (built into Chrome DevTools) and run an audit on the sample page. Record the four category scores. You'll learn to act on these in later lessons — for now, just learn to generate the evidence. - -## ✅ Definition of Done - -- [ ] Browser + DevTools, Node.js, editor, and a local server all working -- [ ] Sample page served over `http://localhost` and opened -- [ ] One finding recorded from each core DevTools panel -- [ ] "What the browser actually is" explainer written in your own words -- [ ] Notebook committed - -## 🪞 Reflection - -Which panel surprised you most with how much it reveals? Where had you previously been guessing about something the browser could have just shown you? diff --git a/Lesson_01.md b/Lesson_01.md deleted file mode 100644 index fbfbdfe..0000000 --- a/Lesson_01.md +++ /dev/null @@ -1,103 +0,0 @@ -# Lesson 01 — Investigate How a Website Loads - -> **Role:** Frontend Engineer · **Competency:** Page Load & the Critical Rendering Path · **Track:** WEB · **Est. time:** 3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: WEB-1010 -TITLE: Explain why the Forge marketing page is slow to first paint -PRIORITY: P2 -TYPE: Investigation -DESCRIPTION: Customers report the landing page "takes a while before anything - shows up." Before anyone changes code, produce an evidence-based - account of exactly what the browser does between the URL and the - first painted pixels, and where the time goes. - -ACCEPTANCE CRITERIA: - - You can describe the critical rendering path: HTML → DOM, CSS → CSSOM, render tree, layout, paint - - You can read a Network waterfall and identify render-blocking resources - - You can distinguish key load milestones (TTFB, FCP, LCP, DOMContentLoaded, load) - - An investigation report explains the slow first paint with evidence -``` - -## 🏢 Business Context - -First impressions are measured in milliseconds. Users abandon pages that paint slowly, and search ranking depends on load metrics (Core Web Vitals). But "make it faster" is meaningless without understanding *what* the browser is doing and *which* step is the bottleneck. This lesson builds the mental model the rest of the module sharpens. - -## 🎯 Learning Objectives - -- Trace the **critical rendering path** from bytes to pixels -- Identify render-blocking CSS and JavaScript -- Read a Network waterfall (queuing, TTFB, download, dependencies) -- Define and locate FCP, LCP, DOMContentLoaded, and load - -## 📚 Technical Deep Dive - -**The critical rendering path.** To show pixels the browser must: - -1. **Parse HTML → DOM** (the document object model: a tree of nodes). -2. **Parse CSS → CSSOM** (the styling model). CSS is **render-blocking** — the browser won't paint until it has the CSSOM. -3. **Build the render tree** (DOM + CSSOM, minus non-visual nodes). -4. **Layout / reflow** — compute the geometry (size, position) of every box. -5. **Paint** — fill in pixels (text, colors, images, borders). -6. **Composite** — assemble layers to the screen. - -**Render-blocking resources.** A `` in `` blocks rendering until downloaded and parsed. A plain ` - - -HTML -cat > styles.css <<'CSS' -body { font-family: system-ui, sans-serif; margin: 2rem; line-height: 1.5; } -h1 { color: #1f6feb; } -button { padding: .5rem 1rem; } -CSS -cat > app.js <<'JS' -document.querySelector('#ping').addEventListener('click', () => { - document.querySelector('#out').textContent = 'pong @ ' + new Date().toLocaleTimeString(); -}); -console.log('app.js loaded; links =', document.querySelectorAll('a').length); -JS -echo "Serve it:"; echo " npx serve . # or: python3 -m http.server 8000" -``` - -## Tasks -Open the served page (e.g. `http://localhost:3000`) and open DevTools (`F12`). For **each panel**, record one concrete finding: -1. **Elements:** find the `

`; what color is applied, and from which CSS rule? -2. **Console:** read the `app.js` log; then type `document.title` and `document.querySelectorAll('a').length`. -3. **Network:** reload with cache disabled; how many requests, and what's the total transfer size? Which file is the document vs the stylesheet vs the script? -4. **Sources:** open `app.js`; set a breakpoint on the click handler; click **Ping** and confirm it pauses. -5. **Performance:** record a reload; find the first paint marker. -6. **(Stretch) Lighthouse:** run an audit; record the four category scores. - -## Deliverable -One finding per panel (six total), your `node --version`/browser/editor note, and a 5–8 sentence "what the browser actually is" explainer in your own words. - -## Cleanup -```bash -rm -rf /tmp/swexp-l00 -``` - -## Check -`../solutions/lab-00-solution.md`. diff --git a/labs/lab-00-environment/README.md b/labs/lab-00-environment/README.md new file mode 100644 index 0000000..c9a0c97 --- /dev/null +++ b/labs/lab-00-environment/README.md @@ -0,0 +1,36 @@ +# Lab 00 — Environment & the DevTools Panels + +**Goal:** a working toolchain, plus fluency reading what each DevTools panel tells you. + +In the LMS lesson you serve a small page locally and record one finding from each DevTools panel +(Elements, Console, Network, Sources, Performance, Lighthouse). That hands-on investigation can't be +autograded, but the **judgement** behind reading a Network entry can: given the data DevTools shows for +one resource, can you classify it correctly? + +## What you do + +In [`src/devtools.js`](src/devtools.js), implement `analyzeResource(entry)`. An `entry` is one row of the +Network panel: + +```js +{ url: 'big.css', type: 'stylesheet', inHead: true, async: false, defer: false, transferBytes: 120000 } +``` + +Return a summary object: + +- `kind` — the resource category: `'document' | 'stylesheet' | 'script' | 'image' | 'other'`, taken from + `type` (anything not in that set is `'other'`). +- `renderBlocking` — `true` when this resource blocks first paint. A **stylesheet** in `` is + render-blocking. A **script** is render-blocking only when it is in `` **and** has neither + `async` nor `defer`. Documents, images, and everything else are not render-blocking. +- `transferKb` — `transferBytes / 1024`, rounded to one decimal place. + +## Definition of done + +- All tests pass (`npx vitest run labs/lab-00-environment`). +- In your LMS notebook, record your six panel findings and a 5–8 sentence "what the browser actually is" + explainer. + +## Submit + +Edit `src/`, run the tests, commit and push. diff --git a/labs/lab-00-environment/src/devtools.js b/labs/lab-00-environment/src/devtools.js new file mode 100644 index 0000000..8fc21d9 --- /dev/null +++ b/labs/lab-00-environment/src/devtools.js @@ -0,0 +1,33 @@ +/** + * Lab 00 — Environment & the DevTools Panels. See README.md. + * + * @typedef {Object} ResourceEntry + * @property {string} url + * @property {string} type e.g. 'document' | 'stylesheet' | 'script' | 'image' | ... + * @property {boolean} [inHead] was the tag in ? + * @property {boolean} [async] - - -

Welcome to Project Forge

-

Hero copy the user is waiting to see.

- hero - - -HTML -# a large-ish CSS file (render-blocking) -node -e "let s='';for(let i=0;i<4000;i++)s+='.c'+i+'{color:#'+(i%9)+''+(i%9)+''+(i%9)+';padding:'+(i%5)+'px}\n';require('fs').writeFileSync('big.css','body{font-family:sans-serif;margin:2rem}h1{color:#1f6feb}\n'+s)" -# a slow blocking script (busy-wait to simulate parse/execute cost) -cat > blocking.js <<'JS' -var t = Date.now(); while (Date.now() - t < 400) {} // 400ms of blocking work in -console.log('blocking.js finished (blocked parsing for ~400ms)'); -JS -cat > hero.svg <<'SVG' -Forge -SVG -echo "Serve it: npx serve . (open with DevTools Network + Performance)" -``` - -## Tasks -1. **Baseline evidence (investigate first):** in the Network panel with **cache disabled**, reload and record: document TTFB, which resources are **render-blocking**, and the time of **FCP** vs **load** (Performance panel shows paint markers). -2. **Identify the bottleneck** from the data — is first paint delayed by the blocking ` - - -

Sign in

-
- - - -
-

- - -HTML -cat > login.js <<'JS' -// This file contains FOUR planted bugs. Find each with the Console/Sources before fixing. -// (The DOM wiring is guarded so Node can require `validate` for tests; in the browser -// `document` exists, so all four bugs are live there.) - -function validate(email, password) { - // pure logic (this part is correct and unit-tested in Node) - if (!email || !email.includes('@')) return 'Enter a valid email'; - if (!password || password.length < 8) return 'Password must be at least 8 characters'; - return null; // null === valid -} - -if (typeof document !== 'undefined') { - var form = document.querySelector('#signin-form'); // BUG A: wrong id (should be #login-form) → null - - function onSubmit(event) { - // BUG C: missing event.preventDefault() → the form does a full page reload - var email = document.querySelector('#email').value; - var password = document.querySelector('#password').value; - var err = validate(email, password); - var msg = document.querySelector('#message'); - if (err) { msg.className = 'error'; msg.textContent = err; } - else { msg.className = 'ok'; msg.textContent = 'Signed in!'; } - } - - form.addEventListener('submit', onSubmit()); // BUG B: calling onSubmit() instead of passing it -} - -// module export for Node tests (ignored by the browser) -if (typeof module !== 'undefined') module.exports = { validate }; -JS - -# Node-testable validation logic (pure) -cat > login.test.js <<'JS' -const assert = require('assert'); -const { validate } = require('./login.js'); // the DOM block is skipped in Node; only `validate` is tested -assert.strictEqual(validate('a@b.com', 'longenough'), null, 'valid input passes'); -assert.strictEqual(validate('bad', 'longenough'), 'Enter a valid email'); -assert.strictEqual(validate('a@b.com', 'short'), 'Password must be at least 8 characters'); -console.log('VALIDATION TESTS PASSED'); -JS -echo "Serve it: npx serve . (open with DevTools Console + Sources)" -echo "Run the pure validation tests anytime: node login.test.js" -``` - -## The four planted bugs -- **A — wrong selector:** `#signin-form` matches nothing → `null`; the next line throws *"Cannot read properties of null"*. -- **B — calling vs passing:** `addEventListener('submit', onSubmit())` runs `onSubmit` immediately (binding its return value, `undefined`) instead of registering it. -- **C — no `preventDefault`:** even once wired, submit triggers a full page reload, so "nothing happens." -- **D — script timing:** the script is in `` with no `defer`, so it runs before `#login-form` exists. - -## Tasks -1. **Reproduce, don't guess.** Load the page; read the Console error. Where does it point? Set a breakpoint in `onSubmit` — does it ever hit? (It won't, until B is fixed.) -2. **Fix A:** correct the selector to `#login-form`. -3. **Fix B:** pass the function reference: `form.addEventListener('submit', onSubmit)`. -4. **Fix C:** add `event.preventDefault()` as the first line of `onSubmit`. -5. **Fix D:** add `defer` to the ` -HTML -``` -Serve it (`npx serve .`), open it, click the button, and try to scroll/select while it runs — note the freeze. Record a Performance trace; find the **long task**. Then fix it by **chunking** the work (process a slice, `setTimeout(…, 0)` to yield, repeat) so `status` updates and the page stays responsive — or move the compute to a **Web Worker** (stretch). - -## Tasks -- Part 1: predicted vs actual order + the rule. -- Part 2: working `loadAsync` returning the same result as `loadCallback`. -- Part 3: a non-freezing version with Performance-trace evidence (long task gone). - -## Deliverable -The ordering result + rule; the async/await refactor; and before/after evidence that the UI no longer freezes, with an explanation of why your fix keeps the main thread free. - -## Cleanup -```bash -rm -rf /tmp/swexp-l06 -``` - -## Check -`../solutions/lab-06-solution.md`. diff --git a/labs/lab-06-async-eventloop/README.md b/labs/lab-06-async-eventloop/README.md new file mode 100644 index 0000000..fe866fb --- /dev/null +++ b/labs/lab-06-async-eventloop/README.md @@ -0,0 +1,51 @@ +# Lab 06 — Async JavaScript & the Event Loop + +**Goal:** know the event-loop ordering rule, convert callbacks → `async/await`, and stop a UI freeze by yielding. + +## What you do + +In [`src/async.js`](src/async.js), implement three things. + +### Part 1 — Event-loop ordering + +`eventLoopOrder()` returns the order the labelled lines of this snippet print, as an array of numbers: + +```js +console.log('1: sync start'); +setTimeout(() => console.log('2: setTimeout (macrotask)'), 0); +Promise.resolve().then(() => console.log('3: promise (microtask)')); +queueMicrotask(() => console.log('4: queueMicrotask (microtask)')); +Promise.resolve().then(() => { + console.log('5: microtask that schedules a macrotask'); + setTimeout(() => console.log('6: nested setTimeout'), 0); +}); +console.log('7: sync end'); +``` + +The rule: **all synchronous code first, then drain every microtask, then macrotasks in order.** Return +the labels in print order (e.g. `[1, 7, ...]`). Predict it before you run. + +### Part 2 — Callbacks → async/await + +Given the callback-style `getUser` / `getOrders` (provided), implement: + +- `getUserP(id)` — a Promise wrapper around `getUser` (`new Promise((resolve, reject) => ...)`). +- `getOrdersP(user)` — a Promise wrapper around `getOrders`. +- `loadAsync(id)` — an `async` function that `await`s both and returns `{ user, orders }`, the same + result the nested-callback `loadCallback` produces. + +### Part 3 — Yield instead of freezing + +`chunk(items, size, process)` — process `items` in slices of `size`, **yielding** between slices with +`setTimeout(..., 0)` so the main thread stays free (this is the fix for the lesson's freezing report). +Call `process(item)` for every item, in order, and return a Promise that resolves once all are processed. + +## Definition of done + +- All tests pass (`npx vitest run labs/lab-06-async-eventloop`). +- In your LMS notebook: predicted vs actual order + the rule, and before/after evidence the UI no longer + freezes. + +## Submit + +Edit `src/`, run the tests, commit and push. diff --git a/labs/lab-06-async-eventloop/src/async.js b/labs/lab-06-async-eventloop/src/async.js new file mode 100644 index 0000000..9024c23 --- /dev/null +++ b/labs/lab-06-async-eventloop/src/async.js @@ -0,0 +1,59 @@ +/** + * Lab 06 — Async JavaScript & the Event Loop. See README.md. + */ + +/* Part 1 — predict the print order of the lesson snippet. */ +export function eventLoopOrder() { + // TODO: return the labels (numbers) in the order they print. + // Rule: all sync first, then ALL microtasks, then macrotasks in order. + return []; +} + +/* Part 2 — callback-style APIs (provided; do not change). */ +export function getUser(id, cb) { + setTimeout(() => cb(null, { id, name: 'User' + id }), 5); +} +export function getOrders(user, cb) { + setTimeout(() => cb(null, [user.name + '-order1', user.name + '-order2']), 5); +} + +/** Reference callback chain you must match (provided). */ +export function loadCallback(id, done) { + getUser(id, (e, user) => { + if (e) return done(e); + getOrders(user, (e2, orders) => { + if (e2) return done(e2); + done(null, { user, orders }); + }); + }); +} + +/** TODO: wrap getUser in a Promise. */ +export function getUserP(id) { + // return new Promise((resolve, reject) => getUser(id, (e, u) => (e ? reject(e) : resolve(u)))); + throw new Error('not implemented'); +} + +/** TODO: wrap getOrders in a Promise. */ +export function getOrdersP(user) { + throw new Error('not implemented'); +} + +/** TODO: await both, return { user, orders } — same shape as loadCallback. */ +export async function loadAsync(id) { + throw new Error('not implemented'); +} + +/* Part 3 — process work in chunks, yielding between slices so the thread stays free. */ +/** + * @template T + * @param {T[]} items + * @param {number} size + * @param {(item: T) => void} process + * @returns {Promise} + */ +export function chunk(items, size, process) { + // TODO: process `size` items, then setTimeout(..., 0) to yield, repeat. + // Resolve the returned Promise once every item has been processed in order. + return Promise.reject(new Error('not implemented')); +} diff --git a/labs/lab-06-async-eventloop/tests/async.test.js b/labs/lab-06-async-eventloop/tests/async.test.js new file mode 100644 index 0000000..462cc43 --- /dev/null +++ b/labs/lab-06-async-eventloop/tests/async.test.js @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import { eventLoopOrder, loadCallback, loadAsync, chunk } from '../src/async.js'; + +describe('lab 06 — event loop ordering', () => { + it('orders sync, then all microtasks, then macrotasks', () => { + expect(eventLoopOrder()).toEqual([1, 7, 3, 4, 5, 2, 6]); + }); +}); + +describe('lab 06 — callbacks to async/await', () => { + it('loadAsync produces the same result as loadCallback', async () => { + const expected = await new Promise((resolve, reject) => + loadCallback('7', (e, r) => (e ? reject(e) : resolve(r))), + ); + const actual = await loadAsync('7'); + expect(actual).toEqual(expected); + }); + + it('loadAsync returns the user and their orders', async () => { + const { user, orders } = await loadAsync('42'); + expect(user).toEqual({ id: '42', name: 'User42' }); + expect(orders).toEqual(['User42-order1', 'User42-order2']); + }); +}); + +describe('lab 06 — chunked processing yields without dropping work', () => { + it('processes every item, in order', async () => { + const seen = []; + const items = Array.from({ length: 25 }, (_, i) => i); + await chunk(items, 10, (n) => seen.push(n)); + expect(seen).toEqual(items); + }); + + it('handles an empty list', async () => { + const seen = []; + await chunk([], 5, (n) => seen.push(n)); + expect(seen).toEqual([]); + }); + + it('does not block: returns before processing the second chunk', async () => { + const order = []; + const items = [1, 2, 3, 4]; + // chunk yields between slices, so it cannot finish the 2nd chunk before + // the synchronous code after this call runs. + const done = chunk(items, 2, (n) => order.push('item' + n)); + order.push('after-call'); + await done; + // The 'after-call' marker must appear before the last item — proof that + // chunk yielded control instead of running the whole loop synchronously. + expect(order).toContain('after-call'); + expect(order.indexOf('after-call')).toBeLessThan(order.lastIndexOf('item4')); + // And every item still ran, in order. + expect(order.filter((x) => x.startsWith('item'))).toEqual(['item1', 'item2', 'item3', 'item4']); + }); +}); diff --git a/labs/lab-07-data-structures.md b/labs/lab-07-data-structures.md deleted file mode 100644 index 55cae9d..0000000 --- a/labs/lab-07-data-structures.md +++ /dev/null @@ -1,88 +0,0 @@ -# Lab 07 — Optimize Customer Data - -**Lesson:** 07 · **Goal:** turn O(n²) array scans into O(n) with a Map index and a Set dedup, and measure it. - -## Goal -See O(n²) vs O(n) in real numbers, then fix the join and dedup with the right data structures. - -## Setup -```bash -mkdir -p /tmp/swexp-l07 && cd /tmp/swexp-l07 -cat > gen.sh <<'GEN' -#!/usr/bin/env bash -set -e -cat > data.js <<'JS' -// Generate N customers and ~N orders referencing them -function makeData(n) { - const customers = Array.from({ length: n }, (_, i) => ({ id: i, name: 'Cust' + i })); - const orders = Array.from({ length: n }, (_, i) => ({ id: i, customerId: (i * 7) % n, total: i })); - return { customers, orders }; -} -module.exports = { makeData }; -JS - -cat > slow.js <<'JS' -// SLOW: linear find() inside a loop → O(orders × customers) -function joinSlow(customers, orders) { - return orders.map(o => ({ ...o, customer: customers.find(c => c.id === o.customerId) })); -} -// SLOW: includes() inside filter → O(n²) dedup -function dedupSlow(values) { - const out = []; - for (const v of values) if (!out.includes(v)) out.push(v); - return out; -} -module.exports = { joinSlow, dedupSlow }; -JS - -cat > fast.js <<'JS' -// TODO (you implement): O(customers + orders) using a Map index -function joinFast(customers, orders) { - // const byId = new Map(...); return orders.map(...) - throw new Error('not implemented'); -} -// TODO (you implement): O(n) dedup using a Set -function dedupFast(values) { - throw new Error('not implemented'); -} -module.exports = { joinFast, dedupFast }; -JS - -cat > bench.js <<'JS' -const { makeData } = require('./data.js'); -const { joinSlow, dedupSlow } = require('./slow.js'); -let fast = {}; -try { fast = require('./fast.js'); } catch {} -function time(label, fn) { const t = process.hrtime.bigint(); const r = fn(); const ms = Number(process.hrtime.bigint() - t) / 1e6; console.log(label.padEnd(22), ms.toFixed(1) + ' ms'); return r; } -for (const n of [1000, 5000, 20000]) { - const { customers, orders } = makeData(n); - const dupes = Array.from({ length: n }, (_, i) => i % (n / 10 | 0 || 1)); - console.log('\n=== n =', n, '==='); - time('joinSlow', () => joinSlow(customers, orders)); - time('dedupSlow', () => dedupSlow(dupes)); - if (fast.joinFast) { try { time('joinFast', () => fast.joinFast(customers, orders)); time('dedupFast', () => fast.dedupFast(dupes)); } catch (e) { console.log('fast not implemented yet'); } } -} -JS -echo "Files in /tmp/swexp-l07: data.js, slow.js, fast.js (TODO), bench.js" -GEN -bash gen.sh -``` - -## Tasks -1. **Measure the baseline:** `node bench.js`. Record `joinSlow`/`dedupSlow` at n = 1000, 5000, 20000. How does the time grow as n grows (roughly ×4 each step → that's quadratic)? -2. **Implement `fast.js`:** - - `joinFast`: build `const byId = new Map(customers.map(c => [c.id, c]))` once, then `orders.map(o => ({ ...o, customer: byId.get(o.customerId) }))`. - - `dedupFast`: `return [...new Set(values)]` (or a Set + filter). -3. **Re-measure:** `node bench.js` and compare. The fast versions should grow roughly linearly. -4. **Verify correctness:** the fast outputs must match the slow outputs (same joined data, same unique set/order semantics you intend). - -## Deliverable -Before/after timings at all three sizes, the complexity analysis (O(n²) → O(n) for each), your `fast.js`, and a note on why a Map/Set was the right structure. - -## Cleanup -```bash -rm -rf /tmp/swexp-l07 -``` - -## Check -`../solutions/lab-07-solution.md`. diff --git a/labs/lab-07-data-structures/README.md b/labs/lab-07-data-structures/README.md new file mode 100644 index 0000000..5622477 --- /dev/null +++ b/labs/lab-07-data-structures/README.md @@ -0,0 +1,26 @@ +# Lab 07 — Optimize Customer Data + +**Goal:** turn O(n²) array scans into O(n) with a `Map` index and a `Set` dedup — and keep the output correct. + +The slow versions (provided) use `Array.find()` inside a loop (an O(orders × customers) join) and +`Array.includes()` inside a filter (O(n²) dedup). In the LMS lab you benchmark them at growing sizes and +watch the time grow ~4× per step — quadratic. Here you implement the fast versions and the tests prove +they produce the **same** results. + +## What you do + +In [`src/fast.js`](src/fast.js): + +- `joinFast(customers, orders)` — build `const byId = new Map(customers.map(c => [c.id, c]))` **once**, + then `orders.map(o => ({ ...o, customer: byId.get(o.customerId) }))`. Same shape as `joinSlow`, O(n). +- `dedupFast(values)` — return the unique values in first-seen order using a `Set` + (`[...new Set(values)]`). Same result as `dedupSlow`, O(n). + +## Definition of done + +- All tests pass (`npx vitest run labs/lab-07-data-structures`). +- In your LMS notebook: before/after timings at n = 1000 / 5000 / 20000 and the O(n²) → O(n) analysis. + +## Submit + +Edit `src/`, run the tests, commit and push. diff --git a/labs/lab-07-data-structures/src/fast.js b/labs/lab-07-data-structures/src/fast.js new file mode 100644 index 0000000..10e1f8b --- /dev/null +++ b/labs/lab-07-data-structures/src/fast.js @@ -0,0 +1,24 @@ +/** + * Lab 07 — Optimize Customer Data. See README.md. + * Match joinSlow / dedupSlow exactly, but in O(n). + */ + +/** + * Join orders to customers in O(customers + orders) using a Map index. + * @param {{id:number,name:string}[]} customers + * @param {{id:number,customerId:number,total:number}[]} orders + */ +export function joinFast(customers, orders) { + // TODO: const byId = new Map(customers.map(c => [c.id, c])); + // return orders.map(o => ({ ...o, customer: byId.get(o.customerId) })); + throw new Error('not implemented'); +} + +/** + * Unique values in first-seen order, in O(n) using a Set. + * @param {any[]} values + */ +export function dedupFast(values) { + // TODO: return [...new Set(values)]; + throw new Error('not implemented'); +} diff --git a/labs/lab-07-data-structures/src/slow.js b/labs/lab-07-data-structures/src/slow.js new file mode 100644 index 0000000..bbeff4d --- /dev/null +++ b/labs/lab-07-data-structures/src/slow.js @@ -0,0 +1,16 @@ +/** + * Lab 07 — the SLOW baselines (provided; do not change). Your fast versions + * in fast.js must produce the same results. + */ + +/** O(orders × customers): a linear find() inside a loop. */ +export function joinSlow(customers, orders) { + return orders.map((o) => ({ ...o, customer: customers.find((c) => c.id === o.customerId) })); +} + +/** O(n²): includes() inside a loop. */ +export function dedupSlow(values) { + const out = []; + for (const v of values) if (!out.includes(v)) out.push(v); + return out; +} diff --git a/labs/lab-07-data-structures/tests/fast.test.js b/labs/lab-07-data-structures/tests/fast.test.js new file mode 100644 index 0000000..75514bd --- /dev/null +++ b/labs/lab-07-data-structures/tests/fast.test.js @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { joinSlow, dedupSlow } from '../src/slow.js'; +import { joinFast, dedupFast } from '../src/fast.js'; + +function makeData(n) { + const customers = Array.from({ length: n }, (_, i) => ({ id: i, name: 'Cust' + i })); + const orders = Array.from({ length: n }, (_, i) => ({ + id: i, + customerId: (i * 7) % n, + total: i, + })); + return { customers, orders }; +} + +describe('lab 07 — Map/Set optimizations', () => { + it('joinFast matches joinSlow', () => { + const { customers, orders } = makeData(50); + expect(joinFast(customers, orders)).toEqual(joinSlow(customers, orders)); + }); + + it('joinFast attaches the right customer object', () => { + const customers = [ + { id: 1, name: 'Ada' }, + { id: 2, name: 'Lin' }, + ]; + const orders = [{ id: 10, customerId: 2, total: 5 }]; + expect(joinFast(customers, orders)).toEqual([ + { id: 10, customerId: 2, total: 5, customer: { id: 2, name: 'Lin' } }, + ]); + }); + + it('joinFast leaves an unknown customer undefined (like find)', () => { + const customers = [{ id: 1, name: 'Ada' }]; + const orders = [{ id: 9, customerId: 99, total: 1 }]; + expect(joinFast(customers, orders)[0].customer).toBeUndefined(); + }); + + it('dedupFast matches dedupSlow (first-seen order)', () => { + const values = [3, 1, 3, 2, 1, 2, 3]; + expect(dedupFast(values)).toEqual(dedupSlow(values)); + expect(dedupFast(values)).toEqual([3, 1, 2]); + }); + + it('dedupFast handles strings and an empty list', () => { + expect(dedupFast(['a', 'b', 'a', 'c', 'b'])).toEqual(['a', 'b', 'c']); + expect(dedupFast([])).toEqual([]); + }); +}); diff --git a/labs/lab-08-search.md b/labs/lab-08-search.md deleted file mode 100644 index 58f7fd6..0000000 --- a/labs/lab-08-search.md +++ /dev/null @@ -1,92 +0,0 @@ -# Lab 08 — The Search Is Too Slow - -**Lesson:** 08 · **Goal:** replace per-keystroke rescans with a pre-built sorted index + binary search, add memoization and debounce, and measure. - -## Goal -Lower the *per-query* complexity (and stop redundant work) so search scales, proving each change with the benchmark. - -## Setup -```bash -mkdir -p /tmp/swexp-l08 && cd /tmp/swexp-l08 -cat > gen.sh <<'GEN' -#!/usr/bin/env bash -set -e -cat > catalog.js <<'JS' -function makeCatalog(n) { - return Array.from({ length: n }, (_, i) => 'product-' + String(i).padStart(6, '0')); -} -module.exports = { makeCatalog }; -JS - -cat > slow.js <<'JS' -// SLOW: re-sorts the WHOLE catalog on every query, then linear-scans for an exact match -function searchSlow(catalog, term) { - const sorted = [...catalog].sort(); // O(n log n) EVERY call — wasteful - return sorted.findIndex(x => x === term); // O(n) scan -} -// An "expensive" pure scoring function called repeatedly with the same args -function score(term) { - let s = 0; - for (let i = 0; i < 200000; i++) s += (term.charCodeAt(i % term.length) * i) % 7; - return s; -} -module.exports = { searchSlow, score }; -JS - -cat > fast.js <<'JS' -// TODO: binary search on a PRE-SORTED array (sort once, outside the per-query path) -function binarySearch(sorted, target) { - // implement O(log n) search; return index or -1 - throw new Error('not implemented'); -} -// TODO: wrap `score` so repeated identical args are cached (memoization) -function memoize(fn) { - throw new Error('not implemented'); -} -module.exports = { binarySearch, memoize }; -JS - -cat > bench.js <<'JS' -const { makeCatalog } = require('./catalog.js'); -const { searchSlow, score } = require('./slow.js'); -let fast = {}; try { fast = require('./fast.js'); } catch {} -function time(label, fn) { const t = process.hrtime.bigint(); fn(); console.log(label.padEnd(26), (Number(process.hrtime.bigint() - t) / 1e6).toFixed(1) + ' ms'); } -const QUERIES = 50; -for (const n of [5000, 20000, 80000]) { - const catalog = makeCatalog(n); - const terms = Array.from({ length: QUERIES }, (_, i) => 'product-' + String((i * 137) % n).padStart(6, '0')); - console.log('\n=== n =', n, '(', QUERIES, 'queries) ==='); - time('searchSlow (sort+scan/qry)', () => terms.forEach(t => searchSlow(catalog, t))); - if (fast.binarySearch) { - const sortedOnce = [...catalog].sort(); // pay sort ONCE - time('binarySearch (sorted once)', () => terms.forEach(t => fast.binarySearch(sortedOnce, t))); - } -} -// memoization demo: same term 20 times -console.log('\n=== memoization ==='); -time('score x20 (no memo)', () => { for (let i = 0; i < 20; i++) score('product-000042'); }); -if (fast.memoize) { const m = fast.memoize(score); time('score x20 (memoized)', () => { for (let i = 0; i < 20; i++) m('product-000042'); }); } -JS -echo "Files in /tmp/swexp-l08: catalog.js, slow.js, fast.js (TODO), bench.js" -GEN -bash gen.sh -``` - -## Tasks -1. **Baseline:** `node bench.js`. Note how `searchSlow` grows with n (it re-sorts every query) and how `score x20` is ~20× one call. -2. **Implement `fast.js`:** - - `binarySearch(sorted, target)` — classic O(log n) loop (see Lesson 8 deep dive). Works only because the array is sorted *once* outside the per-query path. - - `memoize(fn)` — a `Map` cache keyed by the argument (a closure). -3. **Re-measure.** Binary search on the pre-sorted array should be dramatically faster across sizes; memoized `score x20` should cost ~1 call. -4. **Debounce (concept + code):** add the `debounce(fn, ms)` from the lesson; explain in your notebook how it reduces *how often* search runs versus how *expensive* each run is. - -## Deliverable -Complexity analysis (baseline vs improved), before/after benchmarks at all sizes, your `fast.js`, the debounce snippet, and a note on which change mattered most and why Big-O predicted it. - -## Cleanup -```bash -rm -rf /tmp/swexp-l08 -``` - -## Check -`../solutions/lab-08-solution.md`. diff --git a/labs/lab-08-search/README.md b/labs/lab-08-search/README.md new file mode 100644 index 0000000..d8eda1a --- /dev/null +++ b/labs/lab-08-search/README.md @@ -0,0 +1,28 @@ +# Lab 08 — The Search Is Too Slow + +**Goal:** lower the per-query cost — binary search on a pre-sorted array, plus memoization and debounce. + +The slow search re-sorts the whole catalog on **every** query and then linear-scans it. In the LMS lab you +benchmark it growing with n. Here you implement the three improvements and prove them correct. + +## What you do + +In [`src/fast.js`](src/fast.js): + +- `binarySearch(sorted, target)` — classic O(log n) loop over an **already-sorted** array. Return the + index of `target`, or `-1` if absent. (It's fast only because the sort happens once, outside the + per-query path.) +- `memoize(fn)` — return a wrapped function that caches results in a `Map` keyed by the (single, primitive) + argument, so repeated identical calls skip the work and return the cached value. +- `debounce(fn, ms)` — return a function that delays calling `fn` until `ms` have passed since the **last** + call; a burst of calls results in a single trailing call with the most recent arguments. + +## Definition of done + +- All tests pass (`npx vitest run labs/lab-08-search`). +- In your LMS notebook: baseline vs improved benchmarks and a note on which change mattered most and why + Big-O predicted it. + +## Submit + +Edit `src/`, run the tests, commit and push. diff --git a/labs/lab-08-search/src/fast.js b/labs/lab-08-search/src/fast.js new file mode 100644 index 0000000..fea4e72 --- /dev/null +++ b/labs/lab-08-search/src/fast.js @@ -0,0 +1,36 @@ +/** + * Lab 08 — The Search Is Too Slow. See README.md. + */ + +/** + * O(log n) search over an ALREADY-sorted array. Returns the index or -1. + * @param {Array} sorted + * @param {number|string} target + * @returns {number} + */ +export function binarySearch(sorted, target) { + // TODO: lo/hi loop; compare sorted[mid] to target; return mid or -1. + throw new Error('not implemented'); +} + +/** + * Cache results of a pure single-argument function in a Map. + * @template A, R + * @param {(arg: A) => R} fn + * @returns {(arg: A) => R} + */ +export function memoize(fn) { + // TODO: const cache = new Map(); return arg => cache.has(arg) ? cache.get(arg) : . + throw new Error('not implemented'); +} + +/** + * Delay fn until `ms` have passed since the last call (trailing-edge debounce). + * @param {Function} fn + * @param {number} ms + * @returns {Function} + */ +export function debounce(fn, ms) { + // TODO: keep a timer id; on each call clearTimeout then setTimeout(() => fn(...args), ms). + throw new Error('not implemented'); +} diff --git a/labs/lab-08-search/tests/fast.test.js b/labs/lab-08-search/tests/fast.test.js new file mode 100644 index 0000000..45c1a09 --- /dev/null +++ b/labs/lab-08-search/tests/fast.test.js @@ -0,0 +1,74 @@ +import { describe, it, expect, vi } from 'vitest'; +import { binarySearch, memoize, debounce } from '../src/fast.js'; + +describe('lab 08 — binary search', () => { + const sorted = Array.from({ length: 1000 }, (_, i) => i * 2); // 0,2,4,... + + it('finds an element and returns its index', () => { + expect(binarySearch(sorted, 0)).toBe(0); + expect(binarySearch(sorted, 500)).toBe(250); + expect(binarySearch(sorted, 1998)).toBe(999); + }); + + it('returns -1 when absent', () => { + expect(binarySearch(sorted, 1)).toBe(-1); + expect(binarySearch(sorted, -5)).toBe(-1); + expect(binarySearch(sorted, 5000)).toBe(-1); + }); + + it('works on strings (lexicographic order)', () => { + const words = ['apple', 'banana', 'cherry', 'date']; + expect(binarySearch(words, 'cherry')).toBe(2); + expect(binarySearch(words, 'fig')).toBe(-1); + }); + + it('handles an empty array', () => { + expect(binarySearch([], 1)).toBe(-1); + }); +}); + +describe('lab 08 — memoize', () => { + it('computes once per distinct argument', () => { + const spy = vi.fn((n) => n * n); + const m = memoize(spy); + expect(m(4)).toBe(16); + expect(m(4)).toBe(16); + expect(m(5)).toBe(25); + expect(spy).toHaveBeenCalledTimes(2); // 4 cached on the 2nd call + }); + + it('returns the cached value, not a recomputation', () => { + let calls = 0; + const m = memoize(() => ++calls); + expect(m('x')).toBe(1); + expect(m('x')).toBe(1); + }); +}); + +describe('lab 08 — debounce', () => { + it('collapses a burst into a single trailing call', () => { + vi.useFakeTimers(); + const spy = vi.fn(); + const d = debounce(spy, 300); + d('a'); + d('b'); + d('c'); + expect(spy).not.toHaveBeenCalled(); + vi.advanceTimersByTime(300); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('c'); + vi.useRealTimers(); + }); + + it('fires again after the quiet period', () => { + vi.useFakeTimers(); + const spy = vi.fn(); + const d = debounce(spy, 100); + d(1); + vi.advanceTimersByTime(100); + d(2); + vi.advanceTimersByTime(100); + expect(spy).toHaveBeenCalledTimes(2); + vi.useRealTimers(); + }); +}); diff --git a/labs/lab-09-http-rest.md b/labs/lab-09-http-rest.md deleted file mode 100644 index d0f4137..0000000 --- a/labs/lab-09-http-rest.md +++ /dev/null @@ -1,69 +0,0 @@ -# Lab 09 — Investigate the Network (HTTP & REST) - -**Lesson:** 09 · **Goal:** read real HTTP exchanges, interpret status codes, and fix a client that treats errors as success. - -## Goal -Exercise a small local REST API, read each exchange (method/status/headers/body), trigger 4xx and 5xx, and fix the `fetch` client. - -## Setup — a local API (no external network) -```bash -mkdir -p /tmp/swexp-l09 && cd /tmp/swexp-l09 -cat > server.js <<'JS' -const http = require('http'); -const orders = { 1: { id: 1, item: 'Widget', total: 9.99 }, 2: { id: 2, item: 'Gadget', total: 19.5 } }; -const server = http.createServer((req, res) => { - const json = (code, body) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); }; - const m = req.url.match(/^\/api\/orders(?:\/(\w+))?$/); - if (!m) return json(404, { error: 'Not found' }); - const id = m[1]; - // auth: require a header to demonstrate 401 - if (req.headers['authorization'] !== 'Bearer demo-token') return json(401, { error: 'Unauthorized' }); - if (req.method === 'GET' && !id) return json(200, Object.values(orders)); - if (req.method === 'GET' && id) { - if (id === 'boom') { res.writeHead(500); return res.end('Internal Server Error'); } // 5xx demo (non-JSON body!) - return orders[id] ? json(200, orders[id]) : json(404, { error: 'Order not found' }); - } - if (req.method === 'POST' && !id) return json(201, { id: 3, ...JSON.parse('{}') }); - return json(405, { error: 'Method not allowed' }); -}); -server.listen(3009, () => console.log('API on http://localhost:3009')); -JS - -cat > client.js <<'JS' -// BUGGY client: assumes fetch rejects on error statuses (it does NOT) and always .json()s -async function getOrder(id, token) { - const res = await fetch(`http://localhost:3009/api/orders/${id}`, { headers: { Authorization: token } }); - const data = await res.json(); // BUG: 500 returns non-JSON; 401/404 are treated as success - return data; // BUG: never checks res.ok -} -module.exports = { getOrder }; -JS -echo "Files in /tmp/swexp-l09: server.js, client.js (buggy)" -echo "Run the server in one terminal: node server.js" -``` - -## Tasks -1. **Start the API:** `node server.js` (leave running in one terminal). -2. **Read exchanges with curl** (verbose shows headers/status): - ```bash - curl -i -H "Authorization: Bearer demo-token" http://localhost:3009/api/orders - curl -i -H "Authorization: Bearer demo-token" http://localhost:3009/api/orders/1 - curl -i http://localhost:3009/api/orders/1 # 401 (no token) - curl -i -H "Authorization: Bearer demo-token" http://localhost:3009/api/orders/999 # 404 - curl -i -H "Authorization: Bearer demo-token" http://localhost:3009/api/orders/boom # 500 (non-JSON body) - ``` - Record method, status, key headers, and body for each. In the **browser**, do the same via the Network panel and compare. -3. **Reproduce the client bug:** call the buggy `getOrder('boom', 'Bearer demo-token')` and `getOrder('1', 'wrong')` — see it crash on `.json()` or silently return an error body as if it were data. -4. **Fix the client:** check `res.ok`/`res.status` before parsing; branch on content type; throw or return a typed error. Verify each status is handled correctly. - -## Deliverable -A table of each endpoint's method/status/headers/body; a 4xx-vs-5xx diagnosis (which side to investigate); and the fixed client with evidence it now handles 200/401/404/500 correctly. - -## Cleanup -```bash -# stop the server (Ctrl+C), then: -rm -rf /tmp/swexp-l09 -``` - -## Check -`../solutions/lab-09-solution.md`. diff --git a/labs/lab-09-http-rest/README.md b/labs/lab-09-http-rest/README.md new file mode 100644 index 0000000..fbb04cc --- /dev/null +++ b/labs/lab-09-http-rest/README.md @@ -0,0 +1,33 @@ +# Lab 09 — Investigate the Network (HTTP & REST) + +**Goal:** read HTTP exchanges, interpret status codes, and fix a client that treats errors as success. + +`fetch` does **not** reject on 4xx/5xx — only on a network failure. The buggy client calls `.json()` +unconditionally and never checks `res.ok`, so it crashes on a non-JSON 500 body and silently returns +401/404 error bodies as if they were valid data. + +## What you do + +In [`src/client.js`](src/client.js), fix `getOrder(id, token)` so it: + +- **Checks `res.ok` first.** On a non-OK response, read the body **defensively** (it may not be JSON): + if the `content-type` includes `application/json`, parse JSON and use its `error` field; otherwise read + text. Then `throw new Error(\`HTTP ${res.status}: ${detail}\`)`. +- On an OK response, return the parsed JSON body. + +The tests inject a fake `fetch` returning 200 / 401 / 404 / 500 responses (the 500 body is plain text, not +JSON) and assert each is handled correctly. + +## 4xx vs 5xx (the diagnostic point) + +A **4xx** means the *request* was wrong — look at the client (token, URL, params). A **5xx** means the +*server* failed — look at the backend. That distinction ends the blame game; record it in your notebook. + +## Definition of done + +- All tests pass (`npx vitest run labs/lab-09-http-rest`). +- In your LMS notebook: a table of each endpoint's method/status/headers/body and the 4xx-vs-5xx diagnosis. + +## Submit + +Edit `src/`, run the tests, commit and push. diff --git a/labs/lab-09-http-rest/src/client.js b/labs/lab-09-http-rest/src/client.js new file mode 100644 index 0000000..027c33e --- /dev/null +++ b/labs/lab-09-http-rest/src/client.js @@ -0,0 +1,31 @@ +/** + * Lab 09 — Investigate the Network (HTTP & REST). See README.md. + * Fix the client so it checks res.ok and reads error bodies defensively. + */ + +export const API_BASE = 'http://localhost:3009/api/orders'; + +/** + * Fetch one order. On any non-OK status, throw `HTTP : `. + * @param {string|number} id + * @param {string} token e.g. 'Bearer demo-token' + * @returns {Promise} + */ +export async function getOrder(id, token) { + const res = await fetch(`${API_BASE}/${id}`, { headers: { Authorization: token } }); + // BUG: this parses JSON unconditionally and never checks res.ok. + // - a 500 body may be plain text → .json() throws + // - 401/404 error bodies are returned as if they were valid data + const data = await res.json(); + return data; + + // TODO: + // if (!res.ok) { + // const ct = res.headers.get('content-type') || ''; + // const detail = ct.includes('application/json') + // ? (await res.json()).error + // : await res.text(); + // throw new Error(`HTTP ${res.status}: ${detail}`); + // } + // return res.json(); +} diff --git a/labs/lab-09-http-rest/tests/client.test.js b/labs/lab-09-http-rest/tests/client.test.js new file mode 100644 index 0000000..27d427c --- /dev/null +++ b/labs/lab-09-http-rest/tests/client.test.js @@ -0,0 +1,74 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { getOrder } from '../src/client.js'; + +/** Build a minimal Response-like object the client can read. */ +function fakeResponse({ status, json, text, contentType }) { + return { + ok: status >= 200 && status < 300, + status, + headers: { get: (h) => (h.toLowerCase() === 'content-type' ? contentType : null) }, + json: async () => { + if (json === undefined) throw new SyntaxError('Unexpected token in JSON'); + return json; + }, + text: async () => text ?? '', + }; +} + +function stubFetch(response) { + globalThis.fetch = vi.fn(async () => response); +} + +afterEach(() => { + vi.restoreAllMocks(); + delete globalThis.fetch; +}); + +describe('lab 09 — fixed fetch client', () => { + it('returns the parsed body on 200', async () => { + stubFetch( + fakeResponse({ + status: 200, + contentType: 'application/json', + json: { id: 1, item: 'Widget', total: 9.99 }, + }), + ); + await expect(getOrder('1', 'Bearer demo-token')).resolves.toEqual({ + id: 1, + item: 'Widget', + total: 9.99, + }); + }); + + it('throws on 401 instead of returning the error body as data', async () => { + stubFetch( + fakeResponse({ status: 401, contentType: 'application/json', json: { error: 'Unauthorized' } }), + ); + await expect(getOrder('1', 'wrong')).rejects.toThrow(/HTTP 401/); + await expect(getOrder('1', 'wrong')).rejects.toThrow(/Unauthorized/); + }); + + it('throws on 404', async () => { + stubFetch( + fakeResponse({ status: 404, contentType: 'application/json', json: { error: 'Order not found' } }), + ); + await expect(getOrder('999', 'Bearer demo-token')).rejects.toThrow(/HTTP 404/); + }); + + it('throws on a 500 with a NON-JSON body (does not crash on .json())', async () => { + stubFetch( + fakeResponse({ status: 500, contentType: 'text/plain', text: 'Internal Server Error' }), + ); + await expect(getOrder('boom', 'Bearer demo-token')).rejects.toThrow(/HTTP 500/); + await expect(getOrder('boom', 'Bearer demo-token')).rejects.toThrow(/Internal Server Error/); + }); + + it('sends the Authorization header', async () => { + stubFetch(fakeResponse({ status: 200, contentType: 'application/json', json: {} })); + await getOrder('1', 'Bearer demo-token'); + expect(globalThis.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/orders/1'), + expect.objectContaining({ headers: { Authorization: 'Bearer demo-token' } }), + ); + }); +}); diff --git a/labs/lab-10-dns-tcp.md b/labs/lab-10-dns-tcp.md deleted file mode 100644 index b6d1f65..0000000 --- a/labs/lab-10-dns-tcp.md +++ /dev/null @@ -1,49 +0,0 @@ -# Lab 10 — DNS, TCP/IP & the Client–Server Journey - -**Lesson:** 10 · **Goal:** trace URL → first byte with terminal tools and the Network Timing tab; attribute time to layers. - -## Goal -See where the time *before* content download goes — DNS, TCP, TLS — and localize a "slow start" vs "not found" to the right layer. - -## Setup -A local baseline server (negligible DNS/TLS) to compare against a remote HTTPS host: -```bash -mkdir -p /tmp/swexp-l10 && cd /tmp/swexp-l10 -cat > server.js <<'JS' -const http = require('http'); -http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('hello from localhost\n'); }) - .listen(3010, () => console.log('baseline on http://localhost:3010')); -JS -echo "Run it: node server.js" -``` - -## Tasks (use your own machine; pick a well-known public host, e.g. example.com) -1. **DNS resolution.** Resolve the name and note the IP(s): - ```bash - nslookup example.com # or: dig example.com +short - ``` - What does the name resolve to? Try again — is it faster (cached)? -2. **Reachability & latency.** - ```bash - ping -c 4 example.com # round-trip time (ICMP may be blocked — note if so) - traceroute example.com # the path; where does latency accumulate? (Windows: tracert) - ``` -3. **Watch the full journey.** `curl -v` narrates DNS → connect (TCP) → TLS handshake → HTTP: - ```bash - curl -v https://example.com/ -o /dev/null - ``` - Identify in the output: the resolved IP, the TCP connect, the TLS handshake lines, and the HTTP status line. -4. **Local baseline.** `curl -v http://localhost:3010/ -o /dev/null` — note there's no DNS lookup of significance and no TLS; connection setup is near-instant. -5. **Network Timing tab.** In the browser, load the remote host and `localhost:3010`. In Network → click a request → **Timing**: record the split across **DNS Lookup**, **Initial connection**, **SSL**, **Waiting (TTFB)**, **Content Download**. Which phase dominates for the remote host? For localhost? - -## Deliverable -A layered report: the URL→first-byte journey in your own words; tool output (`dig`/`nslookup`, `curl -v`, a Timing breakdown) for remote vs localhost; and a statement of which layer would own a "slow start" vs a "not found," with the evidence that proves it. - -## Cleanup -```bash -# Ctrl+C the server, then: -rm -rf /tmp/swexp-l10 -``` - -## Check -`../solutions/lab-10-solution.md`. diff --git a/labs/lab-10-dns-tcp/README.md b/labs/lab-10-dns-tcp/README.md new file mode 100644 index 0000000..653377d --- /dev/null +++ b/labs/lab-10-dns-tcp/README.md @@ -0,0 +1,44 @@ +# Lab 10 — DNS, TCP/IP & the Client–Server Journey + +**Goal:** attribute the time *before* content download to the right layer — DNS, TCP, TLS, or the server. + +In the LMS lab you run `dig` / `ping` / `curl -v` and read the Network **Timing** tab for a remote host vs +`localhost`. The autogradable core is the **reasoning**: given the phase timings `curl` reports, which +layer dominates, and is a "slow start" a DNS/TCP/TLS problem or a server (TTFB) problem? + +`curl` can print cumulative timing checkpoints with `-w`: + +``` +curl -o /dev/null -s -w "dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n" https://example.com/ +``` + +These are **cumulative** seconds from the start: `time_namelookup ≤ time_connect ≤ time_appconnect ≤ +time_starttransfer ≤ time_total`. + +## What you do + +In [`src/journey.js`](src/journey.js): + +- `parseCurlTiming(line)` — parse a line like + `dns:0.031 connect:0.052 tls:0.120 ttfb:0.210 total:0.260` into the **per-phase** durations (in + milliseconds), by subtracting consecutive checkpoints: + - `dns` = `time_namelookup` + - `tcp` = `connect − dns` + - `tls` = `tls − connect` (appconnect − connect) + - `ttfb` = `ttfb − tls` (server "waiting" / time-to-first-byte) + - `download` = `total − ttfb` + + Return `{ dns, tcp, tls, ttfb, download }` with each value in **milliseconds** (seconds × 1000), + rounded to one decimal place. +- `attributeSlowness(phases)` — return the name of the phase with the largest duration: one of + `'dns' | 'tcp' | 'tls' | 'ttfb' | 'download'`. That's the layer that owns the "slow start". + +## Definition of done + +- All tests pass (`npx vitest run labs/lab-10-dns-tcp`). +- In your LMS notebook: the URL→first-byte journey in your own words, with `dig` / `curl -v` / Timing + evidence for remote vs localhost, and which layer owns a "slow start" vs a "not found". + +## Submit + +Edit `src/`, run the tests, commit and push. diff --git a/labs/lab-10-dns-tcp/src/journey.js b/labs/lab-10-dns-tcp/src/journey.js new file mode 100644 index 0000000..23cfcd8 --- /dev/null +++ b/labs/lab-10-dns-tcp/src/journey.js @@ -0,0 +1,39 @@ +/** + * Lab 10 — DNS, TCP/IP & the Client–Server Journey. See README.md. + * + * @typedef {Object} Phases + * @property {number} dns + * @property {number} tcp + * @property {number} tls + * @property {number} ttfb + * @property {number} download + */ + +/** + * Parse a curl `-w` timing line into per-phase durations in milliseconds. + * Input checkpoints are cumulative SECONDS: + * dns: connect: tls: ttfb: total: + * @param {string} line + * @returns {Phases} + */ +export function parseCurlTiming(line) { + // TODO: extract dns/connect/tls/ttfb/total (seconds) from the line, + // then derive per-phase durations by subtracting consecutive checkpoints: + // dns = namelookup + // tcp = connect - namelookup + // tls = appconnect - connect + // ttfb = starttransfer - appconnect + // download = total - starttransfer + // Convert each to ms (×1000) and round to 1 decimal place. + return { dns: 0, tcp: 0, tls: 0, ttfb: 0, download: 0 }; +} + +/** + * Name the phase with the largest duration — the layer that owns the "slow start". + * @param {Phases} phases + * @returns {'dns'|'tcp'|'tls'|'ttfb'|'download'} + */ +export function attributeSlowness(phases) { + // TODO: return the key of the largest value. + return 'dns'; +} diff --git a/labs/lab-10-dns-tcp/tests/journey.test.js b/labs/lab-10-dns-tcp/tests/journey.test.js new file mode 100644 index 0000000..1ec6feb --- /dev/null +++ b/labs/lab-10-dns-tcp/tests/journey.test.js @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { parseCurlTiming, attributeSlowness } from '../src/journey.js'; + +describe('lab 10 — curl timing breakdown', () => { + it('splits cumulative checkpoints into per-phase ms', () => { + // dns 31ms, tcp 21ms, tls 68ms, ttfb 90ms, download 50ms + const phases = parseCurlTiming('dns:0.031 connect:0.052 tls:0.120 ttfb:0.210 total:0.260'); + expect(phases.dns).toBeCloseTo(31, 1); + expect(phases.tcp).toBeCloseTo(21, 1); + expect(phases.tls).toBeCloseTo(68, 1); + expect(phases.ttfb).toBeCloseTo(90, 1); + expect(phases.download).toBeCloseTo(50, 1); + }); + + it('handles localhost (no DNS, no TLS)', () => { + const phases = parseCurlTiming('dns:0.000 connect:0.001 tls:0.001 ttfb:0.002 total:0.003'); + expect(phases.dns).toBe(0); + expect(phases.tls).toBe(0); + expect(phases.tcp).toBeCloseTo(1, 1); + }); + + it('attributes a TLS-dominated start to tls', () => { + const phases = parseCurlTiming('dns:0.010 connect:0.020 tls:0.300 ttfb:0.320 total:0.340'); + expect(attributeSlowness(phases)).toBe('tls'); + }); + + it('attributes a slow server (high TTFB) to ttfb', () => { + const phases = parseCurlTiming('dns:0.010 connect:0.020 tls:0.030 ttfb:0.530 total:0.560'); + expect(attributeSlowness(phases)).toBe('ttfb'); + }); + + it('attributes a slow DNS lookup to dns', () => { + const phases = parseCurlTiming('dns:0.400 connect:0.410 tls:0.420 ttfb:0.430 total:0.440'); + expect(attributeSlowness(phases)).toBe('dns'); + }); +}); diff --git a/labs/lab-11-rendering.md b/labs/lab-11-rendering.md deleted file mode 100644 index cee8d2e..0000000 --- a/labs/lab-11-rendering.md +++ /dev/null @@ -1,72 +0,0 @@ -# Lab 11 — Investigate the Browser Rendering Engine - -**Lesson:** 11 · **Goal:** record scroll jank, find the forced synchronous layout, fix with batched reads/writes + `transform`. - -## Goal -See the rendering pipeline overworking on scroll in a Performance trace, then eliminate the jank and prove it with a second trace. - -## Setup -```bash -mkdir -p /tmp/swexp-l11 && cd /tmp/swexp-l11 -cat > index.html <<'HTML' - - -Forge List (janky) - - -
- - - -HTML -cat > app.js <<'JS' -// Build a long list -const list = document.getElementById('list'); -for (let i = 0; i < 2000; i++) { - const row = document.createElement('div'); - row.className = 'row'; - row.innerHTML = `Customer #${i} VIP`; - list.appendChild(row); -} - -// JANK: on every scroll, read each row's layout and write a style — interleaved read/write -// forces a synchronous layout (reflow) per row, per scroll event. Also animates `top` (layout). -const rows = [...document.querySelectorAll('.row')]; -list.addEventListener('scroll', () => { - rows.forEach((row) => { - const h = row.offsetHeight; // READ (forces layout) - row.style.top = (h % 3) + 'px'; // WRITE (invalidates layout) → thrash + `top` triggers layout - }); -}); -JS -echo "Serve it: npx serve . (open with DevTools Performance + Rendering)" -``` - -## Tasks -1. **Record the jank (evidence first).** Open Performance, start recording, scroll the list a few seconds, stop. Find the long main-thread tasks during scroll; identify the **layout (purple)** work. Look for a "forced reflow / forced synchronous layout" warning. In the **Rendering** tab, enable **Paint flashing** to see what repaints on scroll. -2. **Diagnose:** the scroll handler reads `offsetHeight` (forcing layout) and writes `style.top` (invalidating it) in the same loop — layout thrashing — and animates `top`, which is a layout-triggering property. -3. **Fix — batch reads then writes:** - ```js - list.addEventListener('scroll', () => { - const heights = rows.map(r => r.offsetHeight); // READ all first - rows.forEach((r, i) => { r.style.transform = `translateY(${heights[i] % 3}px)`; }); // WRITE all (transform = composite) - }); - ``` - (Better still: don't touch per-row layout on scroll at all — and for 2000 rows, consider **virtualization**, the stretch goal.) -4. **Re-record** and compare: fewer/shorter purple layout blocks, steadier frame rate, less paint flashing. - -## Deliverable -The rendering pipeline + cost ladder in your words; before/after Performance traces; identification of the thrashing code and the layout-triggering property; and the fix (batched reads/writes, `transform` instead of `top`) with evidence scrolling is now smooth. - -## Cleanup -```bash -rm -rf /tmp/swexp-l11 -``` - -## Check -`../solutions/lab-11-solution.md`. diff --git a/labs/lab-11-rendering/README.md b/labs/lab-11-rendering/README.md new file mode 100644 index 0000000..95ec4be --- /dev/null +++ b/labs/lab-11-rendering/README.md @@ -0,0 +1,31 @@ +# Lab 11 — Investigate the Browser Rendering Engine + +**Goal:** stop scroll jank — recognize layout-triggering properties and batch reads before writes. + +The janky scroll handler reads each row's `offsetHeight` (forces layout) and writes `style.top` +(invalidates layout) **interleaved**, per row, per scroll event — layout thrashing — and animates `top`, +which is itself a layout-triggering property. The fix is to **read all, then write all**, and animate with +`transform` (composite) instead of `top` (layout). + +## What you do + +In [`src/render.js`](src/render.js): + +- `layoutTriggers(props)` — given an array of CSS property names, return only the ones that trigger + **layout** when animated, in input order. Treat `top`, `left`, `right`, `bottom`, `width`, `height`, + `margin`, `padding` as layout-triggering; treat `transform` and `opacity` as **not** (they're + composite/paint-cheap). Anything else: not layout-triggering for this exercise. +- `scheduleUpdates(rows, measure, apply)` — the de-thrashed update. Given `rows`, a `measure(row)` + function (a READ) and an `apply(row, measurement)` function (a WRITE), perform **all** reads first into + an array, then **all** writes — never interleaving a write between two reads. Return the array of + measurements (in row order) so the batching is observable. + +## Definition of done + +- All tests pass (`npx vitest run labs/lab-11-rendering`). +- In your LMS notebook: before/after Performance traces, the thrashing code identified, and the + layout-triggering property named. + +## Submit + +Edit `src/`, run the tests, commit and push. diff --git a/labs/lab-11-rendering/src/render.js b/labs/lab-11-rendering/src/render.js new file mode 100644 index 0000000..a90eb75 --- /dev/null +++ b/labs/lab-11-rendering/src/render.js @@ -0,0 +1,40 @@ +/** + * Lab 11 — Investigate the Browser Rendering Engine. See README.md. + */ + +const LAYOUT_PROPS = new Set([ + 'top', + 'left', + 'right', + 'bottom', + 'width', + 'height', + 'margin', + 'padding', +]); + +/** + * Keep only the CSS properties that trigger layout when animated, in input order. + * (transform / opacity are composite/paint-cheap and must be excluded.) + * @param {string[]} props + * @returns {string[]} + */ +export function layoutTriggers(props) { + // TODO: filter `props` to those in LAYOUT_PROPS. + return []; +} + +/** + * De-thrashed update: ALL reads first, then ALL writes (never interleaved). + * @template T, M + * @param {T[]} rows + * @param {(row: T) => M} measure a READ (e.g. offsetHeight) + * @param {(row: T, m: M) => void} apply a WRITE (e.g. set transform) + * @returns {M[]} the measurements, in row order + */ +export function scheduleUpdates(rows, measure, apply) { + // TODO: const measurements = rows.map(measure); // read phase + // rows.forEach((row, i) => apply(row, measurements[i])); // write phase + // return measurements; + return []; +} diff --git a/labs/lab-11-rendering/tests/render.test.js b/labs/lab-11-rendering/tests/render.test.js new file mode 100644 index 0000000..a17197d --- /dev/null +++ b/labs/lab-11-rendering/tests/render.test.js @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { layoutTriggers, scheduleUpdates } from '../src/render.js'; + +describe('lab 11 — layout-triggering properties', () => { + it('keeps only layout-triggering props, in order', () => { + expect(layoutTriggers(['transform', 'top', 'opacity', 'width'])).toEqual(['top', 'width']); + }); + + it('excludes transform and opacity entirely', () => { + expect(layoutTriggers(['transform', 'opacity'])).toEqual([]); + }); + + it('recognizes the full layout set', () => { + const all = ['top', 'left', 'right', 'bottom', 'width', 'height', 'margin', 'padding']; + expect(layoutTriggers(all)).toEqual(all); + }); + + it('ignores unknown properties', () => { + expect(layoutTriggers(['color', 'transform', 'height'])).toEqual(['height']); + }); +}); + +describe('lab 11 — batched reads then writes (no thrash)', () => { + it('performs all reads before any write', () => { + const log = []; + const rows = ['a', 'b', 'c']; + const measure = (r) => { + log.push('read:' + r); + return r.toUpperCase(); + }; + const apply = (r, m) => { + log.push('write:' + r + '=' + m); + }; + const measurements = scheduleUpdates(rows, measure, apply); + + expect(measurements).toEqual(['A', 'B', 'C']); + // All reads come strictly before all writes. + const firstWrite = log.findIndex((x) => x.startsWith('write:')); + const lastRead = log.map((x) => x.startsWith('read:')).lastIndexOf(true); + expect(lastRead).toBeLessThan(firstWrite); + expect(log).toEqual([ + 'read:a', + 'read:b', + 'read:c', + 'write:a=A', + 'write:b=B', + 'write:c=C', + ]); + }); + + it('handles an empty row list', () => { + expect(scheduleUpdates([], () => {}, () => {})).toEqual([]); + }); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..def4d48 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2197 @@ +{ + "name": "swexp-module-03-web-platform", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "swexp-module-03-web-platform", + "version": "1.0.0", + "devDependencies": { + "jsdom": "^25.0.1", + "vitest": "^2.1.8" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6f036f3 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "swexp-module-03-web-platform", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Forge SWEXP Module 03 — interactive JavaScript foundations exercises (clone, implement, npm test).", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "grade": "node scripts/grade.mjs" + }, + "devDependencies": { + "jsdom": "^25.0.1", + "vitest": "^2.1.8" + } +} diff --git a/scripts/grade.mjs b/scripts/grade.mjs new file mode 100644 index 0000000..2993b17 --- /dev/null +++ b/scripts/grade.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +/** + * Forge SWEXP autograder (Module 03 — JavaScript Foundations). + * Runs every exercise's behaviour tests (vitest, Node + jsdom), then prints a + * per-exercise score and writes a Markdown report for GitHub Actions. + * + * Grouping is by exercise folder under labs/ and assignments/. The tests are the + * spec — no answer keys are shipped. + */ +import { execSync } from 'node:child_process'; +import { readFileSync, writeFileSync, mkdirSync, appendFileSync, existsSync } from 'node:fs'; + +const REPORT = '.grade/vitest.json'; +mkdirSync('.grade', { recursive: true }); + +function run(cmd) { + try { + return { ok: true, out: execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'] }).toString() }; + } catch (e) { + return { ok: false, out: `${e.stdout ?? ''}${e.stderr ?? ''}` }; + } +} + +// Exercise folder name from a test file path, e.g. ".../labs/lab-04-js-runtime/tests/x.test.js" +function exerciseOf(p) { + const m = p.replace(/\\/g, '/').match(/\/(labs|assignments)\/([^/]+)\//); + return m ? `${m[1]}/${m[2]}` : null; +} + +// 1) Behaviour tests (Node + jsdom). +run(`npx vitest run --reporter=json --outputFile=${REPORT}`); +if (!existsSync(REPORT)) { + console.error('Could not produce a test report. Run `npm install` first.'); + process.exit(2); +} +const report = JSON.parse(readFileSync(REPORT, 'utf8')); + +// 2) No separate type gate in a JavaScript module — the tests are the gate. +const typeGate = { ok: true }; + +// Aggregate per exercise. +const tally = {}; +for (const file of report.testResults ?? []) { + const key = exerciseOf(file.name); + if (!key) continue; + tally[key] ??= { passed: 0, total: 0 }; + for (const a of file.assertionResults ?? []) { + tally[key].total += 1; + if (a.status === 'passed') tally[key].passed += 1; + } +} + +const passed = report.numPassedTests ?? 0; +const total = report.numTotalTests ?? 0; +const pct = total ? Math.round((passed / total) * 100) : 0; +const complete = passed === total && total > 0 && typeGate.ok; + +const rows = Object.keys(tally) + .sort() + .map((k) => { + const t = tally[k]; + const mark = t.passed === t.total ? '✅' : '❌'; + return `| \`${k}\` | ${t.passed}/${t.total} | ${mark} |`; + }); + +const md = [ + `## Forge SWEXP — Module 03 autograde`, + ``, + `**Score: ${passed}/${total} tests (${pct}%)**`, + ``, + `| Exercise | Tests | Status |`, + `| --- | --- | --- |`, + ...rows, + ``, + complete + ? `🎉 **All exercises complete — every behaviour test passes.**` + : `Keep going — open each exercise folder, implement the \`// TODO\`s in its \`src/\`, and run \`npm test\`. The tests in each \`tests/\` folder are the spec.`, +].join('\n'); + +writeFileSync('grade-report.md', md + '\n'); +console.log('\n' + md + '\n'); + +if (process.env.GITHUB_STEP_SUMMARY) { + appendFileSync(process.env.GITHUB_STEP_SUMMARY, md + '\n'); +} + +process.exit(complete ? 0 : 1); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..b9764b3 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // Each exercise's tests/ folder IS the spec. + include: ['labs/**/tests/**/*.test.js', 'assignments/**/tests/**/*.test.js'], + // Default to Node; DOM-dependent specs opt in per file with + // // @vitest-environment jsdom + environment: 'node', + }, +});