Back to .md Directory

Plan: Modernization of bem-core Dependencies

Maps a full migration from Node 8, ENB, and Bower to Node 24, Vite 6, and ESLint 10 across 9 phases.

May 2, 2026
0 downloads
0 views
ai
View source

What this file does

Maps a full migration from Node 8, ENB, and Bower to Node 24, Vite 6, and ESLint 10 across 9 phases.

When to use it

  • You maintain a BEM project stuck on abandoned build tools like ENB or JSCS
  • You need to upgrade a monorepo from lockfile v1 to v3 and drop Bower
  • You plan to replace a custom module system (ym) with standard ES modules
  • You want a phased, risk-mitigated plan for replacing a legacy CI system

Assumes this stack

Node.jsnpmViteESLintPlaywrightGitHub Actions

Plan: Modernization of bem-core Dependencies

Current State Analysis

Runtime Environment

ComponentCurrentTargetStatus
Node.js824 LTS (Krypton, v24.13.1)16 major versions behind
npm5-6 (lockfile v1)11+ (lockfile v3)Needs regeneration

Dependencies — Current vs Latest

Production (dependencies)

PackageCurrentLatestLast PublishedStatus
ym^0.1.20.1.2ancientAbandoned. BEM module system. No updates for years.

Dev Dependencies (devDependencies)

Build System (ENB) — ALL ABANDONED:

PackageCurrentLatestLast PublishedStatus
enb^1.2.01.5.12017-11Abandoned
enb-bem-techs^2.2.22.2.22017-12Abandoned
enb-magic-factory^0.6.00.6.02018-02Abandoned
enb-magic-platform0.7.00.7.02016-04Abandoned
enb-bemxjst^8.10.28.10.6~2018Abandoned
enb-bemxjst-6x^6.5.3Abandoned
enb-bemxjst-7x^7.3.1Abandoned
enb-bemxjst-i18n1.0.0-beta3Abandoned
enb-bh^1.2.1Abandoned
enb-bh-i18n1.0.0-beta2Abandoned
enb-borschik^2.8.0Abandoned
enb-css^1.2.21.2.2Abandoned
enb-js^1.1.11.1.1Abandoned
enb-bem-docs0.14.10.15.02019-02Abandoned
enb-bem-examples^1.0.21.0.22016-04Abandoned
enb-bem-specs^0.11.00.11.02016-12Abandoned
enb-bem-tmpl-specs^1.3.31.3.32018-03Abandoned
enb-bem-i18n^1.1.1Abandoned

Linting — ABANDONED/OUTDATED:

PackageCurrentLatestLast PublishedStatus
jscs^2.11.03.0.72016-07Abandoned (merged into ESLint)
jscs-bem^0.2.0Abandoned
jshint^2.9.12.13.6maintainedFunctional but superseded by ESLint
jshint-groups^0.8.0Abandoned

Testing — PARTIALLY ABANDONED:

PackageCurrentLatestLast PublishedStatus
mocha^3.3.011.7.5active8 major versions behind
mocha-phantomjs^4.1.04.1.02016-06Abandoned (PhantomJS is dead)
chai^3.2.06.2.2active3 major versions behind; v5+ is ESM-only
chai-as-promised^5.1.0Outdated
istanbul^0.4.30.4.52016-08Abandoned (replaced by nyc → c8)

BEM Tools:

PackageCurrentLatestLast PublishedStatus
bem-naming^1.0.11.0.1Abandoned
bem-walk1.0.0-alpha11.0.0-1Never left alpha

Other:

PackageCurrentLatestLast PublishedStatus
borschik^1.5.33.0.02021-02Unmaintained
bower^1.7.91.8.142022-03Deprecated since 2017
git-hooks^1.0.21.1.10Superseded by husky
gitbook-api^3.0.2Abandoned
jsdoc^3.5.54.0.5active1 major version behind
vow^0.4.170.4.202019-07Abandoned (native Promises exist)

Configuration Files to Replace

FilePurposeModern Replacement
.jshintrcJSHint configeslint.config.js (ESLint flat config)
.jscs.jsonJSCS style configeslint.config.js (ESLint flat config)
.jshint-groups.jsJSHint groups configeslint.config.js (ESLint flat config)
.bowerrcBower directory configRemove (drop Bower)
bower.jsonBower package manifestRemove (drop Bower)
.travis.ymlTravis CI config.github/workflows/ci.yml (GitHub Actions)
.enb/ (entire dir)ENB build configNew build system config
.githooks/pre-commit/lintPre-commit hook.husky/pre-commit

CI/CD

ComponentCurrentTarget
CI systemTravis CIGitHub Actions
CoverageIstanbul + Coverallsc8 + Coveralls (or Codecov)
Node.js in CI824

Implementation Plan

Phase 0: Preparation

  1. Create feature branch claude/update-dependencies-fWO1e
  2. Verify project builds and tests in current state (baseline)

Phase 1: Node.js & npm Modernization

  1. Add .nvmrc with 24
  2. Add engines field to package.json: "node": ">=24", "npm": ">=11"
  3. Delete package-lock.json (will regenerate with lockfile v3)

Phase 2: Remove Abandoned/Deprecated Tools

  1. Remove Bower: delete bower.json, .bowerrc, remove bower i from scripts
  2. Remove JSCS: delete .jscs.json, uninstall jscs, jscs-bem
  3. Remove JSHint: delete .jshintrc, .jshint-groups.js, uninstall jshint, jshint-groups
  4. Remove Istanbul: uninstall istanbul
  5. Remove mocha-phantomjs: uninstall mocha-phantomjs
  6. Remove gitbook-api: uninstall gitbook-api
  7. Remove git-hooks: delete .githooks/ directory, uninstall git-hooks

Phase 3: Linting — Migrate to ESLint 10

  1. Install eslint@^10.0.1
  2. Create eslint.config.js (flat config, required for ESLint 10) migrating rules from:
    • .jshintrc rules → ESLint equivalents
    • .jscs.json BEM preset rules → ESLint equivalents
    • .jshint-groups.js file-group-specific overrides → ESLint flat config overrides
  3. Support file extensions: .js, .bemtree, .bemhtml
  4. Update package.json lint script: "lint": "eslint ."
  5. Delete old config files: .jshintrc, .jscs.json, .jshint-groups.js

Phase 4: Testing Modernization

  1. Upgrade Mocha: mocha@^11.7.5
  2. Upgrade Chai: chai@^6.2.2 (ESM-only — requires "type": "module" or .mjs for test files using it)
    • Alternative: stay on chai@^4.x (last CJS version) if ESM migration is too invasive
  3. Replace Istanbul with c8: install c8@^10.1.3
  4. Replace mocha-phantomjs with Playwright: install playwright@^1.58.2 and @playwright/test
    • Browser spec tests (.spec.js files using modules.define) need adaptation for Playwright
  5. Update test scripts in package.json

Phase 5: Build System — ENB → Vite 6 + BEM Levels Plugin

This is the highest-risk, highest-effort phase. The entire ENB ecosystem (17+ packages) is abandoned. Chosen approach: Vite 6 with custom vite-plugin-bem-levels for BEM level resolution + barrel file generation, plus dedicated plugins for BEMHTML/BH template compilation and i18n.

What ENB Currently Does (to be replaced)

ENB FunctionPackagesVite Replacement
BEM level scanning & file resolutionenb-bem-techsvite-plugin-bem-levels (custom)
JS bundling with ym module systemenb-js + ymVite native ES modules + barrel files
Module redefinition (modules.define chains)ym runtimePlatform barrel files (auto-generated)
CSS concatenationenb-cssVite native CSS handling
BEMHTML template compilationenb-bemxjst, enb-bemxjst-6x, enb-bemxjst-7xvite-plugin-bemhtml (custom, wraps bem-xjst)
BH template compilationenb-bhvite-plugin-bh (custom, wraps bh)
i18n keysets processingenb-bem-i18n, enb-bemxjst-i18n, enb-bh-i18nvite-plugin-bem-i18n (custom)
Minification (borschik)enb-borschik, borschikVite built-in (esbuild/terser for JS, lightningcss for CSS)
HTML from BEMJSONenb-bemxjst, enb-bhBuild script using compiled templates
Examples/tests/specs buildingenb-bem-examples, enb-bem-specs, enb-bem-docs, enb-magic-*Vite dev server + Playwright

Phase 5.0: Preparation & Coexistence

ENB and Vite will coexist during migration. ENB stays functional until Vite fully replaces it.

  1. Install Vite 6 and core dependencies:
    npm i -D vite@^6 @anthropic-ai/vite-plugin-bem-levels
    
    (Initially vite-plugin-bem-levels will live in build/plugins/ as a local module)
  2. Create build directory structure:
    build/
    ├── plugins/
    │   ├── vite-plugin-bem-levels.js    — BEM level resolution + barrel generation
    │   ├── vite-plugin-bemhtml.js       — BEMHTML compilation
    │   ├── vite-plugin-bh.js            — BH compilation
    │   └── vite-plugin-bem-i18n.js      — i18n keysets
    ├── platforms/
    │   ├── desktop.js                   — entry point for desktop platform
    │   └── touch.js                     — entry point for touch platform
    └── vite.config.js                   — main Vite config
    
  3. Keep .enb/ directory intact for fallback

Phase 5.1: vite-plugin-bem-levels — Core BEM Resolution Plugin

This is the central piece. The plugin:

5.1.1. Level scanning — scans BEM levels like ENB does:

// Configuration mirrors .enb/config/levels.js
const LEVELS = {
    common: ['common.blocks'],
    desktop: ['common.blocks', 'desktop.blocks'],
    touch: ['common.blocks', 'touch.blocks']
};

5.1.2. Virtual module resolution — resolves bem:* imports:

// In source code:
import $ from 'bem:jquery';
import bemDom from 'bem:i-bem-dom';

// Plugin resolves 'bem:jquery' → generated barrel file

5.1.3. Barrel file generation for module redefinition chains

For each module with redefinitions, generates a platform-specific barrel file. Based on analysis, there are exactly 5 modules with redefinitions (8 total redefinition instances):

ModuleFiles in chainGenerated barrel
jquerybase + 4 redefinitions (3 common + 1 desktop)@bem/desktop/jquery.js, @bem/touch/jquery.js
jquery__configbase (common) + 1 redefinition (desktop)@bem/desktop/jquery__config.js
uaalternative defs (desktop vs touch) + 1 redefinition (touch)@bem/desktop/ua.js, @bem/touch/ua.js
events__observablebase + 1 redefinition@bem/common/events__observable.js
i-bem-dom__initbase + dynamic redefinitionSpecial handling (see below)

Example generated barrel for jquery on desktop platform:

// @generated by vite-plugin-bem-levels for platform: desktop
import { $ } from '../../common.blocks/jquery/jquery.js';

// Redefinition: pointer events polyfill (mutates $.event.special)
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointernative.js';

// Redefinition: pointerclick event
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointerclick.js';

// Redefinition: pointerpress/pointerrelease events
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointerpressrelease.js';

// Redefinition: IE8 window resize fix (desktop only)
import '../../desktop.blocks/jquery/__event/_type/jquery__event_type_winresize.js';

export { $ };

5.1.4. Automatic redefinition detection

The plugin scans all .js files in BEM levels and detects modules.define('name', ...) calls:

  • If a module name appears in multiple files → it's a redefinition chain
  • Files are ordered by level priority (common < desktop/touch)
  • Within a level, element/modifier files redefine block files

5.1.5. deps.js → import graph

Parse existing .deps.js files and generate import statements:

// From: { shouldDeps: [{ block: 'events' }] }
// To:   import 'bem:events';

This runs as a build-time code generation step, not at runtime.

Phase 5.2: ym → ES Modules Migration

5.2.1. Source file transformation — Each modules.define file gets an ES module equivalent:

Before (ym):

modules.define('jquery', ['loader_type_js', 'jquery__config'],
    function(provide, loader, cfg) {
        // ...
        provide(jQuery);
    });

After (ES module):

import loader from 'bem:loader_type_js';
import cfg from 'bem:jquery__config';

let jQuery;
// ... loading logic ...
export default jQuery;

5.2.2. Redefinition files — become side-effect imports or wrapper modules:

Before (ym redefinition):

modules.define('jquery', function(provide, $) {
    $.event.special.pointerclick = { /* ... */ };
    provide($);
});

After (ES module side-effect):

import $ from 'bem:jquery';  // gets the base jquery
$.event.special.pointerclick = { /* ... */ };
// No export needed — this is a side-effect module imported by the barrel

5.2.3. Scope of ym migration

CategoryCountMigration complexity
.vanilla.js files (never redefined)~13Low — straightforward export default
.js files (base definitions, no redefinition)~30Low — import deps + export default
.js files (redefinition participants)14Medium — need barrel coordination
i-bem-dom__init dynamic redefinition1High — needs architectural redesign

5.2.4. i-bem-dom__init special case

The dynamic modules.define monkey-patching in i-bem-dom.js (lines 1141-1158) cannot be directly expressed in ES modules. Solution:

  • The Vite plugin generates the i-bem-dom__init barrel by scanning which blocks depend on i-bem-dom
  • This replaces the runtime monkey-patching with build-time dependency collection
  • The barrel imports all BEM DOM blocks, then calls bemDom.init()

Phase 5.3: Template Engine Plugins

5.3.1. vite-plugin-bemhtml

  • Handles .bemhtml and .bemhtml.js files
  • Wraps bem-xjst compiler (keep as dependency)
  • Produces compiled JS that can be imported as ES module
  • Supports HMR in dev mode

5.3.2. vite-plugin-bh

  • Handles .bh.js files
  • Wraps bh runtime
  • Produces CommonJS-compatible bundle (BH uses module.exports)

Phase 5.4: i18n Plugin

vite-plugin-bem-i18n

  • Scans *.i18n/ directories for keysets
  • Generates per-language JS modules
  • Supports {lang} placeholder pattern from current ENB config
  • Integrates with BEMHTML i18n via bem-xjst

Phase 5.5: Vite Configuration

// build/vite.config.js
import { defineConfig } from 'vite';
import bemLevels from './plugins/vite-plugin-bem-levels.js';
import bemhtml from './plugins/vite-plugin-bemhtml.js';
import bh from './plugins/vite-plugin-bh.js';
import bemI18n from './plugins/vite-plugin-bem-i18n.js';

export default defineConfig(({ mode }) => {
    const platform = process.env.BEM_PLATFORM || 'desktop';

    return {
        plugins: [
            bemLevels({
                platform,
                levels: {
                    common: ['common.blocks'],
                    desktop: ['common.blocks', 'desktop.blocks'],
                    touch: ['common.blocks', 'touch.blocks']
                }
            }),
            bemhtml(),
            bh({ jsAttrName: 'data-bem', jsAttrScheme: 'json' }),
            bemI18n({ langs: ['ru', 'en'] })
        ],
        build: {
            lib: {
                entry: `./build/platforms/${platform}.js`,
                name: 'bemCore',
                formats: ['es', 'umd']
            },
            outDir: `dist/${platform}`,
            rollupOptions: {
                output: {
                    // Reproduce ENB dist structure:
                    // bem-core.js, bem-core.css, bem-core.bemhtml.js, etc.
                }
            }
        }
    };
});

Phase 5.6: dist Task Replacement

ENB dist task currently produces these artifacts per platform:

ArtifactdevminVite equivalent
bem-core.css.dev.css.cssVite CSS output (dev: unminified, build: minified)
bem-core.js.dev.js.jsVite JS bundle (with autoinit)
bem-core.no-autoinit.js.dev.no-autoinit.js.no-autoinit.jsSeparate entry point without i-bem-dom__init_auto
bem-core.bemhtml.js.dev.bemhtml.js.bemhtml.jsBEMHTML-only bundle via separate entry
bem-core.bh.js.dev.bh.js.bh.jsBH-only bundle via separate entry
bem-core.js+bemhtml.js.dev.js+bemhtml.js.js+bemhtml.jsCombined bundle (JS + BEMHTML)
bem-core.js+bh.js.dev.js+bh.js.js+bh.jsCombined bundle (JS + BH)

Vite handles dev/production modes natively (no borschik needed).

npm scripts:

{
    "build": "npm run build:desktop && npm run build:touch",
    "build:desktop": "BEM_PLATFORM=desktop vite build -c build/vite.config.js",
    "build:touch": "BEM_PLATFORM=touch vite build -c build/vite.config.js",
    "dev": "BEM_PLATFORM=desktop vite -c build/vite.config.js"
}

Phase 5.7: specs/tests/examples Migration

  1. Browser specs (*.spec.js) — Currently use enb-bem-specs + mocha-phantomjs:

    • Migrate to Playwright (already planned in Phase 4)
    • Vite dev server serves spec pages instead of ENB magic nodes
    • vite-plugin-bem-levels resolves spec level: libs/bem-pr/spec.blocks
  2. Examples (*.examples/) — Currently use enb-bem-examples:

    • Vite dev server with HTML plugin serves example pages
    • BEMJSON → HTML conversion done via imported compiled templates
  3. Template specs (tmpl-specs) — Currently use enb-bem-tmpl-specs:

    • Run as Node.js tests with Mocha (import compiled templates, compare output)

Phase 5.8: Cleanup

  1. Delete .enb/ directory entirely (17 files)
  2. Uninstall all ENB packages (17 packages):
    npm rm enb enb-bem-techs enb-magic-factory enb-magic-platform \
      enb-bemxjst enb-bemxjst-6x enb-bemxjst-7x enb-bemxjst-i18n \
      enb-bh enb-bh-i18n enb-borschik enb-css enb-js \
      enb-bem-docs enb-bem-examples enb-bem-specs enb-bem-tmpl-specs \
      enb-bem-i18n
    
  3. Uninstall borschik (replaced by Vite built-in minification)
  4. Remove ym from production dependencies (replaced by ES modules)
  5. Remove bem-walk and bem-naming if no longer used outside ENB
  6. Update package.json scripts to use Vite commands

Phase 5 Sub-Phase Execution Order

5.0 Preparation & coexistence setup
 │
 ├── 5.1 vite-plugin-bem-levels (CRITICAL PATH — everything depends on this)
 │    ├── 5.1.1 Level scanning
 │    ├── 5.1.2 Virtual module resolution (bem:* imports)
 │    ├── 5.1.3 Barrel file generation
 │    ├── 5.1.4 Redefinition detection
 │    └── 5.1.5 deps.js parsing
 │
 ├── 5.2 ym → ES modules migration (can start after 5.1 is functional)
 │    ├── 5.2.1 .vanilla.js files (easiest, start here)
 │    ├── 5.2.2 .js base definitions
 │    ├── 5.2.3 .js redefinition files
 │    └── 5.2.4 i-bem-dom__init special case
 │
 ├── 5.3 Template plugins (parallel with 5.2)
 │    ├── 5.3.1 vite-plugin-bemhtml
 │    └── 5.3.2 vite-plugin-bh
 │
 ├── 5.4 i18n plugin (parallel with 5.2, 5.3)
 │
 ├── 5.5 Vite config (after 5.1-5.4 plugins exist)
 │
 ├── 5.6 dist replacement (after 5.5)
 │    └── Verify output matches ENB dist artifacts
 │
 ├── 5.7 specs/tests/examples (after 5.6)
 │
 └── 5.8 Cleanup (LAST — only after full verification)

Phase 5 Risk Mitigation

RiskImpactMitigation
Barrel file import order matters for side-effectsHighPlugin sorts by level priority; test thoroughly
i-bem-dom__init dynamic dep collection hard to replicateHighBuild-time scanning replaces runtime monkey-patching
bem-xjst may not work as Vite pluginMediumKeep as Node.js pre-compilation step if needed
Circular dependencies between BEM blocksMediumVite handles circular ESM; add cycle detection to plugin
Output bundle size differs from ENBLowCompare sizes; adjust Rollup chunking
Dev server HMR with BEM redefinitionsLowRegenerate barrels on file change; full reload as fallback

Phase 6: Git Hooks Modernization

  1. Install husky@^9.1.7 + lint-staged@^16.2.7
  2. Configure .husky/pre-commit to run lint-staged
  3. Configure lint-staged in package.json to run ESLint on staged files
  4. Delete .githooks/ directory

Phase 7: CI/CD — Travis CI → GitHub Actions

  1. Create .github/workflows/ci.yml:
    • Matrix: Node.js 24
    • Steps: install, lint, test
    • Coverage: c8 + upload to Coveralls
  2. Delete .travis.yml

Phase 8: Update Remaining Packages

  1. mocha → ^11.7.5
  2. jsdoc → ^4.0.5
  3. vow → replace with native Promise where possible; remove if fully replaced
  4. borschik → remove (replaced by Vite built-in minification in Phase 5.8)
  5. bem-naming → remove if only used by ENB plugins; keep if used by vite-plugin-bem-levels
  6. bem-walk → remove (replaced by custom level scanning in vite-plugin-bem-levels)
  7. ym → remove from production deps (replaced by ES modules in Phase 5.2)

Phase 9: Regenerate Lock File & Validate

  1. Run npm install to generate new package-lock.json (lockfile v3)
  2. Run npm run lint — fix any ESLint issues
  3. Run npm run build — verify Vite builds succeed for both platforms
  4. Run npm run test — verify all tests pass
  5. Compare Vite dist output with archived ENB dist output (size, functionality)
  6. Run examples in Vite dev server, verify they work

Risk Assessment

RiskImpactMitigation
vite-plugin-bem-levels barrel generation order incorrectCRITICALComprehensive tests comparing ENB and Vite output
i-bem-dom__init dynamic monkey-patching hard to replicate staticallyHIGHBuild-time BEM block scanning replaces runtime logic
bem-xjst (BEMHTML compiler) integration with Vite pluginHighKeep as pre-compilation step if direct plugin fails
Chai 6.x ESM-only breaks test importsMediumUse Chai 4.x (last CJS) or add ESM wrapper
mocha-phantomjs removal breaks browser testsHighPlaywright migration for spec tests
Circular dependencies between BEM blocks in ES modulesMediumVite handles circular ESM natively; add cycle detection
BEM-specific spec files (modules.define) won't work without ymHighMigrate spec files to ES imports as part of Phase 5.2
Output bundle size/behavior differs from ENBMediumComparison testing: run both builds, diff output
ym → ES modules migration introduces regressionsHighPhased migration with coexistence; ENB stays as reference

Suggested Execution Order (by priority/safety)

  1. Phase 0 — Baseline
  2. Phase 1 — Node.js 24 (needed for Vite 6 and modern tooling)
  3. Phase 2 — Remove dead packages (safe, reduces surface)
  4. Phase 3 — ESLint migration (independent, high value)
  5. Phase 6 — Git hooks (small, independent)
  6. Phase 7 — GitHub Actions (independent)
  7. Phase 8 — Update remaining packages
  8. Phase 4 — Testing modernization (Playwright needed for Phase 5.7)
  9. Phase 5 — Build system migration to Vite (largest effort):
    • 5.0 → 5.1 → 5.2 + 5.3 + 5.4 (parallel) → 5.5 → 5.6 → 5.7 → 5.8
  10. Phase 9 — Final validation (verify all Vite builds match ENB output)

What's inside

9 phases, 4 dependency tables, 1 sub-phase tree, 1 risk matrix, 1 execution order

Change this for your project

  • Replace claude/update-dependencies-fWO1e with your own feature branch name
  • Replace @anthropic-ai/vite-plugin-bem-levels with your own plugin package name
  • Replace BEM_PLATFORM environment variable with your platform naming convention
  • Replace libs/bem-pr/spec.blocks with your own spec level path

Where it goes

Keep it in your repository where the agent or team that needs it will read it.

Worth borrowing

  • Coexistence phase where old and new build systems run side by side until the new one is verified
  • Barrel file generation for module redefinition chains, replacing runtime monkey-patching with build-time dependency collection
  • Risk table with concrete mitigations for each migration step, not just generic warnings

Related Documents