Preventing Day.js Locale Pollution in Large Frontend Projects with Static Analysis
This article recounts a date‑misalignment bug caused by an unintended Day.js locale change in a mini‑program, explains why the global locale must remain unique, and details a comprehensive static‑analysis solution using ESLint, custom Webpack loaders, and Babel plugins to intercept and block Day.js locale modifications at compile time.
Preface – The Root Cause
On a quiet Friday afternoon a product alert revealed a date shift: February 22, which should have been a Thursday, appeared as Wednesday. The issue stemmed from the Day.js library handling dates in the project.
Problem Background
All date handling in the project uses the
dayjslibrary, whose default locale is
en. The locale configuration is the standard way to achieve internationalization (i18n) by adapting language, format, and cultural settings.
Localization enables language support, format adjustment (e.g., date formats), and cultural adaptation, improving global user experience.
Calling
dayjs.localesets a global locale. In the calendar component the code obtains the week start with
dayjs().startOf('week')and generates the seven days by looping six times with
add(1, "day"). The bug appeared because the first day returned was Monday instead of Sunday, indicating that the locale had been changed.
Who Modified the Locale?
The change originated from another business line that altered the Day.js locale in a sub‑package. Because the mini‑program uses sub‑packages to reduce bundle size, the configuration file was unintentionally included in the main bundle, affecting the entire app.
Mini‑Program Sub‑Package Logic
Sub‑packages (
subPackages) load non‑critical modules on demand. However, the calendar component imported a configuration file from a sub‑package, causing the global
dayjs.localecall to be bundled with the main package.
Is the Problem Truly Resolved?
The immediate fix was to extract the configuration into a separate file, preventing unrelated code from being bundled. However, similar issues could recur when other sub‑packages modify the locale.
Requirement Analysis
Goal
Ensure the Day.js locale configuration is globally unique across the project to avoid accidental pollution.
Solution Approach
Split the work into governance of existing code and control of new code .
Engineering Governance
Search for all
dayjs.locale()calls, assess impact, and remove them. The team decided to keep the default
enlocale rather than switching to
zh‑cn, which would require extensive changes.
Standardized Control
Implement compile‑time interception to forbid calls that modify the global locale. This requires static analysis of both project source files and third‑party packages in
node_modules.
Code Design and Implementation
Technical Selection
Static Analysis Principle
Static analysis examines code without execution, using tools such as ESLint, Webpack, and Babel. It can be performed via simple string matching or more precise AST (Abstract Syntax Tree) traversal.
ESLint
Using ESLint’s
no-restricted-syntaxrule to block
dayjs.localecalls:
<code>module.exports = {
rules: {
'no-restricted-syntax': ['error', {
selector: "CallExpression[callee.object.name='dayjs'][callee.property.name='locale']",
message: 'Calling dayjs.locale is prohibited'
}]
}
};
</code>For more robust detection, a custom ESLint plugin records imported identifiers and checks member expressions.
<code>module.exports = {
rules: {
'no-dayjs-locale': {
create(context) {
const dayjsIds = new Set();
return {
ImportDeclaration(node) {
if (node.source.value === 'dayjs') {
node.specifiers.forEach(s => dayjsIds.add(s.local.name));
}
},
MemberExpression(node) {
if (dayjsIds.has(node.object.name) && node.property.name === 'locale') {
context.report({ node, message: 'dayjs.locale is not allowed' });
}
}
};
}
}
}
};
</code>Webpack Loader
A custom loader can scan source strings for
dayjs.locale(and emit a compilation error:
<code>module.exports = function(source) {
if (source.includes('dayjs.locale(')) {
this.emitError(new Error(`Forbidden call to dayjs.locale() in ${this.resourcePath}`));
}
return source;
};
</code>However, simple string matching fails when the import is renamed, so an AST‑based loader is preferred.
AST‑Based Loader (Acorn + Estraverse)
<code>const acorn = require('acorn');
const estraverse = require('estraverse');
module.exports = function(source) {
const ast = acorn.parse(source, { ecmaVersion: 2020, sourceType: 'module' });
estraverse.replace(ast, {
enter(node) {
if (node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'dayjs' &&
node.callee.property.name === 'locale') {
const arg = node.arguments[0];
if (arg && arg.type === 'Literal' && arg.value === 'fr') {
arg.value = 'en'; // force default locale
}
}
}
});
const escodegen = require('escodegen');
return escodegen.generate(ast);
};
</code>Babel Plugin
A Babel plugin tracks imported identifiers and rewrites or blocks
.locale()calls:
<code>module.exports = function({ types: t }) {
let dayjsId = null;
return {
visitor: {
ImportDeclaration(path) {
if (path.node.source.value === 'dayjs') {
const spec = path.node.specifiers[0];
if (t.isImportDefaultSpecifier(spec)) dayjsId = spec.local.name;
}
},
MemberExpression(path) {
if (dayjsId && t.isIdentifier(path.node.object, { name: dayjsId }) &&
t.isIdentifier(path.node.property, { name: 'locale' })) {
const call = path.parentPath;
if (call.isCallExpression()) {
const args = call.node.arguments;
if (args.length && t.isStringLiteral(args[0])) {
args[0] = t.stringLiteral('en'); // enforce default
}
}
}
}
}
};
};
</code>Generic Ban‑Usage Plugin
The team later adopted a configurable plugin that can forbid specific imports or method calls (e.g.,
dayjs.default.extend,
dayjs.default.locale) across both source code and third‑party packages.
<code>module.exports = {
plugins: [
["@guming/babel-plugin-ban-usage", {
rules: [
{ source: "dayjs", ban: ["default.extend", "default.locale"] },
{ source: "dayjs/plugin/xxx", import: false }
]
}]
]
};
</code>Summary
The article demonstrates how the team identified a subtle Day.js locale mutation that broke date calculations, and how they established a robust static‑analysis pipeline—leveraging ESLint, custom Webpack loaders, and Babel plugins—to enforce a single global locale configuration and prevent future accidental modifications.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.