@orkestrel/test 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -9
- package/dist/src/browser/index.d.ts +478 -0
- package/dist/src/browser/index.js +619 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/server/index.cjs +51 -12
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +30 -0
- package/dist/src/server/index.d.ts +30 -0
- package/dist/src/server/index.js +48 -13
- package/dist/src/server/index.js.map +1 -1
- package/package.json +24 -10
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/factories.ts"],"sourcesContent":["/**\n * The interactive ARIA roles a bare accessible name is searched across.\n *\n * @remarks\n * A person names a control, not a role, so the one-argument resolver searches every role a control\n * can compute. The two-argument form searches exactly the role it is given, which is how a name\n * shared by a tab and its panel is disambiguated.\n */\nexport const ACCESSIBLE_ROLES: readonly string[] = Object.freeze([\n\t'button',\n\t'checkbox',\n\t'combobox',\n\t'link',\n\t'listbox',\n\t'menuitem',\n\t'option',\n\t'radio',\n\t'searchbox',\n\t'slider',\n\t'spinbutton',\n\t'switch',\n\t'tab',\n\t'tabpanel',\n\t'textbox',\n\t'treeitem',\n])\n","import type { CaptureVariant } from './types.js'\nimport { page, userEvent } from 'vitest/browser'\nimport { ACCESSIBLE_ROLES } from './constants.js'\n\n/**\n * Determines whether a rectangle lies wholly outside the browser viewport.\n *\n * @param rectangle - The measured client rectangle to inspect.\n * @returns `true` when no part of the rectangle intersects the viewport.\n *\n * @example\n * ```ts\n * isOutsideViewport(element.getBoundingClientRect())\n * ```\n */\nexport function isOutsideViewport(rectangle: DOMRectReadOnly): boolean {\n\treturn (\n\t\trectangle.bottom <= 0 ||\n\t\trectangle.right <= 0 ||\n\t\trectangle.top >= window.innerHeight ||\n\t\trectangle.left >= window.innerWidth\n\t)\n}\n\n/**\n * Resolves one rendered, focus-reachable interactive element without requiring it to intersect the\n * viewport yet.\n *\n * @param first - The accessible name, or the exact ARIA role when `second` is present.\n * @param second - The accessible name when `first` supplies the role.\n * @returns The one rendered element carrying that name and optional role.\n * @throws When no matching element exists, every match is hidden or unreachable, or several\n * rendered matches make the name ambiguous.\n *\n * @remarks\n * This is the resolver the acting verbs use, so a click does not fail on a target the act itself\n * scrolls into view. Use {@link resolveAccessible} wherever the target must already be on screen.\n *\n * @example\n * ```ts\n * resolveRendered('tab', 'Drafts')\n * ```\n */\nexport function resolveRendered(first: string, second?: string): HTMLElement {\n\tconst name = second ?? first\n\tconst roles = second === undefined ? ACCESSIBLE_ROLES : [first]\n\tconst matches: HTMLElement[] = []\n\tfor (const role of roles) {\n\t\tfor (const element of page\n\t\t\t.getByRole(role, { name, exact: true, includeHidden: true })\n\t\t\t.elements()) {\n\t\t\tif (element instanceof HTMLElement && !matches.includes(element)) matches.push(element)\n\t\t}\n\t}\n\tif (matches.length === 0) {\n\t\tthrow new Error(`No interactive element has the accessible name \"${name}\"`)\n\t}\n\tconst reachable = matches.filter((element) => {\n\t\tconst rectangle = element.getBoundingClientRect()\n\t\treturn (\n\t\t\telement.isConnected &&\n\t\t\telement.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) &&\n\t\t\trectangle.width > 0 &&\n\t\t\trectangle.height > 0 &&\n\t\t\telement.tabIndex >= 0 &&\n\t\t\t!element.matches(':disabled, [aria-disabled=\"true\"]') &&\n\t\t\telement.closest('[inert]') === null\n\t\t)\n\t})\n\tif (reachable.length === 0) {\n\t\tthrow new Error(`Interactive target \"${name}\" is not visible and focus-reachable`)\n\t}\n\tif (reachable.length > 1) {\n\t\tthrow new Error(`Interactive target \"${name}\" is ambiguous across ${reachable.length} elements`)\n\t}\n\tconst [target] = reachable\n\tif (target === undefined) throw new Error(`Interactive target \"${name}\" could not be resolved`)\n\treturn target\n}\n\n/**\n * Resolves one visible, focus-reachable interactive element by its exact accessible name. A\n * wholly-off-viewport target is scrolled into view before reachability is measured.\n *\n * @param name - The accessible name rendered for the target.\n * @returns The one reachable element carrying that name.\n * @throws When no matching element exists; every match is disconnected, hidden, zero-sized, still\n * outside the viewport after being scrolled into view, removed from sequential focus, disabled, or\n * inside an inert subtree; or several reachable matches make the name ambiguous.\n *\n * @example\n * ```ts\n * resolveAccessible('Save changes')\n * ```\n */\nexport function resolveAccessible(name: string): HTMLElement\n/**\n * Resolves one visible, focus-reachable interactive element by its exact ARIA role and accessible\n * name, disambiguating a bare name that answers for more than one rendered element. A\n * wholly-off-viewport target is scrolled into view before reachability is measured.\n *\n * @param role - The element's exact ARIA role.\n * @param name - The accessible name rendered for the target.\n * @returns The one reachable element carrying that role and name.\n * @throws When no matching element exists; every match is disconnected, hidden, zero-sized, still\n * outside the viewport after being scrolled into view, removed from sequential focus, disabled, or\n * inside an inert subtree; or several reachable matches make the role/name pair ambiguous.\n *\n * @example\n * ```ts\n * resolveAccessible('tab', 'Drafts')\n * ```\n */\nexport function resolveAccessible(role: string, name: string): HTMLElement\nexport function resolveAccessible(first: string, second?: string): HTMLElement {\n\tconst target = resolveRendered(first, second)\n\tlet rectangle = target.getBoundingClientRect()\n\tif (isOutsideViewport(rectangle)) {\n\t\ttarget.scrollIntoView({ block: 'nearest', behavior: 'instant' })\n\t\trectangle = target.getBoundingClientRect()\n\t}\n\tif (isOutsideViewport(rectangle)) {\n\t\tthrow new Error(`Interactive target \"${second ?? first}\" is unreachable after scrolling`)\n\t}\n\treturn target\n}\n\n/**\n * Clicks one visible, focus-reachable control by its accessible name through the browser provider.\n *\n * @param name - The target's exact accessible name.\n * @returns A promise resolving after trusted activation completes.\n *\n * @example\n * ```ts\n * await clickAccessible('Apply')\n * ```\n */\nexport async function clickAccessible(name: string): Promise<void>\n/**\n * Clicks one visible, focus-reachable control by its exact ARIA role and accessible name,\n * disambiguating a bare name that answers for more than one rendered element.\n *\n * @param role - The control's exact ARIA role.\n * @param name - The target's exact accessible name.\n * @returns A promise resolving after trusted activation completes.\n *\n * @example\n * ```ts\n * await clickAccessible('tab', 'Drafts')\n * ```\n */\nexport async function clickAccessible(role: string, name: string): Promise<void>\nexport async function clickAccessible(first: string, second?: string): Promise<void> {\n\tconst target = resolveRendered(first, second)\n\tawait userEvent.click(target)\n}\n\n/**\n * Clicks one human-reachable control by role and accessible-name text inside a named region.\n *\n * @param region - The containing region's exact accessible name.\n * @param role - The control's exact ARIA role.\n * @param name - The rendered accessible-name text that identifies the control in that region.\n * @returns A promise resolving after trusted activation completes.\n * @throws When the named control is absent, unreachable, or ambiguous inside the region.\n *\n * @remarks\n * Use this form when repeated short verbs such as `Add`, or a line whose status completes its\n * accessible name, need the same region context a person uses to disambiguate them.\n *\n * @example\n * ```ts\n * await clickAccessibleWithin('Ledger', 'button', 'Monthly income')\n * ```\n */\nexport async function clickAccessibleWithin(\n\tregion: string,\n\trole: string,\n\tname: string,\n): Promise<void> {\n\tconst matches = page\n\t\t.getByRole('region', { name: region, exact: true })\n\t\t.getByRole(role, { name, exact: false, includeHidden: true })\n\t\t.elements()\n\tconst reachable = matches.filter((element) => {\n\t\tif (!(element instanceof HTMLElement)) return false\n\t\tconst rectangle = element.getBoundingClientRect()\n\t\treturn (\n\t\t\telement.isConnected &&\n\t\t\telement.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) &&\n\t\t\trectangle.width > 0 &&\n\t\t\trectangle.height > 0 &&\n\t\t\telement.tabIndex >= 0 &&\n\t\t\t!element.matches(':disabled, [aria-disabled=\"true\"]') &&\n\t\t\telement.closest('[inert]') === null\n\t\t)\n\t})\n\tif (reachable.length === 0) {\n\t\tthrow new Error(`Interactive target \"${name}\" is not reachable inside \"${region}\"`)\n\t}\n\tif (reachable.length > 1) {\n\t\tthrow new Error(\n\t\t\t`Interactive target \"${name}\" is ambiguous across ${reachable.length} elements inside \"${region}\"`,\n\t\t)\n\t}\n\tconst [target] = reachable\n\tif (!(target instanceof HTMLElement)) {\n\t\tthrow new Error(`Interactive target \"${name}\" could not be resolved inside \"${region}\"`)\n\t}\n\tawait userEvent.click(target)\n}\n\n/**\n * Opens or closes one native details disclosure by its rendered summary.\n *\n * @param name - The summary text a person reads.\n * @returns A promise resolving after trusted activation completes.\n * @throws When no visible, focus-reachable native summary has that rendered name, or several do.\n *\n * @remarks\n * Chromium exposes `<summary>` as a native disclosure rather than through an ARIA role accepted by\n * `getByRole`, so this resolver names the platform element and its rendered text directly.\n *\n * @example\n * ```ts\n * await clickDisclosure('Advanced')\n * ```\n */\nexport async function clickDisclosure(name: string): Promise<void> {\n\tconst matches = [...document.querySelectorAll('summary')].filter(\n\t\t(element) => element.innerText.replaceAll(/\\s+/g, ' ').trim() === name,\n\t)\n\tconst reachable = matches.filter((element) => {\n\t\tconst rectangle = element.getBoundingClientRect()\n\t\treturn (\n\t\t\telement.isConnected &&\n\t\t\telement.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) &&\n\t\t\trectangle.width > 0 &&\n\t\t\trectangle.height > 0 &&\n\t\t\telement.tabIndex >= 0 &&\n\t\t\telement.closest('[inert]') === null\n\t\t)\n\t})\n\tif (reachable.length === 0) {\n\t\tthrow new Error(`Native disclosure \"${name}\" is not visible and focus-reachable`)\n\t}\n\tif (reachable.length > 1) {\n\t\tthrow new Error(`Native disclosure \"${name}\" is ambiguous across ${reachable.length} elements`)\n\t}\n\tconst [target] = reachable\n\tif (target === undefined) throw new Error(`Native disclosure \"${name}\" could not be resolved`)\n\tawait userEvent.click(target)\n}\n\n/**\n * Replaces a named field's value through focus, select-all, deletion, and real keystrokes.\n *\n * @param name - The field's exact accessible name.\n * @param text - The text to type.\n * @returns A promise resolving after every keystroke completes.\n *\n * @example\n * ```ts\n * await typeAccessible('Runs', '3')\n * ```\n */\nexport async function typeAccessible(name: string, text: string): Promise<void> {\n\tawait userEvent.click(resolveRendered(name))\n\tawait userEvent.keyboard('{Control>}a{/Control}{Backspace}')\n\tif (text === '') return\n\tawait userEvent.keyboard(text.replaceAll('{', '{{').replaceAll('[', '[['))\n}\n\n/**\n * Replaces a named field's value in one operation, for text too long to type key by key.\n *\n * @param name - The field's exact accessible name.\n * @param text - The text to place in the field.\n * @returns A promise resolving after the browser commits the value.\n *\n * @remarks\n * The provider drives the real element, so the field publishes the same input event a person's\n * typing publishes. Use {@link typeAccessible} wherever the keystrokes themselves are the subject.\n *\n * @example\n * ```ts\n * await fillAccessible('Payload', '{\"status\":\"ready\"}')\n * ```\n */\nexport async function fillAccessible(name: string, text: string): Promise<void> {\n\tawait userEvent.fill(resolveRendered(name), text)\n}\n\n/**\n * Presses a browser-keyboard sequence using Vitest's installed user-event syntax.\n *\n * @param keys - The keys or key descriptors to press.\n * @returns A promise resolving after the sequence completes.\n *\n * @example\n * ```ts\n * await pressKeys('{ArrowRight}{Enter}')\n * ```\n */\nexport async function pressKeys(keys: string): Promise<void> {\n\tawait userEvent.keyboard(keys)\n}\n\n/**\n * Reaches a named control only through natural forward Tab traversal from the current focus.\n *\n * @param name - The target's exact accessible name.\n * @returns The target after the browser moves focus to it.\n * @throws When one complete traversal cannot reach the target.\n *\n * @example\n * ```ts\n * await traverseAccessible('Evaluate')\n * ```\n */\nexport async function traverseAccessible(name: string): Promise<HTMLElement> {\n\tresolveRendered(name)\n\t// Two facts shape the loop. A Tab pressed before the page has real input focus moves nothing,\n\t// so a step counts only when focus actually lands somewhere; the traversal is over when focus\n\t// revisits an element, because that is one full cycle of the tab order. And the target is\n\t// re-resolved on every step, because a framework may replace the node between resolution and\n\t// focus arrival: the person's target is the role and name, never one node.\n\tconst cap =\n\t\tdocument.querySelectorAll<HTMLElement>('a[href], button, input, select, textarea, [tabindex]')\n\t\t\t.length *\n\t\t\t3 +\n\t\t10\n\tconst visited = new Set<Element>()\n\tconst trail: string[] = []\n\tfor (let attempt = 0; attempt < cap; attempt += 1) {\n\t\tawait userEvent.tab()\n\t\tconst focused = document.activeElement\n\t\tif (!(focused instanceof HTMLElement) || focused === document.body) continue\n\t\tlet current: HTMLElement | undefined\n\t\ttry {\n\t\t\tcurrent = resolveRendered(name)\n\t\t} catch {\n\t\t\tcontinue\n\t\t}\n\t\tif (focused === current) return current\n\t\tif (visited.has(focused)) break\n\t\tvisited.add(focused)\n\t\ttrail.push(`${focused.tagName}:${focused.innerText.slice(0, 20)}`)\n\t}\n\tthrow new Error(\n\t\t`Interactive target \"${name}\" is not reachable through forward Tab traversal: ${trail.join(' > ')}`,\n\t)\n}\n\n/**\n * Reads the normalized visible text of one named region, dialog, table, tab panel, or alert.\n *\n * @param name - The region's exact accessible name.\n * @returns The text a screen reader can perceive in the visible region, including descendant\n * visually-hidden content.\n * @throws When the named region is absent, hidden, or ambiguous.\n *\n * @example\n * ```ts\n * readPerception('Run')\n * ```\n */\nexport function readPerception(name: string): string {\n\tconst matches: HTMLElement[] = []\n\tfor (const role of ['alert', 'alertdialog', 'dialog', 'region', 'status', 'table', 'tabpanel']) {\n\t\tfor (const element of page\n\t\t\t.getByRole(role, { name, exact: true, includeHidden: true })\n\t\t\t.elements()) {\n\t\t\tif (element instanceof HTMLElement && !matches.includes(element)) matches.push(element)\n\t\t}\n\t}\n\tconst visible = matches.filter((element) => {\n\t\tconst rectangle = element.getBoundingClientRect()\n\t\treturn (\n\t\t\telement.isConnected &&\n\t\t\telement.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) &&\n\t\t\trectangle.width > 0 &&\n\t\t\trectangle.height > 0\n\t\t)\n\t})\n\tif (visible.length === 0) throw new Error(`Named region \"${name}\" is not visible`)\n\tif (visible.length > 1) {\n\t\tthrow new Error(`Named region \"${name}\" is ambiguous across ${visible.length} elements`)\n\t}\n\tconst [region] = visible\n\tif (region === undefined) throw new Error(`Named region \"${name}\" could not be resolved`)\n\treturn region.innerText.replaceAll(/\\s+/g, ' ').trim()\n}\n\n/**\n * Reads the normalized visible text of the whole page.\n *\n * @returns Every rendered word in the document body, its whitespace runs collapsed and trimmed.\n *\n * @remarks\n * This is the reader for a sentence that spans two regions and for a vocabulary sweep over the\n * words an interface uses. Reach for {@link readPerception} wherever one named region is the\n * subject, because that one throws when the region is missing and this one returns whatever is\n * there.\n *\n * @example\n * ```ts\n * readPage().includes('No cases yet')\n * ```\n */\nexport function readPage(): string {\n\treturn document.body.innerText.replaceAll(/\\s+/g, ' ').trim()\n}\n\n/**\n * Reads the rendered text of the element that currently holds focus.\n *\n * @returns The focused HTML element's trimmed rendered text, including an empty string, or\n * `undefined` when focus rests on a non-HTML element. When nothing holds focus, the browser\n * reports the document body as active, so the whole page's rendered text returns.\n *\n * @example\n * ```ts\n * await traverseAccessible('Evaluate')\n * readFocus() // 'Evaluate'\n * ```\n */\nexport function readFocus(): string | undefined {\n\tconst focused = document.activeElement\n\treturn focused instanceof HTMLElement ? focused.innerText.trim() : undefined\n}\n\n/**\n * Reads the value a resolved control renders.\n *\n * @param role - The control's exact ARIA role.\n * @param name - The control's exact accessible name.\n * @returns The control's current value.\n * @throws When the target does not resolve, or resolves to an element that carries no value.\n *\n * @remarks\n * A control's value is a rendered fact a person can read, not internal state, so it is read from\n * the resolved element rather than from the component that produced it.\n *\n * @example\n * ```ts\n * readValue('spinbutton', 'Runs') // '3'\n * ```\n */\nexport function readValue(role: string, name: string): string {\n\tconst control = resolveAccessible(role, name)\n\tif (\n\t\t!(control instanceof HTMLInputElement) &&\n\t\t!(control instanceof HTMLTextAreaElement) &&\n\t\t!(control instanceof HTMLSelectElement)\n\t) {\n\t\tthrow new Error(`Interactive target \"${name}\" does not carry a value`)\n\t}\n\treturn control.value\n}\n\n/**\n * Waits for one animation frame to settle pending browser paint work.\n *\n * @returns A promise resolving after one `requestAnimationFrame`.\n *\n * @example\n * ```ts\n * await waitForFrame()\n * ```\n */\nexport function waitForFrame(): Promise<void> {\n\treturn new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))\n}\n\n/**\n * Renders trusted fixture markup into a container attached to the document.\n *\n * @param markup - The fixture markup to render.\n * @returns The attached container.\n *\n * @example\n * ```ts\n * const container = render('<button type=\"button\">Save</button>')\n * container.remove()\n * ```\n */\nexport function render(markup: string): HTMLDivElement {\n\tconst container = document.createElement('div')\n\tcontainer.innerHTML = markup\n\tdocument.body.append(container)\n\treturn container\n}\n\n/**\n * Measures the WCAG 2.x contrast ratio between an element's computed text and background colors.\n *\n * @param element - The element whose rendered text contrast to measure.\n * @returns The relative-luminance contrast ratio.\n * @throws When the browser does not expose parseable computed colors.\n *\n * @remarks\n * A transparent or translucent background resolves through the element's ancestors: every painted\n * layer from the element up to the first opaque one composites top-over-bottom onto that opaque\n * base, so a 3% surface tint reads as a tint over what shows through it rather than as a\n * full-strength paint. A translucent foreground then resolves against that effective background\n * before luminance is measured.\n *\n * Every element from the target upwards must be reachable, and at least one of them must paint:\n * the measurement throws rather than assuming a white canvas when nothing in the chain declares a\n * background color. The element itself must expose a computed foreground color — a detached\n * element exposes none, and the measurement throws rather than guessing one.\n *\n * @example\n * ```ts\n * const container = render('<p style=\"background: #000; color: #fff\">Ready</p>')\n * contrast(requireValue(container.firstElementChild)) // 21\n * ```\n */\nexport function contrast(element: Element): number {\n\tconst foreground = getComputedStyle(element).color.match(/\\d+(?:\\.\\d+)?/g)\n\tif (foreground === null || foreground.length < 3) {\n\t\tthrow new Error('Computed foreground color is unavailable')\n\t}\n\tconst layers: number[][] = []\n\tlet current: Element | null = element\n\tlet opaque = false\n\twhile (current !== null) {\n\t\tconst channels = getComputedStyle(current).backgroundColor.match(/\\d+(?:\\.\\d+)?/g)\n\t\tif (channels !== null && channels.length >= 3) {\n\t\t\tconst layerAlpha = channels[3] === undefined ? 1 : Number(channels[3])\n\t\t\tif (layerAlpha > 0) {\n\t\t\t\tlayers.push([...channels.slice(0, 3).map(Number), layerAlpha])\n\t\t\t}\n\t\t\tif (layerAlpha >= 1) {\n\t\t\t\topaque = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tcurrent = current.parentElement\n\t}\n\tif (layers.length === 0) throw new Error('Computed background color is unavailable')\n\tconst base = layers[layers.length - 1]\n\tif (base === undefined) throw new Error('Computed background color is unavailable')\n\tlet composed = base.slice(0, 3).map((channel) => channel / 255)\n\tif (!opaque) composed = [1, 1, 1]\n\tconst start = opaque ? layers.length - 2 : layers.length - 1\n\tfor (let index = start; index >= 0; index -= 1) {\n\t\tconst layer = layers[index]\n\t\tif (layer === undefined) continue\n\t\tconst layerAlpha = layer[3] ?? 1\n\t\tcomposed = composed.map((channel, position) => {\n\t\t\tconst top = (layer[position] ?? 0) / 255\n\t\t\treturn top * layerAlpha + channel * (1 - layerAlpha)\n\t\t})\n\t}\n\n\tconst alpha = foreground[3] === undefined ? 1 : Number(foreground[3])\n\tconst backgroundChannels = composed\n\tconst foregroundChannels = foreground.slice(0, 3).map((channel, index) => {\n\t\tconst behind = backgroundChannels[index]\n\t\tif (behind === undefined) throw new Error('Computed background channel is unavailable')\n\t\treturn (Number(channel) / 255) * alpha + behind * (1 - alpha)\n\t})\n\tconst foregroundLinear = foregroundChannels.map((channel) =>\n\t\tchannel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4,\n\t)\n\tconst backgroundLinear = backgroundChannels.map((channel) =>\n\t\tchannel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4,\n\t)\n\tconst foregroundLuminance =\n\t\t0.2126 * (foregroundLinear[0] ?? 0) +\n\t\t0.7152 * (foregroundLinear[1] ?? 0) +\n\t\t0.0722 * (foregroundLinear[2] ?? 0)\n\tconst backgroundLuminance =\n\t\t0.2126 * (backgroundLinear[0] ?? 0) +\n\t\t0.7152 * (backgroundLinear[1] ?? 0) +\n\t\t0.0722 * (backgroundLinear[2] ?? 0)\n\tconst lighter = Math.max(foregroundLuminance, backgroundLuminance)\n\tconst darker = Math.min(foregroundLuminance, backgroundLuminance)\n\treturn (lighter + 0.05) / (darker + 0.05)\n}\n\n/**\n * Collects every class token the stylesheets loaded into this document actually define.\n *\n * @returns The set of class names reachable in the shipped cascade.\n *\n * @remarks\n * The set is what an authored-class conformance check measures against, so a class no loaded\n * stylesheet defines — an invented utility, a misspelled framework name — is absent from it.\n *\n * @example\n * ```ts\n * readCascade().has('card')\n * ```\n */\nexport function readCascade(): ReadonlySet<string> {\n\tconst known = new Set<string>()\n\tconst rules: CSSRule[] = []\n\tfor (const sheet of document.styleSheets) rules.push(...sheet.cssRules)\n\twhile (rules.length > 0) {\n\t\tconst rule = rules.pop()\n\t\tif (rule instanceof CSSGroupingRule) rules.push(...rule.cssRules)\n\t\tif (!(rule instanceof CSSStyleRule)) continue\n\t\tfor (const match of rule.selectorText.matchAll(/\\.([a-zA-Z][\\w-]*)/g)) {\n\t\t\tknown.add(String(match[1]))\n\t\t}\n\t}\n\treturn known\n}\n\n/**\n * Reads the normalized visible text of every element a selector matches, in document order.\n *\n * @param root - The subtree to search.\n * @param selector - The CSS selector naming the rows.\n * @returns One line per matched element, its text runs collapsed and single-space joined.\n *\n * @remarks\n * The line is built from the row's text nodes rather than from `textContent`, because adjacent\n * inline elements carry no whitespace between them in compiled template output and would otherwise\n * read as one run-together word.\n *\n * @example\n * ```ts\n * readRows(container, 'li')\n * ```\n */\nexport function readRows(root: ParentNode, selector: string): readonly string[] {\n\tconst rows: string[] = []\n\tfor (const row of root.querySelectorAll(selector)) {\n\t\tconst parts: string[] = []\n\t\tconst walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT)\n\t\twhile (walker.nextNode() !== null) {\n\t\t\tconst text = (walker.currentNode.textContent ?? '').replaceAll(/\\s+/g, ' ').trim()\n\t\t\tif (text !== '') parts.push(text)\n\t\t}\n\t\trows.push(parts.join(' '))\n\t}\n\treturn rows\n}\n\n/**\n * Reads one resolved CSS property from a real browser element.\n *\n * @param element - The element whose resolved style to inspect.\n * @param property - The CSS property name.\n * @returns The browser's resolved property value.\n *\n * @example\n * ```ts\n * style(button, 'padding-left')\n * ```\n */\nexport function style(element: Element, property: string): string {\n\treturn getComputedStyle(element).getPropertyValue(property)\n}\n\n/**\n * Expands a capture registry across every variant into the filenames a complete portfolio holds.\n *\n * @param states - The registered state names.\n * @param variants - The variants the portfolio is rendered in.\n * @returns One `<state>--<variant>.png` name per pair, each state's variants together, in registry\n * order.\n *\n * @remarks\n * The expansion is the portfolio's own definition of complete, so a duplicate in it is a registry\n * defect a proof reads directly rather than a collision discovered on disk.\n *\n * @example\n * ```ts\n * expandCaptures(['start'], [{ name: 'dark-390', width: 390, height: 844 }])\n * // ['start--dark-390.png']\n * ```\n */\nexport function expandCaptures(\n\tstates: readonly string[],\n\tvariants: readonly CaptureVariant[],\n): readonly string[] {\n\tconst files: string[] = []\n\tfor (const state of states) {\n\t\tfor (const variant of variants) files.push(`${state}--${variant.name}.png`)\n\t}\n\treturn files\n}\n","import type { PortfolioInterface, PortfolioOptions } from './types.js'\nimport { page } from 'vitest/browser'\nimport { expandCaptures } from './helpers.js'\n\n/**\n * Creates the capture portfolio one run places its screenshots through.\n *\n * @param options - The state registry, the variant matrix, the variant this run renders, the\n * directory it writes into, and whether it writes at all.\n * @returns The portfolio: its registry expansion, what it has placed, and `place`.\n * @throws When no registered variant carries the name `variant` names.\n *\n * @remarks\n * A disabled portfolio is the ordinary run. `place` then resizes nothing, writes nothing, and\n * records nothing, so a journey calls it unconditionally and a suite with the flag unset pays for\n * none of it. The portfolio refuses an unregistered variant at creation. An enabled run refuses an\n * unregistered state name and a second placement of one state.\n *\n * @example\n * ```ts\n * const portfolio = createPortfolio({\n * \tstates: ['start-empty'],\n * \tvariants: [{ name: 'dark-390', width: 390, height: 844 }],\n * \tvariant: 'dark-390',\n * \tdirectory: '../../tmp/capture/states',\n * })\n * await portfolio.place('start-empty')\n * ```\n */\nexport function createPortfolio(options: PortfolioOptions): PortfolioInterface {\n\tconst selected = options.variants.find((candidate) => candidate.name === options.variant)\n\tif (selected === undefined) {\n\t\tthrow new Error(`Capture variant \"${options.variant}\" is not registered`)\n\t}\n\tconst registry = [...options.states]\n\tconst files = expandCaptures(registry, options.variants)\n\tconst enabled = options.enabled ?? false\n\tconst placed: string[] = []\n\tconst paths: string[] = []\n\treturn {\n\t\tvariant: options.variant,\n\t\tfiles,\n\t\tget states() {\n\t\t\treturn [...placed]\n\t\t},\n\t\tget paths() {\n\t\t\treturn [...paths]\n\t\t},\n\t\tasync place(state) {\n\t\t\tif (!enabled) return undefined\n\t\t\tif (!registry.includes(state)) {\n\t\t\t\tthrow new Error(`Capture state \"${state}\" is not registered`)\n\t\t\t}\n\t\t\tif (placed.includes(state)) {\n\t\t\t\tthrow new Error(`Capture state \"${state}\" is already placed`)\n\t\t\t}\n\t\t\tconst file = `${state}--${options.variant}.png`\n\t\t\tselected.apply?.()\n\t\t\tif (window.innerWidth !== selected.width || window.innerHeight !== selected.height) {\n\t\t\t\tawait page.viewport(selected.width, selected.height)\n\t\t\t}\n\t\t\tconst written = await page.screenshot({ path: `${options.directory}/${file}` })\n\t\t\tplaced.push(state)\n\t\t\tpaths.push(written)\n\t\t\treturn written\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -4,6 +4,24 @@ let node_path = require("node:path");
|
|
|
4
4
|
let node_url = require("node:url");
|
|
5
5
|
let node_events = require("node:events");
|
|
6
6
|
let node_os = require("node:os");
|
|
7
|
+
//#region src/server/constants.ts
|
|
8
|
+
/**
|
|
9
|
+
* The attempts `removeTree` makes before rethrowing a retryable removal error.
|
|
10
|
+
*/
|
|
11
|
+
var REMOVE_TREE_MAX_ATTEMPTS = 10;
|
|
12
|
+
/**
|
|
13
|
+
* The synchronous delay, in milliseconds, `removeTree` waits between attempts.
|
|
14
|
+
*/
|
|
15
|
+
var REMOVE_TREE_RETRY_DELAY_MS = 100;
|
|
16
|
+
/**
|
|
17
|
+
* The error codes `removeTree` retries; every other code rethrows immediately.
|
|
18
|
+
*/
|
|
19
|
+
var REMOVE_TREE_RETRYABLE_CODES = Object.freeze([
|
|
20
|
+
"EBUSY",
|
|
21
|
+
"ENOTEMPTY",
|
|
22
|
+
"EPERM"
|
|
23
|
+
]);
|
|
24
|
+
//#endregion
|
|
7
25
|
//#region src/server/helpers.ts
|
|
8
26
|
/**
|
|
9
27
|
* Resolves a target that stays below a root directory.
|
|
@@ -42,6 +60,32 @@ function isExcluded(key, exclusions) {
|
|
|
42
60
|
return exclusions.some((rule) => rule === "" || key === rule || key.startsWith(`${rule}/`));
|
|
43
61
|
}
|
|
44
62
|
/**
|
|
63
|
+
* Removes a directory tree, retrying past a transient Windows handle-release race.
|
|
64
|
+
*
|
|
65
|
+
* @param path - The absolute directory to remove.
|
|
66
|
+
* @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
|
|
67
|
+
* or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
|
|
68
|
+
* @remarks On Windows, a directory that a just-exited process still holds as its current
|
|
69
|
+
* working directory throws `EPERM` for a short interval after that process exits. Node's own
|
|
70
|
+
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
71
|
+
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
72
|
+
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
73
|
+
* at roughly one second.
|
|
74
|
+
*/
|
|
75
|
+
function removeTree(path) {
|
|
76
|
+
for (let attempt = 1;; attempt++) try {
|
|
77
|
+
(0, node_fs.rmSync)(path, {
|
|
78
|
+
force: true,
|
|
79
|
+
recursive: true
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
84
|
+
if (code === void 0 || !REMOVE_TREE_RETRYABLE_CODES.includes(code) || attempt >= 10) throw error;
|
|
85
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
45
89
|
* Reads files from selected targets below a root directory.
|
|
46
90
|
*
|
|
47
91
|
* @param root - The root directory as a path or file URL.
|
|
@@ -147,10 +191,7 @@ function createScratch(options) {
|
|
|
147
191
|
(0, node_fs.writeFileSync)(candidate, text);
|
|
148
192
|
}
|
|
149
193
|
} catch (error) {
|
|
150
|
-
(
|
|
151
|
-
force: true,
|
|
152
|
-
recursive: true
|
|
153
|
-
});
|
|
194
|
+
removeTree(path);
|
|
154
195
|
throw error;
|
|
155
196
|
}
|
|
156
197
|
const scratch = {
|
|
@@ -218,10 +259,7 @@ function createScratch(options) {
|
|
|
218
259
|
inode: status.ino
|
|
219
260
|
}, allocation)) throw new Error(`${unremovable}: ${target}`);
|
|
220
261
|
}
|
|
221
|
-
(
|
|
222
|
-
force: true,
|
|
223
|
-
recursive: true
|
|
224
|
-
});
|
|
262
|
+
removeTree(candidate);
|
|
225
263
|
},
|
|
226
264
|
destroy() {
|
|
227
265
|
const status = (0, node_fs.lstatSync)(path, { throwIfNoEntry: false });
|
|
@@ -231,10 +269,7 @@ function createScratch(options) {
|
|
|
231
269
|
device: status.dev,
|
|
232
270
|
inode: status.ino
|
|
233
271
|
}, allocation)) return;
|
|
234
|
-
(
|
|
235
|
-
force: true,
|
|
236
|
-
recursive: true
|
|
237
|
-
});
|
|
272
|
+
removeTree(path);
|
|
238
273
|
}
|
|
239
274
|
};
|
|
240
275
|
return scratch;
|
|
@@ -269,11 +304,15 @@ async function createLoopback(server) {
|
|
|
269
304
|
};
|
|
270
305
|
}
|
|
271
306
|
//#endregion
|
|
307
|
+
exports.REMOVE_TREE_MAX_ATTEMPTS = REMOVE_TREE_MAX_ATTEMPTS;
|
|
308
|
+
exports.REMOVE_TREE_RETRYABLE_CODES = REMOVE_TREE_RETRYABLE_CODES;
|
|
309
|
+
exports.REMOVE_TREE_RETRY_DELAY_MS = REMOVE_TREE_RETRY_DELAY_MS;
|
|
272
310
|
exports.createLoopback = createLoopback;
|
|
273
311
|
exports.createScratch = createScratch;
|
|
274
312
|
exports.isExcluded = isExcluded;
|
|
275
313
|
exports.matchesIdentity = matchesIdentity;
|
|
276
314
|
exports.readInventory = readInventory;
|
|
315
|
+
exports.removeTree = removeTree;
|
|
277
316
|
exports.resolveContained = resolveContained;
|
|
278
317
|
|
|
279
318
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { Server } from 'node:net'\nimport type {\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\trmSync(candidate, { force: true, recursive: true })\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * The attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * The synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * The error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync, rmSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a just-exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second.\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === 'object' &&\n\t\t\t\terror !== null &&\n\t\t\t\t'code' in error &&\n\t\t\t\ttypeof error.code === 'string'\n\t\t\t\t\t? error.code\n\t\t\t\t\t: undefined\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { Server } from 'node:net'\nimport type {\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, removeTree, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -86,6 +86,36 @@ export declare function matchesIdentity(current: ScratchIdentity, allocation: Sc
|
|
|
86
86
|
*/
|
|
87
87
|
export declare function readInventory(root: URL | string, targets: readonly string[], options?: InventoryOptions): Readonly<Record<string, string>>;
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The attempts `removeTree` makes before rethrowing a retryable removal error.
|
|
91
|
+
*/
|
|
92
|
+
export declare const REMOVE_TREE_MAX_ATTEMPTS = 10;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The synchronous delay, in milliseconds, `removeTree` waits between attempts.
|
|
96
|
+
*/
|
|
97
|
+
export declare const REMOVE_TREE_RETRY_DELAY_MS = 100;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The error codes `removeTree` retries; every other code rethrows immediately.
|
|
101
|
+
*/
|
|
102
|
+
export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Removes a directory tree, retrying past a transient Windows handle-release race.
|
|
106
|
+
*
|
|
107
|
+
* @param path - The absolute directory to remove.
|
|
108
|
+
* @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
|
|
109
|
+
* or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
|
|
110
|
+
* @remarks On Windows, a directory that a just-exited process still holds as its current
|
|
111
|
+
* working directory throws `EPERM` for a short interval after that process exits. Node's own
|
|
112
|
+
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
113
|
+
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
114
|
+
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
115
|
+
* at roughly one second.
|
|
116
|
+
*/
|
|
117
|
+
export declare function removeTree(path: string): void;
|
|
118
|
+
|
|
89
119
|
/**
|
|
90
120
|
* Resolves a target that stays below a root directory.
|
|
91
121
|
*
|
|
@@ -86,6 +86,36 @@ export declare function matchesIdentity(current: ScratchIdentity, allocation: Sc
|
|
|
86
86
|
*/
|
|
87
87
|
export declare function readInventory(root: URL | string, targets: readonly string[], options?: InventoryOptions): Readonly<Record<string, string>>;
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The attempts `removeTree` makes before rethrowing a retryable removal error.
|
|
91
|
+
*/
|
|
92
|
+
export declare const REMOVE_TREE_MAX_ATTEMPTS = 10;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The synchronous delay, in milliseconds, `removeTree` waits between attempts.
|
|
96
|
+
*/
|
|
97
|
+
export declare const REMOVE_TREE_RETRY_DELAY_MS = 100;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The error codes `removeTree` retries; every other code rethrows immediately.
|
|
101
|
+
*/
|
|
102
|
+
export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Removes a directory tree, retrying past a transient Windows handle-release race.
|
|
106
|
+
*
|
|
107
|
+
* @param path - The absolute directory to remove.
|
|
108
|
+
* @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
|
|
109
|
+
* or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
|
|
110
|
+
* @remarks On Windows, a directory that a just-exited process still holds as its current
|
|
111
|
+
* working directory throws `EPERM` for a short interval after that process exits. Node's own
|
|
112
|
+
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
113
|
+
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
114
|
+
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
115
|
+
* at roughly one second.
|
|
116
|
+
*/
|
|
117
|
+
export declare function removeTree(path: string): void;
|
|
118
|
+
|
|
89
119
|
/**
|
|
90
120
|
* Resolves a target that stays below a root directory.
|
|
91
121
|
*
|
package/dist/src/server/index.js
CHANGED
|
@@ -3,6 +3,24 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { once } from "node:events";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
+
//#region src/server/constants.ts
|
|
7
|
+
/**
|
|
8
|
+
* The attempts `removeTree` makes before rethrowing a retryable removal error.
|
|
9
|
+
*/
|
|
10
|
+
var REMOVE_TREE_MAX_ATTEMPTS = 10;
|
|
11
|
+
/**
|
|
12
|
+
* The synchronous delay, in milliseconds, `removeTree` waits between attempts.
|
|
13
|
+
*/
|
|
14
|
+
var REMOVE_TREE_RETRY_DELAY_MS = 100;
|
|
15
|
+
/**
|
|
16
|
+
* The error codes `removeTree` retries; every other code rethrows immediately.
|
|
17
|
+
*/
|
|
18
|
+
var REMOVE_TREE_RETRYABLE_CODES = Object.freeze([
|
|
19
|
+
"EBUSY",
|
|
20
|
+
"ENOTEMPTY",
|
|
21
|
+
"EPERM"
|
|
22
|
+
]);
|
|
23
|
+
//#endregion
|
|
6
24
|
//#region src/server/helpers.ts
|
|
7
25
|
/**
|
|
8
26
|
* Resolves a target that stays below a root directory.
|
|
@@ -41,6 +59,32 @@ function isExcluded(key, exclusions) {
|
|
|
41
59
|
return exclusions.some((rule) => rule === "" || key === rule || key.startsWith(`${rule}/`));
|
|
42
60
|
}
|
|
43
61
|
/**
|
|
62
|
+
* Removes a directory tree, retrying past a transient Windows handle-release race.
|
|
63
|
+
*
|
|
64
|
+
* @param path - The absolute directory to remove.
|
|
65
|
+
* @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
|
|
66
|
+
* or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
|
|
67
|
+
* @remarks On Windows, a directory that a just-exited process still holds as its current
|
|
68
|
+
* working directory throws `EPERM` for a short interval after that process exits. Node's own
|
|
69
|
+
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
70
|
+
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
71
|
+
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
72
|
+
* at roughly one second.
|
|
73
|
+
*/
|
|
74
|
+
function removeTree(path) {
|
|
75
|
+
for (let attempt = 1;; attempt++) try {
|
|
76
|
+
rmSync(path, {
|
|
77
|
+
force: true,
|
|
78
|
+
recursive: true
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
83
|
+
if (code === void 0 || !REMOVE_TREE_RETRYABLE_CODES.includes(code) || attempt >= 10) throw error;
|
|
84
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
44
88
|
* Reads files from selected targets below a root directory.
|
|
45
89
|
*
|
|
46
90
|
* @param root - The root directory as a path or file URL.
|
|
@@ -146,10 +190,7 @@ function createScratch(options) {
|
|
|
146
190
|
writeFileSync(candidate, text);
|
|
147
191
|
}
|
|
148
192
|
} catch (error) {
|
|
149
|
-
|
|
150
|
-
force: true,
|
|
151
|
-
recursive: true
|
|
152
|
-
});
|
|
193
|
+
removeTree(path);
|
|
153
194
|
throw error;
|
|
154
195
|
}
|
|
155
196
|
const scratch = {
|
|
@@ -217,10 +258,7 @@ function createScratch(options) {
|
|
|
217
258
|
inode: status.ino
|
|
218
259
|
}, allocation)) throw new Error(`${unremovable}: ${target}`);
|
|
219
260
|
}
|
|
220
|
-
|
|
221
|
-
force: true,
|
|
222
|
-
recursive: true
|
|
223
|
-
});
|
|
261
|
+
removeTree(candidate);
|
|
224
262
|
},
|
|
225
263
|
destroy() {
|
|
226
264
|
const status = lstatSync(path, { throwIfNoEntry: false });
|
|
@@ -230,10 +268,7 @@ function createScratch(options) {
|
|
|
230
268
|
device: status.dev,
|
|
231
269
|
inode: status.ino
|
|
232
270
|
}, allocation)) return;
|
|
233
|
-
|
|
234
|
-
force: true,
|
|
235
|
-
recursive: true
|
|
236
|
-
});
|
|
271
|
+
removeTree(path);
|
|
237
272
|
}
|
|
238
273
|
};
|
|
239
274
|
return scratch;
|
|
@@ -268,6 +303,6 @@ async function createLoopback(server) {
|
|
|
268
303
|
};
|
|
269
304
|
}
|
|
270
305
|
//#endregion
|
|
271
|
-
export { createLoopback, createScratch, isExcluded, matchesIdentity, readInventory, resolveContained };
|
|
306
|
+
export { REMOVE_TREE_MAX_ATTEMPTS, REMOVE_TREE_RETRYABLE_CODES, REMOVE_TREE_RETRY_DELAY_MS, createLoopback, createScratch, isExcluded, matchesIdentity, readInventory, removeTree, resolveContained };
|
|
272
307
|
|
|
273
308
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { Server } from 'node:net'\nimport type {\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\trmSync(candidate, { force: true, recursive: true })\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * The attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * The synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * The error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync, rmSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a just-exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second.\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === 'object' &&\n\t\t\t\terror !== null &&\n\t\t\t\t'code' in error &&\n\t\t\t\ttypeof error.code === 'string'\n\t\t\t\t\t? error.code\n\t\t\t\t\t: undefined\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { Server } from 'node:net'\nimport type {\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, removeTree, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|