Compare commits

...

2 Commits

Author SHA1 Message Date
Superset Dev
a99002f4aa fix(build): correct Slovenian/pt_BR locale keys, echarts locale typing, ruff format
Address review-bot findings and CI failures:

- The LOCALE_LOADERS keys are Superset locale codes uppercased, but the
  Slovenian entry used echarts' file naming (SI) and could never match
  Superset's `sl`; key it SL -> langSI.js. Add the same-shaped mapping
  for pt_BR -> langPT-br.js. Both locales silently fell back to English
  on master as well (the old computed import requested langSL.js /
  langPT_BR.js, which don't exist), so this is a behavior fix, not just
  a map cleanup.
- Type the locale bundles as Parameters<typeof registerLocale>[1]
  instead of `object`: the per-package tsc build (stricter than the
  repo-root typecheck) rejects passing `object` to registerLocale.
- ruff-format whitespace in config.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:05:13 -07:00
Superset Dev
3459267625 fix(build): deterministic echarts locale imports; fingerprint loaded config
Two dev-stack reliability fixes:

- plugin-chart-echarts loaded locales via a template-literal dynamic
  import, which makes bundlers build a context module over echarts/i18n.
  Resolving that directory through the echarts package exports map fails
  intermittently in webpack incremental builds ("Package path ./i18n is
  exported ... but no valid target file was found"), leaving the dev
  bundle broken until a full rebuild. Replace the computed import with an
  explicit per-locale loader map: no context module, deterministic
  resolution in every bundler, and code-splitting covers exactly the
  locales listed.

- Log a short md5 fingerprint of the superset_config file bytes as read
  at import time, alongside the existing "Loaded your LOCAL
  configuration" line. Auto-reloaders re-import config on file change,
  and on some filesystems (notably macOS Docker mounts) the re-read can
  race the write and observe stale content while still loading
  "successfully" — with only the path logged, that skew is invisible.
  Comparing the logged digest against `md5 <path>` on the host makes it
  a one-line diagnosis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:20:12 -07:00
3 changed files with 91 additions and 7 deletions

View File

@@ -121,14 +121,62 @@ use([
LabelLayout,
]);
// Explicit per-locale imports rather than a template-literal dynamic
// import: a computed import makes bundlers build a "context module" over
// echarts/i18n, and resolving that directory through the echarts package's
// `exports` map fails intermittently in webpack incremental builds
// ("Package path ./i18n is exported ... but no valid target file was
// found"). A static map is also the only thing that lets bundlers
// code-split exactly the locales listed here. Keys are Superset locales
// uppercased (see LANGUAGES in superset/config.py); values point at the
// echarts bundle, whose naming differs for some locales (Slovenian is
// langSI, Brazilian Portuguese is langPT-br). Superset locales absent
// from this map fall back to English.
type EChartsLocaleOption = Parameters<typeof registerLocale>[1];
const LOCALE_LOADERS: Record<
string,
() => Promise<{ default: EChartsLocaleOption }>
> = {
AR: () => import('echarts/i18n/langAR.js'),
CS: () => import('echarts/i18n/langCS.js'),
DE: () => import('echarts/i18n/langDE.js'),
EL: () => import('echarts/i18n/langEL.js'),
EN: () => import('echarts/i18n/langEN.js'),
ES: () => import('echarts/i18n/langES.js'),
FA: () => import('echarts/i18n/langFA.js'),
FI: () => import('echarts/i18n/langFI.js'),
FR: () => import('echarts/i18n/langFR.js'),
HU: () => import('echarts/i18n/langHU.js'),
IT: () => import('echarts/i18n/langIT.js'),
JA: () => import('echarts/i18n/langJA.js'),
KO: () => import('echarts/i18n/langKO.js'),
LV: () => import('echarts/i18n/langLV.js'),
NL: () => import('echarts/i18n/langNL.js'),
PL: () => import('echarts/i18n/langPL.js'),
RO: () => import('echarts/i18n/langRO.js'),
PT_BR: () => import('echarts/i18n/langPT-br.js'),
RU: () => import('echarts/i18n/langRU.js'),
SL: () => import('echarts/i18n/langSI.js'),
SV: () => import('echarts/i18n/langSV.js'),
TH: () => import('echarts/i18n/langTH.js'),
TR: () => import('echarts/i18n/langTR.js'),
UK: () => import('echarts/i18n/langUK.js'),
VI: () => import('echarts/i18n/langVI.js'),
ZH: () => import('echarts/i18n/langZH.js'),
};
const loadLocale = async (locale: string) => {
let lang;
try {
lang = await import(`echarts/i18n/lang${locale}.js`);
} catch {
const loader = LOCALE_LOADERS[locale];
if (!loader) {
// Locale not supported in ECharts
return undefined;
}
try {
return (await loader()).default;
} catch {
return undefined;
}
return lang?.default;
};
// Report/thumbnail screenshots use standalone="true" (charts) or 3 (reports);

View File

@@ -23,3 +23,11 @@ declare module '*.png' {
}
declare module '*.jpg';
// echarts publishes its i18n bundles through the package `exports` map
// without type declarations; the locale loaders in components/Echart.tsx
// import them explicitly. Their shape is whatever registerLocale accepts.
declare module 'echarts/i18n/lang*' {
const lang: Parameters<typeof import('echarts/core').registerLocale>[1];
export default lang;
}

View File

@@ -25,6 +25,7 @@ at the end of this file.
# pylint: disable=too-many-lines
from __future__ import annotations
import hashlib
import importlib.util
import json
import logging
@@ -3041,6 +3042,28 @@ TASKS_ABORT_CHANNEL_PREFIX = "gtf:abort:"
# -------------------------------------------------------------------
# Don't add config values below this line since local configs won't be
# able to override them.
def _config_fingerprint(config_file: str | None) -> str:
"""
A short digest of the config file's bytes as read at import time.
Auto-reloaders (e.g. werkzeug's) re-import this module when the config
file changes, and on some filesystems (notably macOS Docker mounts) the
re-read can race the write and observe stale content while still
"loading successfully". Logging the digest of what was *actually read*
makes that skew diagnosable: compare it against
``md5 <path>`` on the host.
"""
if not config_file:
return "unknown"
try:
with open(config_file, "rb") as fh:
return hashlib.md5(fh.read()).hexdigest()[:12] # noqa: S324
except OSError:
return "unreadable"
if CONFIG_PATH_ENV_VAR in os.environ:
# Explicitly import config module that is not necessarily in pythonpath; useful
# for case where app is being executed via pex.
@@ -3054,7 +3077,11 @@ if CONFIG_PATH_ENV_VAR in os.environ:
if key.isupper():
setattr(module, key, getattr(override_conf, key))
click.secho(f"Loaded your LOCAL configuration at [{cfg_path}]", fg="cyan")
click.secho(
f"Loaded your LOCAL configuration at [{cfg_path}] "
f"(md5:{_config_fingerprint(cfg_path)})",
fg="cyan",
)
except Exception:
logger.exception(
"Failed to import config for %s=%s", CONFIG_PATH_ENV_VAR, cfg_path
@@ -3067,7 +3094,8 @@ elif importlib.util.find_spec("superset_config"):
from superset_config import * # noqa: F403, F401
click.secho(
f"Loaded your LOCAL configuration at [{superset_config.__file__}]",
f"Loaded your LOCAL configuration at [{superset_config.__file__}] "
f"(md5:{_config_fingerprint(superset_config.__file__)})",
fg="cyan",
)
except Exception: