react-doctor 0.9.1-dev.5d2f66d → 0.9.1-dev.8534d86
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 +2 -0
- package/dist/cli.js +334 -45
- package/dist/index.d.ts +2 -2
- package/dist/index.js +329 -41
- package/dist/lsp.js +331 -42
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2a46b47b-3e8f-5566-a6f0-442aa6df4a39")}catch(e){}}();
|
|
3
3
|
import { i as __toESM$1, r as __require, t as __commonJSMin$1 } from "./chunk-ChKA3wi6.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
@@ -61448,7 +61448,8 @@ var AmbiguousProjectError = class extends Error {
|
|
|
61448
61448
|
}
|
|
61449
61449
|
};
|
|
61450
61450
|
const isProjectDiscoveryError = (value) => value instanceof ProjectNotFoundError || value instanceof NoReactDependencyError || value instanceof PackageJsonNotFoundError || value instanceof NotADirectoryError || value instanceof AmbiguousProjectError;
|
|
61451
|
-
const
|
|
61451
|
+
const HTML_FILE_PATTERN = /\.html$/i;
|
|
61452
|
+
const SOURCE_FILE_PATTERN = /\.(?:tsx?|jsx?|mts|mjs|[hH][tT][mM][lL])$/;
|
|
61452
61453
|
const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
|
|
61453
61454
|
const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
|
|
61454
61455
|
const MINIFIED_SNIFF_BYTES = 65536;
|
|
@@ -70594,6 +70595,12 @@ const appendReanimatedSharedValueHint = (help, rule, project) => {
|
|
|
70594
70595
|
if (!help) return REANIMATED_SHARED_VALUE_HINT;
|
|
70595
70596
|
return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
|
|
70596
70597
|
};
|
|
70598
|
+
const columnOfUtf8Offset = (sourceBuffer, utf8Offset) => {
|
|
70599
|
+
const boundedOffset = Math.min(Math.max(utf8Offset, 0), sourceBuffer.length);
|
|
70600
|
+
let lineStartOffset = boundedOffset;
|
|
70601
|
+
while (lineStartOffset > 0 && sourceBuffer[lineStartOffset - 1] !== 10) lineStartOffset--;
|
|
70602
|
+
return sourceBuffer.subarray(lineStartOffset, boundedOffset).toString("utf8").length + 1;
|
|
70603
|
+
};
|
|
70597
70604
|
const REDACTED_PLACEHOLDER = "<redacted>";
|
|
70598
70605
|
const KEEP_PREFIX = `$1${REDACTED_PLACEHOLDER}`;
|
|
70599
70606
|
const KNOWN_SECRET_RULES = [
|
|
@@ -71309,7 +71316,7 @@ const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORI
|
|
|
71309
71316
|
* with empty text still carry a useful jump target, so they're kept
|
|
71310
71317
|
* with a neutral message.
|
|
71311
71318
|
*/
|
|
71312
|
-
const buildRelatedLocations = (labels, filePath) => {
|
|
71319
|
+
const buildRelatedLocations = (labels, filePath, sourceBuffer) => {
|
|
71313
71320
|
if (labels.length <= 1) return [];
|
|
71314
71321
|
const related = [];
|
|
71315
71322
|
for (let labelIndex = 1; labelIndex < labels.length; labelIndex++) {
|
|
@@ -71317,8 +71324,8 @@ const buildRelatedLocations = (labels, filePath) => {
|
|
|
71317
71324
|
if (!label?.span) continue;
|
|
71318
71325
|
related.push({
|
|
71319
71326
|
filePath,
|
|
71320
|
-
line: label.span.line ?? 0,
|
|
71321
|
-
column: label.span.column ?? 0,
|
|
71327
|
+
line: sourceBuffer === null ? label.span.line ?? 0 : lineOfUtf8Offset(sourceBuffer, label.span.offset),
|
|
71328
|
+
column: sourceBuffer === null ? label.span.column ?? 0 : columnOfUtf8Offset(sourceBuffer, label.span.offset),
|
|
71322
71329
|
offset: label.span.offset,
|
|
71323
71330
|
length: label.span.length,
|
|
71324
71331
|
message: label.label ?? ""
|
|
@@ -71338,7 +71345,7 @@ const isOxlintOutput = (value) => isRecord(value) && Array.isArray(value.diagnos
|
|
|
71338
71345
|
* to the first `{`. Raises `OxlintOutputUnparseable` when the
|
|
71339
71346
|
* stdout isn't valid JSON or doesn't carry a `diagnostics` array.
|
|
71340
71347
|
*/
|
|
71341
|
-
const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
71348
|
+
const parseOxlintOutput = (stdout, project, rootDirectory, sourcePathByLintPath) => {
|
|
71342
71349
|
if (!stdout) return [];
|
|
71343
71350
|
const jsonStart = stdout.indexOf("{");
|
|
71344
71351
|
const sanitizedStdout = jsonStart > 0 ? stdout.slice(jsonStart) : stdout;
|
|
@@ -71355,8 +71362,10 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
71355
71362
|
const sourceBufferCache = /* @__PURE__ */ new Map();
|
|
71356
71363
|
const sourceLinesCache = /* @__PURE__ */ new Map();
|
|
71357
71364
|
const resolveAbsolutePath = (filename) => Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
71365
|
+
const resolveMappedSourceFilename = (filename) => sourcePathByLintPath?.get(Path.normalize(resolveAbsolutePath(filename)));
|
|
71366
|
+
const resolveSourceFilename = (filename) => resolveMappedSourceFilename(filename) ?? filename;
|
|
71358
71367
|
const readSourceBuffer = (filename) => {
|
|
71359
|
-
const absolutePath = resolveAbsolutePath(filename);
|
|
71368
|
+
const absolutePath = resolveAbsolutePath(resolveSourceFilename(filename));
|
|
71360
71369
|
const cached = sourceBufferCache.get(absolutePath);
|
|
71361
71370
|
if (cached !== void 0) return cached;
|
|
71362
71371
|
let sourceBuffer;
|
|
@@ -71369,7 +71378,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
71369
71378
|
return sourceBuffer;
|
|
71370
71379
|
};
|
|
71371
71380
|
const readSourceLines = (filename) => {
|
|
71372
|
-
const absolutePath = resolveAbsolutePath(filename);
|
|
71381
|
+
const absolutePath = resolveAbsolutePath(resolveSourceFilename(filename));
|
|
71373
71382
|
const cached = sourceLinesCache.get(absolutePath);
|
|
71374
71383
|
if (cached !== void 0) return cached;
|
|
71375
71384
|
const sourceBuffer = readSourceBuffer(filename);
|
|
@@ -71378,27 +71387,30 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
71378
71387
|
return sourceLines;
|
|
71379
71388
|
};
|
|
71380
71389
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
71381
|
-
const absolutePath = resolveAbsolutePath(filename);
|
|
71390
|
+
const absolutePath = resolveAbsolutePath(resolveSourceFilename(filename));
|
|
71382
71391
|
const cached = minifiedFileCache.get(absolutePath);
|
|
71383
71392
|
if (cached !== void 0) return cached;
|
|
71384
71393
|
const minified = isMinifiedSource(absolutePath);
|
|
71385
71394
|
minifiedFileCache.set(absolutePath, minified);
|
|
71386
71395
|
return minified;
|
|
71387
71396
|
};
|
|
71388
|
-
return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !(TYPESCRIPT_DECLARATION_FILE_PATTERN.test(diagnostic.filename) && OXLINT_IGNORED_DECLARATION_DIAGNOSTIC_CODES.has(diagnostic.code)) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
|
|
71397
|
+
return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(resolveSourceFilename(diagnostic.filename)) && !(TYPESCRIPT_DECLARATION_FILE_PATTERN.test(diagnostic.filename) && OXLINT_IGNORED_DECLARATION_DIAGNOSTIC_CODES.has(diagnostic.code)) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
|
|
71389
71398
|
const { plugin, rule } = parseRuleCode(diagnostic.code);
|
|
71390
71399
|
const primaryLabel = diagnostic.labels[0];
|
|
71391
71400
|
const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
|
|
71392
|
-
const
|
|
71401
|
+
const mappedSourceFilename = resolveMappedSourceFilename(diagnostic.filename);
|
|
71402
|
+
const normalizedFilePath = (mappedSourceFilename ?? diagnostic.filename).replaceAll("\\", "/");
|
|
71393
71403
|
const primarySpan = primaryLabel?.span;
|
|
71394
71404
|
const sourceBuffer = primarySpan ? readSourceBuffer(diagnostic.filename) : null;
|
|
71395
|
-
const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
|
|
71405
|
+
const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath, mappedSourceFilename === void 0 ? null : sourceBuffer);
|
|
71396
71406
|
const category = resolveDiagnosticCategory(plugin, rule);
|
|
71397
71407
|
const matchByOccurrence = resolveMatchByOccurrence(rule, category);
|
|
71398
71408
|
const sourceLines = primarySpan ? readSourceLines(diagnostic.filename) : null;
|
|
71399
|
-
const
|
|
71409
|
+
const primaryLineNumber = primarySpan && mappedSourceFilename !== void 0 && sourceBuffer !== null ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset) : primarySpan?.line ?? 0;
|
|
71410
|
+
const primaryColumnNumber = primarySpan && mappedSourceFilename !== void 0 && sourceBuffer !== null ? columnOfUtf8Offset(sourceBuffer, primarySpan.offset) : primarySpan?.column ?? 0;
|
|
71411
|
+
const primaryLineIndex = primarySpan ? primaryLineNumber - 1 : -1;
|
|
71400
71412
|
const primaryLine = sourceLines?.[primaryLineIndex];
|
|
71401
|
-
const primaryColumnIndex = primarySpan ?
|
|
71413
|
+
const primaryColumnIndex = primarySpan ? primaryColumnNumber - 1 : -1;
|
|
71402
71414
|
const isJsxTagLabel = primaryLine !== void 0 && primaryColumnIndex >= 0 && (primaryLine[primaryColumnIndex] === "<" || primaryLine[primaryColumnIndex - 1] === "<");
|
|
71403
71415
|
const jsxOpenerEndLineIndex = sourceLines && isJsxTagLabel ? findJsxOpenerSpan(sourceLines, primaryLineIndex) : null;
|
|
71404
71416
|
const primarySpanEndLine = jsxOpenerEndLineIndex !== null ? jsxOpenerEndLineIndex + 1 : primarySpan && sourceBuffer ? lineOfUtf8Offset(sourceBuffer, primarySpan.offset + Math.max(primarySpan.length - 1, 0)) : void 0;
|
|
@@ -71411,8 +71423,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
71411
71423
|
message: cleaned.message,
|
|
71412
71424
|
help: cleaned.help,
|
|
71413
71425
|
url: diagnostic.url,
|
|
71414
|
-
line:
|
|
71415
|
-
column:
|
|
71426
|
+
line: primaryLineNumber,
|
|
71427
|
+
column: primaryColumnNumber,
|
|
71416
71428
|
...primarySpan ? {
|
|
71417
71429
|
offset: primarySpan.offset,
|
|
71418
71430
|
length: primarySpan.length,
|
|
@@ -71600,7 +71612,11 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
71600
71612
|
* loop with a slimmer config in that case.
|
|
71601
71613
|
*/
|
|
71602
71614
|
const spawnLintBatches = async (input) => {
|
|
71603
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
|
|
71615
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
|
|
71616
|
+
const resolveSourcePath = (filePath) => {
|
|
71617
|
+
const absoluteFilePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory, filePath);
|
|
71618
|
+
return sourcePathByLintPath?.get(Path.normalize(absoluteFilePath)) ?? filePath;
|
|
71619
|
+
};
|
|
71604
71620
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
71605
71621
|
let rescueDeadlineEpochMs;
|
|
71606
71622
|
const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
|
|
@@ -71620,7 +71636,7 @@ const spawnLintBatches = async (input) => {
|
|
|
71620
71636
|
}
|
|
71621
71637
|
const batchArgs = [...baseArgs, ...batch];
|
|
71622
71638
|
try {
|
|
71623
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
71639
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory, sourcePathByLintPath);
|
|
71624
71640
|
} catch (error) {
|
|
71625
71641
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
71626
71642
|
if (isPastDeadline()) {
|
|
@@ -71726,10 +71742,12 @@ const spawnLintBatches = async (input) => {
|
|
|
71726
71742
|
onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
|
|
71727
71743
|
};
|
|
71728
71744
|
const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
|
|
71729
|
-
|
|
71730
|
-
|
|
71745
|
+
const droppedSourceFiles = [...new Set(droppedFiles.map(resolveSourcePath))];
|
|
71746
|
+
const deadlineSkippedSourceFiles = [...new Set(outcome.deadlineSkippedFiles.map(resolveSourcePath))];
|
|
71747
|
+
reportSkippedFiles(droppedSourceFiles, (fileListPreview) => `${droppedSourceFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
|
|
71748
|
+
reportSkippedFiles(deadlineSkippedSourceFiles, (fileListPreview) => `${deadlineSkippedSourceFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
|
|
71731
71749
|
const unavailableFiles = new Set([...droppedFiles, ...outcome.deadlineSkippedFiles]);
|
|
71732
|
-
input.onAnalyzedFiles?.([...new Set(fileBatches.flat().filter((filePath) => !unavailableFiles.has(filePath)))].sort());
|
|
71750
|
+
input.onAnalyzedFiles?.([...new Set(fileBatches.flat().filter((filePath) => !unavailableFiles.has(filePath)).map(resolveSourcePath))].sort());
|
|
71733
71751
|
return dedupeDiagnostics(diagnostics);
|
|
71734
71752
|
};
|
|
71735
71753
|
let didValidate = false;
|
|
@@ -71828,6 +71846,266 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
|
|
|
71828
71846
|
}
|
|
71829
71847
|
return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
|
|
71830
71848
|
};
|
|
71849
|
+
const THREE_MODULE_IMPORT_PATTERN = /\b(?:from\s*|import\s*(?:\(\s*)?|require\s*\(\s*)["']three(?:\/[^"']*)?["']/;
|
|
71850
|
+
const containsThreeModuleImport = (sourceText) => THREE_MODULE_IMPORT_PATTERN.test(sourceText);
|
|
71851
|
+
const COMMENT_OPEN = "<!--";
|
|
71852
|
+
const COMMENT_CLOSE = "-->";
|
|
71853
|
+
const FRONTMATTER_FENCE = "---";
|
|
71854
|
+
const SCRIPT_TAG_NAME = "script";
|
|
71855
|
+
const SCRIPT_OPEN = `<${SCRIPT_TAG_NAME}`;
|
|
71856
|
+
const SCRIPT_CLOSE = `</${SCRIPT_TAG_NAME}`;
|
|
71857
|
+
const TEMPLATE_TAG_NAME = "template";
|
|
71858
|
+
const TEMPLATE_CLOSE = `</${TEMPLATE_TAG_NAME}`;
|
|
71859
|
+
const JAVASCRIPT_MIME_TYPES = new Set([
|
|
71860
|
+
"application/ecmascript",
|
|
71861
|
+
"application/javascript",
|
|
71862
|
+
"application/x-ecmascript",
|
|
71863
|
+
"application/x-javascript",
|
|
71864
|
+
"text/ecmascript",
|
|
71865
|
+
"text/javascript",
|
|
71866
|
+
"text/javascript1.0",
|
|
71867
|
+
"text/javascript1.1",
|
|
71868
|
+
"text/javascript1.2",
|
|
71869
|
+
"text/javascript1.3",
|
|
71870
|
+
"text/javascript1.4",
|
|
71871
|
+
"text/javascript1.5",
|
|
71872
|
+
"text/jscript",
|
|
71873
|
+
"text/livescript",
|
|
71874
|
+
"text/x-ecmascript",
|
|
71875
|
+
"text/x-javascript"
|
|
71876
|
+
]);
|
|
71877
|
+
const RAW_TEXT_TAG_NAMES = new Set([
|
|
71878
|
+
"iframe",
|
|
71879
|
+
"noembed",
|
|
71880
|
+
"noframes",
|
|
71881
|
+
"noscript",
|
|
71882
|
+
"style",
|
|
71883
|
+
"textarea",
|
|
71884
|
+
"title",
|
|
71885
|
+
"xmp"
|
|
71886
|
+
]);
|
|
71887
|
+
const PLAINTEXT_TAG_NAME = "plaintext";
|
|
71888
|
+
const TAG_OPEN_UTF8_BYTE = "<".charCodeAt(0);
|
|
71889
|
+
const TAG_CLOSE_UTF8_BYTE = ">".charCodeAt(0);
|
|
71890
|
+
const SINGLE_QUOTE_UTF8_BYTE = "'".charCodeAt(0);
|
|
71891
|
+
const DOUBLE_QUOTE_UTF8_BYTE = "\"".charCodeAt(0);
|
|
71892
|
+
const ATTRIBUTE_ASSIGNMENT_UTF8_BYTE = "=".charCodeAt(0);
|
|
71893
|
+
const TAG_SELF_CLOSE_UTF8_BYTE = "/".charCodeAt(0);
|
|
71894
|
+
const TAB_UTF8_BYTE = " ".charCodeAt(0);
|
|
71895
|
+
const FORM_FEED_UTF8_BYTE = "\f".charCodeAt(0);
|
|
71896
|
+
const isHtmlWhitespace = (byte) => byte === TAB_UTF8_BYTE || byte === 10 || byte === FORM_FEED_UTF8_BYTE || byte === 13 || byte === 32;
|
|
71897
|
+
const matchesAsciiCaseInsensitive = (sourceBuffer, startByte, expected) => {
|
|
71898
|
+
if (startByte + expected.length > sourceBuffer.length) return false;
|
|
71899
|
+
for (let expectedIndex = 0; expectedIndex < expected.length; expectedIndex++) {
|
|
71900
|
+
const sourceByte = sourceBuffer[startByte + expectedIndex];
|
|
71901
|
+
if (sourceByte === void 0 || String.fromCharCode(sourceByte).toLowerCase() !== expected[expectedIndex]) return false;
|
|
71902
|
+
}
|
|
71903
|
+
return true;
|
|
71904
|
+
};
|
|
71905
|
+
const isTagBoundary = (byte) => byte === TAG_CLOSE_UTF8_BYTE || byte === TAG_SELF_CLOSE_UTF8_BYTE || isHtmlWhitespace(byte);
|
|
71906
|
+
const isPotentialTagStart = (byte) => {
|
|
71907
|
+
if (byte === void 0) return false;
|
|
71908
|
+
return /[A-Za-z!/?]/.test(String.fromCharCode(byte));
|
|
71909
|
+
};
|
|
71910
|
+
const findLineEnd = (sourceBuffer, lineStartByte) => {
|
|
71911
|
+
let byteIndex = lineStartByte;
|
|
71912
|
+
while (byteIndex < sourceBuffer.length && sourceBuffer[byteIndex] !== 10 && sourceBuffer[byteIndex] !== 13) byteIndex++;
|
|
71913
|
+
return byteIndex;
|
|
71914
|
+
};
|
|
71915
|
+
const findNextLineStart = (sourceBuffer, lineStartByte) => {
|
|
71916
|
+
const lineEndByte = findLineEnd(sourceBuffer, lineStartByte);
|
|
71917
|
+
if (sourceBuffer[lineEndByte] === 13 && sourceBuffer[lineEndByte + 1] === 10) return lineEndByte + 2;
|
|
71918
|
+
return Math.min(lineEndByte + 1, sourceBuffer.length);
|
|
71919
|
+
};
|
|
71920
|
+
const isFrontmatterFenceLine = (sourceBuffer, lineStartByte) => {
|
|
71921
|
+
if (!matchesAsciiCaseInsensitive(sourceBuffer, lineStartByte, FRONTMATTER_FENCE)) return false;
|
|
71922
|
+
const lineEndByte = findLineEnd(sourceBuffer, lineStartByte);
|
|
71923
|
+
for (let byteIndex = lineStartByte + 3; byteIndex < lineEndByte; byteIndex++) if (!isHtmlWhitespace(sourceBuffer[byteIndex])) return false;
|
|
71924
|
+
return true;
|
|
71925
|
+
};
|
|
71926
|
+
const findFrontmatterEnd = (sourceBuffer) => {
|
|
71927
|
+
if (!isFrontmatterFenceLine(sourceBuffer, 0)) return 0;
|
|
71928
|
+
const openingFenceEndByte = findLineEnd(sourceBuffer, 0);
|
|
71929
|
+
let lineStartByte = findNextLineStart(sourceBuffer, 0);
|
|
71930
|
+
while (lineStartByte < sourceBuffer.length) {
|
|
71931
|
+
if (isFrontmatterFenceLine(sourceBuffer, lineStartByte)) return findLineEnd(sourceBuffer, lineStartByte);
|
|
71932
|
+
lineStartByte = findNextLineStart(sourceBuffer, lineStartByte);
|
|
71933
|
+
}
|
|
71934
|
+
return openingFenceEndByte;
|
|
71935
|
+
};
|
|
71936
|
+
const maskByteRange = (maskedBuffer, startByte, endByte) => {
|
|
71937
|
+
for (let byteIndex = startByte; byteIndex < endByte; byteIndex++) {
|
|
71938
|
+
const byte = maskedBuffer[byteIndex];
|
|
71939
|
+
if (byte !== 10 && byte !== 13) maskedBuffer[byteIndex] = 32;
|
|
71940
|
+
}
|
|
71941
|
+
};
|
|
71942
|
+
const readStartTagName = (sourceBuffer, tagStartByte) => {
|
|
71943
|
+
const firstNameByte = sourceBuffer[tagStartByte + 1];
|
|
71944
|
+
if (firstNameByte === void 0 || !/[A-Za-z]/.test(String.fromCharCode(firstNameByte))) return null;
|
|
71945
|
+
let nameEndByte = tagStartByte + 2;
|
|
71946
|
+
while (nameEndByte < sourceBuffer.length && /[A-Za-z0-9:-]/.test(String.fromCharCode(sourceBuffer[nameEndByte] ?? 32))) nameEndByte++;
|
|
71947
|
+
return sourceBuffer.subarray(tagStartByte + 1, nameEndByte).toString("ascii").toLowerCase();
|
|
71948
|
+
};
|
|
71949
|
+
const findTagEnd = (sourceBuffer, startByte) => {
|
|
71950
|
+
let quoteByte = null;
|
|
71951
|
+
for (let byteIndex = startByte; byteIndex < sourceBuffer.length; byteIndex++) {
|
|
71952
|
+
const byte = sourceBuffer[byteIndex];
|
|
71953
|
+
if (quoteByte !== null) {
|
|
71954
|
+
if (byte === quoteByte) quoteByte = null;
|
|
71955
|
+
continue;
|
|
71956
|
+
}
|
|
71957
|
+
if (byte === SINGLE_QUOTE_UTF8_BYTE || byte === DOUBLE_QUOTE_UTF8_BYTE) {
|
|
71958
|
+
quoteByte = byte;
|
|
71959
|
+
continue;
|
|
71960
|
+
}
|
|
71961
|
+
if (byte === TAG_CLOSE_UTF8_BYTE) return byteIndex;
|
|
71962
|
+
}
|
|
71963
|
+
return sourceBuffer.length;
|
|
71964
|
+
};
|
|
71965
|
+
const parseScriptAttributes = (sourceBuffer, attributesStartByte, tagEndByte) => {
|
|
71966
|
+
let byteIndex = attributesStartByte;
|
|
71967
|
+
let hasSource = false;
|
|
71968
|
+
let language = null;
|
|
71969
|
+
let scriptType = null;
|
|
71970
|
+
while (byteIndex < tagEndByte) {
|
|
71971
|
+
while (byteIndex < tagEndByte && (isHtmlWhitespace(sourceBuffer[byteIndex]) || sourceBuffer[byteIndex] === TAG_SELF_CLOSE_UTF8_BYTE)) byteIndex++;
|
|
71972
|
+
const attributeNameStartByte = byteIndex;
|
|
71973
|
+
while (byteIndex < tagEndByte && !isHtmlWhitespace(sourceBuffer[byteIndex]) && sourceBuffer[byteIndex] !== ATTRIBUTE_ASSIGNMENT_UTF8_BYTE && sourceBuffer[byteIndex] !== TAG_SELF_CLOSE_UTF8_BYTE) byteIndex++;
|
|
71974
|
+
const attributeName = sourceBuffer.subarray(attributeNameStartByte, byteIndex).toString("ascii").toLowerCase();
|
|
71975
|
+
while (byteIndex < tagEndByte && isHtmlWhitespace(sourceBuffer[byteIndex])) byteIndex++;
|
|
71976
|
+
let attributeValue = "";
|
|
71977
|
+
if (sourceBuffer[byteIndex] === ATTRIBUTE_ASSIGNMENT_UTF8_BYTE) {
|
|
71978
|
+
byteIndex++;
|
|
71979
|
+
while (byteIndex < tagEndByte && isHtmlWhitespace(sourceBuffer[byteIndex])) byteIndex++;
|
|
71980
|
+
const quoteByte = sourceBuffer[byteIndex];
|
|
71981
|
+
if (quoteByte === SINGLE_QUOTE_UTF8_BYTE || quoteByte === DOUBLE_QUOTE_UTF8_BYTE) {
|
|
71982
|
+
const valueStartByte = byteIndex + 1;
|
|
71983
|
+
byteIndex = valueStartByte;
|
|
71984
|
+
while (byteIndex < tagEndByte && sourceBuffer[byteIndex] !== quoteByte) byteIndex++;
|
|
71985
|
+
attributeValue = sourceBuffer.subarray(valueStartByte, byteIndex).toString("utf8");
|
|
71986
|
+
if (byteIndex < tagEndByte) byteIndex++;
|
|
71987
|
+
} else {
|
|
71988
|
+
const valueStartByte = byteIndex;
|
|
71989
|
+
while (byteIndex < tagEndByte && !isHtmlWhitespace(sourceBuffer[byteIndex]) && sourceBuffer[byteIndex] !== TAG_CLOSE_UTF8_BYTE) byteIndex++;
|
|
71990
|
+
attributeValue = sourceBuffer.subarray(valueStartByte, byteIndex).toString("utf8");
|
|
71991
|
+
}
|
|
71992
|
+
}
|
|
71993
|
+
if (attributeName === "src") hasSource = true;
|
|
71994
|
+
else if (attributeName === "type" && scriptType === null) scriptType = attributeValue;
|
|
71995
|
+
else if (attributeName === "language" && language === null) language = attributeValue;
|
|
71996
|
+
}
|
|
71997
|
+
return {
|
|
71998
|
+
hasSource,
|
|
71999
|
+
language,
|
|
72000
|
+
type: scriptType
|
|
72001
|
+
};
|
|
72002
|
+
};
|
|
72003
|
+
const isExecutableScript = (attributes) => {
|
|
72004
|
+
if (attributes.hasSource) return false;
|
|
72005
|
+
const type = attributes.type?.trim().toLowerCase() ?? "";
|
|
72006
|
+
if (type !== "") {
|
|
72007
|
+
const mimeType = type.split(";")[0]?.trim() ?? "";
|
|
72008
|
+
return mimeType === "module" || JAVASCRIPT_MIME_TYPES.has(mimeType);
|
|
72009
|
+
}
|
|
72010
|
+
const language = attributes.language?.trim().toLowerCase() ?? "";
|
|
72011
|
+
return language === "" || JAVASCRIPT_MIME_TYPES.has(`text/${language}`);
|
|
72012
|
+
};
|
|
72013
|
+
const findElementClose = (sourceBuffer, startByte, tagName) => {
|
|
72014
|
+
const closeTagStart = `</${tagName}`;
|
|
72015
|
+
for (let byteIndex = startByte; byteIndex < sourceBuffer.length; byteIndex++) if (sourceBuffer[byteIndex] === TAG_OPEN_UTF8_BYTE && matchesAsciiCaseInsensitive(sourceBuffer, byteIndex, closeTagStart) && isTagBoundary(sourceBuffer[byteIndex + closeTagStart.length])) return byteIndex;
|
|
72016
|
+
return sourceBuffer.length;
|
|
72017
|
+
};
|
|
72018
|
+
const prepareHtmlScriptSource = (sourceBuffer) => {
|
|
72019
|
+
const frontmatterEndByte = findFrontmatterEnd(sourceBuffer);
|
|
72020
|
+
let maskedBuffer = frontmatterEndByte > 0 ? Buffer.from(sourceBuffer) : null;
|
|
72021
|
+
if (maskedBuffer !== null) maskByteRange(maskedBuffer, 0, frontmatterEndByte);
|
|
72022
|
+
const executableScriptBodies = [];
|
|
72023
|
+
let byteIndex = frontmatterEndByte;
|
|
72024
|
+
let templateDepth = 0;
|
|
72025
|
+
while (byteIndex < sourceBuffer.length) {
|
|
72026
|
+
if (sourceBuffer[byteIndex] !== TAG_OPEN_UTF8_BYTE) {
|
|
72027
|
+
byteIndex++;
|
|
72028
|
+
continue;
|
|
72029
|
+
}
|
|
72030
|
+
if (matchesAsciiCaseInsensitive(sourceBuffer, byteIndex, COMMENT_OPEN)) {
|
|
72031
|
+
const commentEndByte = sourceBuffer.indexOf(COMMENT_CLOSE, byteIndex + 4);
|
|
72032
|
+
byteIndex = commentEndByte === -1 ? sourceBuffer.length : commentEndByte + 3;
|
|
72033
|
+
continue;
|
|
72034
|
+
}
|
|
72035
|
+
if (matchesAsciiCaseInsensitive(sourceBuffer, byteIndex, TEMPLATE_CLOSE) && isTagBoundary(sourceBuffer[byteIndex + TEMPLATE_CLOSE.length])) {
|
|
72036
|
+
const tagEndByte = findTagEnd(sourceBuffer, byteIndex + TEMPLATE_CLOSE.length);
|
|
72037
|
+
if (templateDepth > 0) templateDepth--;
|
|
72038
|
+
byteIndex = Math.min(tagEndByte + 1, sourceBuffer.length);
|
|
72039
|
+
continue;
|
|
72040
|
+
}
|
|
72041
|
+
if (!matchesAsciiCaseInsensitive(sourceBuffer, byteIndex, SCRIPT_OPEN) || !isTagBoundary(sourceBuffer[byteIndex + SCRIPT_OPEN.length])) {
|
|
72042
|
+
if (!isPotentialTagStart(sourceBuffer[byteIndex + 1])) {
|
|
72043
|
+
byteIndex++;
|
|
72044
|
+
continue;
|
|
72045
|
+
}
|
|
72046
|
+
const tagEndByte = findTagEnd(sourceBuffer, byteIndex + 1);
|
|
72047
|
+
const tagName = readStartTagName(sourceBuffer, byteIndex);
|
|
72048
|
+
if (tagName === PLAINTEXT_TAG_NAME) break;
|
|
72049
|
+
if (tagName === TEMPLATE_TAG_NAME) templateDepth++;
|
|
72050
|
+
if (tagName !== null && RAW_TEXT_TAG_NAMES.has(tagName)) {
|
|
72051
|
+
const closeTagStartByte = findElementClose(sourceBuffer, tagEndByte + 1, tagName);
|
|
72052
|
+
if (closeTagStartByte === sourceBuffer.length) break;
|
|
72053
|
+
const closeTagEndByte = findTagEnd(sourceBuffer, closeTagStartByte + tagName.length + 2);
|
|
72054
|
+
byteIndex = Math.min(closeTagEndByte + 1, sourceBuffer.length);
|
|
72055
|
+
continue;
|
|
72056
|
+
}
|
|
72057
|
+
byteIndex = Math.min(tagEndByte + 1, sourceBuffer.length);
|
|
72058
|
+
continue;
|
|
72059
|
+
}
|
|
72060
|
+
const tagEndByte = findTagEnd(sourceBuffer, byteIndex + SCRIPT_OPEN.length);
|
|
72061
|
+
if (tagEndByte === sourceBuffer.length) break;
|
|
72062
|
+
const bodyStartByte = tagEndByte + 1;
|
|
72063
|
+
const bodyEndByte = findElementClose(sourceBuffer, bodyStartByte, SCRIPT_TAG_NAME);
|
|
72064
|
+
const attributes = parseScriptAttributes(sourceBuffer, byteIndex + SCRIPT_OPEN.length, tagEndByte);
|
|
72065
|
+
if (attributes.hasSource || templateDepth > 0) {
|
|
72066
|
+
maskedBuffer ??= Buffer.from(sourceBuffer);
|
|
72067
|
+
maskByteRange(maskedBuffer, bodyStartByte, bodyEndByte);
|
|
72068
|
+
} else if (isExecutableScript(attributes)) executableScriptBodies.push(sourceBuffer.subarray(bodyStartByte, bodyEndByte));
|
|
72069
|
+
if (bodyEndByte === sourceBuffer.length) break;
|
|
72070
|
+
const closeTagEndByte = findTagEnd(sourceBuffer, bodyEndByte + SCRIPT_CLOSE.length);
|
|
72071
|
+
byteIndex = Math.min(closeTagEndByte + 1, sourceBuffer.length);
|
|
72072
|
+
}
|
|
72073
|
+
return {
|
|
72074
|
+
executableScriptBodies,
|
|
72075
|
+
lintBuffer: maskedBuffer ?? sourceBuffer
|
|
72076
|
+
};
|
|
72077
|
+
};
|
|
72078
|
+
const prepareHtmlLintSources = (rootDirectory, temporaryDirectory, candidateFiles) => {
|
|
72079
|
+
const lintFiles = [];
|
|
72080
|
+
const sourcePathByLintPath = /* @__PURE__ */ new Map();
|
|
72081
|
+
const sizeByLintPath = /* @__PURE__ */ new Map();
|
|
72082
|
+
const htmlSourcesDirectory = Path.join(temporaryDirectory, "html-sources");
|
|
72083
|
+
let htmlFileIndex = 0;
|
|
72084
|
+
let hasThreeModuleImport = false;
|
|
72085
|
+
for (const candidateFile of candidateFiles) {
|
|
72086
|
+
if (!HTML_FILE_PATTERN.test(candidateFile)) {
|
|
72087
|
+
lintFiles.push(candidateFile);
|
|
72088
|
+
continue;
|
|
72089
|
+
}
|
|
72090
|
+
const absoluteSourcePath = Path.isAbsolute(candidateFile) ? candidateFile : Path.resolve(rootDirectory, candidateFile);
|
|
72091
|
+
const sourceBuffer = NFS.readFileSync(absoluteSourcePath);
|
|
72092
|
+
const { executableScriptBodies, lintBuffer } = prepareHtmlScriptSource(sourceBuffer);
|
|
72093
|
+
if (htmlFileIndex === 0) NFS.mkdirSync(htmlSourcesDirectory, { recursive: true });
|
|
72094
|
+
const lintPath = Path.join(htmlSourcesDirectory, `${htmlFileIndex}.astro`);
|
|
72095
|
+
NFS.writeFileSync(lintPath, lintBuffer);
|
|
72096
|
+
lintFiles.push(lintPath);
|
|
72097
|
+
sourcePathByLintPath.set(Path.resolve(lintPath), candidateFile);
|
|
72098
|
+
sizeByLintPath.set(lintPath, sourceBuffer.length);
|
|
72099
|
+
hasThreeModuleImport ||= executableScriptBodies.some((scriptBody) => containsThreeModuleImport(scriptBody.toString("utf8")));
|
|
72100
|
+
htmlFileIndex++;
|
|
72101
|
+
}
|
|
72102
|
+
return {
|
|
72103
|
+
lintFiles,
|
|
72104
|
+
sourcePathByLintPath,
|
|
72105
|
+
sizeByLintPath,
|
|
72106
|
+
hasThreeModuleImport
|
|
72107
|
+
};
|
|
72108
|
+
};
|
|
71831
72109
|
/**
|
|
71832
72110
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
71833
72111
|
* the runner: once for the primary scan, once for the
|
|
@@ -72001,21 +72279,6 @@ const runOxlint = async (options) => {
|
|
|
72001
72279
|
const pluginPath = resolvePluginPath();
|
|
72002
72280
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
72003
72281
|
const userPlugins = includedTags.size > 0 ? [] : resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
72004
|
-
const buildConfig = (overrides) => createOxlintConfig({
|
|
72005
|
-
pluginPath,
|
|
72006
|
-
project,
|
|
72007
|
-
customRulesOnly,
|
|
72008
|
-
extendsPaths: overrides.extendsPaths,
|
|
72009
|
-
ignoredTags,
|
|
72010
|
-
includedTags,
|
|
72011
|
-
includeTagDefaults,
|
|
72012
|
-
serverAuthFunctionNames,
|
|
72013
|
-
severityControls,
|
|
72014
|
-
userPlugins,
|
|
72015
|
-
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
72016
|
-
ruleSelection: overrides.ruleSelection,
|
|
72017
|
-
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
72018
|
-
});
|
|
72019
72282
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
72020
72283
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
72021
72284
|
const configPath = Path.join(configDirectory, "oxlintrc.json");
|
|
@@ -72051,7 +72314,30 @@ const runOxlint = async (options) => {
|
|
|
72051
72314
|
await yieldToEventLoop();
|
|
72052
72315
|
const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
|
|
72053
72316
|
const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
|
|
72317
|
+
const preparedHtmlSources = prepareHtmlLintSources(rootDirectory, configDirectory, candidateFiles);
|
|
72318
|
+
const lintFiles = preparedHtmlSources.lintFiles;
|
|
72319
|
+
const hasHtmlFiles = preparedHtmlSources.sourcePathByLintPath.size > 0;
|
|
72320
|
+
const lintProject = !project.hasThree && preparedHtmlSources.hasThreeModuleImport ? {
|
|
72321
|
+
...project,
|
|
72322
|
+
hasThree: true
|
|
72323
|
+
} : project;
|
|
72324
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
72325
|
+
pluginPath,
|
|
72326
|
+
project: lintProject,
|
|
72327
|
+
customRulesOnly,
|
|
72328
|
+
extendsPaths: overrides.extendsPaths,
|
|
72329
|
+
ignoredTags,
|
|
72330
|
+
includedTags,
|
|
72331
|
+
includeTagDefaults,
|
|
72332
|
+
serverAuthFunctionNames,
|
|
72333
|
+
severityControls,
|
|
72334
|
+
userPlugins,
|
|
72335
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
72336
|
+
ruleSelection: overrides.ruleSelection,
|
|
72337
|
+
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
72338
|
+
});
|
|
72054
72339
|
const sizeByFile = sizedScanFiles !== null && lintBatchOrdering === "cost" ? new Map(sizedScanFiles.map((entry) => [entry.path, entry.sizeBytes])) : null;
|
|
72340
|
+
if (sizeByFile !== null) for (const [lintPath, sizeBytes] of preparedHtmlSources.sizeByLintPath) sizeByFile.set(lintPath, sizeBytes);
|
|
72055
72341
|
const buildFileBatches = (passBaseArgs, passFiles) => sizeByFile !== null ? planLintBatches({
|
|
72056
72342
|
baseArgs: passBaseArgs,
|
|
72057
72343
|
files: passFiles,
|
|
@@ -72080,7 +72366,8 @@ const runOxlint = async (options) => {
|
|
|
72080
72366
|
fileBatches: passFileBatches,
|
|
72081
72367
|
rootDirectory,
|
|
72082
72368
|
nodeBinaryPath,
|
|
72083
|
-
project,
|
|
72369
|
+
project: lintProject,
|
|
72370
|
+
sourcePathByLintPath: preparedHtmlSources.sourcePathByLintPath,
|
|
72084
72371
|
onPartialFailure: reportPartialFailure,
|
|
72085
72372
|
onAnalyzedFiles: (filePaths) => {
|
|
72086
72373
|
analyzedFiles = filePaths;
|
|
@@ -72115,7 +72402,7 @@ const runOxlint = async (options) => {
|
|
|
72115
72402
|
};
|
|
72116
72403
|
}
|
|
72117
72404
|
};
|
|
72118
|
-
if (perFileLintCacheEnabled && !project.hasReactCompiler && project.hasReactCompilerLintPlugin !== true && extendsPaths.length === 0 && userPlugins.length === 0) {
|
|
72405
|
+
if (perFileLintCacheEnabled && !project.hasReactCompiler && project.hasReactCompilerLintPlugin !== true && extendsPaths.length === 0 && userPlugins.length === 0 && !hasHtmlFiles) {
|
|
72119
72406
|
const rulesetHash = computeRulesetHash({
|
|
72120
72407
|
config: buildConfig({
|
|
72121
72408
|
extendsPaths: [],
|
|
@@ -72273,14 +72560,15 @@ const runOxlint = async (options) => {
|
|
|
72273
72560
|
]);
|
|
72274
72561
|
}
|
|
72275
72562
|
const baseArgs = makeBaseArgs(configPath);
|
|
72276
|
-
const fileBatches = buildFileBatches(baseArgs,
|
|
72563
|
+
const fileBatches = buildFileBatches(baseArgs, lintFiles);
|
|
72277
72564
|
let analyzedFiles = [];
|
|
72278
72565
|
const runBatches = () => spawnLintBatches({
|
|
72279
72566
|
baseArgs,
|
|
72280
72567
|
fileBatches,
|
|
72281
72568
|
rootDirectory,
|
|
72282
72569
|
nodeBinaryPath,
|
|
72283
|
-
project,
|
|
72570
|
+
project: lintProject,
|
|
72571
|
+
sourcePathByLintPath: preparedHtmlSources.sourcePathByLintPath,
|
|
72284
72572
|
onPartialFailure,
|
|
72285
72573
|
onAnalyzedFiles: (filePaths) => {
|
|
72286
72574
|
analyzedFiles = filePaths;
|
|
@@ -74202,4 +74490,4 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
74202
74490
|
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
74203
74491
|
|
|
74204
74492
|
//# sourceMappingURL=index.js.map
|
|
74205
|
-
//# debugId=
|
|
74493
|
+
//# debugId=2a46b47b-3e8f-5566-a6f0-442aa6df4a39
|