fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

9
node_modules/lighthouse/report/generator/README.md generated vendored Normal file
View File

@@ -0,0 +1,9 @@
# Lighthouse Report Generator
## Overview
Lighthouse's report generator is the entry point for creating reports from an **LHR** (Lighthouse Result object). It returns results as HTML, JSON, and CSV.
It runs natively in Node.js but can run in the browser after a compile step is applied during our bundling pipeline. That compile step uses `inline-fs`, which takes any `fs.readFileSync()` calls and replaces them with the stringified file content.
Because it's shared between core and the report, dependencies (both code and types) should be kept minimal.

View File

@@ -0,0 +1,35 @@
/**
* Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS.
* @param {{finalDisplayedUrl: string, fetchTime: string}} lhr
* @return {string}
*/
export function getLhrFilenamePrefix(lhr: {
finalDisplayedUrl: string;
fetchTime: string;
}): string;
/**
* @fileoverview
* @suppress {reportUnknownTypes}
*/
/**
* Generate a filenamePrefix of name_YYYY-MM-DD_HH-MM-SS
* Date/time uses the local timezone, however Node has unreliable ICU
* support, so we must construct a YYYY-MM-DD date format manually. :/
* @param {string} name
* @param {string|undefined} fetchTime
*/
export function getFilenamePrefix(name: string, fetchTime: string | undefined): string;
/**
* Generate a filenamePrefix of name_YYYY-MM-DD_HH-MM-SS.
* @param {{name: string, steps: Array<{lhr: {fetchTime: string}}>}} flowResult
* @return {string}
*/
export function getFlowResultFilenamePrefix(flowResult: {
name: string;
steps: {
lhr: {
fetchTime: string;
};
}[];
}): string;
//# sourceMappingURL=file-namer.d.ts.map

61
node_modules/lighthouse/report/generator/file-namer.js generated vendored Normal file
View File

@@ -0,0 +1,61 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
/**
* @fileoverview
* @suppress {reportUnknownTypes}
*/
/**
* Generate a filenamePrefix of name_YYYY-MM-DD_HH-MM-SS
* Date/time uses the local timezone, however Node has unreliable ICU
* support, so we must construct a YYYY-MM-DD date format manually. :/
* @param {string} name
* @param {string|undefined} fetchTime
*/
function getFilenamePrefix(name, fetchTime) {
const date = fetchTime ? new Date(fetchTime) : new Date();
const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
const dateParts = date.toLocaleDateString('en-US', {
year: 'numeric', month: '2-digit', day: '2-digit',
}).split('/');
// @ts-expect-error - parts exists
dateParts.unshift(dateParts.pop());
const dateStr = dateParts.join('-');
const filenamePrefix = `${name}_${dateStr}_${timeStr}`;
// replace characters that are unfriendly to filenames
return filenamePrefix.replace(/[/?<>\\:*|"]/g, '-');
}
/**
* Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS.
* @param {{finalDisplayedUrl: string, fetchTime: string}} lhr
* @return {string}
*/
function getLhrFilenamePrefix(lhr) {
const hostname = new URL(lhr.finalDisplayedUrl).hostname;
return getFilenamePrefix(hostname, lhr.fetchTime);
}
/**
* Generate a filenamePrefix of name_YYYY-MM-DD_HH-MM-SS.
* @param {{name: string, steps: Array<{lhr: {fetchTime: string}}>}} flowResult
* @return {string}
*/
function getFlowResultFilenamePrefix(flowResult) {
const lhr = flowResult.steps[0].lhr;
const name = flowResult.name.replace(/\s/g, '-');
return getFilenamePrefix(name, lhr.fetchTime);
}
export {
getLhrFilenamePrefix,
getFilenamePrefix,
getFlowResultFilenamePrefix,
};

View File

@@ -0,0 +1,9 @@
export namespace flowReportAssets {
export { FLOW_REPORT_TEMPLATE };
export const FLOW_REPORT_CSS: string;
export { FLOW_REPORT_JAVASCRIPT };
}
declare const FLOW_REPORT_TEMPLATE: string;
declare const FLOW_REPORT_JAVASCRIPT: string;
export {};
//# sourceMappingURL=flow-report-assets.d.ts.map

View File

@@ -0,0 +1,25 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
import fs from 'fs';
import {getModuleDirectory} from '../../esm-utils.js';
const moduleDir = getModuleDirectory(import.meta);
/* eslint-disable max-len */
const FLOW_REPORT_TEMPLATE = fs.readFileSync(`${moduleDir}/../../flow-report/assets/standalone-flow-template.html`, 'utf8');
const REGULAR_REPORT_CSS = fs.readFileSync(moduleDir + '/../assets/styles.css', 'utf8');
const FLOW_REPORT_CSS = fs.readFileSync(`${moduleDir}/../../flow-report/assets/styles.css`, 'utf8');
const FLOW_REPORT_JAVASCRIPT = fs.readFileSync(`${moduleDir}/../../dist/report/flow.js`, 'utf8');
/* eslint-enable max-len */
export const flowReportAssets = {
FLOW_REPORT_TEMPLATE,
FLOW_REPORT_CSS: [REGULAR_REPORT_CSS, FLOW_REPORT_CSS].join('\n'),
FLOW_REPORT_JAVASCRIPT,
};

View File

@@ -0,0 +1,8 @@
export const reportAssets: {
FLOW_REPORT_TEMPLATE: string;
FLOW_REPORT_CSS: string;
FLOW_REPORT_JAVASCRIPT: string;
REPORT_TEMPLATE: string;
REPORT_JAVASCRIPT: string;
};
//# sourceMappingURL=report-assets.d.ts.map

View File

@@ -0,0 +1,26 @@
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
import fs from 'fs';
import {flowReportAssets} from './flow-report-assets.js';
import {getModuleDirectory} from '../../esm-utils.js';
const moduleDir = getModuleDirectory(import.meta);
const REPORT_TEMPLATE = fs.readFileSync(moduleDir + '/../assets/standalone-template.html',
'utf8');
const REPORT_JAVASCRIPT = fs.readFileSync(moduleDir + '/../../dist/report/standalone.js', 'utf8');
export const reportAssets = {
REPORT_TEMPLATE,
REPORT_JAVASCRIPT,
// Flow report assets are not needed for every bundle.
// Replacing/ignoring flow-report-assets.js (e.g. `rollupPlugins.shim`) will
// remove the flow assets from the bundle.
...flowReportAssets,
};

View File

@@ -0,0 +1,59 @@
export type LHResult = import('../../types/lhr/lhr').default;
export type FlowResult = import('../../types/lhr/flow-result').default;
/** @typedef {import('../../types/lhr/lhr').default} LHResult */
/** @typedef {import('../../types/lhr/flow-result').default} FlowResult */
export class ReportGenerator {
/**
* Replaces all the specified strings in source without serial replacements.
* @param {string} source
* @param {!Array<{search: string, replacement: string}>} replacements
* @return {string}
*/
static replaceStrings(source: string, replacements: Array<{
search: string;
replacement: string;
}>): string;
/**
* @param {unknown} object
* @return {string}
*/
static sanitizeJson(object: unknown): string;
/**
* Returns the standalone report HTML as a string with the report JSON and renderer JS inlined.
* @param {LHResult} lhr
* @return {string}
*/
static generateReportHtml(lhr: LHResult): string;
/**
* Returns the standalone flow report HTML as a string with the report JSON and renderer JS inlined.
* @param {FlowResult} flow
* @return {string}
*/
static generateFlowReportHtml(flow: FlowResult): string;
/**
* Converts the results to a CSV formatted string
* Each row describes the result of 1 audit with
* - the name of the category the audit belongs to
* - the name of the audit
* - a description of the audit
* - the score type that is used for the audit
* - the score value of the audit
*
* @param {LHResult} lhr
* @return {string}
*/
static generateReportCSV(lhr: LHResult): string;
/**
* @param {LHResult|FlowResult} result
* @return {result is FlowResult}
*/
static isFlowResult(result: LHResult | FlowResult): result is import("../../types/lhr/flow-result").default;
/**
* Creates the results output in a format based on the `mode`.
* @param {LHResult|FlowResult} result
* @param {LHResult['configSettings']['output']} outputModes
* @return {string|string[]}
*/
static generateReport(result: LHResult | FlowResult, outputModes: LHResult['configSettings']['output']): string | string[];
}
//# sourceMappingURL=report-generator.d.ts.map

View File

@@ -0,0 +1,194 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
import {reportAssets} from './report-assets.js';
/** @typedef {import('../../types/lhr/lhr').default} LHResult */
/** @typedef {import('../../types/lhr/flow-result').default} FlowResult */
class ReportGenerator {
/**
* Replaces all the specified strings in source without serial replacements.
* @param {string} source
* @param {!Array<{search: string, replacement: string}>} replacements
* @return {string}
*/
static replaceStrings(source, replacements) {
if (replacements.length === 0) {
return source;
}
const firstReplacement = replacements[0];
const nextReplacements = replacements.slice(1);
return source
.split(firstReplacement.search)
.map(part => ReportGenerator.replaceStrings(part, nextReplacements))
.join(firstReplacement.replacement);
}
/**
* @param {unknown} object
* @return {string}
*/
static sanitizeJson(object) {
return JSON.stringify(object)
.replace(/</g, '\\u003c') // replaces opening script tags
.replace(/\u2028/g, '\\u2028') // replaces line separators ()
.replace(/\u2029/g, '\\u2029'); // replaces paragraph separators
}
/**
* Returns the standalone report HTML as a string with the report JSON and renderer JS inlined.
* @param {LHResult} lhr
* @return {string}
*/
static generateReportHtml(lhr) {
const sanitizedJson = ReportGenerator.sanitizeJson(lhr);
// terser does its own sanitization, but keep this basic replace for when
// we want to generate a report without minification.
const sanitizedJavascript = reportAssets.REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/');
return ReportGenerator.replaceStrings(reportAssets.REPORT_TEMPLATE, [
{search: '%%LIGHTHOUSE_JSON%%', replacement: sanitizedJson},
{search: '%%LIGHTHOUSE_JAVASCRIPT%%', replacement: sanitizedJavascript},
]);
}
/**
* Returns the standalone flow report HTML as a string with the report JSON and renderer JS inlined.
* @param {FlowResult} flow
* @return {string}
*/
static generateFlowReportHtml(flow) {
const sanitizedJson = ReportGenerator.sanitizeJson(flow);
// terser does its own sanitization, but keep this basic replace for when
// we want to generate a report without minification.
const sanitizedJavascript = reportAssets.FLOW_REPORT_JAVASCRIPT.replace(/<\//g, '\\u003c/');
return ReportGenerator.replaceStrings(reportAssets.FLOW_REPORT_TEMPLATE, [
/* eslint-disable max-len */
{search: '%%LIGHTHOUSE_FLOW_JSON%%', replacement: sanitizedJson},
{search: '%%LIGHTHOUSE_FLOW_JAVASCRIPT%%', replacement: sanitizedJavascript},
{search: '/*%%LIGHTHOUSE_FLOW_CSS%%*/', replacement: reportAssets.FLOW_REPORT_CSS},
/* eslint-enable max-len */
]);
}
/**
* Converts the results to a CSV formatted string
* Each row describes the result of 1 audit with
* - the name of the category the audit belongs to
* - the name of the audit
* - a description of the audit
* - the score type that is used for the audit
* - the score value of the audit
*
* @param {LHResult} lhr
* @return {string}
*/
static generateReportCSV(lhr) {
// To keep things "official" we follow the CSV specification (RFC4180)
// The document describes how to deal with escaping commas and quotes etc.
const CRLF = '\r\n';
const separator = ',';
/** @param {string} value @return {string} */
const escape = value => `"${value.replace(/"/g, '""')}"`;
/** @param {ReadonlyArray<string | number | null>} row @return {string[]} */
const rowFormatter = row => row.map(value => {
if (value === null) return 'null';
return value.toString();
}).map(escape);
const rows = [];
const topLevelKeys = /** @type {const} */(
['requestedUrl', 'finalDisplayedUrl', 'fetchTime', 'gatherMode']);
// First we have metadata about the LHR.
rows.push(rowFormatter(topLevelKeys));
rows.push(rowFormatter(topLevelKeys.map(key => lhr[key] ?? null)));
// Some spacing.
rows.push([]);
// Categories.
rows.push(['category', 'score']);
for (const category of Object.values(lhr.categories)) {
rows.push(rowFormatter([
category.id,
category.score,
]));
}
rows.push([]);
// Audits.
rows.push(['category', 'audit', 'score', 'displayValue', 'description']);
for (const category of Object.values(lhr.categories)) {
for (const auditRef of category.auditRefs) {
const audit = lhr.audits[auditRef.id];
if (!audit) continue;
rows.push(rowFormatter([
category.id,
auditRef.id,
audit.score,
audit.displayValue || '',
audit.description,
]));
}
}
return rows
.map(row => row.join(separator))
.join(CRLF);
}
/**
* @param {LHResult|FlowResult} result
* @return {result is FlowResult}
*/
static isFlowResult(result) {
return 'steps' in result;
}
/**
* Creates the results output in a format based on the `mode`.
* @param {LHResult|FlowResult} result
* @param {LHResult['configSettings']['output']} outputModes
* @return {string|string[]}
*/
static generateReport(result, outputModes) {
const outputAsArray = Array.isArray(outputModes);
if (typeof outputModes === 'string') outputModes = [outputModes];
const output = outputModes.map(outputMode => {
// HTML report.
if (outputMode === 'html') {
if (ReportGenerator.isFlowResult(result)) {
return ReportGenerator.generateFlowReportHtml(result);
}
return ReportGenerator.generateReportHtml(result);
}
// CSV report.
if (outputMode === 'csv') {
if (ReportGenerator.isFlowResult(result)) {
throw new Error('CSV output is not support for user flows');
}
return ReportGenerator.generateReportCSV(result);
}
// JSON report.
if (outputMode === 'json') {
return JSON.stringify(result, null, 2);
}
throw new Error('Invalid output mode: ' + outputMode);
});
return outputAsArray ? output : output[0];
}
}
export {ReportGenerator};

16
node_modules/lighthouse/report/generator/tsconfig.json generated vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig-base.json",
"compilerOptions": {
// Limit defs to base JS and DOM (for URL: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34960).
"lib": ["es2020", "dom"],
// Only include `@types/node` from node_modules/.
"types": ["node"],
},
"references": [
{"path": "../../types/lhr/"},
],
"include": [
"**/*.js",
"../../esm-utils.js",
],
}