feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.

Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
  (updateURL repointed to oikos-web releases), compose/, own CI (web +
  desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
  version-tagged images, prune-to-3), own webhook receiver on :9798 +
  launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
  stopped+removed, oikos-web project brought up on 8091; outer Caddy
  untouched (targets the published port) — serving + Authentik flow +
  /wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
  workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
  targets, the compose web service, oikos-web from deploy.sh's fallback
  prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
  point at the new repo; mbse + mascot design docs carry a path note.

Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.

Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
This commit is contained in:
2026-08-15 22:27:52 +02:00
parent e074f04bdf
commit d4d99a7473
18854 changed files with 2615729 additions and 173735 deletions

View File

@@ -0,0 +1,745 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const { parseIdentifier } = require("./identifier");
/** @typedef {string | (string | ConditionalMapping)[]} DirectMapping */
/** @typedef {{ [k: string]: MappingValue }} ConditionalMapping */
/** @typedef {ConditionalMapping | DirectMapping | null} MappingValue */
/** @typedef {Record<string, MappingValue> | ConditionalMapping | DirectMapping} ExportsField */
/** @typedef {Record<string, MappingValue>} ImportsField */
/**
* Processing exports/imports field
* @callback FieldProcessor
* @param {string} request request
* @param {Set<string>} conditionNames condition names
* @returns {[string[], string | null]} resolved paths with used field
*/
/*
Example exports field:
{
".": "./main.js",
"./feature": {
"browser": "./feature-browser.js",
"default": "./feature.js"
}
}
Terminology:
Enhanced-resolve name keys ("." and "./feature") as exports field keys.
If value is string or string[], mapping is called as a direct mapping
and value called as a direct export.
If value is key-value object, mapping is called as a conditional mapping
and value called as a conditional export.
Key in conditional mapping is called condition name.
Conditional mapping nested in another conditional mapping is called nested mapping.
----------
Example imports field:
{
"#a": "./main.js",
"#moment": {
"browser": "./moment/index.js",
"default": "moment"
},
"#moment/": {
"browser": "./moment/",
"default": "moment/"
}
}
Terminology:
Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
If value is string or string[], mapping is called as a direct mapping
and value called as a direct export.
If value is key-value object, mapping is called as a conditional mapping
and value called as a conditional export.
Key in conditional mapping is called condition name.
Conditional mapping nested in another conditional mapping is called nested mapping.
*/
const slashCode = "/".charCodeAt(0);
const dotCode = ".".charCodeAt(0);
const hashCode = "#".charCodeAt(0);
const patternRegEx = /\*/g;
const DOLLAR_ESCAPE_RE = /\$/g;
/** @typedef {Record<string, MappingValue>} RecordMapping */
/**
* Cached `Object.keys()` for objects whose shape does not change after the
* first observation — i.e. parsed `package.json` fields and the nested
* conditional mappings inside them. `Object.keys` allocates a fresh array
* on every call; since `findMatch` / `conditionalMapping` run on every
* bare-specifier resolve, the allocation adds up quickly.
* @type {WeakMap<RecordMapping, string[]>}
*/
const _keysCache = new WeakMap();
/**
* @param {RecordMapping} obj object to read keys from
* @returns {string[]} cached keys array (DO NOT mutate)
*/
function cachedKeys(obj) {
let keys = _keysCache.get(obj);
if (keys === undefined) {
keys = Object.keys(obj);
_keysCache.set(obj, keys);
}
return keys;
}
/**
* Per-key precomputed info used by `findMatch`. Equivalent to what the
* previous implementation recomputed inline on every resolve.
* @typedef {object} FieldKeyInfo
* @property {string} key the original key
* @property {number} patternIndex position of the single "*" in the key, or -1 when absent
* @property {string} wildcardPrefix substring before "*" (empty when patternIndex === -1)
* @property {string} wildcardSuffix substring after "*" (empty when patternIndex === -1)
* @property {boolean} isLegacySubpath true when key is a legacy `./foo/`-style folder key with no "*"
* @property {boolean} isPattern true when key contains "*"
* @property {boolean} isSubpathMapping true when key ends with "/"
* @property {boolean} isValidPattern true when key has at most one "*"
*/
/**
* Cached per-field key metadata, keyed by the exports/imports field
* object. Computed lazily on first `findMatch` call and reused forever.
* Safe because `package.json` fields are immutable JSON values.
* @type {WeakMap<RecordMapping, FieldKeyInfo[]>}
*/
const _fieldKeyInfoCache = new WeakMap();
/**
* @param {ExportsField | ImportsField} field field object
* @returns {FieldKeyInfo[]} precomputed per-key info
*/
function getFieldKeyInfos(field) {
const fieldKey = /** @type {RecordMapping} */ (field);
let infos = _fieldKeyInfoCache.get(fieldKey);
if (infos !== undefined) return infos;
const keys = Object.getOwnPropertyNames(field);
infos = Array.from({ length: keys.length });
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const patternIndex = key.indexOf("*");
// `isValidPattern` is true when the key has at most one `*`. Searching
// from `patternIndex + 1` stops as soon as a second `*` is found, so
// we avoid the full-string scan that `lastIndexOf` would do — and the
// single-star common case finishes in one pass.
const isValidPattern =
patternIndex === -1 || !key.includes("*", patternIndex + 1);
const keyLen = key.length;
const endsWithSlash =
keyLen > 0 && key.charCodeAt(keyLen - 1) === slashCode;
infos[i] = {
key,
patternIndex,
wildcardPrefix: patternIndex === -1 ? "" : key.slice(0, patternIndex),
wildcardSuffix: patternIndex === -1 ? "" : key.slice(patternIndex + 1),
isLegacySubpath: patternIndex === -1 && endsWithSlash,
isPattern: patternIndex !== -1,
isSubpathMapping: endsWithSlash,
isValidPattern,
};
}
_fieldKeyInfoCache.set(fieldKey, infos);
return infos;
}
/**
* @param {string} a first string
* @param {string} b second string
* @returns {number} compare result
*/
function patternKeyCompare(a, b) {
const aPatternIndex = a.indexOf("*");
const bPatternIndex = b.indexOf("*");
const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
if (baseLenA > baseLenB) return -1;
if (baseLenB > baseLenA) return 1;
if (aPatternIndex === -1) return 1;
if (bPatternIndex === -1) return -1;
if (a.length > b.length) return -1;
if (b.length > a.length) return 1;
return 0;
}
/** @typedef {[MappingValue, string, boolean, boolean, string] | null} MatchTuple */
/**
* Per-field memoization of `findMatch(request, field)`. For a given field
* the result depends only on the `request` string (it does NOT depend on
* `conditionNames` — that's applied separately by `conditionalMapping`),
* so we can cache the tuple keyed by request.
*
* Typical build traffic runs the same request through the resolver
* repeatedly (same import re-resolved from different source files, module
* graph traversals that revisit a package, etc.), and every one of those
* hits walks the same key list and allocates the same tuple. Caching the
* tuple turns the second-and-onward call into a single Map lookup.
*
* Keyed on the field object via a module-level `WeakMap`, so the cache
* is freed automatically when the owning description file is GC'd.
* @type {WeakMap<RecordMapping, Map<string, MatchTuple>>}
*/
const _findMatchCache = new WeakMap();
/**
* @param {string} request request
* @param {ExportsField | ImportsField} field exports or import field
* @returns {MatchTuple} match result (uncached)
*/
function computeFindMatch(request, field) {
const requestLen = request.length;
const requestEndsWithSlash =
requestLen > 0 && request.charCodeAt(requestLen - 1) === slashCode;
const requestHasStar = request.includes("*");
if (
!requestHasStar &&
!requestEndsWithSlash &&
Object.prototype.hasOwnProperty.call(field, request)
) {
const target = /** @type {{ [k: string]: MappingValue }} */ (field)[
request
];
return [target, "", false, false, request];
}
/** @type {string} */
let bestMatch = "";
/** @type {FieldKeyInfo | null} */
let bestMatchInfo = null;
/** @type {string | undefined} */
let bestMatchSubpath;
const infos = getFieldKeyInfos(field);
for (let i = 0; i < infos.length; i++) {
const info = infos[i];
const { key, patternIndex } = info;
if (patternIndex !== -1) {
if (
!info.isValidPattern ||
!request.startsWith(info.wildcardPrefix) ||
requestLen < key.length ||
!request.endsWith(info.wildcardSuffix) ||
patternKeyCompare(bestMatch, key) !== 1
) {
continue;
}
bestMatch = key;
bestMatchInfo = info;
bestMatchSubpath = request.slice(
patternIndex,
requestLen - info.wildcardSuffix.length,
);
} else if (
info.isLegacySubpath &&
request.startsWith(key) &&
patternKeyCompare(bestMatch, key) === 1
) {
bestMatch = key;
bestMatchInfo = info;
bestMatchSubpath = request.slice(key.length);
}
}
if (bestMatch === "") return null;
const target =
/** @type {{ [k: string]: MappingValue }} */
(field)[bestMatch];
return [
target,
/** @type {string} */ (bestMatchSubpath),
/** @type {FieldKeyInfo} */ (bestMatchInfo).isSubpathMapping,
/** @type {FieldKeyInfo} */ (bestMatchInfo).isPattern,
bestMatch,
];
}
/**
* Trying to match request to field
* @param {string} request request
* @param {ExportsField | ImportsField} field exports or import field
* @returns {MatchTuple} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
*/
function findMatch(request, field) {
const fieldKey = /** @type {RecordMapping} */ (field);
let perRequest = _findMatchCache.get(fieldKey);
if (perRequest === undefined) {
perRequest = new Map();
_findMatchCache.set(fieldKey, perRequest);
} else {
// `computeFindMatch` only ever returns `MatchTuple | null` — never
// `undefined` — and `Map.set(k, null)` then `Map.get(k)` returns
// `null`, not `undefined`. So `get(...) === undefined` already
// unambiguously means "not cached yet"; one Map lookup is enough,
// no follow-up `has` needed to disambiguate "cached null".
const cached = perRequest.get(request);
if (cached !== undefined) return cached;
}
const result = computeFindMatch(request, field);
perRequest.set(request, result);
return result;
}
/**
* Sentinel stored in the conditional-mapping cache for inputs whose walk
* returns `null` ("no condition matched"). Using a non-null marker lets the
* cache-hit path be a single `WeakMap.get()` — we distinguish
* "cached null" from "not cached yet" without a second `has` call.
*/
const NULL_RESULT = Symbol("NULL_RESULT");
/**
* Memoization of `conditionalMapping(mapping, conditionNames)`. The result
* depends only on the mapping object (immutable — owned by a parsed
* `package.json`) and the `conditionNames` Set (owned by the resolver's
* options and stable for its lifetime), so it is safe to cache per (mapping,
* conditionNames) pair.
*
* A conditional `exports` entry that appears inside a `directMapping` array
* (the common `"browser": [...fallback list...]` shape, plus nested
* conditions) gets walked on every resolve that traverses the parent entry.
* Without this cache each of those walks re-reads `Object.keys` on the
* mapping and re-visits every condition until one matches, even though the
* inputs are identical.
*
* Outer key is the conditional mapping itself; inner key is the condition
* Set. Both are object references, so WeakMap-of-WeakMap lets both levels
* be collected automatically when the description file or resolver go away.
* @type {WeakMap<ConditionalMapping, WeakMap<Set<string>, DirectMapping | typeof NULL_RESULT>>}
*/
const _conditionalMappingCache = new WeakMap();
/**
* @param {ConditionalMapping} conditionalMapping_ conditional mapping
* @param {Set<string>} conditionNames condition names
* @returns {DirectMapping | null} direct mapping if found (uncached)
*/
function computeConditionalMapping(conditionalMapping_, conditionNames) {
/** @type {[ConditionalMapping, string[], number][]} */
const lookup = [[conditionalMapping_, cachedKeys(conditionalMapping_), 0]];
loop: while (lookup.length > 0) {
const top = lookup[lookup.length - 1];
const [mapping, conditions, j] = top;
for (let i = j; i < conditions.length; i++) {
const condition = conditions[i];
if (condition === "default" || conditionNames.has(condition)) {
const innerMapping = mapping[condition];
if (
innerMapping !== null &&
typeof innerMapping === "object" &&
!Array.isArray(innerMapping)
) {
const nested = /** @type {ConditionalMapping} */ (innerMapping);
top[2] = i + 1;
lookup.push([nested, cachedKeys(nested), 0]);
continue loop;
}
return /** @type {DirectMapping} */ (innerMapping);
}
}
lookup.pop();
}
return null;
}
/**
* @param {ConditionalMapping} conditionalMapping_ conditional mapping
* @param {Set<string>} conditionNames condition names
* @returns {DirectMapping | null} direct mapping if found
*/
function conditionalMapping(conditionalMapping_, conditionNames) {
let perSet = _conditionalMappingCache.get(conditionalMapping_);
if (perSet !== undefined) {
const cached = perSet.get(conditionNames);
if (cached !== undefined) {
return cached === NULL_RESULT
? null
: /** @type {DirectMapping} */ (cached);
}
} else {
perSet = new WeakMap();
_conditionalMappingCache.set(conditionalMapping_, perSet);
}
const result = computeConditionalMapping(conditionalMapping_, conditionNames);
perSet.set(conditionNames, result === null ? NULL_RESULT : result);
return result;
}
/**
* @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
* @param {boolean} isPattern true, if mapping is a pattern (contains "*")
* @param {boolean} isSubpathMapping true, for subpath mappings
* @param {string} mappingTarget direct export
* @param {(d: string, f: boolean) => void} assert asserting direct value
* @returns {string} mapping result
*/
function targetMapping(
remainingRequest,
isPattern,
isSubpathMapping,
mappingTarget,
assert,
) {
if (remainingRequest === undefined) {
assert(mappingTarget, false);
return mappingTarget;
}
if (isSubpathMapping) {
assert(mappingTarget, true);
return mappingTarget + remainingRequest;
}
assert(mappingTarget, false);
let result = mappingTarget;
if (isPattern) {
const escapedRemainder = remainingRequest.includes("$")
? remainingRequest.replace(DOLLAR_ESCAPE_RE, "$$")
: remainingRequest;
result = result.replace(patternRegEx, escapedRemainder);
}
return result;
}
/**
* @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
* @param {boolean} isPattern true, if mapping is a pattern (contains "*")
* @param {boolean} isSubpathMapping true, for subpath mappings
* @param {DirectMapping | null} mappingTarget direct export
* @param {Set<string>} conditionNames condition names
* @param {(d: string, f: boolean) => void} assert asserting direct value
* @returns {string[]} mapping result
*/
function directMapping(
remainingRequest,
isPattern,
isSubpathMapping,
mappingTarget,
conditionNames,
assert,
) {
if (mappingTarget === null) return [];
if (typeof mappingTarget === "string") {
return [
targetMapping(
remainingRequest,
isPattern,
isSubpathMapping,
mappingTarget,
assert,
),
];
}
/** @type {string[]} */
const targets = [];
for (let i = 0, len = mappingTarget.length; i < len; i++) {
const exp = mappingTarget[i];
if (typeof exp === "string") {
targets.push(
targetMapping(
remainingRequest,
isPattern,
isSubpathMapping,
exp,
assert,
),
);
continue;
}
const mapping = conditionalMapping(exp, conditionNames);
if (!mapping) continue;
const innerExports = directMapping(
remainingRequest,
isPattern,
isSubpathMapping,
mapping,
conditionNames,
assert,
);
for (let j = 0, innerLen = innerExports.length; j < innerLen; j++) {
targets.push(innerExports[j]);
}
}
return targets;
}
/** @type {[string[], null]} */
const EMPTY_NO_MATCH = /** @type {[string[], null]} */ ([[], null]);
/**
* @param {ExportsField | ImportsField} field root
* @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./`
* @param {(s: string) => string} assertRequest assertRequest
* @param {(s: string, f: boolean) => void} assertTarget assertTarget
* @returns {FieldProcessor} field processor
*/
function createFieldProcessor(
field,
normalizeRequest,
assertRequest,
assertTarget,
) {
return function fieldProcessor(request, conditionNames) {
const match = findMatch(normalizeRequest(assertRequest(request)), field);
if (match === null) return EMPTY_NO_MATCH;
const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
match;
/** @type {DirectMapping | null} */
let direct;
if (
mapping !== null &&
typeof mapping === "object" &&
!Array.isArray(mapping)
) {
direct = conditionalMapping(
/** @type {ConditionalMapping} */ (mapping),
conditionNames,
);
if (direct === null) return EMPTY_NO_MATCH;
} else {
direct = /** @type {DirectMapping} */ (mapping);
}
return [
directMapping(
remainingRequest,
isPattern,
isSubpathMapping,
direct,
conditionNames,
assertTarget,
),
usedField,
];
};
}
/**
* @param {string} request request
* @returns {string} updated request
*/
function assertExportsFieldRequest(request) {
if (request.charCodeAt(0) !== dotCode) {
throw new Error('Request should be relative path and start with "."');
}
if (request.length === 1) return "";
if (request.charCodeAt(1) !== slashCode) {
throw new Error('Request should be relative path and start with "./"');
}
if (request.charCodeAt(request.length - 1) === slashCode) {
throw new Error("Only requesting file allowed");
}
return request.slice(2);
}
/**
* @param {ExportsField} field exports field
* @returns {ExportsField} normalized exports field
*/
function buildExportsField(field) {
// handle syntax sugar, if exports field is direct mapping for "."
if (typeof field === "string" || Array.isArray(field)) {
return { ".": field };
}
const keys = Object.keys(field);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key.charCodeAt(0) !== dotCode) {
// handle syntax sugar, if exports field is conditional mapping for "."
if (i === 0) {
while (i < keys.length) {
const charCode = keys[i].charCodeAt(0);
if (charCode === dotCode || charCode === slashCode) {
throw new Error(
`Exports field key should be relative path and start with "." (key: ${JSON.stringify(
key,
)})`,
);
}
i++;
}
return { ".": field };
}
throw new Error(
`Exports field key should be relative path and start with "." (key: ${JSON.stringify(
key,
)})`,
);
}
if (key.length === 1) {
continue;
}
if (key.charCodeAt(1) !== slashCode) {
throw new Error(
`Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
key,
)})`,
);
}
}
return field;
}
/**
* @param {string} exp export target
* @param {boolean} expectFolder is folder expected
*/
function assertExportTarget(exp, expectFolder) {
const parsedIdentifier = parseIdentifier(exp);
if (!parsedIdentifier) {
return;
}
const [relativePath] = parsedIdentifier;
const isFolder =
relativePath.charCodeAt(relativePath.length - 1) === slashCode;
if (isFolder !== expectFolder) {
throw new Error(
expectFolder
? `Expecting folder to folder mapping. ${JSON.stringify(
exp,
)} should end with "/"`
: `Expecting file to file mapping. ${JSON.stringify(
exp,
)} should not end with "/"`,
);
}
}
/**
* @param {ExportsField} exportsField the exports field
* @returns {FieldProcessor} process callback
*/
module.exports.processExportsField = function processExportsField(
exportsField,
) {
return createFieldProcessor(
buildExportsField(exportsField),
(request) => (request.length === 0 ? "." : `./${request}`),
assertExportsFieldRequest,
assertExportTarget,
);
};
/**
* @param {string} request request
* @returns {string} updated request
*/
function assertImportsFieldRequest(request) {
if (request.charCodeAt(0) !== hashCode) {
throw new Error('Request should start with "#"');
}
if (request.length === 1) {
throw new Error("Request should have at least 2 characters");
}
// Note: #/ patterns are now allowed per Node.js PR #60864
// https://github.com/nodejs/node/pull/60864
if (request.charCodeAt(request.length - 1) === slashCode) {
throw new Error("Only requesting file allowed");
}
return request.slice(1);
}
/**
* @param {string} imp import target
* @param {boolean} expectFolder is folder expected
*/
function assertImportTarget(imp, expectFolder) {
const parsedIdentifier = parseIdentifier(imp);
if (!parsedIdentifier) {
return;
}
const [relativePath] = parsedIdentifier;
const isFolder =
relativePath.charCodeAt(relativePath.length - 1) === slashCode;
if (isFolder !== expectFolder) {
throw new Error(
expectFolder
? `Expecting folder to folder mapping. ${JSON.stringify(
imp,
)} should end with "/"`
: `Expecting file to file mapping. ${JSON.stringify(
imp,
)} should not end with "/"`,
);
}
}
/**
* @param {ImportsField} importsField the exports field
* @returns {FieldProcessor} process callback
*/
module.exports.processImportsField = function processImportsField(
importsField,
) {
return createFieldProcessor(
importsField,
(request) => `#${request}`,
assertImportsFieldRequest,
assertImportTarget,
);
};

View File

@@ -0,0 +1,93 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
// A runtime-agnostic port of Node's `url.fileURLToPath`, built on the global
// `URL` (available in Node, browsers, Deno and Bun) so the resolver does not
// depend on the Node `url` builtin. The platform branch can be forced via the
// `windows` option; otherwise it follows the host platform like Node does.
const CHAR_LOWERCASE_A = 97;
const CHAR_LOWERCASE_Z = 122;
const isWindows =
typeof process !== "undefined" && process.platform === "win32";
const forwardSlashRegEx = /\//g;
/**
* @param {URL} url file: URL instance
* @returns {string} Windows filesystem path
*/
function getPathFromURLWin32(url) {
const { hostname } = url;
let { pathname } = url;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === "%") {
const third = (pathname.codePointAt(n + 2) || 0) | 0x20;
if (
(pathname[n + 1] === "2" && third === 102) || // 2f 2F (/)
(pathname[n + 1] === "5" && third === 99) // 5c 5C (\)
) {
throw new TypeError(
"File URL path must not include encoded \\ or / characters",
);
}
}
}
pathname = pathname.replace(forwardSlashRegEx, "\\");
pathname = decodeURIComponent(pathname);
if (hostname !== "") {
// UNC path: `\\host\share\…`. Node runs the host through
// `domainToUnicode`; we keep the (already punycode) hostname as-is,
// which is identical for ASCII hosts and only differs for rare IDN UNC.
return `\\\\${hostname}${pathname}`;
}
const letter = (pathname.codePointAt(1) || 0) | 0x20;
const sep = pathname.charAt(2);
if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || sep !== ":") {
throw new TypeError("File URL path must be absolute");
}
return pathname.slice(1);
}
/**
* @param {URL} url file: URL instance
* @returns {string} POSIX filesystem path
*/
function getPathFromURLPosix(url) {
if (url.hostname !== "") {
throw new TypeError('File URL host must be "localhost" or empty');
}
const { pathname } = url;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === "%") {
const third = (pathname.codePointAt(n + 2) || 0) | 0x20;
if (pathname[n + 1] === "2" && third === 102) {
throw new TypeError(
"File URL path must not include encoded / characters",
);
}
}
}
return decodeURIComponent(pathname);
}
/**
* @param {string | URL} path a `file:` URL string or `URL` instance
* @param {{ windows?: boolean }=} options force the platform branch
* @returns {string} the filesystem path
*/
function fileURLToPath(path, options) {
const url = typeof path === "string" ? new URL(path) : path;
if (url.protocol !== "file:") {
throw new TypeError("The URL must be of scheme file");
}
const windows =
options && options.windows !== undefined ? options.windows : isWindows;
return windows ? getPathFromURLWin32(url) : getPathFromURLPosix(url);
}
module.exports = fileURLToPath;

106
web/node_modules/enhanced-resolve/lib/util/fs.js generated vendored Normal file
View File

@@ -0,0 +1,106 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Natsu @xiaoxiaojx
*/
"use strict";
const memoize = require("./memoize");
const stripJsonComments = require("./strip-json-comments");
/** @typedef {import("../Resolver").FileSystem} FileSystem */
/** @typedef {import("../Resolver").JsonObject} JsonObject */
/**
* @typedef {object} ReadJsonOptions
* @property {boolean=} stripComments Whether to strip JSONC comments
*/
/** @type {WeakMap<Buffer | Uint8Array, JsonObject>} */
const _stripCommentsCache = new WeakMap();
// Only constructed for non-Buffer input: on Node the `Buffer.isBuffer` branch
// in `decodeText` handles decoding, so the global `TextDecoder` (Node 11+,
// always present in browsers/Deno/Bun) is only reached off the Buffer path.
// `ignoreBOM: true` keeps a leading BOM in the output, matching
// `Buffer.toString("utf8")` so both decode paths behave identically.
// eslint-disable-next-line n/no-unsupported-features/node-builtins
const getDecoder = memoize(() => new TextDecoder("utf-8", { ignoreBOM: true }));
/**
* Decode a file's raw contents to text without assuming a Node runtime. A
* `Buffer` (Node) uses its fast native `toString`; any other binary input
* (`Uint8Array` from a browser/Deno/Bun file system) goes through
* `TextDecoder`, and strings are returned as-is.
* @param {string | Buffer | Uint8Array} data raw file contents
* @returns {string} decoded text
*/
const decodeText = (data) => {
if (typeof data === "string") return data;
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
return data.toString("utf8");
}
return getDecoder().decode(data);
};
/**
* Read and parse JSON file (supports JSONC with comments).
* Callback-based so a synchronous `fileSystem` stays synchronous all the
* way through — Promise wrapping would defer resolution by a Promise tick
* and break `resolveSync` when `tsconfig` is used together with
* `useSyncFileSystemCalls: true`.
* @param {FileSystem} fileSystem the file system
* @param {string} jsonFilePath absolute path to JSON file
* @param {ReadJsonOptions} options Options
* @param {(err: NodeJS.ErrnoException | Error | null, content?: JsonObject) => void} callback callback
* @returns {void}
*/
function readJson(fileSystem, jsonFilePath, options, callback) {
const { stripComments = false } = options;
const { readJson: fsReadJson } = fileSystem;
if (fsReadJson && !stripComments) {
fsReadJson(jsonFilePath, (err, content) => {
if (err) return callback(err);
callback(null, /** @type {JsonObject} */ (content));
});
return;
}
fileSystem.readFile(jsonFilePath, (err, data) => {
if (err) return callback(err);
const buf = /** @type {Buffer | Uint8Array | string} */ (data);
// The strip-comments cache is keyed by the file-contents object; a file
// system may hand back a plain string, which cannot be a WeakMap key, so
// only cache when the contents are an object.
const cacheable = stripComments && typeof buf === "object";
if (cacheable) {
const cached = _stripCommentsCache.get(buf);
if (cached !== undefined) return callback(null, cached);
}
let result;
try {
const jsonText = decodeText(buf);
const jsonWithoutComments = stripComments
? stripJsonComments(jsonText, {
trailingCommas: true,
whitespace: true,
})
: jsonText;
result = JSON.parse(jsonWithoutComments);
} catch (parseErr) {
return callback(/** @type {Error} */ (parseErr));
}
if (cacheable) {
_stripCommentsCache.set(buf, result);
}
callback(null, result);
});
}
module.exports.decodeText = decodeText;
module.exports.readJson = readJson;

View File

@@ -0,0 +1,40 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
// Browser stub for the default `graceful-fs` file system. A browser has no
// ambient file system, so the convenience entry's default resolver has nothing
// to read — pass your own `fileSystem` to `create()` /
// `ResolverFactory.createResolver()` instead. Each method throws a clear error
// if the default is actually used, while keeping the package able to bundle for
// the browser (no Node `fs` dependency is pulled in).
const message =
"enhanced-resolve: no default file system is available in the browser. " +
"Pass a `fileSystem` to `create()` or `ResolverFactory.createResolver()`.";
/**
* @returns {never} always throws
*/
const unavailable = () => {
throw new Error(message);
};
// The same set of methods `CachedInputFileSystem` reads from the file system
// (graceful-fs has no `readJson`/`readJsonSync`, so those stay undefined).
module.exports = {
lstat: unavailable,
lstatSync: unavailable,
readFile: unavailable,
readFileSync: unavailable,
readdir: unavailable,
readdirSync: unavailable,
readlink: unavailable,
readlinkSync: unavailable,
realpath: unavailable,
realpathSync: unavailable,
stat: unavailable,
statSync: unavailable,
};

View File

@@ -0,0 +1,101 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const { fileURLToPath } = require("url");
const PATH_QUERY_FRAGMENT_REGEXP =
/^(#?(?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;
const ZERO_ESCAPE_REGEXP = /\0(.)/g;
const FILE_REG_EXP = /file:/i;
/**
* Index past a DOS device path prefix (`\\?\…` or `\\.\…`), or 0. Kept
* out of `parseIdentifier` on purpose: inlining it back bloats the caller
* beyond the size where V8's interpreter and JIT both handle it well
* (the cause of the description-files-multi CodSpeed regression).
* @param {string} identifier identifier known to start with `\`
* @returns {number} 4 if identifier starts with a DOS device prefix, else 0
*/
function dosPrefixEnd(identifier) {
if (
identifier.length >= 4 &&
identifier.charCodeAt(1) === 92 &&
identifier.charCodeAt(3) === 92
) {
const c2 = identifier.charCodeAt(2);
if (c2 === 63 || c2 === 46) return 4;
}
return 0;
}
/**
* @param {string} identifier identifier
* @returns {[string, string, string] | null} parsed identifier
*/
function parseIdentifier(identifier) {
if (!identifier) {
return null;
}
if (FILE_REG_EXP.test(identifier)) {
identifier = fileURLToPath(identifier);
}
const firstEscape = identifier.indexOf("\0");
// Handle `\0`
if (firstEscape !== -1) {
const match = PATH_QUERY_FRAGMENT_REGEXP.exec(identifier);
if (!match) return null;
return [
match[1].replace(ZERO_ESCAPE_REGEXP, "$1"),
match[2] ? match[2].replace(ZERO_ESCAPE_REGEXP, "$1") : "",
match[3] || "",
];
}
// Fast path for inputs that don't use \0 escaping. DOS device paths
// (`\\?\…`, `\\.\…`) embed a literal `?` / `.` that must not be read
// as a query separator; skip past the prefix when the input actually
// starts with `\`. Gate is a single char-code compare so this function
// stays inside V8's inline budget for its hot callers (resolver parse).
const scanStart =
identifier.charCodeAt(0) === 92 ? dosPrefixEnd(identifier) : 0;
const queryStart = identifier.indexOf("?", scanStart);
// Start at index 1 (or past a DOS prefix) to ignore a possible leading hash.
const fragmentStart = identifier.indexOf("#", scanStart || 1);
if (fragmentStart < 0) {
if (queryStart < 0) {
// No fragment, no query
return [identifier, "", ""];
}
// Query, no fragment
return [identifier.slice(0, queryStart), identifier.slice(queryStart), ""];
}
if (queryStart < 0 || fragmentStart < queryStart) {
// Fragment, no query
return [
identifier.slice(0, fragmentStart),
"",
identifier.slice(fragmentStart),
];
}
// Query and fragment
return [
identifier.slice(0, queryStart),
identifier.slice(queryStart, fragmentStart),
identifier.slice(fragmentStart),
];
}
module.exports.parseIdentifier = parseIdentifier;

37
web/node_modules/enhanced-resolve/lib/util/memoize.js generated vendored Normal file
View File

@@ -0,0 +1,37 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
/**
* @template T
* @typedef {() => T} FunctionReturning
*/
/**
* @template T
* @param {FunctionReturning<T>} fn memorized function
* @returns {FunctionReturning<T>} new function
*/
const memoize = (fn) => {
let cache = false;
/** @type {T | undefined} */
let result;
return () => {
if (cache) {
return /** @type {T} */ (result);
}
result = fn();
cache = true;
// Allow to clean up memory for fn
// and all dependent resources
/** @type {FunctionReturning<T> | undefined} */
(fn) = undefined;
return /** @type {T} */ (result);
};
};
module.exports = memoize;

View File

@@ -0,0 +1,8 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
module.exports = {};

View File

@@ -0,0 +1,407 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
// A browser shim for the Node `path` builtin, exposing only the subset used by
// `lib/util/path.js`: `posix.normalize`, `posix.dirname`, `win32.normalize`,
// `win32.dirname` and a (posix) `basename`. It is a faithful port of Node's
// `path` implementation and is wired in through the package `browser` field, so
// Node, Deno and Bun keep using their native `path` and only browser bundles
// use this. Inputs are always strings here, so Node's `validateString` guards
// are omitted.
const CHAR_UPPERCASE_A = 65;
const CHAR_UPPERCASE_Z = 90;
const CHAR_LOWERCASE_A = 97;
const CHAR_LOWERCASE_Z = 122;
const CHAR_DOT = 46;
const CHAR_FORWARD_SLASH = 47;
const CHAR_BACKWARD_SLASH = 92;
const CHAR_COLON = 58;
/**
* @param {number} code char code
* @returns {boolean} true for `/`
*/
const isPosixPathSeparator = (code) => code === CHAR_FORWARD_SLASH;
/**
* @param {number} code char code
* @returns {boolean} true for `/` or `\`
*/
const isPathSeparator = (code) =>
code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
/**
* @param {number} code char code
* @returns {boolean} true for an ASCII letter (a windows device root)
*/
const isWindowsDeviceRoot = (code) =>
(code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
(code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z);
/**
* Resolve `.` and `..` segments in a path. Ported from Node's internal
* `normalizeString`.
* @param {string} path path
* @param {boolean} allowAboveRoot whether leading `..` may be kept
* @param {string} separator path separator to emit
* @param {(code: number) => boolean} isSeparator separator predicate
* @returns {string} normalized path string (without root)
*/
function normalizeString(path, allowAboveRoot, separator, isSeparator) {
let res = "";
let lastSegmentLength = 0;
let lastSlash = -1;
let dots = 0;
let code = 0;
for (let i = 0; i <= path.length; ++i) {
if (i < path.length) {
code = path.charCodeAt(i);
} else if (isSeparator(code)) {
break;
} else {
code = CHAR_FORWARD_SLASH;
}
if (isSeparator(code)) {
if (lastSlash === i - 1 || dots === 1) {
// NOOP
} else if (dots === 2) {
if (
res.length < 2 ||
lastSegmentLength !== 2 ||
res.charCodeAt(res.length - 1) !== CHAR_DOT ||
res.charCodeAt(res.length - 2) !== CHAR_DOT
) {
if (res.length > 2) {
const lastSlashIndex = res.lastIndexOf(separator);
if (lastSlashIndex === -1) {
res = "";
lastSegmentLength = 0;
} else {
res = res.slice(0, lastSlashIndex);
lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
}
lastSlash = i;
dots = 0;
continue;
} else if (res.length !== 0) {
res = "";
lastSegmentLength = 0;
lastSlash = i;
dots = 0;
continue;
}
}
if (allowAboveRoot) {
res += res.length > 0 ? `${separator}..` : "..";
lastSegmentLength = 2;
}
} else {
if (res.length > 0) {
res += `${separator}${path.slice(lastSlash + 1, i)}`;
} else {
res = path.slice(lastSlash + 1, i);
}
lastSegmentLength = i - lastSlash - 1;
}
lastSlash = i;
dots = 0;
} else if (code === CHAR_DOT && dots !== -1) {
++dots;
} else {
dots = -1;
}
}
return res;
}
/**
* @param {string} path path
* @returns {string} normalized posix path
*/
function posixNormalize(path) {
if (path.length === 0) return ".";
const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
const trailingSeparator =
path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
path = normalizeString(path, !isAbsolute, "/", isPosixPathSeparator);
if (path.length === 0) {
if (isAbsolute) return "/";
return trailingSeparator ? "./" : ".";
}
if (trailingSeparator) path += "/";
return isAbsolute ? `/${path}` : path;
}
/**
* @param {string} path path
* @returns {string} posix dirname
*/
function posixDirname(path) {
if (path.length === 0) return ".";
const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
let end = -1;
let matchedSlash = true;
for (let i = path.length - 1; i >= 1; --i) {
if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
if (!matchedSlash) {
end = i;
break;
}
} else {
matchedSlash = false;
}
}
if (end === -1) return hasRoot ? "/" : ".";
if (hasRoot && end === 1) return "//";
return path.slice(0, end);
}
/**
* Normalizes drive paths (`C:\…`), UNC paths (`\\server\share\…`), DOS device
* paths (`\\.\…`, `\\?\…`) and relative/normal segments like Node's
* `path.win32.normalize`, including the CVE-2024-36139 colon-segment guard.
*
* Scope note: reserved Windows device names (`CON`, `COM1`, `LPT1`, …) are not
* special-cased, so e.g. `\\.\COM1:` differs from Node. Such names cannot occur
* in a browser (the only place this shim is used) and the resolver never routes
* them to `win32.normalize`, so this does not affect resolution.
* @param {string} path path
* @returns {string} normalized win32 path
*/
function win32Normalize(path) {
const len = path.length;
if (len === 0) return ".";
let rootEnd = 0;
/** @type {string | undefined} */
let device;
let isAbsolute = false;
const code = path.charCodeAt(0);
if (len === 1) {
return isPosixPathSeparator(code) ? "\\" : path;
}
if (isPathSeparator(code)) {
// Possible UNC root; an initial separator means an absolute path.
isAbsolute = true;
if (isPathSeparator(path.charCodeAt(1))) {
// Matched double path separator at beginning
let j = 2;
let last = j;
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
if (j < len && j !== last) {
const firstPart = path.slice(last, j);
last = j;
while (j < len && isPathSeparator(path.charCodeAt(j))) j++;
if (j < len && j !== last) {
last = j;
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
if (j === len || j !== last) {
if (firstPart === "." || firstPart === "?") {
// Device root, e.g. `\\.\pipe\…` or `\\?\C:\…`
device = `\\\\${firstPart}`;
rootEnd = 4;
} else if (j === len) {
// Matched a UNC root only
return `\\\\${firstPart}\\${path.slice(last)}\\`;
} else {
// Matched a UNC root with leftovers
device = `\\\\${firstPart}\\${path.slice(last, j)}`;
rootEnd = j;
}
}
}
}
} else {
rootEnd = 1;
}
} else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) {
// Possible device root
device = path.slice(0, 2);
rootEnd = 2;
if (len > 2 && isPathSeparator(path.charCodeAt(2))) {
isAbsolute = true;
rootEnd = 3;
}
}
let tail =
rootEnd < len
? normalizeString(path.slice(rootEnd), !isAbsolute, "\\", isPathSeparator)
: "";
if (tail.length === 0 && !isAbsolute) tail = ".";
if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) {
tail += "\\";
}
if (!isAbsolute && device === undefined && path.includes(":")) {
// A relative path that wasn't resolved to a device must not turn into
// something Windows could read as an absolute/drive path (CVE-2024-36139).
if (
tail.length >= 2 &&
isWindowsDeviceRoot(tail.charCodeAt(0)) &&
tail.charCodeAt(1) === CHAR_COLON
) {
return `.\\${tail}`;
}
let index = path.indexOf(":");
do {
if (index === len - 1 || isPathSeparator(path.charCodeAt(index + 1))) {
return `.\\${tail}`;
}
} while ((index = path.indexOf(":", index + 1)) !== -1);
}
if (device === undefined) {
return isAbsolute ? `\\${tail}` : tail;
}
return isAbsolute ? `${device}\\${tail}` : `${device}${tail}`;
}
/**
* @param {string} path path
* @returns {string} win32 dirname
*/
function win32Dirname(path) {
const len = path.length;
if (len === 0) return ".";
let rootEnd = -1;
let offset = 0;
const code = path.charCodeAt(0);
if (len === 1) {
return isPathSeparator(code) ? path : ".";
}
if (isPathSeparator(code)) {
// Possible UNC root
rootEnd = offset = 1;
if (isPathSeparator(path.charCodeAt(1))) {
let j = 2;
let last = j;
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
if (j < len && j !== last) {
last = j;
while (j < len && isPathSeparator(path.charCodeAt(j))) j++;
if (j < len && j !== last) {
last = j;
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
if (j === len) {
// Matched a UNC root only
return path;
}
if (j !== last) {
// Matched a UNC root with leftovers
rootEnd = offset = j + 1;
}
}
}
}
} else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) {
rootEnd = len > 2 && isPathSeparator(path.charCodeAt(2)) ? 3 : 2;
offset = rootEnd;
}
let end = -1;
let matchedSlash = true;
for (let i = len - 1; i >= offset; --i) {
if (isPathSeparator(path.charCodeAt(i))) {
if (!matchedSlash) {
end = i;
break;
}
} else {
matchedSlash = false;
}
}
if (end === -1) {
if (rootEnd === -1) return ".";
end = rootEnd;
}
return path.slice(0, end);
}
/**
* Posix `basename` — the browser is treated as a posix platform, matching how
* Node picks the posix variant for `path.basename` on non-Windows systems.
* @param {string} path path
* @param {string=} suffix optional suffix to strip
* @returns {string} basename
*/
function basename(path, suffix) {
let start = 0;
let end = -1;
let matchedSlash = true;
if (
suffix !== undefined &&
suffix.length > 0 &&
suffix.length <= path.length
) {
if (suffix === path) return "";
let extIdx = suffix.length - 1;
let firstNonSlashEnd = -1;
for (let i = path.length - 1; i >= 0; --i) {
const code = path.charCodeAt(i);
if (code === CHAR_FORWARD_SLASH) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else {
if (firstNonSlashEnd === -1) {
matchedSlash = false;
firstNonSlashEnd = i + 1;
}
if (extIdx >= 0) {
if (code === suffix.charCodeAt(extIdx)) {
if (--extIdx === -1) {
end = i;
}
} else {
extIdx = -1;
end = firstNonSlashEnd;
}
}
}
}
if (start === end) {
end = firstNonSlashEnd;
} else if (end === -1) {
end = path.length;
}
return path.slice(start, end);
}
for (let i = path.length - 1; i >= 0; --i) {
if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else if (end === -1) {
matchedSlash = false;
end = i + 1;
}
}
if (end === -1) return "";
return path.slice(start, end);
}
module.exports = {
basename,
posix: { normalize: posixNormalize, dirname: posixDirname },
win32: { normalize: win32Normalize, dirname: win32Dirname },
};

450
web/node_modules/enhanced-resolve/lib/util/path.js generated vendored Normal file
View File

@@ -0,0 +1,450 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const path = require("path");
const { fileURLToPath } = require("url");
const CHAR_HASH = "#".charCodeAt(0);
const CHAR_SLASH = "/".charCodeAt(0);
const CHAR_BACKSLASH = "\\".charCodeAt(0);
const CHAR_A = "A".charCodeAt(0);
const CHAR_Z = "Z".charCodeAt(0);
const CHAR_LOWER_A = "a".charCodeAt(0);
const CHAR_LOWER_Z = "z".charCodeAt(0);
const CHAR_DOT = ".".charCodeAt(0);
const CHAR_COLON = ":".charCodeAt(0);
const posixNormalize = path.posix.normalize;
const winNormalize = path.win32.normalize;
/**
* @enum {number}
*/
const PathType = Object.freeze({
Empty: 0,
Normal: 1,
Relative: 2,
AbsoluteWin: 3,
AbsolutePosix: 4,
Internal: 5,
});
const deprecatedInvalidSegmentRegEx =
/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
const invalidSegmentRegEx =
/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i;
/**
* @param {string} maybePath a path
* @returns {PathType} type of path
*/
const getType = (maybePath) => {
switch (maybePath.length) {
case 0:
return PathType.Empty;
case 1: {
const c0 = maybePath.charCodeAt(0);
switch (c0) {
case CHAR_DOT:
return PathType.Relative;
case CHAR_SLASH:
return PathType.AbsolutePosix;
case CHAR_HASH:
return PathType.Internal;
}
return PathType.Normal;
}
case 2: {
const c0 = maybePath.charCodeAt(0);
switch (c0) {
case CHAR_DOT: {
const c1 = maybePath.charCodeAt(1);
switch (c1) {
case CHAR_DOT:
case CHAR_SLASH:
return PathType.Relative;
}
return PathType.Normal;
}
case CHAR_SLASH:
return PathType.AbsolutePosix;
case CHAR_HASH:
return PathType.Internal;
}
const c1 = maybePath.charCodeAt(1);
if (
c1 === CHAR_COLON &&
((c0 >= CHAR_A && c0 <= CHAR_Z) ||
(c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z))
) {
return PathType.AbsoluteWin;
}
if (c0 === CHAR_BACKSLASH && c1 === CHAR_BACKSLASH) {
return PathType.AbsoluteWin;
}
return PathType.Normal;
}
}
const c0 = maybePath.charCodeAt(0);
switch (c0) {
case CHAR_DOT: {
const c1 = maybePath.charCodeAt(1);
switch (c1) {
case CHAR_SLASH:
return PathType.Relative;
case CHAR_DOT: {
const c2 = maybePath.charCodeAt(2);
if (c2 === CHAR_SLASH) return PathType.Relative;
return PathType.Normal;
}
}
return PathType.Normal;
}
case CHAR_SLASH:
return PathType.AbsolutePosix;
case CHAR_HASH:
return PathType.Internal;
}
const c1 = maybePath.charCodeAt(1);
if (c1 === CHAR_COLON) {
const c2 = maybePath.charCodeAt(2);
if (
(c2 === CHAR_BACKSLASH || c2 === CHAR_SLASH) &&
((c0 >= CHAR_A && c0 <= CHAR_Z) ||
(c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z))
) {
return PathType.AbsoluteWin;
}
}
// Two leading backslashes root a UNC share (`\\server\share`) or a DOS
// device path (`\\?\…`, `\\.\…`); `path.win32` reads either as absolute,
// so both belong on `path.win32` and not on posix.
if (c0 === CHAR_BACKSLASH && c1 === CHAR_BACKSLASH) {
return PathType.AbsoluteWin;
}
return PathType.Normal;
};
/**
* @param {string} maybePath a path
* @returns {string} the normalized path
*/
const normalize = (maybePath) => {
switch (getType(maybePath)) {
case PathType.Empty:
return maybePath;
case PathType.AbsoluteWin:
return winNormalize(maybePath);
case PathType.Relative: {
const r = posixNormalize(maybePath);
return getType(r) === PathType.Relative ? r : `./${r}`;
}
}
return posixNormalize(maybePath);
};
/**
* @param {string} rootPath the root path
* @param {string | undefined} request the request path
* @returns {string} the joined path
*/
const join = (rootPath, request) => {
if (!request) return normalize(rootPath);
const requestType = getType(request);
switch (requestType) {
case PathType.AbsolutePosix:
return posixNormalize(request);
case PathType.AbsoluteWin:
return winNormalize(request);
}
switch (getType(rootPath)) {
case PathType.Normal:
case PathType.Relative:
case PathType.AbsolutePosix:
return posixNormalize(`${rootPath}/${request}`);
case PathType.AbsoluteWin:
return winNormalize(`${rootPath}\\${request}`);
}
switch (requestType) {
case PathType.Empty:
return rootPath;
case PathType.Relative: {
const r = posixNormalize(rootPath);
return getType(r) === PathType.Relative ? r : `./${r}`;
}
}
return posixNormalize(rootPath);
};
/**
* @param {string} maybePath a path
* @returns {string} the directory name
*/
const dirname = (maybePath) => {
switch (getType(maybePath)) {
case PathType.AbsoluteWin:
return path.win32.dirname(maybePath);
}
return path.posix.dirname(maybePath);
};
/** @typedef {{ fn: (rootPath: string, request: string) => string, cache: Map<string, Map<string, string | undefined>> }} CachedJoin */
/**
* @returns {CachedJoin} cached join
*/
const createCachedJoin = () => {
/** @type {CachedJoin["cache"]} */
const cache = new Map();
/** @type {CachedJoin["fn"]} */
const fn = (rootPath, request) => {
/** @type {string | undefined} */
let cacheEntry;
let inner = cache.get(rootPath);
if (inner === undefined) {
cache.set(rootPath, (inner = new Map()));
} else {
cacheEntry = inner.get(request);
if (cacheEntry !== undefined) return cacheEntry;
}
cacheEntry = join(rootPath, request);
inner.set(request, cacheEntry);
return cacheEntry;
};
return { fn, cache };
};
/** @typedef {{ fn: (maybePath: string) => string, cache: Map<string, string> }} CachedDirname */
/**
* @returns {CachedDirname} cached dirname
*/
const createCachedDirname = () => {
/** @type {CachedDirname["cache"]} */
const cache = new Map();
/** @type {CachedDirname["fn"]} */
const fn = (maybePath) => {
const cacheEntry = cache.get(maybePath);
if (cacheEntry !== undefined) return cacheEntry;
const result = dirname(maybePath);
cache.set(maybePath, result);
return result;
};
return { fn, cache };
};
/** @typedef {{ fn: (maybePath: string, suffix?: string) => string, cache: Map<string, Map<string | undefined, string | undefined>> }} CachedBasename */
/**
* @returns {CachedBasename} cached basename
*/
const createCachedBasename = () => {
/** @type {CachedBasename["cache"]} */
const cache = new Map();
/** @type {CachedBasename["fn"]} */
const fn = (maybePath, suffix) => {
/** @type {string | undefined} */
let cacheEntry;
let inner = cache.get(maybePath);
if (inner === undefined) {
cache.set(maybePath, (inner = new Map()));
} else {
cacheEntry = inner.get(suffix);
if (cacheEntry !== undefined) return cacheEntry;
}
cacheEntry = path.basename(maybePath, suffix);
inner.set(suffix, cacheEntry);
return cacheEntry;
};
return { fn, cache };
};
/**
* Whether `request` is a relative request — i.e. matches `^\.\.?(?:\/|$)`.
*
* This is called on every `doResolve` via `UnsafeCachePlugin` and
* `getInnerRequest`, so the char-code form is meaningfully faster than the
* equivalent regex test: no regex state machine, no string object churn.
* @param {string} request request string
* @returns {boolean} true if request is relative
*/
const isRelativeRequest = (request) => {
const len = request.length;
if (len === 0 || request.charCodeAt(0) !== CHAR_DOT) return false;
if (len === 1) return true; // "."
const c1 = request.charCodeAt(1);
if (c1 === CHAR_SLASH) return true; // "./..."
if (c1 !== CHAR_DOT) return false; // ".x..."
if (len === 2) return true; // ".."
return request.charCodeAt(2) === CHAR_SLASH; // "../..."
};
/**
* Whether this is a Windows path, in which `/` and `\` are interchangeable and
* paths compare case-insensitively, as opposed to a posix path, in which `\` is
* an ordinary filename character. Decided by the root — a drive letter or a
* leading `\` — which is where `path.win32` and `path.posix` disagree about
* `parse(maybePath).root`, and never by the host platform, since Windows paths
* are resolved on posix hosts and in browsers too. A path starting with `//`
* stays posix: `path.win32` reads it as a UNC root, but here it cannot be told
* apart from a posix path, where `\` has to keep being a filename character.
* @param {string} maybePath a path
* @returns {boolean} true, when the path is a Windows path
*/
const isWindowsPath = (maybePath) => {
const c0 = maybePath.charCodeAt(0);
if (c0 === CHAR_BACKSLASH) return true;
if (maybePath.charCodeAt(1) !== CHAR_COLON) return false;
return (
(c0 >= CHAR_A && c0 <= CHAR_Z) || (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z)
);
};
/**
* @param {number} charCode a char code
* @param {boolean} windowsPath whether `\` separates segments
* @returns {boolean} true, when the char code separates path segments
*/
const isSeparator = (charCode, windowsPath) =>
charCode === CHAR_SLASH || (windowsPath && charCode === CHAR_BACKSLASH);
/**
* @param {string} parentPath parent directory path
* @param {boolean} windowsPath whether `parentPath` is a Windows path
* @returns {number} length of `parentPath` without its trailing separators
*/
const parentPathLength = (parentPath, windowsPath) => {
let end = parentPath.length;
while (end > 0 && isSeparator(parentPath.charCodeAt(end - 1), windowsPath)) {
end--;
}
return end;
};
/**
* Cold path of `startsWithPath`: Node lowercases whole paths rather than single
* characters, which only makes a difference outside of ASCII.
* @param {string} parentPath parent directory path
* @param {number} length number of characters to compare
* @param {string} childPath child path to check
* @returns {boolean} true, when both prefixes name the same Windows path
*/
const equalsWindowsPrefix = (parentPath, length, childPath) =>
childPath.slice(0, length).replace(/\//g, "\\").toLowerCase() ===
parentPath.slice(0, length).replace(/\//g, "\\").toLowerCase();
/**
* @param {string} parentPath parent directory path
* @param {number} length number of characters of `parentPath` to compare
* @param {string} childPath child path to check
* @param {boolean} windowsPath whether `parentPath` is a Windows path
* @returns {boolean} true, when `childPath` starts with that prefix
*/
const startsWithPath = (parentPath, length, childPath, windowsPath) => {
if (childPath.length < length) return false;
// The common case is an exact prefix of a parent without trailing
// separators, which `startsWith` answers natively and without a slice. For
// a posix parent that is the whole answer, only a Windows one has more
// spellings of the same path to try.
if (length === parentPath.length) {
if (childPath.startsWith(parentPath)) return true;
if (!windowsPath) return false;
}
for (let i = 0; i < length; i++) {
const childCharCode = childPath.charCodeAt(i);
const parentCharCode = parentPath.charCodeAt(i);
if (childCharCode === parentCharCode) continue;
if (!windowsPath) return false;
// Windows mixes `/` and `\` freely and compares case-insensitively.
if (isSeparator(childCharCode, true) && isSeparator(parentCharCode, true)) {
continue;
}
if (childCharCode > 127 || parentCharCode > 127) {
return equalsWindowsPrefix(parentPath, length, childPath);
}
const childLower =
childCharCode >= CHAR_A && childCharCode <= CHAR_Z
? childCharCode + 32
: childCharCode;
const parentLower =
parentCharCode >= CHAR_A && parentCharCode <= CHAR_Z
? parentCharCode + 32
: parentCharCode;
if (childLower !== parentLower) return false;
}
return true;
};
/**
* Whether childPath is parentPath itself or a path under it, the answer node's
* `relative(parentPath, childPath)` gives: not escaping upward and not
* absolute. A trailing separator on the parent is not part of the boundary, so
* `/a/b/` contains exactly what `/a/b` contains.
* @param {string} parentPath parent directory path
* @param {string} childPath child path to check
* @returns {boolean} true if childPath is parentPath or is under it
*/
const isInside = (parentPath, childPath) => {
const windowsPath = isWindowsPath(parentPath);
const length = parentPathLength(parentPath, windowsPath);
if (!startsWithPath(parentPath, length, childPath, windowsPath)) return false;
// The parent itself, or a segment boundary right after it so that `/a/b`
// does not contain the sibling `/a/b-other`.
return (
childPath.length === length ||
isSeparator(childPath.charCodeAt(length), windowsPath)
);
};
/**
* Check if childPath is a subdirectory of parentPath. Compares like `isInside`,
* except that a path is not a subpath of itself.
*
* Called from `TsconfigPathsPlugin._selectPathsDataForContext` inside a loop
* over every tsconfig-paths context on every resolve, so it's worth keeping
* cheap: a native `startsWith` plus a separator char check answers it, and the
* character loop only runs for a Windows path that the prefix test missed.
* @param {string} parentPath parent directory path
* @param {string} childPath child path to check
* @returns {boolean} true if childPath is under parentPath
*/
const isSubPath = (parentPath, childPath) => {
const windowsPath = isWindowsPath(parentPath);
const length = parentPathLength(parentPath, windowsPath);
if (childPath.length <= length) return false;
if (!startsWithPath(parentPath, length, childPath, windowsPath)) return false;
return isSeparator(childPath.charCodeAt(length), windowsPath);
};
/**
* Convert a `file:` `URL` instance to a filesystem path; any other input
* (including plain strings) is returned unchanged. Mirrors Node's `fs`, which
* treats strings as literal paths and only `URL` objects as URLs (see
* nodejs/node#17658) — so a directory literally named `file:` is never
* mistaken for a URL.
* @param {string | URL} maybeURL a path string or a `file:` `URL` instance
* @returns {string} a filesystem path
*/
const toPath = (maybeURL) =>
maybeURL instanceof URL ? fileURLToPath(maybeURL) : maybeURL;
module.exports.PathType = PathType;
module.exports.createCachedBasename = createCachedBasename;
module.exports.createCachedDirname = createCachedDirname;
module.exports.createCachedJoin = createCachedJoin;
module.exports.deprecatedInvalidSegmentRegEx = deprecatedInvalidSegmentRegEx;
module.exports.dirname = dirname;
module.exports.getType = getType;
module.exports.invalidSegmentRegEx = invalidSegmentRegEx;
module.exports.isInside = isInside;
module.exports.isRelativeRequest = isRelativeRequest;
module.exports.isSubPath = isSubPath;
module.exports.isWindowsPath = isWindowsPath;
module.exports.join = join;
module.exports.normalize = normalize;
module.exports.toPath = toPath;

View File

@@ -0,0 +1,23 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
module.exports = {
/**
* @type {Record<string, string>}
*/
versions: {},
// eslint-disable-next-line jsdoc/reject-function-type
/** @param {Function} fn function */
nextTick(fn) {
// eslint-disable-next-line prefer-rest-params
const args = Array.prototype.slice.call(arguments, 1);
Promise.resolve().then(() => {
// eslint-disable-next-line prefer-spread
fn.apply(null, args);
});
},
};

View File

@@ -0,0 +1,177 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Natsu @xiaoxiaojx
This file contains code ported from strip-json-comments:
https://github.com/sindresorhus/strip-json-comments
Original license: MIT
Original author: Sindre Sorhus
*/
"use strict";
/**
* @typedef {object} StripJsonCommentsOptions
* @property {boolean=} whitespace Replace comments with whitespace
* @property {boolean=} trailingCommas Strip trailing commas
*/
const singleComment = Symbol("singleComment");
const multiComment = Symbol("multiComment");
/**
* Strip without whitespace (returns empty string)
* @param {string} _string Unused
* @param {number} _start Unused
* @param {number} _end Unused
* @returns {string} Empty string for all input
*/
const stripWithoutWhitespace = (_string, _start, _end) => "";
/**
* Replace all characters except ASCII spaces, tabs and line endings with regular spaces to ensure valid JSON output.
* @param {string} string String to process
* @param {number} start Start index
* @param {number} end End index
* @returns {string} Processed string with comments replaced by whitespace
*/
const stripWithWhitespace = (string, start, end) =>
string.slice(start, end).replace(/[^ \t\r\n]/g, " ");
/**
* Check if a quote is escaped
* @param {string} jsonString JSON string
* @param {number} quotePosition Position of the quote
* @returns {boolean} True if the quote at the given position is escaped
*/
const isEscaped = (jsonString, quotePosition) => {
let index = quotePosition - 1;
let backslashCount = 0;
while (jsonString[index] === "\\") {
index -= 1;
backslashCount += 1;
}
return Boolean(backslashCount % 2);
};
/**
* Strip comments from JSON string
* @param {string} jsonString JSON string with potential comments
* @param {StripJsonCommentsOptions} options Options
* @returns {string} JSON string without comments
*/
function stripJsonComments(
jsonString,
{ whitespace = true, trailingCommas = false } = {},
) {
if (typeof jsonString !== "string") {
throw new TypeError(
`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``,
);
}
const strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace;
let isInsideString = false;
/** @type {false | typeof singleComment | typeof multiComment} */
let isInsideComment = false;
let offset = 0;
let buffer = "";
let result = "";
let commaIndex = -1;
for (let index = 0; index < jsonString.length; index++) {
const currentCharacter = jsonString[index];
const nextCharacter = jsonString[index + 1];
if (!isInsideComment && currentCharacter === '"') {
// Enter or exit string
const escaped = isEscaped(jsonString, index);
if (!escaped) {
isInsideString = !isInsideString;
}
}
if (isInsideString) {
continue;
}
if (!isInsideComment && currentCharacter + nextCharacter === "//") {
// Enter single-line comment
buffer += jsonString.slice(offset, index);
offset = index;
isInsideComment = singleComment;
index++;
} else if (
isInsideComment === singleComment &&
currentCharacter + nextCharacter === "\r\n"
) {
// Exit single-line comment via \r\n
index++;
isInsideComment = false;
buffer += strip(jsonString, offset, index);
offset = index;
continue;
} else if (isInsideComment === singleComment && currentCharacter === "\n") {
// Exit single-line comment via \n
isInsideComment = false;
buffer += strip(jsonString, offset, index);
offset = index;
} else if (!isInsideComment && currentCharacter + nextCharacter === "/*") {
// Enter multiline comment
buffer += jsonString.slice(offset, index);
offset = index;
isInsideComment = multiComment;
index++;
continue;
} else if (
isInsideComment === multiComment &&
currentCharacter + nextCharacter === "*/"
) {
// Exit multiline comment
index++;
isInsideComment = false;
buffer += strip(jsonString, offset, index + 1);
offset = index + 1;
continue;
} else if (trailingCommas && !isInsideComment) {
if (commaIndex !== -1) {
if (currentCharacter === "}" || currentCharacter === "]") {
// Strip trailing comma
buffer += jsonString.slice(offset, index);
result += strip(buffer, 0, 1) + buffer.slice(1);
buffer = "";
offset = index;
commaIndex = -1;
} else if (
currentCharacter !== " " &&
currentCharacter !== "\t" &&
currentCharacter !== "\r" &&
currentCharacter !== "\n"
) {
// Hit non-whitespace following a comma; comma is not trailing
buffer += jsonString.slice(offset, index);
offset = index;
commaIndex = -1;
}
} else if (currentCharacter === ",") {
// Flush buffer prior to this point, and save new comma index
result += buffer + jsonString.slice(offset, index);
buffer = "";
offset = index;
commaIndex = index;
}
}
}
const remaining =
isInsideComment === singleComment
? strip(jsonString, offset, jsonString.length)
: jsonString.slice(offset);
return result + buffer + remaining;
}
module.exports = stripJsonComments;

View File

@@ -0,0 +1,14 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
// Browser shim for the Node `url` builtin, providing the `fileURLToPath` the
// resolver uses. Node, Deno and Bun use the native `url` (so they get the fully
// correct implementation); only browser bundles use this, wired in through the
// package `browser` field. The implementation is a URL-based port — see
// ./fileURLToPath.
module.exports = {
fileURLToPath: require("./fileURLToPath"),
};