This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch refactor/layer-dashboards-admin-decompose in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 25f77a8156d73e3740b3f385262300291f8e50a9 Author: Wu Sheng <[email protected]> AuthorDate: Sun Jun 28 00:21:20 2026 +0800 chore(ci): enforce per-file source-size budget (2000 code / 500 comment lines) ESLint max-lines (2000 lines of code, comments and blank lines excluded) across ui + bff, plus a comment-volume budget script (≤500 comment lines per file) that ESLint can't express, wired into `pnpm lint` and `pnpm lint:budget`. A new CI `lint` job runs both and is added to the Required status rollup, so per-file source size and style are CI-gated for the first time. The previous 1000-line cap is raised to 2000 (a dense single-file component legitimately carries a large cohesive template/style block; 1000 shredded coupled CSS). LayerDashboardsAdmin.vue stays grandfathered here; its decomposition lands in the next commit and removes the exemption. --- .github/workflows/ci.yaml | 21 +++++++++- apps/bff/eslint.config.mjs | 7 ++-- apps/ui/eslint.config.mjs | 34 +++++----------- package.json | 3 +- scripts/check-source-budget.mjs | 87 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d9781fe..118b374 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -161,13 +161,31 @@ jobs: # needed — keeps the CI line robust against pnpm-flag parsing quirks. - run: pnpm -r run test:unit + lint: + name: Lint + source budget + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + # ESLint (rules + 2000-line cap, comments excluded) across ui + bff, + # then the source-budget gate (≤2000 code / ≤500 comment lines per file). + - run: pnpm lint + required: # Status-check name pinned to "Required" so Apache branch protection # (`.asf.yaml: protected_branches.main.required_status_checks.contexts: # [Required]`) can gate on a single rolled-up signal. if: always() && !cancelled() name: Required - needs: [license-header, dependency-license, type-check, build-ui, build-bff, test] + needs: [license-header, dependency-license, type-check, build-ui, build-bff, test, lint] runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -179,3 +197,4 @@ jobs: [[ ${{ needs.build-ui.result }} == 'success' ]] || exit 1 [[ ${{ needs.build-bff.result }} == 'success' ]] || exit 1 [[ ${{ needs.test.result }} == 'success' ]] || exit 1 + [[ ${{ needs.lint.result }} == 'success' ]] || exit 1 diff --git a/apps/bff/eslint.config.mjs b/apps/bff/eslint.config.mjs index 9c1dd18..c55fb19 100644 --- a/apps/bff/eslint.config.mjs +++ b/apps/bff/eslint.config.mjs @@ -39,11 +39,10 @@ export default tseslint.config( }, }, { - // File-size guardrail. No NEW file may exceed 1000 lines — split it - // instead. The grandfather list below is the decomposition backlog: remove - // a file when its split lands; never add to it. + // File-size guardrail. No file may exceed 2000 lines of code (comments + // and blank lines excluded) — split it instead. files: ['src/**/*.ts'], ignores: ['**/*.test.ts'], - rules: { 'max-lines': ['error', 1000] }, + rules: { 'max-lines': ['error', { max: 2000, skipComments: true, skipBlankLines: true }] }, }, ); diff --git a/apps/ui/eslint.config.mjs b/apps/ui/eslint.config.mjs index d8b006d..e6bc781 100644 --- a/apps/ui/eslint.config.mjs +++ b/apps/ui/eslint.config.mjs @@ -38,35 +38,19 @@ export default [ }, }, { - // File-size guardrail. No NEW file may exceed 1000 lines — split it - // (extract composables / sub-components) instead. The grandfather list - // below is the live decomposition backlog: remove a file when its split - // lands; never add to it. + // File-size guardrail. No file may exceed 2000 lines of code (comments + // and blank lines excluded) — split it (extract composables / + // sub-components) instead. The grandfather list below is the live + // decomposition backlog: remove a file when its split lands; never add to it. files: ['src/**/*.vue', 'src/**/*.ts'], ignores: ['**/*.test.ts', '**/*.d.ts'], - rules: { 'max-lines': ['error', 1000] }, + rules: { 'max-lines': ['error', { max: 2000, skipComments: true, skipBlankLines: true }] }, }, { - // ── DECOMPOSE BACKLOG ─ files still >1000 loc after the decomposition - // pass. This list only ever SHRINKS. Two groups: - // • cohesive cores (borderline) — the seam was extracted but the - // d3 / scene / dense-render core legitimately stays large; forcing - // it under 1000 would scatter coupled state. May stay grandfathered: - // Infra3DScene, LayerServiceMapView, LayerEndpointDependencyView, - // AlarmsView, DebugMal, DebugLal. - // • awaiting further / dedicated decomposition: OverviewTemplatesAdmin - // (just over), InspectView, and LayerDashboardsAdmin (XL — its own PR). - files: [ - 'src/features/admin/layer-templates/LayerDashboardsAdmin.vue', - 'src/features/admin/overview-templates/OverviewTemplatesAdmin.vue', - 'src/features/alarms/AlarmsView.vue', - 'src/features/infra-3d/Infra3DScene.vue', - 'src/features/operate/inspect/InspectView.vue', - 'src/features/operate/live-debug/DebugLal.vue', - 'src/features/operate/live-debug/DebugMal.vue', - 'src/layer/endpoint-dependency/LayerEndpointDependencyView.vue', - 'src/layer/service-map/LayerServiceMapView.vue', - ], + // ── DECOMPOSE BACKLOG ─ the only file still over 2000 loc; its split is + // in flight (LayerDashboardsAdmin → per-scope editors + composables). + // This list only ever SHRINKS — remove the entry when the split lands. + files: ['src/features/admin/layer-templates/LayerDashboardsAdmin.vue'], rules: { 'max-lines': 'off' }, }, ]; diff --git a/package.json b/package.json index 61fe0f1..c88a354 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "dev:bff": "pnpm --filter @skywalking-horizon-ui/bff dev", "build": "pnpm -r run build", "type-check": "pnpm -r run type-check", - "lint": "pnpm -r run lint", + "lint": "pnpm -r run lint && node scripts/check-source-budget.mjs", + "lint:budget": "node scripts/check-source-budget.mjs", "test:unit": "pnpm -r run test:unit", "license:check": "license-eye -c .licenserc.yaml header check", "license:fix": "license-eye -c .licenserc.yaml header fix", diff --git a/scripts/check-source-budget.mjs b/scripts/check-source-budget.mjs new file mode 100644 index 0000000..59f50b7 --- /dev/null +++ b/scripts/check-source-budget.mjs @@ -0,0 +1,87 @@ +/* + * 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. + */ + +// Source-budget guardrail. ESLint's max-lines covers code size in-editor; +// this gate adds the comment-VOLUME cap ESLint cannot express, and is the +// CI-runnable enforcer of both. A file is over budget when it carries more +// than CODE_MAX lines of code OR more than COMMENT_MAX lines of comment. +// SKIP lists files still mid-decomposition (mirrors eslint's grandfather +// block) — it only ever shrinks. + +import { readFileSync } from 'node:fs'; +import { execSync } from 'node:child_process'; + +const CODE_MAX = 2000; +const COMMENT_MAX = 500; +const SKIP = new Set([ + 'apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue', +]); + +const files = execSync('git ls-files', { encoding: 'utf8' }) + .split('\n') + .filter((f) => /\.(ts|vue|js|mjs)$/.test(f) && !f.endsWith('.d.ts')) + .filter((f) => f.startsWith('apps/') || f.startsWith('packages/') || f.startsWith('scripts/')) + .filter((f) => !/\.(test|spec)\.ts$/.test(f) && !SKIP.has(f)); + +// Line-level classification: a line is comment if it sits inside a block +// comment (/* */ or <!-- -->) or opens with // or /* or <!--. Inline trailing +// comments count as code (the line carries code). Approximate by design — a +// guardrail flags comment-heavy files, it isn't a forensic tokenizer. +function classify(src, isVue) { + let comment = 0; + let blank = 0; + let inBlock = false; + let close = ''; + const lines = src.split('\n'); + for (const raw of lines) { + const line = raw.trim(); + if (inBlock) { + comment++; + if (line.includes(close)) inBlock = false; + continue; + } + if (line === '') { blank++; continue; } + if (line.startsWith('//')) { comment++; continue; } + if (line.startsWith('/*')) { + comment++; + if (!line.includes('*/')) { inBlock = true; close = '*/'; } + continue; + } + if (isVue && line.startsWith('<!--')) { + comment++; + if (!line.includes('-->')) { inBlock = true; close = '-->'; } + continue; + } + } + return { code: lines.length - comment - blank, comment }; +} + +const over = []; +for (const f of files) { + const { code, comment } = classify(readFileSync(f, 'utf8'), f.endsWith('.vue')); + const probs = []; + if (code > CODE_MAX) probs.push(`code ${code} > ${CODE_MAX}`); + if (comment > COMMENT_MAX) probs.push(`comments ${comment} > ${COMMENT_MAX}`); + if (probs.length) over.push(` ${f} — ${probs.join('; ')}`); +} + +if (over.length) { + console.error(`✗ source-budget: ${over.length} file(s) over budget (code ≤ ${CODE_MAX}, comments ≤ ${COMMENT_MAX}):`); + console.error(over.join('\n')); + process.exit(1); +} +console.log(`✓ source-budget OK: ${files.length} files within ${CODE_MAX} code / ${COMMENT_MAX} comment lines`);
