Compare commits

...
Author SHA1 Message Date
hainenber fd489851fc Merge branch 'master' into feat/migrate-frontend-to-esm 2026-09-05 22:48:20 +07:00
hainenber 3be843a5bf Merge branch 'master' into feat/migrate-frontend-to-esm
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-31 13:45:41 +07:00
hainenber e62265c795 fix(build): add file extension to avoid ERR_MODULE_NOT_FOUND
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-30 10:17:03 +07:00
hainenber 4309b758ce fix(build): migrate scripts using import.meta to internal folder for easier Jest testing
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-30 07:52:43 +07:00
hainenber 9db1bb8933 fix(build): resolve ESM issues for E2E spec files
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-30 01:09:28 +07:00
hainenber 2d0603d543 fix(build): prevent root webpack config to override Storybook handling of generated stories regarding to ESM
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-30 01:06:39 +07:00
hainenber daed810eff fix(build): resolve bot's PR reviews
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-30 00:21:58 +07:00
hainenber b1940c8108 fix(build): revert to browser-safe require calls to load SupersetText config
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-30 00:06:27 +07:00
hainenber 2871895dc1 fix(build): proper type and load for YAML-based superset_text import
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-29 23:54:07 +07:00
hainenber e60dd25ced chore: revert ESM migration for frozen generator-superset
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-29 23:45:54 +07:00
hainenber 93bebdc957 feat(build): migrate superset-frontend to ESM
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-29 23:20:16 +07:00
46 changed files with 570 additions and 453 deletions
+9 -1
View File
@@ -101,7 +101,15 @@ export default {
...config,
module: {
...config.module,
rules: disableDevModeInRules(customConfig.module.rules),
rules: [
...(config.module?.rules ?? []).filter(
rule => {
const test = rule.test?.toString() ?? '';
return test !== '/\\.css$/' && !test.includes('svg');
},
),
...disableDevModeInRules(customConfig.module.rules),
],
},
resolve: {
...config.resolve,
+2 -2
View File
@@ -16,9 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
const packageConfig = require('./package');
import packageConfig from './package.json' with { type: 'json' };
module.exports = {
export default {
sourceMaps: true,
sourceType: 'module',
retainLines: true,
+1 -1
View File
@@ -17,7 +17,7 @@
* under the License.
*/
module.exports = {
export default {
header: `<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
@@ -134,7 +134,7 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
messageId: 'eager',
data: { property: keyName, fn: callee.callee.name },
fix(fixer) {
const source = context.getSourceCode().getText(prop.value);
const source = context.sourceCode.getText(prop.value);
return fixer.replaceText(prop.value, `() => ${source}`);
},
});
@@ -229,4 +229,4 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
},
};
module.exports = plugin;
export default plugin;
@@ -16,14 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { Rule } from 'eslint';
const { RuleTester } = require('eslint');
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
import { type Rule, RuleTester } from 'eslint';
import plugin from '.';
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
});
} as any);
const rule: Rule.RuleModule = plugin.rules['no-eager-t-in-config'];
ruleTester.run('no-eager-t-in-config', rule, {
@@ -22,10 +22,8 @@
* @author Apache
*/
/* eslint-disable no-template-curly-in-string */
import type { Rule } from 'eslint';
const { RuleTester } = require('eslint');
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
import { type Rule, RuleTester } from 'eslint';
import plugin from '.';
//------------------------------------------------------------------------------
// Tests
@@ -5,6 +5,7 @@
"keywords": [],
"license": "Apache-2.0",
"author": "Apache",
"type": "module",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
@@ -44,11 +44,11 @@ interface JSXElementNode {
openingElement: JSXOpeningElement;
}
const plugin: { rules: Record<string, Rule.RuleModule> } = {
const plugin = {
rules: {
'no-fa-icons-usage': {
meta: {
type: 'problem',
type: 'problem' as const,
docs: {
description:
'Disallow the usage of FontAwesome icons in the codebase',
@@ -91,4 +91,4 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
},
};
module.exports = plugin;
export default plugin;
@@ -22,10 +22,8 @@
* @author Apache
*/
import type { Rule } from 'eslint';
const { RuleTester } = require('eslint');
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
import { RuleTester } from 'eslint';
import plugin from '.';
//------------------------------------------------------------------------------
// Tests
@@ -36,7 +34,7 @@ const ruleTester = new RuleTester({
parserOptions: { ecmaFeatures: { jsx: true } },
},
});
const rule: Rule.RuleModule = plugin.rules['no-fa-icons-usage'];
const rule = plugin.rules['no-fa-icons-usage'];
const errors: Array<{ message: string }> = [
{
@@ -5,6 +5,7 @@
"keywords": [],
"license": "Apache-2.0",
"author": "Apache",
"type": "module",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
@@ -159,4 +159,4 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
},
};
module.exports = plugin;
export default plugin;
@@ -5,6 +5,7 @@
"keywords": [],
"license": "Apache-2.0",
"author": "Apache",
"type": "module",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
+6 -6
View File
@@ -34,14 +34,14 @@
// Register the TypeScript require hook so ESLint can load the .ts plugin files
// from eslint-rules/*.
require('tsx/cjs');
import 'tsx/cjs';
const tsParser = require('@typescript-eslint/parser');
const themeColorsPlugin = require('@superset-ui/eslint-plugin-theme-colors');
const iconsPlugin = require('@superset-ui/eslint-plugin-icons');
const i18nStringsPlugin = require('@superset-ui/eslint-plugin-i18n-strings');
import tsParser from '@typescript-eslint/parser';
import themeColorsPlugin from '@superset-ui/eslint-plugin-theme-colors';
import iconsPlugin from '@superset-ui/eslint-plugin-icons';
import i18nStringsPlugin from '@superset-ui/eslint-plugin-i18n-strings';
module.exports = [
export default [
// Files this config applies to. Flat config has no `--ext`; globs live here.
// Only check src/ files where the theme/icon/i18n rules matter.
{
+1 -1
View File
@@ -31,7 +31,7 @@ if (!process.env.CI) {
]);
}
module.exports = {
export default {
// [/\\] matches both path separators so the suite also collects on
// native Windows, where jest hands the regex backslash-separated paths.
testRegex:
+2 -1
View File
@@ -36,6 +36,7 @@
"plugins/*",
"src/setup/*"
],
"type": "module",
"scripts": {
"_format": "oxfmt './{src,spec,cypress-base,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --color --mode production",
@@ -82,7 +83,7 @@
"prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache ./coverage ./storybook-static",
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
"test-storybook": "test-storybook",
"test-storybook:ci": "concurrently --kill-others --success first --names \"SB,TEST\" --prefix-colors \"magenta,blue\" \"python3 -m http.server 6006 --directory storybook-static\" \"npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook -- --maxWorkers=2\"",
"test-storybook:ci": "concurrently --kill-others --success first --names \"SB,TEST\" --prefix-colors \"magenta,blue\" \"python3 -m http.server 6006 --directory storybook-static\" \"npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook -- --maxWorkers=2 --testTimeout 60000\"",
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --watch",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
@@ -17,4 +17,5 @@ config.plugins = [
['babel-plugin-typescript-to-proptypes', { loose: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
];
module.exports = config;
@@ -16,4 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
module.exports = 'test-file-stub';
@@ -16,4 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
module.exports = {};
export default {};
@@ -16,4 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
module.exports = 'test-file-stub';
export default 'test-file-stub';
@@ -39,6 +39,9 @@ import path from 'path';
import { Page, test, expect } from '@playwright/test';
import { URL } from '../../utils/urls';
import { apiDelete, apiGet } from '../../helpers/api/requests';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DOCS_STATIC = path.resolve(__dirname, '../../../../docs/static/img');
const SCREENSHOTS_DIR = path.join(DOCS_STATIC, 'screenshots');
@@ -38,6 +38,9 @@ import fs from 'fs';
import path from 'path';
import { Page, test, expect } from '@playwright/test';
import { URL } from '../../utils/urls';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MOBILE_SCREENSHOTS_DIR = path.resolve(
__dirname,
@@ -30,6 +30,9 @@
import path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from '@playwright/test';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const serverURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088';
const baseURL = serverURL.endsWith('/') ? serverURL : `${serverURL}/`;
@@ -21,7 +21,7 @@ import { test, expect, Browser, BrowserContext, Page } from '@playwright/test';
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
import { AddressInfo, Socket } from 'net';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { dirname, join } from 'path';
import {
apiEnableEmbedding,
getAccessToken,
@@ -29,6 +29,9 @@ import {
} from '../../helpers/api/embedded';
import { getDashboardBySlug } from '../../helpers/api/dashboard';
import { EmbeddedPage } from '../../pages/EmbeddedPage';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Superset domain (Flask server) set by CI or defaults to local dev
@@ -51,7 +51,7 @@ import { test, expect, Browser, BrowserContext, Page } from '@playwright/test';
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
import { AddressInfo, Socket } from 'net';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { dirname, join } from 'path';
import {
apiEnableEmbedding,
getAccessToken,
@@ -66,6 +66,7 @@ import {
import { apiDeleteChart } from '../../helpers/api/chart';
import { EmbeddedPage } from '../../pages/EmbeddedPage';
import { EMBEDDED } from '../../utils/constants';
import { fileURLToPath } from 'url';
const SUPERSET_DOMAIN = (() => {
const url = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088';
@@ -75,6 +76,8 @@ const SUPERSET_BASE_URL = SUPERSET_DOMAIN.endsWith('/')
? SUPERSET_DOMAIN
: `${SUPERSET_DOMAIN}/`;
const __dirname = dirname(fileURLToPath(import.meta.url));
const SDK_BUNDLE_PATH = join(
__dirname,
'../../../../superset-embedded-sdk/bundle/index.js',
+6 -5
View File
@@ -23,12 +23,13 @@
/**
* Build packages/plugins filtered by globs
*/
process.env.PATH = `./node_modules/.bin:${process.env.PATH}`;
const { spawnSync } = require('child_process');
const fastGlob = require('fast-glob');
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
import { spawnSync } from 'node:child_process';
import fastGlob from 'fast-glob';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
process.env.PATH = `./node_modules/.bin:${process.env.PATH}`;
const { globs } = yargs(hideBin(process.argv)).parse();
const glob = globs?.length > 1 ? `{${globs.join(',')}}` : globs?.[0] || '*';
@@ -26,61 +26,11 @@
//
// Usage: node scripts/bundle-size-summary.js <path-to-stats.json>
const fs = require('fs');
import { fileURLToPath } from 'node:url';
import { main } from './internal/bundle-size-summary.js';
// Entrypoints worth tracking: the two user-facing app shells. `menu`,
// `preamble`, `theme`, and `service-worker` are small, low-variance
// infrastructure chunks, not where bundle bloat actually shows up.
const TRACKED_ENTRYPOINTS = ['spa', 'embedded'];
const __filename = fileURLToPath(import.meta.url);
function entrypointSizeByExt(entrypoint, ext) {
return (entrypoint.assets || [])
.filter(asset => asset.name.endsWith(ext))
.reduce((total, asset) => total + asset.size, 0);
}
function main() {
const statsPath = process.argv[2];
if (!statsPath) {
console.error('Usage: bundle-size-summary.js <path-to-stats.json>');
process.exit(1);
}
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
const { entrypoints } = stats;
if (!entrypoints) {
console.error(
'stats.json has no `entrypoints` key -- was it built with ' +
'`BUNDLE_SIZE_STATS=true` set? Without it, webpack.config.js uses ' +
'`stats: "minimal"`, which omits `entrypoints`.',
);
process.exit(1);
}
const results = [];
TRACKED_ENTRYPOINTS.forEach(name => {
const entrypoint = entrypoints[name];
if (!entrypoint) {
console.error(`stats.json is missing the "${name}" entrypoint`);
process.exit(1);
}
results.push({
name: `${name} entrypoint (JS)`,
unit: 'bytes',
value: entrypointSizeByExt(entrypoint, '.js'),
});
results.push({
name: `${name} entrypoint (CSS)`,
unit: 'bytes',
value: entrypointSizeByExt(entrypoint, '.css'),
});
});
console.log(JSON.stringify(results, null, 2));
}
if (require.main === module) {
if (__filename === process.argv[1]) {
main();
}
module.exports = { entrypointSizeByExt, main, TRACKED_ENTRYPOINTS };
@@ -23,11 +23,16 @@
* Runs as a separate check without needing custom binaries
*/
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import glob from 'glob';
import * as parser from '@babel/parser';
import traverseModule from '@babel/traverse';
const traverse = traverseModule.default;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ANSI color codes
const RED = '\x1B[31m';
@@ -779,11 +784,11 @@ function main() {
}
// Run if called directly
if (require.main === module) {
if (__filename === process.argv[1]) {
main();
}
module.exports = {
export default {
checkNoLiteralColors,
checkNoFaIcons,
checkI18nTemplates,
@@ -21,10 +21,13 @@
// This script checks that UI components and plugins have corresponding
// Storybook story files. Run with --fix to see suggestions.
const fs = require('fs');
const path = require('path');
const glob = require('glob');
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import glob from 'glob';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT = path.resolve(__dirname, '..');
// Directories to check for storybook coverage
+1 -1
View File
@@ -22,7 +22,7 @@
/**
* Check commit messages only for the first commit in branch.
*/
const { execSync, spawnSync } = require('child_process');
import { execSync, spawnSync } from 'node:child_process';
const envVariable = process.argv[2] || 'GIT_PARAMS';
+2 -2
View File
@@ -20,8 +20,8 @@
*/
/* eslint-disable no-console */
const { sync } = require('fast-glob');
const { copy } = require('fs-extra');
import { sync } from 'fast-glob';
import { copy } from 'fs-extra';
const pkgGlob = process.argv[2] || '*';
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
// Reduces a webpack `--json` stats file down to the handful of headline
// numbers worth tracking over time, in the flat array format
// benchmark-action/github-action-benchmark expects for its
// "customSmallerIsBetter" tool. The full stats file also includes a
// `modules`/`chunks` graph across ~15k modules, which is enormous and not
// useful for this purpose, so we only ever read `entrypoints`.
//
// Usage: node scripts/bundle-size-summary.js <path-to-stats.json>
import fs from 'node:fs';
// Entrypoints worth tracking: the two user-facing app shells. `menu`,
// `preamble`, `theme`, and `service-worker` are small, low-variance
// infrastructure chunks, not where bundle bloat actually shows up.
const TRACKED_ENTRYPOINTS = ['spa', 'embedded'];
function entrypointSizeByExt(entrypoint, ext) {
return (entrypoint.assets || [])
.filter(asset => asset.name.endsWith(ext))
.reduce((total, asset) => total + asset.size, 0);
}
function main() {
const statsPath = process.argv[2];
if (!statsPath) {
console.error('Usage: bundle-size-summary.js <path-to-stats.json>');
process.exit(1);
}
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
const { entrypoints } = stats;
if (!entrypoints) {
console.error(
'stats.json has no `entrypoints` key -- was it built with ' +
'`BUNDLE_SIZE_STATS=true` set? Without it, webpack.config.js uses ' +
'`stats: "minimal"`, which omits `entrypoints`.',
);
process.exit(1);
}
const results = [];
TRACKED_ENTRYPOINTS.forEach(name => {
const entrypoint = entrypoints[name];
if (!entrypoint) {
console.error(`stats.json is missing the "${name}" entrypoint`);
process.exit(1);
}
results.push({
name: `${name} entrypoint (JS)`,
unit: 'bytes',
value: entrypointSizeByExt(entrypoint, '.js'),
});
results.push({
name: `${name} entrypoint (CSS)`,
unit: 'bytes',
value: entrypointSizeByExt(entrypoint, '.css'),
});
});
console.log(JSON.stringify(results, null, 2));
}
export { entrypointSizeByExt, main, TRACKED_ENTRYPOINTS };
@@ -0,0 +1,281 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import { execSync } from 'node:child_process';
import { GoogleAuth } from 'google-auth-library';
import googleSheets from '@googleapis/sheets';
const { SPREADSHEET_ID } = process.env;
const SERVICE_ACCOUNT_KEY = JSON.parse(process.env.SERVICE_ACCOUNT_KEY || '{}');
// Only set up Google Sheets if we have credentials
let sheets;
if (SERVICE_ACCOUNT_KEY.client_email) {
const auth = new GoogleAuth({
credentials: SERVICE_ACCOUNT_KEY,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
sheets = googleSheets.sheets({ version: 'v4', auth });
}
const DATETIME = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
/**
* Turn an oxlint diagnostic code into the canonical rule id used by the metrics
* series.
*
* oxlint reports `<plugin>(<rule>)`, where the plugin is the linter the rule came
* from: `eslint(no-console)`, `react-hooks(exhaustive-deps)`, `react(jsx-key)`,
* `jest(no-conditional-expect)`, `oxc(erasing-op)`, and the legacy
* `eslint-plugin-unicorn(no-new-array)` spelling.
*
* `eslint` is the implicit namespace, so its rules keep their bare name and stay
* comparable with the rows recorded before the oxlint migration. Every other
* plugin becomes `<plugin>/<rule>`, which is the id those rules are known by in
* config and in the pre-migration history.
*
* @param {string | undefined} code the diagnostic's `code` field
* @returns {string} the rule id to record
*/
function parseRuleId(code) {
if (!code) {
return 'unknown';
}
const match = code.match(/^([\w-]+)\(([^)]+)\)$/);
if (!match) {
return code;
}
const [, namespace, rule] = match;
if (namespace === 'eslint') {
return rule;
}
// `eslint-plugin-unicorn(...)` is the same rule as `unicorn/...`
const plugin = namespace.replace(/^eslint-plugin-/, '');
return `${plugin}/${rule}`;
}
async function writeToGoogleSheet(data, range, headers, append = false) {
if (!sheets) {
console.log('No Google Sheets credentials, skipping upload');
return;
}
const request = {
spreadsheetId: SPREADSHEET_ID,
range,
valueInputOption: 'USER_ENTERED',
resource: { values: append ? data : [headers, ...data] },
};
const method = append ? 'append' : 'update';
await sheets.spreadsheets.values[method](request);
}
// Run OXC and get JSON output
async function runOxlintAndProcess() {
const enrichedRules = {
'react-prefer-function-component/react-prefer-function-component': {
description: 'We prefer function components to class-based components',
},
'react/jsx-filename-extension': {
description:
'We prefer Typescript - all JSX files should be converted to TSX',
},
'react/forbid-component-props': {
description:
'We prefer Emotion for styling rather than `className` or `style` props',
},
'no-restricted-imports': {
description:
"This rule catches several things that shouldn't be used anymore. LESS, antD, etc. See individual occurrence messages for details",
},
'no-console': {
description:
"We don't want a bunch of console noise, but you can use the `logger` from `@superset-ui/core` when there's a reason to.",
},
};
try {
// Run OXC with JSON format
console.log('Running OXC linter...');
// `oxlint.json` is not the `.oxlintrc.json` oxlint auto-discovers, so the
// config has to be passed explicitly or the run reports oxlint's defaults
// instead of the project's ruleset. Matches the `lint` scripts in
// package.json.
const oxlintOutput = execSync(
'npx oxlint --config oxlint.json --format json',
{
encoding: 'utf8',
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large outputs
stdio: ['pipe', 'pipe', 'ignore'], // Ignore stderr to avoid error output
},
);
const results = JSON.parse(oxlintOutput);
// Process OXC JSON output
const metricsByRule = {};
let occurrencesData = [];
// OXC JSON format has diagnostics array
if (results.diagnostics && Array.isArray(results.diagnostics)) {
results.diagnostics.forEach(diagnostic => {
const ruleId = parseRuleId(diagnostic.code);
const file = diagnostic.filename || 'unknown';
const line = diagnostic.labels?.[0]?.span?.line || 0;
const column = diagnostic.labels?.[0]?.span?.column || 0;
const message = diagnostic.message || '';
const ruleData = metricsByRule[ruleId] || { count: 0 };
ruleData.count += 1;
metricsByRule[ruleId] = ruleData;
occurrencesData.push({
rule: ruleId,
message,
file,
line,
column,
ts: DATETIME,
});
});
}
console.log(
`OXC found ${results.diagnostics?.length || 0} issues across ${results.number_of_files} files`,
);
// Also run minimal ESLint for custom rules and merge results
console.log('Running minimal ESLint for custom rules...');
let eslintOutput = '[]';
try {
// Run ESLint and capture output directly.
// Flat config (eslint.config.minimal.js) is explicitly selected via
// --config; ESLint v9+/v10 no longer support eslintrc or --no-eslintrc.
eslintOutput = execSync(
'npx eslint --config eslint.config.minimal.js --no-inline-config --format json src',
{
encoding: 'utf8',
maxBuffer: 50 * 1024 * 1024,
stdio: ['pipe', 'pipe', 'ignore'], // Ignore stderr
},
);
} catch (e) {
// ESLint exits with non-zero when it finds issues, capture the stdout
if (e.stdout) {
eslintOutput = e.stdout.toString();
}
}
// Parse minimal ESLint output
try {
const eslintResults = JSON.parse(eslintOutput);
eslintResults.forEach(result => {
result.messages.forEach(({ ruleId, line, column, message }) => {
const ruleData = metricsByRule[ruleId] || { count: 0 };
ruleData.count += 1;
metricsByRule[ruleId] = ruleData;
occurrencesData.push({
rule: ruleId,
message,
file: result.filePath,
line,
column,
ts: DATETIME,
});
});
});
console.log(
`ESLint found ${eslintResults.reduce((sum, r) => sum + r.messages.length, 0)} custom rule violations`,
);
} catch (e) {
console.log('No ESLint issues found or parsing error:', e.message);
}
// Transform data for Google Sheets
const metricsData = Object.entries(metricsByRule).map(
([rule, { count }]) => [
'OXC+ESLint',
rule,
enrichedRules[rule]?.description || 'N/A',
`${count}`,
DATETIME,
],
);
occurrencesData = occurrencesData.map(
({ rule, message, file, line, column }) => [
rule,
enrichedRules[rule]?.description || 'N/A',
message,
file,
`${line}`,
`${column}`,
DATETIME,
],
);
const aggregatedHistoryHeaders = [
'Process',
'Rule',
'Description',
'Count',
'Timestamp',
];
const eslintBacklogHeaders = [
'Rule',
'Rule Description',
'ESLint Message',
'File',
'Line',
'Column',
'Timestamp',
];
console.log(
`Found ${Object.keys(metricsByRule).length} unique rules with ${occurrencesData.length} total occurrences`,
);
await writeToGoogleSheet(
metricsData,
'Aggregated History!A:E',
aggregatedHistoryHeaders,
true,
);
await writeToGoogleSheet(
occurrencesData,
'ESLint Backlog!A:G',
eslintBacklogHeaders,
);
console.log('Successfully uploaded metrics to Google Sheets');
} catch (error) {
console.error('Error processing lint results:', error);
process.exit(1);
}
}
export { parseRuleId, runOxlintAndProcess };
@@ -16,272 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
const { execSync } = require('child_process');
const { GoogleAuth } = require('google-auth-library');
const googleSheets = require('@googleapis/sheets');
import { fileURLToPath } from 'node:url';
import { runOxlintAndProcess } from './internal/oxlint-metrics-uploader.js';
const { SPREADSHEET_ID } = process.env;
const SERVICE_ACCOUNT_KEY = JSON.parse(process.env.SERVICE_ACCOUNT_KEY || '{}');
// Only set up Google Sheets if we have credentials
let sheets;
if (SERVICE_ACCOUNT_KEY.client_email) {
const auth = new GoogleAuth({
credentials: SERVICE_ACCOUNT_KEY,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
sheets = googleSheets.sheets({ version: 'v4', auth });
}
const DATETIME = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
/**
* Turn an oxlint diagnostic code into the canonical rule id used by the metrics
* series.
*
* oxlint reports `<plugin>(<rule>)`, where the plugin is the linter the rule came
* from: `eslint(no-console)`, `react-hooks(exhaustive-deps)`, `react(jsx-key)`,
* `jest(no-conditional-expect)`, `oxc(erasing-op)`, and the legacy
* `eslint-plugin-unicorn(no-new-array)` spelling.
*
* `eslint` is the implicit namespace, so its rules keep their bare name and stay
* comparable with the rows recorded before the oxlint migration. Every other
* plugin becomes `<plugin>/<rule>`, which is the id those rules are known by in
* config and in the pre-migration history.
*
* @param {string | undefined} code the diagnostic's `code` field
* @returns {string} the rule id to record
*/
function parseRuleId(code) {
if (!code) {
return 'unknown';
}
const match = code.match(/^([\w-]+)\(([^)]+)\)$/);
if (!match) {
return code;
}
const [, namespace, rule] = match;
if (namespace === 'eslint') {
return rule;
}
// `eslint-plugin-unicorn(...)` is the same rule as `unicorn/...`
const plugin = namespace.replace(/^eslint-plugin-/, '');
return `${plugin}/${rule}`;
}
async function writeToGoogleSheet(data, range, headers, append = false) {
if (!sheets) {
console.log('No Google Sheets credentials, skipping upload');
return;
}
const request = {
spreadsheetId: SPREADSHEET_ID,
range,
valueInputOption: 'USER_ENTERED',
resource: { values: append ? data : [headers, ...data] },
};
const method = append ? 'append' : 'update';
await sheets.spreadsheets.values[method](request);
}
// Run OXC and get JSON output
async function runOxlintAndProcess() {
const enrichedRules = {
'react-prefer-function-component/react-prefer-function-component': {
description: 'We prefer function components to class-based components',
},
'react/jsx-filename-extension': {
description:
'We prefer Typescript - all JSX files should be converted to TSX',
},
'react/forbid-component-props': {
description:
'We prefer Emotion for styling rather than `className` or `style` props',
},
'no-restricted-imports': {
description:
"This rule catches several things that shouldn't be used anymore. LESS, antD, etc. See individual occurrence messages for details",
},
'no-console': {
description:
"We don't want a bunch of console noise, but you can use the `logger` from `@superset-ui/core` when there's a reason to.",
},
};
try {
// Run OXC with JSON format
console.log('Running OXC linter...');
// `oxlint.json` is not the `.oxlintrc.json` oxlint auto-discovers, so the
// config has to be passed explicitly or the run reports oxlint's defaults
// instead of the project's ruleset. Matches the `lint` scripts in
// package.json.
const oxlintOutput = execSync(
'npx oxlint --config oxlint.json --format json',
{
encoding: 'utf8',
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large outputs
stdio: ['pipe', 'pipe', 'ignore'], // Ignore stderr to avoid error output
},
);
const results = JSON.parse(oxlintOutput);
// Process OXC JSON output
const metricsByRule = {};
let occurrencesData = [];
// OXC JSON format has diagnostics array
if (results.diagnostics && Array.isArray(results.diagnostics)) {
results.diagnostics.forEach(diagnostic => {
const ruleId = parseRuleId(diagnostic.code);
const file = diagnostic.filename || 'unknown';
const line = diagnostic.labels?.[0]?.span?.line || 0;
const column = diagnostic.labels?.[0]?.span?.column || 0;
const message = diagnostic.message || '';
const ruleData = metricsByRule[ruleId] || { count: 0 };
ruleData.count += 1;
metricsByRule[ruleId] = ruleData;
occurrencesData.push({
rule: ruleId,
message,
file,
line,
column,
ts: DATETIME,
});
});
}
console.log(
`OXC found ${results.diagnostics?.length || 0} issues across ${results.number_of_files} files`,
);
// Also run minimal ESLint for custom rules and merge results
console.log('Running minimal ESLint for custom rules...');
let eslintOutput = '[]';
try {
// Run ESLint and capture output directly.
// Flat config (eslint.config.minimal.js) is explicitly selected via
// --config; ESLint v9+/v10 no longer support eslintrc or --no-eslintrc.
eslintOutput = execSync(
'npx eslint --config eslint.config.minimal.js --no-inline-config --format json src',
{
encoding: 'utf8',
maxBuffer: 50 * 1024 * 1024,
stdio: ['pipe', 'pipe', 'ignore'], // Ignore stderr
},
);
} catch (e) {
// ESLint exits with non-zero when it finds issues, capture the stdout
if (e.stdout) {
eslintOutput = e.stdout.toString();
}
}
// Parse minimal ESLint output
try {
const eslintResults = JSON.parse(eslintOutput);
eslintResults.forEach(result => {
result.messages.forEach(({ ruleId, line, column, message }) => {
const ruleData = metricsByRule[ruleId] || { count: 0 };
ruleData.count += 1;
metricsByRule[ruleId] = ruleData;
occurrencesData.push({
rule: ruleId,
message,
file: result.filePath,
line,
column,
ts: DATETIME,
});
});
});
console.log(
`ESLint found ${eslintResults.reduce((sum, r) => sum + r.messages.length, 0)} custom rule violations`,
);
} catch (e) {
console.log('No ESLint issues found or parsing error:', e.message);
}
// Transform data for Google Sheets
const metricsData = Object.entries(metricsByRule).map(
([rule, { count }]) => [
'OXC+ESLint',
rule,
enrichedRules[rule]?.description || 'N/A',
`${count}`,
DATETIME,
],
);
occurrencesData = occurrencesData.map(
({ rule, message, file, line, column }) => [
rule,
enrichedRules[rule]?.description || 'N/A',
message,
file,
`${line}`,
`${column}`,
DATETIME,
],
);
const aggregatedHistoryHeaders = [
'Process',
'Rule',
'Description',
'Count',
'Timestamp',
];
const eslintBacklogHeaders = [
'Rule',
'Rule Description',
'ESLint Message',
'File',
'Line',
'Column',
'Timestamp',
];
console.log(
`Found ${Object.keys(metricsByRule).length} unique rules with ${occurrencesData.length} total occurrences`,
);
await writeToGoogleSheet(
metricsData,
'Aggregated History!A:E',
aggregatedHistoryHeaders,
true,
);
await writeToGoogleSheet(
occurrencesData,
'ESLint Backlog!A:G',
eslintBacklogHeaders,
);
console.log('Successfully uploaded metrics to Google Sheets');
} catch (error) {
console.error('Error processing lint results:', error);
process.exit(1);
}
}
const __filename = fileURLToPath(import.meta.url);
// Run the process, unless this file was imported (e.g. by a test) rather than
// executed, in which case nothing should be linted or uploaded on import.
if (require.main === module) {
if (__filename === process.argv[1]) {
runOxlintAndProcess().catch(console.error);
}
module.exports = { parseRuleId };
@@ -16,4 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
module.exports = {};
export default {};
@@ -16,4 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
module.exports = 'test-file-stub';
export default 'test-file-stub';
@@ -16,13 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
const fs = require('fs');
const {
import fs from 'fs';
import {
entrypointSizeByExt,
main,
} = require('../../scripts/bundle-size-summary');
} from '../../scripts/internal/bundle-size-summary';
function mockStats(entrypoints) {
function mockStats(entrypoints: object) {
jest
.spyOn(fs, 'readFileSync')
.mockReturnValue(JSON.stringify({ entrypoints }));
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
const { parseRuleId } = require('../../scripts/oxlint-metrics-uploader');
import { parseRuleId } from '../../scripts/internal/oxlint-metrics-uploader';
test('eslint rules keep their bare name', () => {
expect(parseRuleId('eslint(no-console)')).toBe('no-console');
@@ -61,7 +61,7 @@ const irregularDocumentationLinks = {
};
const documentationLink = (engine: string | undefined) => {
if (!engine) return null;
if (!engine) return undefined;
if (supersetTextDocs) {
// override doc link for superset_txt yml
@@ -1315,7 +1315,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
className="preferred-item"
onClick={() => setDatabaseModel(database.name)}
buttonText={database.name}
icon={dbImages?.[database.engine]}
icon={dbImages?.[database.engine] || ''}
key={`${database.name}`}
/>
))}
@@ -17,17 +17,12 @@
* under the License.
*/
/**
* Jest configuration for @storybook/test-runner
*
* This extends the default test-runner config with custom timeouts
* to handle slow story rendering in CI environments.
*/
const { getJestConfig } = require('@storybook/test-runner');
const testRunnerConfig = getJestConfig();
declare module '*.yaml' {
const content: Record<string, any>;
export default content;
}
module.exports = {
...testRunnerConfig,
// Increase timeout from default 15s to 60s for CI environments
testTimeout: 60000,
};
declare module '*.yml' {
const content: Record<string, any>;
export default content;
}
+38 -4
View File
@@ -15,15 +15,49 @@
* specific language governing permissions and limitations
* under the License.
*/
const loadModule = () => {
export interface SupersetTextConfig {
DB_IMAGES?: Record<string, string>;
DB_CONNECTION_ALERTS?: {
DEFAULT?: {
message?: string;
description?: string;
};
ADD_DATABASE?: {
message?: string;
description?: string;
contact_link?: string;
contact_description_link?: string;
};
REGIONAL_IPS?: Record<string, string>;
[key: string]: unknown;
};
DB_CONNECTION_DOC_LINKS?: Record<string, string> & {
default?: string;
support?: string;
};
DB_MODAL_SQLALCHEMY_FORM?: {
SQLALCHEMY_DOCS_URL?: string;
SQLALCHEMY_DISPLAY_TEXT?: string;
};
THEME_MODAL?: {
THEME_EDITOR_URL?: string;
DOCUMENTATION_URL?: string;
};
ERRORS?: Record<string, string>;
[key: string]: unknown;
}
const loadModule = (): SupersetTextConfig => {
try {
// eslint-disable-next-line global-require, import/no-unresolved
return require('../../../superset_text') || {};
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require
const config = require('../../../superset_text.yml') as SupersetTextConfig;
return config || {};
} catch (e) {
return {};
}
};
const supersetText = loadModule();
const supersetText: SupersetTextConfig = loadModule();
export default supersetText;
+2 -2
View File
@@ -904,9 +904,9 @@ export const copyQueryLink = (
});
};
export const getDatabaseImages = () => SupersetText.DB_IMAGES;
export const getDatabaseImages = () => SupersetText?.DB_IMAGES;
export const getConnectionAlert = () => SupersetText.DB_CONNECTION_ALERTS;
export const getConnectionAlert = () => SupersetText?.DB_CONNECTION_ALERTS;
export const getDatabaseDocumentationLinks = () =>
SupersetText.DB_CONNECTION_DOC_LINKS;
+2 -1
View File
@@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
module.exports = {
export default {
customSyntax: 'postcss-styled-syntax',
rules: {
'property-no-unknown': true,
@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
const http = require('http');
const zlib = require('zlib');
const { ZSTDCompress } = require('simple-zstd');
const { createProxyMiddleware } = require('http-proxy-middleware');
import http from 'node:http';
import zlib from 'node:zlib';
import { ZSTDCompress } from 'simple-zstd';
import { createProxyMiddleware } from 'http-proxy-middleware';
// yargs ships ESM-only and jest's default transform doesn't cover
// node_modules; webpack.proxy-config.js only uses it to parse a `--env`
@@ -40,11 +40,11 @@ const HANG_GUARD_MS = 2000;
async function startProxy(backendPort) {
const previousPort = process.env.supersetPort;
// webpack.proxy-config.js resolves its target port from process.env at
// require()-time, so the module must be (re-)required after this is set.
// import-time, so the module must be re-imported after this is set.
process.env.supersetPort = String(backendPort);
jest.resetModules();
// eslint-disable-next-line global-require
const getProxyConfig = require('../webpack.proxy-config');
const { default: getProxyConfig } =
await import('../webpack.proxy-config.js');
process.env.supersetPort = previousPort;
const proxyMiddleware = createProxyMiddleware(getProxyConfig(undefined));
+35 -38
View File
@@ -17,29 +17,40 @@
* specific language governing permissions and limitations
* under the License.
*/
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const { ModuleFederationPlugin } = webpack.container;
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CopyPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MinimizerPlugin = require('minimizer-webpack-plugin');
const LightningCSS = require('lightningcss');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const {
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import webpack from 'webpack';
import * as webpackBundleAnalyzer from 'webpack-bundle-analyzer';
import CopyPlugin from 'copy-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import MinimizerPlugin from 'minimizer-webpack-plugin';
import * as LightningCSS from 'lightningcss';
import SpeedMeasurePlugin from 'speed-measure-webpack-plugin';
import {
WebpackManifestPlugin,
getCompilerHooks,
} = require('webpack-manifest-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const Visualizer = require('webpack-visualizer-plugin2');
const getProxyConfig = require('./webpack.proxy-config');
const packageConfig = require('./package.json');
} from 'webpack-manifest-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import Visualizer from 'webpack-visualizer-plugin2';
import getProxyConfig from './webpack.proxy-config.js';
const { ModuleFederationPlugin } = webpack.container;
const { BundleAnalyzerPlugin } = webpackBundleAnalyzer;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const resolveEsmModule = specifier =>
fileURLToPath(import.meta.resolve(specifier));
const packageConfig = JSON.parse(
fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8'),
);
const parsedArgs = yargs(hideBin(process.argv)).parse();
@@ -478,8 +489,8 @@ const config = {
fs: false,
vm: false,
path: false,
stream: require.resolve('stream-browserify'),
...(isDevMode ? { buffer: require.resolve('buffer/') } : {}), // Fix plugin-chart-paired-t-test broken Story
stream: resolveEsmModule('stream-browserify'),
...(isDevMode ? { buffer: resolveEsmModule('buffer/') } : {}), // Fix plugin-chart-paired-t-test broken Story
},
},
context: APP_DIR, // to automatically find tsconfig.json
@@ -600,20 +611,6 @@ const config = {
test: /\.geojson$/,
type: 'asset/resource',
},
// {
// test: /\.mdx?$/,
// use: [
// {
// loader: require.resolve('@storybook/mdx2-csf/loader'),
// options: {
// skipCsf: false,
// mdxCompileOptions: {
// remarkPlugins: [remarkGfm],
// },
// },
// },
// ],
// },
],
},
externals: {
@@ -748,4 +745,4 @@ if (process.env.BUNDLE_SIZE_STATS) {
config.stats = { all: false, assets: true, entrypoints: true };
}
module.exports = smp.wrap(config);
export default smp.wrap(config);
+8 -8
View File
@@ -16,13 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
const zlib = require('zlib');
const { Writable } = require('stream');
const { pipeline } = require('stream/promises');
const { ZSTDDecompress } = require('simple-zstd');
import zlib from 'node:zlib';
import { Writable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { ZSTDDecompress } from 'simple-zstd';
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const parsedArgs = yargs(hideBin(process.argv)).parse();
@@ -185,7 +185,7 @@ async function processHTML(proxyResponse, response) {
response.end(toDevHTML(Buffer.concat(chunks).toString()));
}
module.exports = newManifest => {
export default function getProxyConfig(newManifest) {
manifest = newManifest;
return {
context: path => {
@@ -269,4 +269,4 @@ module.exports = newManifest => {
}
},
};
};
}