CHAPTER 01

Kapitel 1: Makroskopische Erkenntnis: Die Engineering-Designphilosophie des core-Repositories

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 1 von 14

Bevor wir mit der Verfolgung irgendeiner Zeile der Implementierung von Reaktivität oder virtuellem DOM beginnen, müssen wir zunächst die Engineering-Matrix verstehen, in der dieser Code lebt. Wenn man das Vue core-Repository öffnet, springen einem zuerst nicht die Framework-Kernlogik, sondernpackage.jsonundpnpm-workspace.yamlsolche Engineering-Konfigurationsdateien ins Auge – sie enthalten keinerlei Laufzeitfunktionalität, bestimmen jedoch, ob das gesamte Framework korrekt gebaut, getestet und veröffentlicht werden kann. Dieses Kapitel beantwortet genau diese vorgelagerte Frage: Was ist das core-Repository eigentlich. Es ist nicht@vue/runtime-corejenes npm-Paket, sondern die Engineering-Matrix, dieruntime-core、reactivity、compiler-sfcund über zehn weitere öffentlich veröffentlichte Pakete sowiesfc-playground、template-explorerund andere private experimentelle Pakete trägt. Das Verständnis der Organisationsweise dieser Matrix ist die Voraussetzung für alle nachfolgenden Kapitel (Build, Typen, Release, Größenbudget). Dieses Kapitel entfaltet sich entlang dreier Hauptlinien: die Doppelverzeichnisstruktur des Workspace, die einheitlichen Einschränkungen durch TypeScript und Rollup auf Root-Ebene sowie die Entkopplungsphilosophie von „Quellcode-Repository" und „Release-Artefakten".

I. Doppelverzeichnisstruktur: Die physische Isolation von packages und packages-private

Intuitives Modell

Stellen Sie sich das core-Repository als ein Forschungs- und Entwicklungsgebäude vor.packages/ist die offizielle Produktlinie, die produzierten Dinge werden mit Markenzeichen versehen und auf dem Markt verkauft;packages-private/ist das interne Testlabor, die darin befindlichen Muster dienen nur zum Debuggen und Demonstrieren und werden niemals ausgeliefert. Beide teilen sich dieselbe Infrastruktur (Abhängigkeiten, Build-Tools), aber das Zugangskontrollsystem (Release-Prozess) behandelt sie unterschiedlich.

Ohne diese physische Isolation könnte ein internes Debug-playground-Paket leicht versehentlich auf npm veröffentlicht werden – das ist keine Hypothese, sondern ein klassischer Unfall in monorepos.

Datenstruktur und Speicherlayout

Die Grenze des Workspace wird durchpnpm-workspace.yamldefiniert. Es enthält nur drei wirksame Deklarationen:

📎 pnpm-workspace.yaml:1-3

yaml
packages:
  - 'packages/*'
  - 'packages-private/*'

Diese beiden globs teilen pnpm mit:packages/undpackages-private/jedes Unterverzeichnis unter@vue/runtime-coreist ein eigenständiges Paket. pnpm erstellt für sie symbolische Links, sodass@vue/reactivitybei Referenzierung von

direkt auf das lokale Quellcodeverzeichnis zeigt, anstatt vom registry herunterzuladen.catalog:Der unmittelbar folgende-Abschnitt ist pnpmsAbhängigkeitsversions-Katalog

📎 pnpm-workspace.yaml:5-13

yaml
catalog:
  '@babel/parser': ^7.29.8
  '@babel/types': ^7.29.8
  'entities': '^7.0.1'
  'estree-walker': ^2.0.2
  'magic-string': ^0.30.21
  'source-map-js': ^1.2.1
  'vite': ^8.3.0
  '@vitejs/plugin-vue': ^6.0.9

Kopierenpackage.jsonIm Root-"@babel/parser": "catalog:" 📎 package.json:65-65。catalog:steht entsprechend@babel/parserist ein Platzhalter, den pnpm bei der Installation durch die im catalog-Abschnitt deklarierte Version ersetzt. Der Nutzen davon ist:pnpm-workspace.yamlDie Version von

wird nur an einer Stelle inpnpm installgepflegt, alle Pakete, die es referenzieren, werden automatisch ausgerichtet, wodurch Versionsdrift wie „Paket A verwendet 7.28, Paket B verwendet 7.29" eliminiert wird.

Szenario-getriebener Walkthrough: Was nach einempnpm installpassiert

Angenommen, Sie führen im Repository-Rootaus. Versetzen Sie sich in dieses Szenario und verfolgen Sie schrittweise:package.jsonErster Schritt: preinstall-Zugangskontrolle.preinstallpnpm löst vor der Installation das

📎 package.json:45-45

json
"preinstall": "npx only-allow pnpm"
aus:

only-allow pnpmKopierencatalog:〔Design-Inferenz und Architektur-Abwägung〕createRequireprüft, ob der aktuelle Paketmanager pnpm ist, andernfalls wird direkt ein Fehler ausgegeben und beendet. Die Existenz dieses Skripts bedeutet: Die Installation des core-Repositories mit npm oder yarn wird fehlschlagen. Warum muss pnpm zwingend festgelegt werden? Weil das core-Repository auf pnpms Workspace-Symlinks und den catalog-Mechanismus angewiesen ist, npm's workspaces die

Syntax nicht unterstützt, und yarns PnP-Modus die Modulauflösungspfade ändert, was zu inkonsistentemVerhalten in Build-Skripten führt.pnpm-workspace.yamlZweiter Schritt: Workspace auflösen.packages/*pnpm liestpackages-private/*, scanntpackage.jsonund

, erstellt für jedes Verzeichnis miteinen Paketeintrag.package.jsonDritter Schritt: catalog-Ersetzung anwenden.catalog:Platzhalter werden durch die tatsächliche Version des catalog-Abschnitts ersetzt und anschließend einheitlich installiert.

Vierter Schritt: postinstall-Hook.Nach Abschluss der Installation wird ausgelöst:

📎 package.json:46-46

json
"postinstall": "simple-git-hooks"

simple-git-hooksLiest das Root-package.jsonin dersimple-git-hooks-Feld, schreibt Git-Hooks nach.git/hooks/:

📎 package.json:48-51

json
"simple-git-hooks": {
  "pre-commit": "pnpm lint-staged && pnpm check",
  "commit-msg": "node scripts/verify-commit.js"
}

pre-commitDer Hook führt vor jedem Commit lint-staged und Typprüfung aus,commit-msgDer Hook validiert das Format der Commit-Nachricht (Vue verwendet conventional commits). BeachtepreinstallundpostinstallSymmetrie: Ersteres bewacht (erlaubt nur pnpm), Letzteres sichert ab (installiert Git-Hooks).

Designüberlegungen und Stolperfallen

〔Design-Inferenz und Architektur-Abwägung〕

Warum zwei globs statt einerpackages*/?Die explizite Auflistung zweier Verzeichnisse macht die Semantik von „öffentlich" und „privat" bereits auf Konfigurationsebene sichtbar. Jeder neu hinzukommende Entwickler, derpnpm-workspace.yamlliest, weiß sofort, dass das Repository zwei Arten von Paketen enthält. Würde manpackages*/schreiben, wäre diese Semantik verborgen.

allowBuildsund Supply-Chain-Sicherheit.Beachte diesen Konfigurationsabschnitt:

📎 pnpm-workspace.yaml:15-21

yaml
allowBuilds:
  '@parcel/watcher': true
  '@swc/core': true
  'esbuild': true
  'puppeteer': true
  'simple-git-hooks': true
  'unrs-resolver': true

pnpm verbietet standardmäßig die Ausführung von Installationsskripten (postinstall) durch Abhängigkeitspakete, da dies ein häufiger Einstiegspunkt für Supply-Chain-Angriffe ist.allowBuildsist eine Whitelist: Nur die aufgeführten Pakete dürfen Build-Skripte ausführen.@swc/core、esbuildmuss plattformspezifische native Binärdateien herunterladen,puppeteermuss Chromium herunterladen,simple-git-hooksmuss Git-Hooks schreiben – all dies sind legitime Build-Zeit-Aktionen und werden daher explizit zugelassen.

minimumReleaseAge: 1440Die tiefere Bedeutung.Diese Konfigurationszeile verlangt, dass neu veröffentlichte Abhängigkeitsversionen „volle 24 Stunden" (1440 Minuten) alt sein müssen, bevor sie installiert werden dürfen:

📎 pnpm-workspace.yaml:33-33

yaml
minimumReleaseAge: 1440
〔Design-Inferenz und Architektur-Abwägung〕

Dies ist ein Cooldown-Mechanismus zur Abwehr von npm-Supply-Chain-Vergiftungen. Wenn ein Angreifer ein Paket übernimmt und eine bösartige Version veröffentlicht, wird dies normalerweise innerhalb weniger Stunden entdeckt und zurückgezogen. Eine 24-stündige Abklingzeit ermöglicht es dem core-Repository, dieses Zeitfenster zu umgehen. UndminimumReleaseAgeExcludeerlaubt Ausnahmen für bestimmte Sicherheitspatches:

📎 pnpm-workspace.yaml:36-38

yaml
minimumReleaseAgeExclude:
  # Renovate security update: vitest@4.1.11
  - vitest@4.1.11

Der Kommentar stellt klar, dass dies ein von Renovate ausgelöstes Sicherheitsupdate ist, das sofort wirksam werden muss, und daher von der Abklingzeit ausgenommen wird.

---

Zwei, Root-Level tsconfig: Einheitliche Typgrenzen für alle Unterpakete

Intuitives Modell

Wenn jedes Unterpaket seine eigene tsconfig pflegt, entstehen Risse wie „Paket A verwendetstrict: false, Paket B verwendetstrict: true". Die Root-Level tsconfig ist dieVerfassung: Sie legt die Typregeln fest, die alle Unterpakete gemeinsam befolgen müssen; Unterpakete können nur darauf aufbauen, dürfen sie aber nicht verletzen.

Datenstruktur und Speicherlayout

Root-tsconfig.jsonDiecompilerOptionsist das Fundament des gesamten Repository-Typsystems. Einige Schlüsselfelder herausgegriffen:

📎 tsconfig.json:5-29

json
"target": "es2016",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": true,
"isolatedModules": true,
"isolatedDeclarations": true,
"composite": true,
"paths": {
  "@vue/compat": ["./packages/vue-compat/src"],
  "@vue/*": ["./packages/*/src"],
  "vue": ["./packages/vue/src"]
}

Zeile für Zeile erklärt:

  • target: es2016: Ausgabesyntax auf ES2016 heruntergestuft. Dies korrespondiert mit esbuild'stargetin der Rollup-Konfiguration (isServerRenderer || isCJSBuild ? 'es2019' : 'es2016' 📎 rollup.config.js:337-337)。
  • moduleResolution: bundler: Verwendet bundler-artige Modulauflösung, erlaubt das Weglassen von Erweiterungen, unterstütztexportsFeld.
  • strict: true: Aktiviert alle strikten Prüfungen, einschließlichstrictNullChecks、noImplicitAnyusw.
  • noUnusedLocals: true: Unbenutzte lokale Variablen führen direkt zu einem Fehler. Diese Regel hat in Verbindung mit Tree-shaking praktische Bedeutung – unbenutzte Variablen sind oft ein Signal für toten Code.
  • isolatedModules: true: Erfordert, dass jede Datei unabhängig transpiliert werden kann. Dies ist die Voraussetzung für Tools wie esbuild/swc, die „dateiweise transpilieren, ohne dateiübergreifende Typanalyse".
  • isolatedDeclarations: true: Erfordert, dass alle Exporte explizit typisiert werden. Diese Regel dient direkt der.d.tsGenerierungspipeline – nur explizite Annotationen ermöglichen estsc, Deklarationsdateien schnell zu generieren, ohne vollständige Typinferenz durchzuführen.
  • composite: true: Aktiviert die für Projektverweise (project references) erforderlichen inkrementellen Build-Metadaten.

pathsDas Feld ist dasTyp-Ebenen-Spiegelbild:@vue/*des Workspace, abgebildet auf./packages/*/src, sodass TypeScript zur Kompilierzeit direkt den Quellcode auflöst, anstattnode_modulesSymlinks. Dies ergänzt die Laufzeit-Symlinks von pnpm – Laufzeit verlässt sich auf pnpm, Kompilierzeit auf paths.

Szenario-getriebener Walkthrough: Einepnpm checkTypprüfung

checkDas Skript isttsc --incremental --noEmit 📎 package.json:15-15. In dieses Szenario eintauchen:

Erster Schritt: include-Bereich lesen.tsconfig'sincludebestimmt, welche Dateien an der Prüfung teilnehmen:

📎 tsconfig.json:31-39

json
"include": [
  "packages/global.d.ts",
  "packages/*/src",
  "packages/*/__tests__",
  "packages/vue/jsx-runtime",
  "packages/runtime-dom/types/jsx.d.ts",
  "scripts/*",
  "rollup.*.js"
]

Beachtescripts/*undrollup.*.jssind ebenfalls im Prüfbereich. Das bedeutet, dass auch Build-Skripte selbst Typbeschränkungen unterliegen –rollup.config.jsDas// @ts-check 📎 rollup.config.js:1-1am Anfang in Verbindung mit JSDoc-Typannotationen ermöglicht es, dass diese reine JS-Datei vontscgeprüft wird.

Zweiter Schritt: exclude-Ausschluss anwenden.

📎 tsconfig.json:40-40

json
"exclude": ["packages-private/sfc-playground/src/vue-dev-proxy*"]
〔Design-Inferenz und Architektur-Abwägung〕

sfc-playgroundInvue-dev-proxyDateien werden ausgeschlossen. Warum? Solche Dateien sind normalerweise zur Laufzeit dynamisch generierter Proxy-Code, dessen Typform instabil ist und dessen Einbeziehung in die Prüfung Rauschen erzeugt.

Dritter Schritt: Inkrementelle Prüfung. --incrementalLässttscdas letzte Prüfergebnis in.tsbuildinfozwischenspeichern und nur geänderte Dateien erneut prüfen.--noEmitbedeutet nur prüfen, nicht ausgeben – Typprüfung und Artefaktgenerierung sind zwei unabhängige Pipelines.

Designüberlegungen und Stolperfallen

isolatedDeclarationsKosten und Nutzen.Nach Aktivierung dieser Regel muss jeder Export explizit einen Rückgabetyp annotieren, z. B.export function foo(): numberstattexport function foo() { return 1 }. Dies erhöht den Schreibaufwand, bringt aber eine deutliche Beschleunigung der.d.tsGenerierung –tscDeklarationsdateien können ohne dateiübergreifende Inferenz erzeugt werden. Dies korrespondiert mitbuild-dtsSkripttsc -p tsconfig.build.json --noCheckDas--noCheckFlag: Da Typen bereits explizit annotiert sind, kann beim Generieren von Deklarationsdateien sogar die Prüfung übersprungen werden.

typesGlobale Injektion des Feldes.

📎 tsconfig.json:21-21

json
"types": ["vitest/globals", "puppeteer", "node"]

Diese drei Typ-Pakete werden global injiziert, was bedeutet, dass Testdateiendescribe、it、expectdirekt verwenden können, ohne import, und e2e-Tests direktpuppeteerTypen verwenden können. Dies ist eine Abwägung zwischen Bequemlichkeit und Verschmutzung – je mehr globale Typen, desto größer das Risiko von Namenskonflikten, aber desto besser die Schreiberfahrung für Testcode.

---

Drei. Rollup-Konfiguration: Von buildOptions zu einer einheitlichen Fabrik für Multi-Format-Artefakte

Intuitives Modell

Die Rollup-Konfiguration ist dasMontagewerkdes core-Repositories. Es kümmert sich nicht darum, was ein bestimmtes Paket tut, sondern nur darum, „welche Formate dieses Paket produzieren soll, wo die Einstiegsdatei für jedes Format liegt und welche Abhängigkeiten externalisiert werden sollen". Das Feldpackage.jsonin derbuildOptionsjedes Unterpakets ist der Lieferschein, der auf dem Paket klebt, und das Montagewerk arbeitet nach diesem Schein.

Datenstruktur und Speicherlayout

Am Einstiegspunkt der Konfigurationsdatei wird das Modell „Build pro Paket" etabliert:

📎 rollup.config.js:32-44

js
if (!process.env.TARGET) {
  throw new Error('TARGET package must be specified via --environment flag.')
}
...
const privatePackages = fs.readdirSync('packages-private')
const pkgBase = privatePackages.includes(process.env.TARGET)
  ? `packages-private`
  : `packages`
const packagesDir = path.resolve(__dirname, pkgBase)
const packageDir = path.resolve(packagesDir, process.env.TARGET)
...
const pkg = require(resolve(`package.json`))
const packageOptions = pkg.buildOptions || {}
const name = packageOptions.filename || path.basename(packageDir)

Wichtige Designentscheidungen:TARGETDie Umgebungsvariable gibt an, welches Paket gebaut werden soll. Die Konfiguration prüft überfs.readdirSync('packages-private'), ob das Paket zum öffentlichen oder privaten Verzeichnis gehört, und entscheidet dadurch überpkgBase. Dies ist eineLaufzeit-Verzeichniserkennung– es ist keine Liste „welche Pakete privat sind" zu pflegen, die Verzeichnisstruktur selbst ist die Wahrheit.

buildOptionsist ein benutzerdefiniertes Feld in derpackage.jsondes Unterpakets,packageOptions.filenamebestimmt das Präfix des Artefaktdateinamens,packageOptions.formatsbestimmt das Standard-Build-Format.

Die Zuordnung von Format zu Artefakt wird durchoutputConfigsdefiniert:

📎 rollup.config.js:58-88

js
const outputConfigs = {
  'esm-bundler': { file: resolve(`dist/${name}.esm-bundler.js`), format: 'es' },
  'esm-browser': { file: resolve(`dist/${name}.esm-browser.js`), format: 'es' },
  cjs:           { file: resolve(`dist/${name}.cjs.js`),         format: 'cjs' },
  global:        { file: resolve(`dist/${name}.global.js`),      format: 'iife' },
  'esm-bundler-runtime': { file: resolve(`dist/${name}.runtime.esm-bundler.js`), format: 'es' },
  'esm-browser-runtime': { file: resolve(`dist/${name}.runtime.esm-browser.js`), format: 'es' },
  'global-runtime':      { file: resolve(`dist/${name}.runtime.global.js`),      format: 'iife' },
}

Sieben Formate, die drei Konsumszenarien abdecken:esm-bundlerfür Bundler wie Vite/webpack,esm-browserfür natives Browser-ESM,globalfür das<script>-Tag. Die mit-runtime-Suffix sind „nur-Laufzeit"-Builds, die nur für das Haupt-vue-Paket verfügbar sind.

Szenario-getriebener Walkthrough: Der vollständige Entscheidungsfluss einespnpm build vue

Versetzen wir uns in die Ausführung vonnode scripts/build.js vueund verfolgen die Entscheidungen innerhalb vonTARGET=vue:createConfig

Erster Schritt: Formatliste bestimmen.

📎 rollup.config.js:91-92

js
const defaultFormats = ['esm-bundler', 'cjs']
const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',')
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
const packageConfigs = process.env.PROD_ONLY
  ? []
  : packageFormats.map(format => createConfig(format, outputConfigs[format]))

Priorität: Kommandozeilen-FORMATS> Unterpaket-buildOptions.formats> Standard-['esm-bundler', 'cjs']。PROD_ONLYWenn die Umgebungsvariable wahr ist, werden Nicht-Produktions-Builds übersprungen und nur die später angehängten.prod.js-Konfigurationen beibehalten.

Zweiter Schritt: Build-Flags berechnen. createConfigIntern wird aus dem Format-String eine Gruppe boolescher Flags abgeleitet:

📎 rollup.config.js:131-142

js
const isProductionBuild = process.env.__DEV__ === 'false' || /\.prod\.js$/.test(output.file)
const isBundlerESMBuild = /esm-bundler/.test(format)
const isBrowserESMBuild = /esm-browser/.test(format)
const isServerRenderer = name === 'server-renderer'
const isCJSBuild = format === 'cjs'
const isGlobalBuild = /global/.test(format)
const isCompatPackage = pkg.name === '@vue/compat'
const isCompatBuild = !!packageOptions.compat
const isBrowserBuild =
  (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
  !packageOptions.enableNonBrowserBranches

Diese Flags sind dieeinzige Wahrheitsquellefür alle nachfolgenden Entscheidungen: Einstiegsdateiauswahl, define-Ersetzung, external-Bestimmung, Plugin-Zusammenstellung – alles hängt von ihnen ab.

Dritter Schritt: Einstiegsdatei auswählen.

📎 rollup.config.js:159-168

js
let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`

if (isCompatPackage && (isBrowserESMBuild || isBundlerESMBuild)) {
  entryFile = /runtime$/.test(format)
    ? `src/esm-runtime.ts`
    : `src/esm-index.ts`
}

Der Standard-Einstieg istsrc/index.ts, Nur-Laufzeit-Builds verwendensrc/runtime.ts. Das compat-Paket (@vue/compat, also der Vue-2-Kompatibilitäts-Build) muss sowohl default- als auch named-Exporte bereitstellen, was Rollup bei Nicht-ESM-Zielen zu Fehlern veranlasst, daher wird für den ESM-Build separat deresm-index.ts / esm-runtime.ts-Einstieg verwendet.

Vierter Schritt: define-Ersetzungstabelle generieren. resolveDefineErsetzt Kompilierzeit-Konstanten wie__DEV__、__BROWSER__im Quellcode durch Literale:

📎 rollup.config.js:170-201

js
const replacements = {
  __COMMIT__: `"${process.env.COMMIT}"`,
  __VERSION__: `"${masterVersion}"`,
  __TEST__: `false`,
  __BROWSER__: String(isBrowserBuild),
  __GLOBAL__: String(isGlobalBuild),
  __ESM_BUNDLER__: String(isBundlerESMBuild),
  __ESM_BROWSER__: String(isBrowserESMBuild),
  __CJS__: String(isCJSBuild),
  __SSR__: String(!isGlobalBuild),
  __COMPAT__: String(isCompatBuild),
  __FEATURE_SUSPENSE__: `true`,
  __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : `true`,
  __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild ? `__VUE_PROD_DEVTOOLS__` : `false`,
  __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: isBundlerESMBuild ? `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` : `false`,
}

Hier gibt es eine raffinierte Schichtung:Feature-Flags werden im esm-bundler-Build nicht hartkodiert, sondern als Bezeichner wie__VUE_OPTIONS_API__beibehaltenund dem Bundler des Endnutzers zur Ersetzung überlassen. So kann der Nutzer überdefine: { __VUE_OPTIONS_API__: false }die Options-API-Unterstützung deaktivieren und den zugehörigen Code tree-shaken. In global/esm-browser-Builds hingegen werden diese Flags auftrue/falsehartkodiert, da die direkt vom Browser konsumierten Artefakte keinen Bundler dazwischen haben.

Fünfter Schritt: Umgebungsvariablen-Überschreibung erlauben.

📎 rollup.config.js:208-216

js
// allow inline overrides like
//__RUNTIME_COMPILE__=true pnpm build runtime-core
Object.keys(replacements).forEach(key => {
  if (key in process.env) {
    const value = process.env[key]
    assert(typeof value === 'string')
    replacements[key] = value
  }
})

Jeder define-Schlüssel kann durch eine gleichnamige Umgebungsvariable überschrieben werden. Das im Kommentar gegebene Beispiel ist__RUNTIME_COMPILE__=true pnpm build runtime-core– zum Debuggen bestimmter Kompilierungszweige.

Sechster Schritt: Plugin-Kette zusammenstellen.

📎 rollup.config.js:324-342

js
plugins: [
  json({ namedExports: false }),
  alias({ entries }),
  enumPlugin,
  ...resolveReplace(),
  esbuild({
    tsconfig: path.resolve(__dirname, 'tsconfig.json'),
    sourceMap: output.sourcemap,
    minify: false,
    target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2016',
    define: resolveDefine(),
  }),
  ...resolveNodePlugins(),
  ...plugins,
],

Die Plugin-Reihenfolge ist wohlüberlegt:jsonverarbeitet zuerst JSON-Importe,aliasmappt@vue/*auf Quellcodepfade,enumPluginmacht Enum-Inlining,replacemacht String-Ersetzung,esbuildmacht TS-Transpilierung. Beachten Sie, dassesbuildvontsconfigauf das Root-tsconfig zeigt –alle Unterpakete teilen dieselbe Typkonfiguration, was die im zweiten Abschnitt diskutierte „Verfassung" zur Build-Zeit widerspiegelt.

Siebter Schritt: Produktions-Build-Anhängsel.WennNODE_ENV=production:

📎 rollup.config.js:97-114

js
if (process.env.NODE_ENV === 'production') {
  packageFormats.forEach(format => {
    if (packageOptions.prod === false) {
      return
    }
    if (format === 'cjs') {
      packageConfigs.push(createProductionConfig(format))
    }
    if (/^(global|esm-browser)(-runtime)?/.test(format)) {
      packageConfigs.push(createMinifiedConfig(format))
    }
  })
}

wird beim CJS-Format eine.prod.js-Version angehängt (ersetzt durch__DEV__=false), und beim global- und esm-browser-Format wird eine minifizierte Version angehängt (Minifizierung mit swc).packageOptions.prod === falsePakete mit

können sich diesem Mechanismus entziehen.

mermaid
flowchart TD
    start["node scripts/build.js vue"] --> check_target{"process.env.TARGET 存在?"}
    check_target -->|否| throw_err["throw Error: TARGET must be specified"]
    check_target -->|是| detect_dir{"TARGET 在 packages-private 中?"}
    detect_dir -->|是| base_priv["pkgBase = packages-private"]
    detect_dir -->|否| base_pub["pkgBase = packages"]
    base_priv --> read_pkg["require(package.json) 读取 buildOptions"]
    base_pub --> read_pkg
    read_pkg --> resolve_formats{"FORMATS 环境变量?"}
    resolve_formats -->|有| use_inline["使用命令行格式"]
    resolve_formats -->|无| check_buildopts{"buildOptions.formats?"}
    check_buildopts -->|有| use_pkg["使用包声明格式"]
    check_buildopts -->|无| use_default["使用默认 esm-bundler,cjs"]
    use_inline --> create_cfg["createConfig(format, output)"]
    use_pkg --> create_cfg
    use_default --> create_cfg
    create_cfg --> check_output{"output 配置存在?"}
    check_output -->|否| exit_err["console.log invalid format; process.exit(1)"]
    check_output -->|是| pick_entry{"格式含 runtime?"}
    pick_entry -->|是| entry_rt["entryFile = src/runtime.ts"]
    pick_entry -->|否| entry_idx["entryFile = src/index.ts"]
    entry_rt --> build_flags["计算 isBundlerESMBuild/isCJSBuild 等标志"]
    entry_idx --> build_flags
    build_flags --> prod_check{"NODE_ENV == production?"}
    prod_check -->|是| add_prod["追加 .prod.js 与 minified 配置"]
    prod_check -->|否| done["导出 packageConfigs"]
    add_prod --> done

Kopieren

externalDesignüberlegungen und Stolperfallen resolveExternalDie Drei-Zweig-Strategie von

📎 rollup.config.js:257-283

js
function resolveExternal() {
  const treeShakenDeps = ['source-map-js', '@babel/parser', 'estree-walker', 'entities/decode']

  if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) {
    if (!packageOptions.enableNonBrowserBranches) {
      return treeShakenDeps
    }
  } else {
    return [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {}),
      ...['path', 'url', 'stream'],
      ...treeShakenDeps,
    ]
  }
}

KopierentreeShakenDepsBrowser-Builds (global/esm-browser) inlinen alle Abhängigkeiten und listen nurdependenciesals external auf, um Warnungen zu unterdrücken – diese Abhängigkeiten werden im Browser-Zweig nicht tatsächlich referenziert und durch Tree-Shaking entfernt. Node/esm-bundler-Builds externalisieren allepeerDependenciesund

onwarn, sodass der Konsument die Abhängigkeitsversionen selbst verwaltet.

📎 rollup.config.js:344-348

js
onwarn: (msg, warn) => {
  if (msg.code !== 'CIRCULAR_DEPENDENCY') {
    warn(msg)
  }
},

Kopierenruntime-coreWarnungen über zirkuläre Abhängigkeiten werden stillschweigend unterdrückt. Zwischenreactivityund

treeshake.moduleSideEffects: falsevon Vue existieren legitime zirkuläre Referenzen (das Reaktivitätssystem muss auf den Komponenteninstanztyp verweisen), diese Zyklen sind zur Laufzeit sicher und werden daher gefiltert.

📎 rollup.config.js:355-355

js
treeshake: {
  moduleSideEffects: false,
},

KopierenDies teilt Rollup mit: Alle Module haben keine Seiteneffekte, ungenutzte Importe können bedenkenlos entfernt werden. Dies ist eineaggressive Annahme

– wenn ein Modul auf oberster Ebene Seiteneffekt-Code ausführt (z. B. globale Variablen registriert), könnte es fälschlicherweise entfernt werden. Der Vue-Quellcode garantiert durch Konvention, dass alle Module rein sind, daher kann diese Optimierung aktiviert werden.pure_gettersDie

📎 rollup.config.js:373-388

js
async renderChunk(contents, _, { format }) {
  const { code } = await minifySwc(contents, {
    module: format === 'es',
    format: { comments: false },
    compress: { ecma: 2016, pure_getters: true },
    safari10: true,
    mangle: true,
  })
  return { code: banner + code, map: null }
}

pure_getters: trueKopierenobj.footeilt dem Minifier mit, dass „Property-Zugriffe keine Seiteneffekte haben", ungenutzte Getter-Aufrufe sicher entfernt werden können. Dies ist gefährlich für Vues Reaktivitätscode –track()) statt durch implizite Getter-Seiteneffekte abgeschlossen wird und daher sicher ist.map: nullbedeutet, dass nach der Komprimierung keine Sourcemap generiert wird – Produktionsartefakte benötigen keine Debug-Mappings.

---

Designüberlegung: Warum Quellcode-Repository und Veröffentlichungsartefakte entkoppelt sein müssen

Zurück zum Kernanliegen dieses Kapitels. Das Engineering-Design des core-Repositories hat eine durchgängige Leitlinie:Die Aufgabe des Quellcode-Repositories ist die „Produktion", die Aufgabe der Veröffentlichungsartefakte ist die „Konsumption", beide werden durch die Build-Pipeline entkoppelt。

Konkret zeigt sich dies auf drei Ebenen:

Erstens: Quellcode wird nicht direkt veröffentlicht. package.jsonvonprivate: true 📎 package.json:2-2zeigt an, dass das Root-Paket niemals veröffentlicht wird. Daspackage.jsonjedes Unterpaketsmain/module/exportsFeld verweist aufdist/unter den Artefakten, nicht aufsrc/. Wenn Benutzervueinstallieren, erhalten sie das gebaute.jsund.d.ts, der Quellcode bleibt im Repository.

Zweitens: Das Artefaktformat wird durch das Konsumszenario bestimmt.Die sieben Formate sind nicht willkürlich aufgelistet, sondern entsprechen sieben realen Konsumpfaden: Vite-Benutzer erhaltenesm-bundler, CDN-Benutzer erhaltenglobal, Node-SSR-Benutzer erhaltencjs. Die Format-Auswahllogik ist zentral inrollup.config.jsan einer Stelle konzentriert, Unterpakete müssen nur inbuildOptions.formatsdeklarieren, welche benötigt werden.

Drittens: Typen und Implementierung sind getrennt. build-dtsSkripttsc -p tsconfig.build.json --noCheck && rollup -c rollup.dts.config.js 📎 package.json:9-9zeigt an, dass.d.ts-Generierung eine unabhängige Pipeline ist.isolatedDeclarations: trueermöglicht es der Deklarationsdatei-Generierung, die Typprüfung zu überspringen (--noCheck), da die Typen bereits explizit annotiert sind.

〔Design-Inferenz und Architektur-Abwägung〕

Die tiefere Motivation dieser Entkopplung ist:Die Organisationsweise des Quellcodes dient dem Entwickler, die Organisationsweise der Artefakte dient dem Konsumenten, die optimalen Lösungen beider unterscheiden sich. Quellcode benötigt klare Verzeichnisstruktur, vollständige Typinformationen, debugbare Sourcemaps; Artefakte benötigen minimale Größe, korrektes Modulformat, stabile API-Oberfläche. Eine erzwungene Vereinheitlichung beider (z. B. direktes Veröffentlichen von TS-Quellcode) würde die Erfahrung auf beiden Seiten gleichzeitig beeinträchtigen.

---

Kapitelzusammenfassung

Dieses Kapitel hat aus drei Dimensionen ein makroskopisches Verständnis des core-Repositories aufgebaut:

1. Doppelverzeichnisstruktur:packages/undpackages-private/die physische Trennung, kombiniert mit den Symlinks des pnpm-Workspace und dem catalog-Versionsverzeichnis, realisiert eine klare Grenze zwischen „öffentlichen Paketen" und „privaten Paketen".preinstall-Gate,allowBuildsWhitelist,minimumReleaseAgeAbklingzeit bilden gemeinsam die Lieferketten-Sicherheitslinie.

2. Root-Level tsconfig: Als Typenverfassung aller Unterpakete, durchpaths-Mapping wird die Workspace-Auflösung zur Kompilierzeit realisiert, durchisolatedDeclarationsundcompositewerden inkrementelle Builds und schnelle Deklarationsdatei-Generierung unterstützt.

3. Rollup-einheitliche Fabrik: MitTARGETUmgebungsvariable als Einstieg, durchbuildOptionswerden Unterpaket-Metainformationen gelesen, durch eine Gruppe von Boolean-Flags werden Einstiegsauswahl, define-Ersetzung, external-Bestimmung und Plugin-Zusammenstellung gesteuert, schließlich werden Artefakte in sieben Formaten produziert.

Die Kernphilosophie istdie Entkopplung von Quellcode-Repository und Veröffentlichungsartefakten: Das Repository ist für die Produktion verantwortlich, die Artefakte für die Konsumption, die Build-Pipeline ist die einzige Brücke zwischen beiden.

---

Kapitelübergang

Dieses Kapitel hat beantwortet, „was das core-Repository ist". Aber die statische Struktur des Repositories ist nur die Bühne, das eigentliche Drama findet während der Ausführung einer Build-Anfrage statt:scripts/build.jsWie Kommandozeilenargumente geparst werden, wie die Rollup-API aufgerufen wird, wie Build-Fehler und Nebenläufigkeit behandelt werden. Das nächste Kapitel wird die End-to-End-Reise einer Build-Anfrage von der Eingabe bis zum Artefakt verfolgen und das in diesem Kapitel aufgebaute statische Verständnis in eine dynamische Ausführungsansicht verwandeln.

Kapitel-Reflexion und Selbsttest

Q1: Wenn man inpnpm-workspace.yamldasminimumReleaseAge: 1440zu0ändern würde, welches Risiko würde im Szenario eines Dependency-Upgrades entstehen? Warum istminimumReleaseAgeExcludenotwendig?

Referenzanalyse:

minimumReleaseAge: 1440 📎 pnpm-workspace.yaml:33-33verlangt, dass neu veröffentlichte Dependency-Versionen mindestens 24 Stunden alt sein müssen, bevor sie installiert werden dürfen. Wenn man es zu0ändern würde, könnte jede gerade veröffentlichte Version sofort hereingezogen werden.

Risikoszenario: Ein Angreifer kapert eine transitive Dependency (z. B.@babel/parsereine bestimmte Patch-Version), veröffentlicht eine Version mit bösartigem postinstall-Skript. Innerhalb der 24-stündigen Abklingzeit entdeckt die Community normalerweise das Problem und zieht die Version zurück; wäre die Abklingzeit 0, könnte die CI des core-Repositories innerhalb des Angriffsfensters automatisch upgraden und das bösartige Skript ausführen.

minimumReleaseAgeExclude 📎 pnpm-workspace.yaml:36-38existiert, weil der Abklingzeit-Mechanismus mit der Dringlichkeit von Sicherheitspatches kollidiert. Dasvitest@4.1.11im Kommentar ist ein von Renovate erkanntes Sicherheitsupdate – solche Updates müssen sofort wirksam werden, 24 Stunden zu warten würde das Expositionsfenster verlängern. Daher braucht es eine explizite Ausnahmeliste, damit Sicherheitsupdates die Abklingzeit umgehen. Dies verkörpert das Sicherheitsdesignprinzip „standardmäßig konservativ, Ausnahmen explizit".

Q2: rollup.config.jsInresolveDefineist die Behandlung von__FEATURE_OPTIONS_API__durchisBundlerESMBuild ? '__VUE_OPTIONS_API__' : 'true'. Wenn man fälschlicherweise ändern würde, dass für alle Formate'true'zurückgegeben wird, welche Auswirkung hätte das auf Endbenutzer?

Referenzanalyse:

📎 rollup.config.js:192-194

js
__FEATURE_OPTIONS_API__: isBundlerESMBuild
  ? `__VUE_OPTIONS_API__`
  : `true`,

Im esm-bundler-Build wird__FEATURE_OPTIONS_API__als Identifier__VUE_OPTIONS_API__beibehalten und dem Bundler des Endbenutzers zur Ersetzung überlassen. Benutzer können in ihrer eigenen Build-Konfigurationdefine: { __VUE_OPTIONS_API__: false }setzen, wodurch Tree-shaking den gesamten Options-API-bezogenen Code entfernt (die Verarbeitungslogik vondata、methods、computedund anderen Optionen), was die Artefaktgröße erheblich reduziert.

Wenn man ändern würde, dass für alle Formate'true'zurückgegeben wird, wäre der Options-API-Code im esm-bundler-Artefakt hartkodiert beibehalten, diedefine-Konfiguration des Benutzers würde wirkungslos, Tree-shaking unmöglich. Für ein Projekt, das nur die Composition API verwendet, würde dies unnötig mehrere KB Artefaktgröße hinzufügen.

Die Schlüsseleinsicht dieses Designs ist:Die endgültige Form des esm-bundler-Artefakts wird vom Bundler des Benutzers bestimmt, daher muss das Feature-Flag bis zur Build-Zeit des Benutzers aufgeschoben werden. Die global/esm-browser-Artefakte hingegen laufen direkt im Browser, ohne dass ein Bundler eingreift, daher müssen sie hartkodiert sein.

Q3: rollup.config.jsvonresolveExternalgibt der Browser-Build nurtreeShakenDepsals external zurück, während der Node-Build alledependencieszurückgibt. Angenommen, jemand fügt eines Tagesruntime-coreeine neue Laufzeitabhängigkeitfoo-libhinzu, vergisst aber, die Logik vonresolveExternalzu aktualisieren. Was passiert im Browser-Build?

Referenzanalyse:

📎 rollup.config.js:257-283

Der Browser-Build (isGlobalBuild || isBrowserESMBuild) gibt bei!packageOptions.enableNonBrowserBranchesnurtreeShakenDeps(source-map-js、@babel/parser、estree-walker、entities/decodezurück). Das bedeutet,foo-libist nicht in der external-Liste,

Bis hierhin haben wir auf makroskopischer Ebene die gesamte Designphilosophie des core-Repositories als engineeringtechnische Mutterbasis deutlich gesehen: Die Workspace-Struktur mit zwei Verzeichnissen zieht die Grenze zwischen öffentlichen Paketen und privaten experimentellen Paketen, die TypeScript- und Rollup-Konfiguration auf Root-Ebene bietet einheitliche Vorgaben, und die Entkopplung von Quellcode-Repository und Veröffentlichungsartefakten ermöglicht Multi-Format-Ausgaben. Diese Erkenntnisse ebnen den Weg für die spätere Vertiefung in konkrete Engineering-Ketten. Im nächsten Kapitel richten wir unseren Blick von der statischen Struktur auf den dynamischen Ablauf und verfolgen, ausgehend vonnode scripts/build.js vue, die End-to-End-Reise einer vollständigen Build-Anfrage von der Kommandozeilen-Argumentanalyse über die Zielpaket-Lokalisierung und die Rollup-Konfigurationsgenerierung bis zur Ablage der Artefakte, und sehen, wie build.js über parseArgs die Flags formats/devOnly/release parst, wie es dynamisch die package.json des Zielpakets per require lädt und buildOptions liest und schließlich rollup.config.js dazu antreibt, Multi-Format-Artefakte wie esm-bundler, cjs, global usw. zu erzeugen.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 02

Kapitel 2: Lebenszyklus des Hauptzweigs: Die End-to-End-Reise einer Build-Anfrage

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 2 von 14

Im vorherigen Kapitel haben wir die Positionierung des core-Repositories als engineeringtechnische Mutterbasis geklärt und wie der pnpm-Workspace und die Root-Konfiguration alle Unterpakete einheitlich einschränken. Jetzt tauchen wir in den Kern des Build-Systems ein und verfolgen, wie ein Befehl den gesamten Build-Prozess antreibt.node scripts/build.js vueerscheint einfach, ist aber der einzige Einstiegspunkt für alle Artefakte – esm-bundler, cjs, global. Zu verstehen, wie es Benutzerabsichten in ausführbare Build-Aufgaben übersetzt, ist ein entscheidender Schritt zum Verständnis des Vue-Build-Mechanismus.

Rollup-Konfigurationsgenerierung: Von Umgebungsvariablen zu Multi-Format-Artefakten

build.jsNach dem Start von Rollup überexecgeht die Kontrolle anrollup.config.jsüber. Diese Datei ist das „Gehirn“ des Build-Systems – sie liest Umgebungsvariablen und generiert dynamisch ein Array von Rollup-Konfigurationsobjekten.

Validierung von Umgebungsvariablen und Paketlokalisierung

📎 rollup.config.js:27-29

WennTARGETnicht gesetzt ist, wird direkt ein Fehler geworfen. Das ist defensives Programmieren: Die Rollup-Konfiguration könnte direkt aufgerufen werden (z. B.rollup -c), wobei keinebuild.jsUmgebungsvariablen injiziert werden, und muss schnell fehlschlagen.

📎 rollup.config.js:32-44

Hier wird die Logik zur Bestimmung privater Pakete ausbuild.jsdupliziert – weilrollup.config.jsein unabhängiger Prozess ist und den Speicherzustand vonbuild.jsnicht teilen kann.resolveDie Funktionpkglöst relative Pfade in absolute Pfade im Paketverzeichnis auf,package.jsonist der Inhalt derpackageOptionsdes Zielpakets,buildOptionsist das darin enthaltenename-Feld,buildOptions.filenameist das Präfix des Artefaktdateinamens (vorzugsweise

, andernfalls der Verzeichnisname).outputConfigs

📎 rollup.config.js:58-88

Format-Zuordnungstabelle:

  • esm-bundler、esm-browser、esm-bundler-runtime、esm-browser-runtimeDiese Tabelle definiert die Zuordnung von 7 Formaten zu Ausgabekonfigurationen. Wichtige Beobachtungen:format: 'es'sind alle
  • cjs, der Unterschied liegt nur im Dateinamen.format: 'cjs'。
  • globalistglobal-runtimeundformat: 'iife'ist<script>(Immediately Invoked Function Expression), geeignet für die direkte Einbindung über
  • runtime-Tags.vueFormate mit dem Suffix

sind nur für das Haupt-

📎 rollup.config.js:91-92

-Paket sinnvoll – sie enthalten keinen Compiler und sind kleiner.FORMATSFormat-Auswahl: Drei PrioritätsebenenbuildOptions.formatsDie Format-Auswahl folgt drei Prioritätsebenen: Kommandozeilen-['esm-bundler', 'cjs']。PROD_ONLYUmgebungsvariable > Paket-

> Standard-

📎 rollup.config.js:97-114

Die Umgebungsvariable steuert, ob die Basiskonfiguration übersprungen wird – wenn nur die Produktionsversion gebaut wird, ist das Basiskonfigurations-Array leer und es werden anschließend nur Produktionskonfigurationen eingefügt.NODE_ENV === 'production'Anhänge-Logik der Produktionskonfiguration

  • WennpackageOptions.prod === false, dann für jedes Format:
  • Wenncjs, überspringen (das Paket benötigt keine Produktionsversion).createProductionConfigWenn es.prod.jsist, wird
  • angehängt – erzeugt die/^(global|esm-browser)(-runtime)?/-Datei.createMinifiedConfigWenn
übereinstimmt, wird

angehängt – erzeugt die komprimierte Version.cjs〔Design-Inferenz und Architektur-Abwägung〕createProductionConfigWarum verwendetglobal/esm-browsercreateMinifiedConfig, während

createConfig

createConfigverwendet? Weil CJS für Node gedacht ist, die Node-Umgebung keine Komprimierung benötigt (der Benutzer kümmert sich selbst darum), aber zwischen dev/prod-Zweigen unterscheiden muss; während direkt im Browser eingebundene Artefakte komprimiert werden müssen, um die Größe zu reduzieren. Dieser Unterschied zeigt sich in der Implementierung der beiden Factory-Funktionen.

📎 rollup.config.js:125-142

: Der Kern der Konfigurationsgenerierung

  • isProductionBuildist die größte Funktion; sie empfängt Format und Ausgabekonfiguration und gibt ein vollständiges Rollup-Konfigurationsobjekt zurück.__DEV__Am Anfang steht die Berechnung einer Reihe boolescher Flags:.prod.js: Bestimmt durch die
  • isBundlerESMBuild、isBrowserESMBuild、isCJSBuild、isGlobalBuildUmgebungsvariable oder ob der Dateiname
  • isServerRendererenthält.server-renderer。
  • isCompatPackage、isCompatBuild: Bestimmt durch regulären Ausdruck auf den Formatnamen.
  • isBrowserBuild: Ob der Paketname

ist: im Zusammenhang mit Vue-2-kompatiblen Builds.resolveDefine、resolveReplace、resolveExternal: Global-Build oder Browser-ESM-Build, und der Nicht-Browser-Zweig ist nicht aktiviert.

📎 rollup.config.js:144-157

Diese Flags werden später inexportswiederholt verwendet und sind die zentrale Grundlage für die Konfigurationsdifferenzierung.autoGrundeinstellungen der Ausgabekonfiguration: Banner-Copyright-Header,named-Modus (compat-Pakete verwendenesModule, die übrigenexternalLiveBindings: false), CJS-Build aktiviertreexportProtoFromExternal: false-Interoperabilität, Sourcemap wird durch Umgebungsvariablen gesteuert,output.nameundwindowsind Kompatibilitätseinstellungen von Rollup 4. Der Global-Build setzt zusätzlich

, also den Variablennamen, der an

📎 rollup.config.js:159-168

gehängt wird.src/index.tsAuswahl der EinstiegsdateiruntimeDer Standard-Einstieg istsrc/runtime.ts.compat-Paket-ESM-Build muss sowohl default als auch named exportieren, daher wird ein separateresm-index.ts / esm-runtime.tsEinstiegspunkt verwendet.

Makrodefinition:resolveDefine

📎 rollup.config.js:170-218

resolveDefineGibt eine Ersetzungstabelle zurück, die im Quellcode__COMMIT__、__VERSION__、__BROWSER__Makros durch Literale ersetzt. Diese Makros werden im Quellcode für bedingte Kompilierung verwendet – zum Beispielif (__DEV__) { ... }wird im Produktions-Build durchif (false) { ... }ersetzt und dann durch Tree-Shaking entfernt.

Schlüssendesign:__FEATURE_OPTIONS_API__、__FEATURE_PROD_DEVTOOLS__Feature-Flags wie diese bleiben imesm-bundlerBuild als__VUE_OPTIONS_API__solche Bezeichner erhalten, damit Endbenutzer sie über die Bundler-Konfiguration überschreiben können; in anderen Builds werden sie direkt hartcodiert alstrueoderfalse。

📎 rollup.config.js:203-206

Nicht-esm-bundlerBuild hartcodiert__DEV__, da ihre dev/prod-Verzweigungen zur Build-Zeit bereits feststehen.

📎 rollup.config.js:210-216

Der letzte Schritt erlaubt Umgebungsvariablen, jede Makrodefinition zu überschreiben, und unterstützt__RUNTIME_COMPILE__=true pnpm build runtime-coresolche Inline-Überschreibungen.

Ersetzungs-Plugin:resolveReplace

📎 rollup.config.js:222-255

resolveReplaceVerarbeitet außerhalb vonresolveDefineErsetzungen, die esbuild nicht verarbeiten kann:

  • FührtenumDefineszusammen (ausinlineEnumsdie Inline-Enum-Definitionen).
  • Im Produktions-Browser-Build wird der Fehlererstellungsfunktion eine/*@__PURE__*/Annotation hinzugefügt, um Tree-Shaking zu unterstützen.
  • esm-bundlerIm__DEV__Build wird!!(process.env.NODE_ENV !== 'production')durch
  • ersetzt, damit der Bundler entscheidet.process.envIm Browser-ESM-Build wird

durch ein leeres Objekt ersetzt, um Browserfehler zu vermeiden.resolveExternal

📎 rollup.config.js:257-283

Externe Abhängigkeiten:treeShakenDepsDies ist der Kern der Denkaufgabe am Ende des vorherigen Kapitels. Der Browser-Build gibt nurdependenciesals external zurück – diese Abhängigkeiten werden zwar importiert, aber im Browser-Zweig nicht tatsächlich ausgeführt; sie werden hier nur aufgeführt, um Rollup-Warnungen zu unterdrücken. Node/ESM-Bundler-Builds externalisieren allepeerDependenciesundpath、url、streamsowie Node-Built-in-Module wie

.

📎 rollup.config.js:319-352

Finales Konfigurationsobjekt

  • inputDas zurückgegebene Konfigurationsobjekt enthält:
  • external: Absoluter Pfad zur Einstiegsdatei.
  • plugins: Liste externer Abhängigkeiten.
  • output: Plugin-Array in der Reihenfolge json → alias → enumPlugin → replace → esbuild → nodePlugins.
  • onwarn: Ausgabekonfiguration.CIRCULAR_DEPENDENCY: Filtert
  • treeshake.moduleSideEffects: falseWarnungen heraus (im Vue-Quellcode existieren zirkuläre Abhängigkeiten, die zur Laufzeit harmlos sind).

: Teilt Rollup mit, dass alle Module keine Seiteneffekte haben, aggressives Tree-Shaking.

mermaid
flowchart LR
    env["process.env<br/>TARGET, FORMATS, NODE_ENV"] --> pkg_load["require(package.json)"]
    pkg_load --> pkg_opts["packageOptions<br/>= pkg.buildOptions"]
    env --> fmt_sel["packageFormats<br/>= FORMATS || buildOptions.formats || default"]
    fmt_sel --> cfg_map["outputConfigs[format]"]
    pkg_opts --> create_cfg["createConfig(format, output)"]
    cfg_map --> create_cfg
    create_cfg --> define["resolveDefine()<br/>__DEV__, __BROWSER__ ..."]
    create_cfg --> replace["resolveReplace()<br/>enumDefines, __DEV__"]
    create_cfg --> external["resolveExternal()<br/>treeShakenDeps / deps"]
    create_cfg --> node_plugins["resolveNodePlugins()<br/>commonJS, nodeResolve"]
    define --> rollup_cfg["RollupOptions<br/>{ input, external, plugins, output }"]
    replace --> rollup_cfg
    external --> rollup_cfg
    node_plugins --> rollup_cfg
    rollup_cfg --> rollup_run["Rollup 执行构建"]
    rollup_run --> dist["dist/*.js 产物落盘"]

Kopieren

execArtefakt-Persistenz und Größenprüfung

build.jsProzessverwaltung vonexecStartet den Rollup-Unterprozess über

📎 scripts/utils.js:64-114

exec: Kapseltspawn, gibt ein Promise zurück. Schlüssendesign:

  • stdioStandardmäßig ist['ignore', 'pipe', 'pipe']– stdin ignoriert, stdout/stderr als Pipe erfasst.
  • shell: process.platform === 'win32'– unter Windows wird eine Shell benötigt, um Befehle korrekt aufzulösen.
  • Sammelt Ausgaben überstderrChunksundstdoutChunksArrays, die imexit-Event zusammengefügt werden.
  • Bei Exit-Code 0 wird resolved, andernfalls rejected mit stderr-Inhalt.
〔Design-Inferenz und Architektur-Abwägungen〕

Beachten Sie, dassbuild.jsbeim Aufruf vonexecdas Argument{ stdio: 'inherit' }übergibt, was die Standard-Pipe-Konfiguration überschreibt und Rollups Ausgabe direkt an das Terminal weiterleitet. Dies ist das korrekte Verhalten für Build-Tools – Benutzer müssen den Build-Fortschritt in Echtzeit sehen.

Größenprüfung:checkAllSizes

📎 scripts/build.js:206-215

Die Größenprüfung hat zwei Überspringbedingungen:devOnlyist wahr, oder es wurde ein Format angegeben, dasglobalnicht enthält. Denn die Größenprüfung gilt nur für globale Build-Artefakte – das sind Dateien, die Endbenutzer direkt einbinden, und die größenempfindlichsten.

📎 scripts/build.js:222-228

checkSizePrüft zwei Dateien:${target}.global.prod.jsund${target}.runtime.global.prod.js(letztere wird nur geprüft, wenn kein Format angegeben wurde oderglobal-runtimeangegeben wurde).

📎 scripts/build.js:235-264

checkFileSizeLiest die Datei, berechnet die komprimierte Größe mitgzipSyncundbrotliCompressSync, formatiert die Ausgabe mitprettyBytes. WennwriteSizewahr ist, werden die Ergebnisse intemp/size/${fileName}.jsongeschrieben – dies ist die Datenquelle für die Größenbudget-Prüfung in CI.

Typdeklarations-Build

📎 scripts/build.js:94-108

WennbuildTypeswahr ist, wirdpnpm run build-dtsaufgerufen und die Zielliste über--environment TARGETS:...übergeben. Dies stellt sicher, dass Typdeklarationen nur für tatsächlich gebaute Pakete generiert werden.

Design-Überlegungen und Produktions-Fallstricke

Warum--environmentstatt direkter Parameterübergabe verwenden?Rollups--environmentist die einzige Möglichkeit, in der Konfigurationsdatei überprocess.envParameter zu lesen. Direkte Übergabe von--config-Parametern erfordert Parsen vonprocess.argv, während--environmentstrukturierte Schlüssel-Wert-Paar-Analyse bietet.

fuzzyMatchTargetDie Regex-Falle von target.match(partialTarget).partialTargetInruntime-core,-istruntime.core,.Benutzereingabe. Wenn der Benutzer

eingibt, ist es ein Literal in der Regex, kein Problem; aber wenn runParalleleingegeben wird, matcht es beliebige Zeichen und könnte unerwartete Ziele treffen. Dies ist das inhärente Risiko von Fuzzy-Matching, aber Vues Paketnamen enthalten keine Regex-Sonderzeichen, sodass es praktisch nicht ausgelöst wird.cpus().lengthRessourcenkonkurrenz bei parallelen Builds.--max-old-space-sizeverwendet

scanEnumsals Parallelitätsobergrenze, aber jeder Rollup-Prozess startet selbst Worker. In CI-Containern mit wenigen Kernen kann dies zu Speicherüberlauf führen. Wenn in der Produktion OOM auftritt, kann dies durch removeCacheoder Reduzierung der Parallelität gemildert werden.finallyDer Cache-Lebenszyklus vonscanEnums.removeCachewird infinallyaufgerufen, aber wennscanEnumsselbst einen Fehler wirft, wirdtrynicht zugewiesen und der Aufruf in

resolveExternalschlägt fehl. Tatsächlich ist die vonzurückgegebene Funktion bereits vorruntime-corebestimmt, sodass dieses Risiko nicht besteht – aber dies ist ein Timing-Detail, das beim Lesen bestätigt werden muss.resolveExternalDas Auslassungsrisiko von

.

Die Denkaufgabe des vorherigen Kapitels hat bereits gezeigt: Wennnode scripts/build.js vueeine neue Abhängigkeit hinzugefügt wird, aber vergessen wird,

1. parseArgszu aktualisieren, packt der Browser-Build diese Abhängigkeit ein (da sie nicht in der external-Liste steht), was zu Größenaufblähung führt. Dies sind die inhärenten Kosten der „Whitelist-external"-Strategie.commitKapitelzusammenfassung

2. run()Eine vollständige Reise vonscanEnums:fuzzyMatchTargetparst die Kommandozeile,allTargets)。

3. buildAllwird synchron abgerufen.runParallelRuftbuild。

4. buildauf, generiert den Enum-Cache, parst das Ziel (package.jsonoderdistüber--environmentparallel geplantexecRollup starten.

5. rollup.config.jsUmgebungsvariablen lesen, übercreateConfigKonfigurationsarray generieren,resolveDefine/resolveReplace/resolveExternalMakros, Ersetzungen und externe Abhängigkeiten separat verarbeiten.

6. Rollup führt den Build aus, die Artefakte werden auf die Festplatte geschrieben nachdist/。

7. checkAllSizesgzip/brotli-Größe berechnen, optional schreiben nachtemp/size/。

8. Wenn--withTypes, aufrufenbuild-dtsTypdeklarationen generieren.

Gedanken und Selbsttest dieses Kapitels

Q1: Inbuild.jsderbuildFunktion,if (!formats && fs.existsSync(...))Diese Bedingung entscheidet, obdistVerzeichnis gelöscht wird. Wenn!formatsdiese Bedingung entfernt wird (d. h. unabhängig davon, ob ein Format angegeben ist,distwird immer gelöscht), was passiert inpnpm build-all-cjseinem solchen Skript?

Referenzanalyse:

📎 scripts/build.js:172-175

pnpm build-all-cjsentsprichtnode scripts/build.js vue runtime compiler reactivity shared -af cjs(siehe📎 package.json:40). Es gibt an-f cjs, daher istformatsfür'cjs',!formatsfalsch, die aktuelle Logik löschtdist。

nicht. Wenn!formatsentfernt wird, wird bei jedem Builddistgelöscht. Aberbuild-all-cjsbaut nurcjsFormat, nach dem Löschen bleibt indistnur nochcjsArtefakt übrig, die zuvor gebautenesm-bundler、globalund andere Formate gehen alle verloren. Noch schwerwiegender ist, dassbuild-runtime-esm、build-browser-esmund andere Skripte nacheinander ausgeführt werden (siehe📎 package.json:39derbuild-sfc-playgroundSkripte), jedes Skript löscht die Artefakte des vorherigen Skripts, sodass schließlich indistnur noch das Format des letzten Skripts übrig bleibt. Dies zerstört den Build des SFC Playground – er benötigt Artefakte in mehreren Formaten gleichzeitig.

Q2: runParallelInif (maxConcurrency <= source.length)Welche Rolle spielt diese Bedingung? Wenn sie entfernt wird, was passiert beim Bauen eines einzelnen Pakets (targets.length === 1)?

Referenzanalyse:

📎 scripts/build.js:131-151

Diese Bedingung steuert, ob die Nebenläufigkeitsbegrenzung aktiviert wird. WennmaxConcurrency > source.length, ist keine Begrenzung nötig – alle Aufgaben können gleichzeitig gestartet werden. Wenn diese Bedingung entfernt wird, wird selbst bei nur einer AufgabeexecutingArray erstellt undawait Promise.race(executing)。

ausgeführt. Für eine einzelne Aufgabeexecutinggibt es nur ein Promise ine,Promise.race, das auf dessen Abschluss wartet. Dies führt nicht zu Fehlern, aber zu unnötigem Promise-Chaining und Microtask-Scheduling-Overhead. Wichtiger ist, dassexecuting.splice(executing.indexOf(e), 1)im Single-Task-Szenario immer noch korrekt funktioniert, also funktional kein Unterschied besteht, nur ein geringer Performanceverlust.

Das eigentliche Risiko besteht darin: WennmaxConcurrency0 ist (theoretisch unmöglich, dacpus().lengthmindestens 1 ist),executing.length >= 0immer wahr ist,Promise.race([])ewig hängen würde. Abercpus().lengthgarantiert, dass diese Grenze nicht ausgelöst wird.

Q3: resolveExternalIntreeShakenDepsgibt der Browser-Build

als external zurück, aber diese Abhängigkeiten werden im Browser-Zweig nicht tatsächlich ausgeführt. Was passiert, wenn man sie aus der external-Liste entfernt (d. h. Rollup versuchen lässt, sie zu bündeln)?:

📎 rollup.config.js:257-283

treeShakenDepsReferenzanalysesource-map-js、@babel/parser、estree-walker、entities/decodeenthältcompiler-sfc. Dies sind Abhängigkeiten von__BROWSER__und anderen Paketen, die im Browser-Build durch

Makros bedingt kompiliert und ausgeschlossen werden.treeshake.moduleSideEffects: false(📎 rollup.config.js:355-355Wenn sie aus external entfernt werden, versucht Rollup, diese Abhängigkeiten aufzulösen und zu bündeln. Daif (!__BROWSER__)), und die Import-Anweisungen dieser Abhängigkeiten im__BROWSER__Zweig liegen, ersetzt esbuilds definetruedurch

, wodurch der Zweig als toter Code markiert wird. Rollups Tree-Shaking entfernt diese Importe, sodass der endgültige Output den Code dieser Abhängigkeiten nicht enthält.onwarnDas Problem ist jedoch: Rollup muss Module auflösen, bevor Tree-Shaking stattfindet. Wenn diese Abhängigkeiten nicht installiert sind (z. B. in einer minimalen CI-Umgebung), meldet Rollup einen „Modul kann nicht aufgelöst werden"-Fehler. Sie als external aufzulisten ist eine defensive Maßnahme – selbst wenn die Abhängigkeit nicht existiert, versucht Rollup nicht, sie aufzulösen, sondern gibt nur eine Warnung aus (und

filtert Warnungen für nicht-zyklische Abhängigkeiten heraus).scripts/dev.jsDamit haben wir die vollständige Build-Reise von der Befehlsanalyse bis zum Rollup-Aufruf durchlaufen und Kernmechanismen wie nebenläufige Planung und Filterung privater Pakete aufgedeckt. Der Produktions-Build ist jedoch nur die Hälfte der Geschichte. Im nächsten Kapitel wenden wir uns der Entwicklungszeit-Pipeline zu und sehen, wie

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 03

Nächstes Kapitel: Kapitel 3 →

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 3 von 14

Verifikationsstatus: FACT-Zeilennummern echt verankertscripts/dev.jsIm vorherigen Kapitel haben wir die vollständige Kette des Produktions-Builds von der Parameteranalyse bis zum Schreiben der Multi-Format-Artefakte auf die Festplatte verfolgt; diese Kette strebt Vollständigkeit und Normkonformität der Artefakte an. Das Kernanliegen der Entwicklungszeit ist jedoch nur eines: eine Zeile Code ändern, sofort im Browser das Ergebnis sehen. Die Kette des Produktions-Builds „Parameter parsen → Konfiguration generieren → vollständiges Bündeln → auf die Festplatte schreiben" dauert oft Dutzende Sekunden und kann dieses Anliegen überhaupt nicht erfüllen. Das Vue-core-Repository unterhält dafür eine unabhängige Entwicklungszeit-Pipeline:scripts/pre-dev-sfc.jsverwendet den Watch-Modus von esbuild für inkrementelle Builds,

kompiliert den SFC-Compiler vor dem Haupt-Build. Dieses Kapitel zerlegt den Kooperationsmechanismus der beiden.

3.1 dev.js: Inkrementeller Builder mit esbuild für Geschwindigkeit

Intuitives Modell📎 scripts/dev.js:3-5

Der Produktions-Build ist wie „der offizielle Satz und Druck in einer Druckerei" – Qualität hat Priorität, etwas langsamer ist egal; der Entwicklungs-Build ist wie „eine Bleistiftskizze auf einem Notizzettel" – nicht auf Schönheit ausgerichtet, nur darauf, sofort sichtbar zu sein. Vue wählt esbuild statt Rollup, um diese Skizze zu zeichnen; der Grund steht im Kommentar am Anfang der Datei: Rollup-Artefakte sind kleiner, Tree-Shaking ist besser, aber esbuild ist viel schneller.

Ohne dieses Skript müsste der Entwickler bei jeder Änderung einen vollständigen Produktions-Build ausführen, der Feedback-Zyklus würde von Millisekunden auf Minuten degradieren, und das Hot-Update-Erlebnis wäre völlig verloren.

Parameteranalyse und Format-AbleitungparseArgsDer Skripteinstieg verwendet Nodes eingebautesformat, um drei Optionen zu parsen:global)、prod(Standardfalse)、inline(Standardfalse)。📎 scripts/dev.js:18-40Positionsargumente werden gesammelt alstargets, falls leer, dann standardmäßig['vue']。📎 scripts/dev.js:42-53

〔Designableitung und Architekturabwägungen〕

Hier gibt es ein leicht zu übersehendes Detail:rawFormatundformatsind zwei Zuweisungen.parseArgsDasdefault: 'global'vonrawFormathat bereits sichergestellt, dassconst format = rawFormat || 'global'einen Wert hat, aber das Skript schreibt dennoch📎 scripts/dev.js:42als Fallback.parseArgsDies ist eine defensive Schreibweise, um zu vermeiden, dassformat.startsWithbei Verhaltensänderungen oder expliziter Übergabe eines leeren Strings nachgelagert

formateinen Fehler wirft.globalDie Zuordnung zum esbuild-Ausgabeformat erfolgt über drei Verzweigungen: beginnend mitiifewird zucjszugeordnet, gleichcjswird zuesm。📎 scripts/dev.js:42-53zugeordnet, alles andere einheitlich-runtimeDer Dateinamensuffix des Produkts wird dann durchglobal-runtimeSuffix separat behandelt:runtime.globalwird zu📎 scripts/dev.js:42-53

, der Rest bleibt unverändert.

Zielpaketlokalisierung und Ausgabepfadpackages-privateDas Skript liest zuerst die📎 scripts/dev.js:56Verzeichnisliste, um zu bestimmen, ob das Zielpaket zu einem öffentlichen oder privaten Paket gehört.packagesFür jedes target wird entschieden, ob der Paketbasispathpackages-privateoderrequireist, dannpackage.jsondessenversion, umbuildOptions。📎 scripts/dev.js:58-63

undvue-compatzu erhalten.vueDer Ausgabedateiname hat einen Sonderfall:vue-compat.global.js。📎 scripts/dev.js:64-69Das Ziel wird umbenannt zupackages/vue/dist/vue.global.js,prod, um zu vermeiden, dass das Produktprod.heißt. Der endgültige Pfad hat die Form

Wenn wahr, wird

externalSegment eingefügt.

external-Auflösung: Vermeidung, Abhängigkeiten ins Produkt zu packeninlineDascjsArray bestimmt, welche Module nicht gebündelt werden. Die Logik ist zweischichtig:esm-bundlerErste Schicht: Wenndependencies、peerDependenciesnicht aktiviert ist und das Formatpath、url、streamist oder📎 scripts/dev.js:76-88enthält, werden alle Schlüssel von@vue/compiler-sfczu external hinzugefügt undserver-rendererdrei Node-Built-in-Module hartcodiert.

Der Kommentar erklärt explizit, dass diese drei fürcompiler-sfcund@vue/consolidatevorbereitet sind.devDependenciesZweite Schicht: Für dasfs、vm、cryptoZiel werden zusätzlich📎 scripts/dev.js:90-112vonreact-dom/server、teacup/lib/express、arc-templates/dist/es5、then-pug、then-jadeaufgelöst, diese sowie

usw. ebenfalls externalisiert.

Im Code sind auchrollup.config.jsusw. Template-Engine-Pfade hartcodiert – dies sind von consolidate unterstützte Template-Engines, optionale Abhängigkeiten, die nicht zwangsweise installiert werden dürfen.TODO this logic is largely duplicated from rollup.config.js〔Designableitung und Architekturabwägungen〕

Diese Logik ist hochgradig redundant mit

, was auch im Quellcode-Kommentar zugegeben wird (log-rebuild). Der Grund, warum keine gemeinsame Funktion extrahiert wurde, ist, dass es feine Unterschiede in der external-Strategie zwischen dev und prod gibt (dev externalisiert aggressiver, um Builds zu beschleunigen), und eine erzwungene Vereinheitlichung würde die Kopplung erhöhen.onEndPlugins und define-Injektion📎 scripts/dev.js:115-124Das Plugin-Array hat standardmäßig nur ein

, das im

Hook den relativen Pfad des Build-Produkts ausgibt.cjsDies ist das einzige Feedback-Signal für Entwickler, um wahrzunehmen, dass „Änderungen wirksam geworden sind".buildOptions.enableNonBrowserBranches〔Designableitung und Architekturabwägungen〕polyfillNode()。📎 scripts/dev.js:126-128Das zweite Plugin ist bedingt: Wenn das Format nichtcompiler-sfcist und das

definedes Pakets wahr ist, wird📎 scripts/dev.js:141-159eingehängt.__XXX__Solche Pakete (wie

  • __COMMIT__) durchlaufen im Browser-Build immer noch den Node-Zweig und benötigen Polyfills für Node-Built-in-Module, um in der Browser-Umgebung zu funktionieren."dev",__VERSION__Der
  • __DEV__Block ist der informationsdichteste Teil dieses Kapitels.prodEr ersetzt alle__TEST__Makros im Quellcode durch Literale:false;
  • __BROWSER__ist fest aufformat !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches。📎 scripts/dev.js:146-148gesetzt,
  • __SSR__nimmt die Paketversion;format !== 'global'wird durch das
  • __COMPAT__Flag bestimmt,vue-compatist konstant
  • Die Ableitung von__FEATURE_SUSPENSE__、__FEATURE_OPTIONS_API__、__FEATURE_PROD_DEVTOOLS__、__FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ist am subtilsten:

Das heißt, nur „nicht cjs und Paket unterstützt keinen Nicht-Browser-Zweig" wird als Browser-Umgebung markiert;vitest.config.tsistdefine, d.h. der global-Build aktiviert den SSR-Zweig nicht;📎 vitest.config.ts:6-21wird dadurch bestimmt, ob das target__TEST__ist;true、__DEV__Drei Feature-Flags (true) sind im dev-Modus alle fest verdrahtet.

Diese Makros entsprechen eins zu eins dem

Block inesbuild.context(...).then(ctx => ctx.watch())。📎 scripts/dev.js:130-161 context.watch()Die Testumgebung setztonEndauf

mermaid
flowchart TD
    start["parseArgs 解析 format/prod/inline"] --> targets{"positionals 为空?"}
    targets -->|是| def["targets = ['vue']"]
    targets -->|否| use["targets = positionals"]
    def --> loop["遍历每个 target"]
    use --> loop
    loop --> priv{"target 在 packages-private?"}
    priv -->|是| pbase["pkgBase = packages-private"]
    priv -->|否| pub["pkgBase = packages"]
    pbase --> req["require package.json"]
    pub --> req
    req --> ext{"inline 开启?"}
    ext -->|是| noext["external = []"]
    ext -->|否| fmt{"format 是 cjs 或 esm-bundler?"}
    fmt -->|是| deps["加入 dependencies/peerDependencies + path/url/stream"]
    fmt -->|否| sfc{"target == compiler-sfc?"}
    deps --> sfc
    sfc -->|是| cons["加入 consolidate devDeps + fs/vm/crypto"]
    sfc -->|否| noext
    cons --> ctx["esbuild.context 创建上下文"]
    noext --> ctx
    ctx --> watch["ctx.watch() 启动监听"]
    watch --> onend["onEnd 打印 built: 相对路径"]

auf

. Der Unterschied zum dev-Build ist genau der Unterscheidungspunkt zwischen den beiden Laufzeitzuständen „Test vs. Entwicklung".

watch-Modus-Startcompiler-sfcDer letzte Schritt ist, dasscompiler-coreeinen Build-Kontext erstellt, aber nicht sofort ausführt,compiler-coreerst dann wird die Dateiüberwachung tatsächlich gestartet. Danach pflegt esbuild intern den Abhängigkeitsgraphen; jede Änderung an einer abhängigen Datei löst einen inkrementellen Rebuild aus, und der Rebuild-Abschluss-Callbackcompiler-sfcgibt das Log aus..vueKopierenpre-dev-sfc.js3.2 pre-dev-sfc.js: Der Precompile-Wächter zur Auflösung zirkulärer Abhängigkeiten

Intuitives Modell

Stellen Sie sich ein „Henne-Ei"-Dilemma vor:compiler-sfc、compiler-core、compiler-dom、compiler-ssr、shared。📎 scripts/pre-dev-sfc.js:4-10Der Quellcode vonpackages/${pkg}/dist/${pkg}.cjs.jsimportiert📎 scripts/pre-dev-sfc.js:4-23

, undallFilesPresentbenötigt im Entwicklungszustandfalse, umbreakDateien zu verarbeiten. Wenn beide auf esbuild watch in Echtzeit kompilieren angewiesen sind, blockiert derjenige, der zuerst kompiliert.📎 scripts/pre-dev-sfc.js:20-21Die Rolle vonallFilesPresentist „zuerst das Ei ausbrüten, dann das Huhn aufziehen" – vor dem Start des Haupt-Builds sicherstellen, dass die CJS-Produkte dieser Pakete bereits existieren.process.exit(1)Checkliste und Kurzschlusslogik📎 scripts/pre-dev-sfc.js:25-27

Das Skript pflegt eine feste Liste:

Für jedes Paket wird geprüft, obexit(1)existiert.&&Sobald eines fehlt, wird

mermaid
flowchart TD
    start["遍历 packagesToCheck 清单"] --> check{"dist/pkg.cjs.js 存在?"}
    check -->|是| next{"还有下一个包?"}
    next -->|是| check
    next -->|否| ok["allFilesPresent 保持 true"]
    check -->|否| fail["allFilesPresent = false 并 break"]
    ok --> exit0["正常退出 退出码 0"]
    fail --> exit1["process.exit(1) 退出码 1"]

gesetzt und sofort

scripts/dev.js, ohne die restlichen Pakete zu prüfen.scripts/aliases.jsWenn schließlich📎 scripts/aliases.js:7-7

falsch ist,

resolveEntryForPkgwird mit einem Nicht-Null-Code beendet.packages/${p}/src/index.ts。📎 scripts/aliases.js:7-7Semantik des Exit-Codesvue、vue/compiler-sfc、vue/server-renderer、@vue/compat。📎 scripts/aliases.js:16-21

Dieses Skript führt selbst keine Kompilierung durch, es macht nur „Existenz-Assertions".packagesEs ist ein Signal für den aufrufenden Layer (normalerweise dievue-Kette des npm-Skripts oder CI-Skripte): Die Produkte sind unvollständig, es muss zuerst ein vollständiger Build ausgeführt werden. Wenn alle existieren, wird normal beendet (Exit-Code 0), und der Haupt-Build fährt fort.nonSrcPackages(sfc-playground、template-explorer、dts-testKopieren@vue/${dir}3.3 aliases.js und vitest.config.ts: Die andere Hälfte der Entwicklungskette📎 scripts/aliases.js:23-35

löst „wie Produkte schnell generiert werden", aber während der Entwicklung gibt es noch einen anderen Pfad: Tests ausführen.

Bietet gemeinsame Pfadalias für vitest und rollup.nonSrcPackagesDie Ausschlussliste hingegen, weil diese drei Pakete keinesrc/index.tsEinstiegspunkte haben, ein erzwungenes Mapping würde zu Parsing-Fehlern führen.

vitest's define und Alias-Konsum

vitest.config.tsdirekt importentriesalsresolve.alias。📎 vitest.config.ts:3📎 vitest.config.ts:22-24dessendefineBlock steht im Kontrast zur Makro-Injektion von dev.js: Testumgebung__DEV__: true、__TEST__: true、__BROWSER__: false、__CJS__: true。📎 vitest.config.ts:6-21

Die Tests sind in fünf Projekte aufgeteilt:unit、unit-gc、unit-jsdom、e2e、e2e-browser。📎 vitest.config.ts:51-118wobeiunit-gcverwendetpool: 'forks'und übergibt--expose-gc, um speziell SSR-Tests auszuführen, die manuell GC auslösen müssen.📎 vitest.config.ts:65-76 e2e-browserhingegen aktiviert die Chromium-Instanz von Playwright, um Transition-bezogene Tests auszuführen.📎 vitest.config.ts:99-117

mermaid
sequenceDiagram
    participant Dev as 开发者
    participant NPM as npm script
    participant Pre as pre-dev-sfc.js
    participant DevJS as dev.js
    participant ESB as esbuild context
    participant FS as 文件系统

    Dev->>NPM: 启动开发
    NPM->>Pre: 检查 SFC 产物
    Pre->>FS: existsSync(dist/*.cjs.js)
    alt 产物缺失
        FS-->>Pre: false
        Pre-->>NPM: exit(1)
        NPM-->>Dev: 提示先跑完整构建
    else 产物齐全
        FS-->>Pre: true
        Pre-->>NPM: exit(0)
        NPM->>DevJS: 启动 dev.js
        DevJS->>ESB: context(...).watch()
        ESB->>FS: 监听源码变化
        Dev->>FS: 修改 src/index.ts
        FS-->>ESB: 文件变更事件
        ESB->>ESB: 增量重建
        ESB-->>Dev: onEnd 打印 built: 路径
    end

Designüberlegungen

Warum verwendet dev esbuild und prod Rollup?Dies ist keine willkürliche Technologiewahl, sondern die Einschränkungen der beiden Szenarien unterscheiden sich. Im Entwicklungsmodus ist die Artefaktgröße unkritisch, die Feedback-Latenz jedoch äußerst kritisch; im Produktionsmodus ist es umgekehrt. esbuild ist in Go geschrieben und hochgradig parallelisiert, Kaltstart und inkrementelle Builds sind um eine Größenordnung schneller, aber seine Tree-Shaking- und Code-Splitting-Fähigkeiten sind schwächer als die von Rollup.📎 scripts/dev.js:3-5Zwei Werkzeuge für zwei Szenarien einzusetzen, ist ein pragmatischer Kompromiss im Engineering.

〔Design-Inferenz und Architektur-Abwägung〕

Warum prüft pre-dev-sfc nur und kompiliert nicht?Wenn es selbst die Kompilierung auslösen würde, würde es die zirkuläre Abhängigkeit wieder einführen – es musscompiler-sfckompilieren, und der Kompilierungsprozess selbst könnte von dencompiler-sfcArtefakten abhängen. Daher kann es nur eine „Assertion" durchführen und die Tatsache „fehlende Artefakte" an die obere Ebene melden, die dann entscheidet, ob ein vollständiger Build ausgeführt oder mit Fehler beendet wird. Dies ist ein „Wächter-Muster": Es löst das Problem nicht, sondern meldet es nur.

Ist die Duplizierung der external-Liste technische Schuld?Die external-Logik von dev.js und rollup.config.js ist dupliziert, was auch in den Quellcode-Kommentaren zugegeben wird.📎 scripts/dev.js:73Aber die external-Mengen der beiden sind nicht vollständig identisch – dev externalisiert aggressiver für Geschwindigkeit. Eine gewaltsame Extraktion in eine gemeinsame Funktion würde einen parametrisierten Differenz-Schalter erfordern, was beide Logiken schwerer lesbar machen würde. Dies ist ein typischer Kompromiss von „Duplizierung ist besser als falsche Abstraktion".

Kapitelzusammenfassung

Dieses Kapitel hat die drei Puzzleteile der Vue-Core-Entwicklungsmodus-Kette auseinandergenommen:

1. scripts/dev.js: Inkrementelle Builds mit esbuild'scontext().watch()implementieren, durchparseArgsFormat und Flags auflösen, dynamischrequireZielpaketpackage.jsonAusgabepfad lokalisieren, Makros wie__DEV__、__BROWSER__injizieren, um bedingte Kompilierung zu steuern, und mit demlog-rebuildPlugin nach jedem Rebuild Feedback ausgeben.

2. scripts/pre-dev-sfc.js: Vor dem Haupt-Build prüfen, ob die CJS-Artefakte der fünf Kernpakete existieren; bei Fehlen mit Exit-Code 1 kurzschließen, um Build-Deadlocks durch zirkuläre Abhängigkeiten zu vermeiden.

3. scripts/aliases.js + vitest.config.ts: Gemeinsame Pfad-Aliase für die Testkette bereitstellen, spezielle Einträge hartcodiert plus dynamisches Scannen allgemeiner Einträge, kombiniert mit Multi-Projekt-Konfiguration, die fünf Test-Szenarien abdeckt: Unit, GC, jsdom, e2e, Browser-e2e.

Kapitel-Überlegungen und Selbsttest

Q1: Wenn man inscripts/pre-dev-sfc.jsdasbreakentfernt (d.h. erst nach Prüfung aller Pakete über den Exit entscheidet), in welchen Szenarien würde dies die Entwicklererfahrung verschlechtern? Warum hat der Quellcode-Autor „beim ersten fehlenden Paket kurzschließen" gewählt?

Referenzanalyse:

📎 scripts/pre-dev-sfc.js:4-23

breakbefindet sich imif (!fs.existsSync(...))Zweig; sobald ein fehlendes Paket-Artefakt entdeckt wird, wird die Schleife sofort verlassen.

Wenn manbreakentfernt, würde das Skript weiter die restlichen Pakete prüfen, letztendlich wäreallFilesPresentimmer nochfalse, der Exit-Code wäre immer noch 1,funktional äquivalent. Der Unterschied liegt jedoch in:

1. Performance: Die fünfexistsSyncAufrufe selbst sind schnell, aber wenn die Liste auf Dutzende Pakete erweitert würde, könnte das Kurzschließen eine große Anzahl unnötiger stat-Systemaufrufe einsparen.

2. Semantik: Kurzschließen drückt aus: „Wenn auch nur eines fehlt, ist das Ganze unvollständig" – dies ist eine boolesche Assertion, man muss nicht wissen, wie viele genau fehlen. Weiteres Prüfen erzeugt keine zusätzlichen Informationen.

3. Entwicklererfahrung: Tatsächlich verschlechtert sich die „Fehlermeldung". Das aktuelle Skript gibt nicht aus, welches Paket fehlt; der Entwickler sieht nur Exit-Code 1. Wenn manbreakentfernen und Logging hinzufügen würde, könnte man dem Entwickler stattdessen mitteilen „compiler-core und shared fehlen" – aber das erfordert zusätzlichen Code. Der Autor wählte die einfachste Implementierung und überlässt die Diagnose „welches fehlt" der Fehlermeldung des übergeordneten Build-Skripts.

Daher istbreakdie Kernmotivation „Assertion-Semantik + Performance", nicht Erfahrungsoptimierung.

Q2: scripts/dev.jsIn__BROWSER__ist die Ableitung vonformat !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches. Angenommen, dasbuildOptions.enableNonBrowserBrancheseines Pakets isttrue, und der Entwickler baut mit-f global, dann ist__BROWSER__gleichfalse. Welche Konsequenzen hat das? Was passiert, wenn man es fälschlicherweise zutrueändert?

Referenzanalyse:

📎 scripts/dev.js:146-148

Wennformat = 'global'undenableNonBrowserBranches = true:

  • format !== 'cjs'isttrue
  • !pkg.buildOptions?.enableNonBrowserBranchesistfalse
  • Insgesamt__BROWSER__ = false

Das bedeutet, alleif (__BROWSER__)Zweige im Quellcode werden durch esbuild's define ersetzt durchif (false), browserspezifischer Code wird durch Tree-Shaking entfernt, Nicht-Browser-Zweige (Node-spezifische Logik) bleiben erhalten.

Konsequenz: Das global-Build-Artefakt sollte eigentlich im Browser laufen, enthält aber Node-spezifische Zweige. Wenn diese Zweige Node-Built-in-Module wiefs、pathreferenzieren, meldet der Browser beim Laden „Modul nicht definiert". Genau deshalb werden Pakete, bei denenenableNonBrowserBrancheswahr ist (wiecompiler-sfc), normalerweise nicht für global-Builds verwendet, oder es wird daspolyfillNode()Plugin als Fallback benötigt.📎 scripts/dev.js:126-128

Wenn man es fälschlicherweise zutrue:__BROWSER__ = trueändert, bleiben Browser-Zweige erhalten, Node-Zweige werden entfernt. Für Pakete wiecompiler-sfc, die SFC-Kompilierung in der Node-Umgebung ausführen müssen, würde dies dazu führen, dass Kernfunktionen (Dateien lesen, Node-APIs aufrufen) durch Tree-Shaking entfernt werden, und das Artefakt würde zur Laufzeit in Node „Funktion nicht definiert" melden.

Q3: scripts/aliases.jsInpackageswird beim dynamischen Scannen desnonSrcPackages(sfc-playground、template-explorer、dts-testVerzeichnissespackagesübersprungen). Wenn ein neues Paket zumsrc/index.ts, und nicht hinzugefügt wurde zunonSrcPackages, was passiert? An welcher Stelle wird vitest zur Laufzeit einen Fehler melden?

Referenzanalyse:

📎 scripts/aliases.js:23-35

Die dynamische Scan-Logik ist: Für jedes Verzeichnis, wenndir !== 'vue', nicht innonSrcPackages, der Key nicht existiert und es ein Verzeichnis ist, dann füge hinzuentries['@vue/${dir}'] = resolveEntryForPkg(dir)。

resolveEntryForPkggibt den Pfad vonpackages/${p}/src/index.tszurück.📎 scripts/aliases.js:7-7Beachte, dass esnicht prüft, ob die Datei existiert, sondern nur den Pfad zusammensetzt.

Konsequenz: Der Alias wird registriert, zeigt aber auf eine nicht existierende Datei. Wenn vitest einen Import auflöst und eine Testdatei dieses Paket importiert, versucht das resolve-Plugin von Vite, diesen Pfad zu laden, und meldet „Modul kann nicht aufgelöst werden" oder „Datei existiert nicht".

Fehlerstelle: Nicht während der Ausführung vonaliases.js(es macht nur String-Verkettung), sondern nach dem Start von vitest, beim ersten Auflösen dieses Imports. Wenn kein Test dieses Paket importiert, tritt kein Fehler auf – der Alias liegt einfach imentries-Objekt.

Umgehung: Füge solche Pakete ohnesrc/index.tszunonSrcPackageshinzu, oder stelle sicher, dass neue Pakete einen Standard-Einstiegspunkt haben. Deshalb mussnonSrcPackagesmanuell gepflegt werden – es ist die Ausnahmeliste für „Konvention vor Konfiguration".

Die Grenzen der Zusammenarbeit der drei sind klar:pre-dev-sfcregelt „ob das Artefakt bereit ist",dev.jsregelt „wie das Artefakt schnell aktualisiert wird",aliasesregelt „wie Tests den Quellcode auflösen". Die Entwicklungskette löst das Geschwindigkeitsproblem, aber zur Build-Zeit gibt es noch eine andere, verstecktere Optimierung – Transformationen, die abgeschlossen sind, bevor der Code vom Browser ausgeführt wird. Das nächste Kapitel betritt die Compile-Zeit-Magie und zeigt, wie Enum-Inlining und Tree-shaking-Verifikationsmechanismen zur Build-Zeit TypeScript-Enums durch Literale ersetzen und sicherstellen, dass das Versprechen des bedarfsgerechten Imports nicht gebrochen wird.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 04

Kapitel 4: Compile-Zeit-Magie: Enum-Inlining und Tree-shaking-Verifikationsmechanismen

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 4 von 14

Im vorherigen Kapitel haben wir gesehen, wie die Entwicklungskette durch Dateiüberwachung und inkrementelle Builds die Geschwindigkeit von „eine Zeile ändern, sofort wirksam" erreicht. Aber jenseits der Geschwindigkeit gibt es bei Vue eine verstecktere Einschränkung: Die Größe des veröffentlichten Artefakts muss kontrollierbar sein. Einer der Feinde dieser Einschränkung ist das TypeScript-Enum – es ist zur Laufzeit ein real existierendes Objekt und zerstört Tree-shaking. Dieses Kapitel betritt die Compile-Zeit und zeigt, wie scripts/inline-enums.js Enums in Literale „auflöst", bevor der Code vom Browser ausgeführt wird; und wie scripts/verify-treeshaking.js nach dem Build mithilfe von Artefakt-Strings rückwärts verifiziert, dass das Versprechen des bedarfsgerechten Imports nicht stillschweigend gebrochen wurde.

4.1 Enum-Inlining: Laufzeitobjekte in Literale auflösen

Intuitives Modell

Stell dir vor, du schreibst ein Rezept, in dem wiederholt „eine Prise Salz" vorkommt. Wenn du bei jedem Kochen zum Anhang blättern müsstest, um „eine Prise = 3 Gramm" nachzuschlagen, wäre das langsam und platzraubend. Enum-Inlining ersetzt vor dem Drucken im gesamten Buch „eine Prise Salz" direkt durch „3 Gramm Salz" und reißt dann die Anhangsseite heraus. Für den Leser (die Laufzeit) ist das Ergebnis völlig identisch, aber das Buch ist dünner.

Welche Katastrophe droht dem System ohne es? Ein normales TypeScript-enumgeneriert nach der Kompilierung ein echtes Objektliteral mit bidirektionalem Mapping (Enum[Enum.A] === 'A'). Dieses Objekt ist einemodulweite Deklaration mit Seiteneffekten, Rollup kann nicht beweisen, dass es ungenutzt ist, und muss es daher behalten – selbst wenn du nur ein Mitglied importierst, werden das gesamte Enum-Objekt samt Rückwärts-Mapping ins Artefakt eingefügt.📎 scripts/inline-enums.js:3-9Der Kommentar inconst enumsagt es deutlich: Sie verwendeten einst

, wechselten aber wegen Issue #1228 zu normalen Enums und nutzen dieses Skript, um „den Nullkosten-Vorteil von const enum manuell zurückzugewinnen".

Datenstruktur und Speicherlayout📎 scripts/inline-enums.js:33-36

  • EnumMember:{ name, value }Der Kern des Skripts sind drei Typdefinitionen; wer sie versteht, versteht den gesamten Datenfluss.
  • EnumDeclaration:{ id, range: [start, end], members }。range, der Name eines einzelnen Enum-Mitglieds und das ausgewertete Literal.ist derQuellcode-Byte-Offsetexport enum X { ... }, der auf die Start- und Endposition der gesamten Deklaration von
  • EnumData:{ declarations, defines }。declarationsin der Datei zeigt – dies ist der Anker für die spätere präzise Ersetzung durch MagicString.defineswird nach Dateipfad indexiert und zeichnet die Ersetzungsbereiche aller Enum-Deklarationen in dieser Datei auf; ist ein flaches Mapping, dessen Schlüssel ` 形式的字符串,值是 ${Enum-Name}.${Mitgliedsname}

JSON.stringify` ist das Literal danach.definesHier gibt es ein entscheidendes Design:Der Schlüssel von。📎 scripts/inline-enums.js:98-103enthält keinen DateipfadErrorCodesDer Kommentar erklärt den Grund –@vue/compiler-corekann gleichzeitig in@vue/runtime-coreundErrorCodes.__EXTEND_POINT__existieren, daher sind gleichnamige Enums über Dateien hinweg erlaubt; aber derselbefullKey in definesdarf nicht in zwei gleichnamigen Enums wiederholt werden, sonst greiftname conflictund wirft direkt

. Dies ist eine Einschränkung „global eindeutig pro Mitgliedsname", nicht „global eindeutig pro Enum-Name".temp/enum.json。📎 scripts/inline-enums.js:33-36Der Cache liegt inscanEnums()Warum muss er auf die Festplatte geschrieben werden? Weilam Build-Eingang nur einmal aufgerufen wird, während Rollup für jedes Paket und jedes Format。📎 scripts/inline-enums.js:39-41unabhängige ProzesseinlineEnums()startet. Der Kommentar weist darauf hin: Die Daten müssen über gleichzeitige Rollup-Prozesse hinweg geteilt werden, daher müssen sie auf die Festplatte serialisiert und von den

der einzelnen Prozesse zurückgelesen werden.

Schritt für Schritt: Von grep zur Literal-Ersetzungexport enumErster Schritt: Alle Dateien mit📎 scripts/inline-enums.js:51-61per grep finden.spawnSync('git', ['grep', 'export enum'])verwendetpath:line:content, die Ausgabe hat die Form:, dann wird nachSetdas erste Segment (Dateipfad) abgetrennt und mitgit grepanstatt das Dateisystem zu durchlaufen – es scannt natürlicherweise nur die von Git verfolgten Dateien und schließt automatischnode_modulesund Build-Artefakte aus.

Zweiter Schritt: Babel parst und sammelt Enum-Informationen.📎 scripts/inline-enums.js:64-70Für jede Datei wird@babel/parsermittypescriptPlugin,sourceType: 'module'zu einem AST geparst und dann nur dieast.program.bodyTop-Level-Knoten durchlaufen.📎 scripts/inline-enums.js:74-79Es werden nurExportNamedDeclarationund derendeclaration.type === 'TSEnumDeclaration'Knoten erkannt – das heißt,nicht-exportierte enums werden nicht verarbeitet.。

Für jede Enum-Deklaration wertet das Skript jedes Mitglied einzeln aus. Die Mitgliedsauswertung hat drei Pfade:

1. Literal-Initialisierung:StringLiteraloderNumericLiteraldirektinit.value。📎 scripts/inline-enums.js:114-119

2. Binärer Ausdruck: wie1 << 2. RekursivresolveValuewerden linke und rechte Operanden verarbeitet; Operanden können Literale sein oderMemberExpression(d. h. Verweise auf zuvor definierte Enum-Mitglieder).📎 scripts/inline-enums.js:121-151Der Schlüssel liegt imMemberExpressionZweig: Er verwendetcontent.slice(node.start, node.end)ausdem ursprünglichen Quelltext, um den Ausdrucksstring herauszuschneiden (wieErrorCodes.FOO), und schlägt danndefinesnach. Wenn nichts gefunden wird, wirdunhandled enum initialization expression。📎 scripts/inline-enums.js:132-141geworfen. Das erklärt, warumdefineseine globale flache Zuordnung sein muss – bei enum-übergreifenden Verweisen kann das referenzierte Element aus einer anderen Datei stammen, aber der Schlüssel erkennt nur枚举名.成员名。

3. Unärer Ausdruck: wie-1, zusammengesetzt zum-1String und dann mitevaluateausgewertet.📎 scripts/inline-enums.js:152-163

Die Auswertung selbst verwendetnew Function('return ' + exp)()。📎 scripts/inline-enums.js:39-41Dies ist einkontrolliertes eval: Die Eingabe stammt aus bereits geparsten AST-Fragmenten im Quelltext, nicht aus beliebiger Benutzereingabe, daher ist die Sicherheitsgrenze kontrollierbar.

Dritter Schritt: Verarbeitung von Mitgliedern ohne Initialisierer (Auto-Inkrement-Semantik).📎 scripts/inline-enums.js:171-183Wenn ein Mitglied keininitializerhat: Das erste Mitglied ist standardmäßig0; wenn bei nachfolgenden MitgliedernlastInitializedeine Zahl ist, dann++; wenn es ein String ist, wirdwrong enum initialization sequencegeworfen – denn String-Enum-Mitglieder erlauben kein implizites Auto-Inkrement. Genau das ist die Semantik von TypeScript enums.

Vierter Schritt: Cache schreiben und eine Cleanup-Funktion zurückgeben.📎 scripts/inline-enums.js:200-213 scanEnums()Es wird eine Closure zurückgegeben; beim Aufruf wirdrmSyncdie Cache-Datei gelöscht.build.jsSie wird intry/finallyverwendet.📎 scripts/build.js:81-112Dies stellt sicher, dass der Cache auch dann bereinigt wird, wenn während des Builds ein Fehler geworfen wird, und den nächsten Build nicht verunreinigt.

Fünfter Schritt: Ersetzung in der Rollup-transform-Phase. inlineEnums()Der Cache wird zurückgelesen und ein Rollup-Plugin konstruiert.📎 scripts/inline-enums.js:219-234Intransform(code, id), wennidaufenumData.declarationstrifft, wird MagicString verwendet, um[start, end]diese Deklaration durch ein Objektliteral zu ersetzen.📎 scripts/inline-enums.js:242-274

Die ersetzte Form istexport const X = { ... }. Beachten Sie, dass esnicht einfach das enum löscht, sondern es in ein Objektliteral umschreibt und für numerische Mitglieder zusätzlich Reverse-Mappings erzeugt:JSON.stringify(value.toString()) + ': ' + JSON.stringify(name)。📎 scripts/inline-enums.js:257-270Der Kommentar verweist auf die reverse-mappings-Regel der offiziellen TypeScript-Dokumentation: String-Enum-Mitglieder erzeugen keine Reverse-Mappings, numerische Mitglieder schon. Dies stellt sicher, dass das Laufzeitverhalten nach der Ersetzung vollständig mit dem ursprünglichen enum übereinstimmt.

Und was den Laufzeit-Overhead wirklich beseitigt, istdefineswird an@rollup/plugin-replace。📎 rollup.config.js:222-223übergeben. AlleX.MemberReferenzenaufwerden im Ersetzungs-Plugin direkt durch Literale ersetzt, sodass das umgeschriebene Objektliteral, wenn es niemand verwendet, durch Tree-shaking entfernt werden kann.

Das folgende Flussdiagramm zeigt den vollständigen Entscheidungspfad von grep bis zur Ersetzung:

mermaid
flowchart TD
    grep["spawnSync git grep 'export enum'"] --> files["去重得到文件列表"]
    files --> parse["@babel/parser 解析 AST"]
    parse --> check{"顶层节点是<br/>ExportNamedDeclaration<br/>且 declaration 为 TSEnumDeclaration?"}
    check -->|否| skip["跳过该节点"]
    check -->|是| dup{"enumIds 已含该 id?"}
    dup -->|是| err1["throw 不支持声明合并"]
    dup -->|否| member["遍历 members 求值"]
    member --> init{"有 initializer?"}
    init -->|有| eval["字面量/二元/一元求值"]
    init -->|无| auto["lastInitialized 自增或默认 0"]
    eval --> conflict{"fullKey 已在 defines?"}
    auto --> conflict
    conflict -->|是| err2["throw name conflict"]
    conflict -->|否| save["saveValue 写入 members 与 defines"]
    save --> cache["writeFileSync temp/enum.json"]
    cache --> transform["Rollup transform: MagicString 重写声明"]
    transform --> replace["plugin-replace 用 defines 替换引用"]

Designüberlegungen und Stolperfallen

Warum MagicString statt die gesamte Datei neu zu generieren?Weils.update(start, end, ...)nur den Abschnitt der Enum-Deklaration ersetzt und die übrigen Quelltextbytes vollständig unverändert bleiben,s.generateMap()und außerdem präzise Sourcemaps erzeugen kann.📎 scripts/inline-enums.js:277-281Wenn Babel den gesamten AST neu ausgeben würde, gingen ursprüngliche Formatierung und Kommentare verloren, und die Sourcemap-Qualität würde sinken.

rangeWarumnode.start/node.endstattdeclaration.start?📎 scripts/inline-enums.js:189-193behauptet wirdnode.start(d. h.ExportNamedDeclarationKnoten), deckt der Ersetzungsbereichexport enum X {...}den gesamten Abschnitt ab, einschließlichexportSchlüsselwort. Der Ersetzungstext beginnt mitexport constund schließt direkt an.

Stolperfallen:definesDie globale Eindeutigkeitsbeschränkung vonWenn es in zwei verschiedenen Dateien jeweils einErrorCodesgibt und beide__EXTEND_POINT__definieren, schlägt der Build direkt fehl.📎 scripts/inline-enums.js:101-103Das ist kein Bug, sondern bewusstes Design – weildefineseine globale Ersetzungstabelle ist und nicht zwischen Dateiquellen unterscheiden kann. Wenn in der Produktionsumgebung neue Enum-Mitglieder hinzugefügt werden und der Name mit einem vorhandenen Enum-Mitglied kollidiert, fliegt es hier auf.

Stolperfalle:new FunctionDer Auswertungszeitpunkt vonDie Auswertung binärer Ausdrücke erfolgt in derscanEnumsPhase; zu diesem Zeitpunkt ist das referenzierte Mitglied möglicherweise noch nicht indefines(wenn die Referenzreihenfolge vertauscht ist).📎 scripts/inline-enums.js:136-140wirdunhandled enum initialization expressiongeworfen. Dies erfordert, dass Verweise auf Enum-Mitglieder der Quelltextreihenfolge „erst definieren, dann referenzieren“ folgen müssen.

4.2 Tree-shaking-Verifikation: Das Versprechen anhand von Artefakt-Strings rückwirkend beweisen

Intuitives Modell

Enum-Inlining ist eine „Vorab-Optimierung“, aber greift die Optimierung wirklich? Wenn ein Helper aufgrund unsachgemäßer Schreibweise versehentlich beibehalten wird, wächst die Größe still und heimlich, ohne dass der Entwickler es bemerkt.verify-treeshaking.jsist genau dieser „nachträgliche Qualitätsprüfer“: Es baut das Artefakt und prüft dann wie bei einer Autopsie, ob im ArtefaktDinge auftauchen, die nicht auftauchen sollten. Ohne es könnte das On-Demand-Import-Versprechen von Vue nach einem Refactoring stillschweigend brechen, bis Benutzer sich über größere Pakete beschweren.

Datenstruktur und Prüfelemente

Dieses Skript hat keine komplexe Datenstruktur; der Kern ist einerrorsArray und dreiincludesPrüfungen.📎 scripts/verify-treeshaking.js:6-6Es baut zuerstglobal-runtimeFormat und liest dann jeweils die dev- und prod-Artefakte.

Die drei Prüfelemente entsprechen drei Arten von „Tree-shaking-Fehlern“:

1. dev-Artefakt enthält__spreadValues。📎 scripts/verify-treeshaking.js:13-19Dies ist der von esbuild für{ ...obj }Objekt-Spread-Syntax generierte Helper. Wenn er auftaucht, bedeutet das, dass im Laufzeitcode Objekt-Spread verwendet wurde, während Vue vereinbarungsgemäßextendHelper verwenden sollte, um zusätzlichen Code zu vermeiden.

2. prod-Artefakt enthältVue warn。📎 scripts/verify-treeshaking.js:26-31bedeutet, dass eswarn()Aufrufe gibt, die nicht durch__DEV__Bedingungen umschlossen sind, sodass Warncode in das Produktionspaket gelangt.

3. prod-Artefakt enthält DOM-Tag-Konfigurationslisten。📎 scripts/verify-treeshaking.js:33-42wiehtml,body,base、svg,animate,animateMotion、annotation,annotation-xml,maction. Diese sindisHTMLTag()Daten innerhalb von Helfern wie `helper`, die eigentlich nur im Compiler existieren sollten und von der Runtime wegoptimiert werden. Wenn sie im Runtime-Artefakt auftauchen, bedeutet das, dass der Runtime-Pfad fälschlicherweise compiler-exklusive Helper verwendet.

Step-by-Step: Validierungsablauf

📎 scripts/verify-treeshaking.js:5-5Zuerstexec('pnpm', ['build', 'vue', '-f', 'global-runtime']), nur bauenvuedes Paketsglobal-runtimeFormat – dies ist das minimalste Runtime-Artefakt und am besten geeignet, um Leaks aufzudecken. Nach dem Build werden beide Dateien synchron gelesen, einzelnincludesgeprüft, bei Treffer wird inerrorseine Nachricht mit Erklärung gepusht. Wenn schließlicherrors.lengthungleich null ist, wird ein aggregierter Fehler geworfen.📎 scripts/verify-treeshaking.js:44-48

mermaid
flowchart TD
    build["exec pnpm build vue -f global-runtime"] --> readDev["读取 vue.runtime.global.js"]
    readDev --> c1{"dev 含 __spreadValues?"}
    c1 -->|是| e1["push: 应改用 extend helper"]
    c1 -->|否| readProd["读取 vue.runtime.global.prod.js"]
    e1 --> readProd
    readProd --> c2{"prod 含 'Vue warn'?"}
    c2 -->|是| e2["push: warn 未被 __DEV__ 包裹"]
    c2 -->|否| c3{"prod 含 DOM tag 配置?"}
    e2 --> c3
    c3 -->|是| e3["push: 编译器 helper 泄漏到运行时"]
    c3 -->|否| done{"errors 为空?"}
    e3 --> done
    done -->|是| pass["验证通过"]
    done -->|否| fail["throw 聚合错误"]

Designüberlegungen und Stolperfallen

〔Design-Inferenz und Architektur-Abwägung〕

Warum String-includesstatt AST-Analyse?Weil dies eine „Sentinel-Prüfung" und keine „präzise Analyse" ist. Sie strebt keine Vollständigkeit an, sondern setzt kostengünstige Alarme für drei Regressionsarten, die historisch tatsächlich aufgetreten sind. String-Matching hat null Abhängigkeiten, null Parsing-Overhead und ist auch bei minifizierten Artefakten wirksam – AST-Analyse ist nach dem Minify sogar schwieriger durchzuführen.

〔Design-Inferenz und Architektur-Abwägung〕

Warum nur validierenglobal-runtime?Dieses Format inlined alle Abhängigkeiten (externalist leer), ist das volumenempfindlichste und am leichtesten versehentlich eingeführte Artefakt. Wenn es sauber ist, sind andere Formate normalerweise auch sauber. Gleichzeitig ist sein Build schnell und eignet sich für häufige CI-Läufe.

〔Design-Inferenz und Architektur-Abwägung〕

Stolperfalle: Die Prüfelemente sind eine „Blacklist", die mit der Code-Evolution ungültig werden kann.Wenn eines TagesisHTMLTagdie Datenstruktur geändert wird,html,body,basedieser String nicht mehr auftaucht, ist die Prüfung wirkungslos. Dies erfordert, dass Maintainer beim Ändern relevanter Helper die Sentinel-Strings hier synchron aktualisieren. Dies ist der inhärente Preis der Blacklist-Validierung.

4.3 Zusammenarbeit mit Rollup: Plugin-Reihenfolge und define-Injektion

Enum-Inlining läuft nicht isoliert, sondern ist in die Rollup-Plugin-Pipeline eingebettet. Um zu verstehen, warumdefinesanreplaceübergeben werden muss und nichtesbuild。

📎 rollup.config.js:47-50im Konfigurationsmodul auf oberster Ebene aufgerufen wirdinlineEnums(), muss man den Kontext verstehen: Es wird[enumPlugin, enumDefines]bei jedem Rollup-Prozessstartausgeführt und liest den vongeschriebenen Cache.scanEnumsDie Reihenfolge des Plugin-Arrays ist:

steht vorjson → alias → enumPlugin → ...resolveReplace() → esbuild。📎 rollup.config.js:324-339 enumPlugin, was bedeutet, dass das Umschreiben der Enum-Deklarationen zuerst erfolgt, dannreplaceerst mitreplacedie Referenzen ersetzt. Unddefinessteht am Ende und ist für die TS-Transpilierung zuständig.esbuildWarum

überdefinesund nicht überreplaceläuftesbuild, gibt derdefine?📎 rollup.config.js:220-221-Kommentar die Antwort: esbuilds define „ist etwas streng, erlaubt nur literales JSON oder Bezeichner". Enum-Member-Namen wieErrorCodes.__EXTEND_POINT__sind Member-Ausdrücke mit Punkt, und esbuilds define kann solche Schlüssel nicht direkt verarbeiten. Daher muss@rollup/plugin-replaceverwendet werden, das die Ersetzung beliebiger String-Schlüssel unterstützt.📎 rollup.config.js:250-251und setztpreventAssignment: true, um zu vermeiden, dass auch die linke Seite von Zuweisungsanweisungen ersetzt wird.

resolveReplace()Inconst replacements = { ...enumDefines }ist📎 rollup.config.js:222-223der erste Schritt./*@__PURE__*/Danach werden erst die Produktions-__DEV__-Annotationen,

usw. ersetzt. Diese Reihenfolge stellt sicher, dass die Enum-Literal-Ersetzung immer wirksam ist.

DesignüberlegungenDas Wesen des Enum-Inlinings ist „Build-Zeit-Komplexität gegen Runtime-Volumen tauschen".scanEnumsEs reproduziert die TypeScript-Typsystem-Semantik (Enum-Auswertung, Auto-Inkrement, Reverse-Mapping) vollständig zur Build-Zeit –📎 scripts/inline-enums.js:110-183die Auswertungslogik inunhandledist fast eine Teilmenge der Enum-Auswertung des TS-Compilers.

Dies bringt Wartungskosten mit sich: Wenn TS neue Enum-Syntax hinzufügt (z. B. komplexere Konstantenausdrücke), muss hier nachgezogen werden, sonst wird

ein Fehler geworfen. Aber der Nutzen ist klar: null Enum-Objekte zur Runtime, Tree-Shaking wird vollständig möglich.〔Design-Inferenz und Architektur-Abwägung〕

Validierungsskript und Inline-Skript sind ein Paar aus „Versprechen und Einlösung". scanEnumsDas Inline-Skript verspricht „Enums belegen kein Runtime-Volumen", das Validierungsskript prüft „anderer Code belegt auch nicht heimlich Volumen". Beide zusammen schützen das Volumenbudget von Vue. Dieses paarweise Design aus „Optimierung + Validierung" ist ein typisches Muster der Engineering-Praxis großer Frontend-Bibliotheken: Jede Optimierung benötigt eine automatisierte Prüfung, um Regressionen zu verhindern.inlineEnumsProzessübergreifender Cache ist ein Muss für parallele Builds.📎 scripts/inline-enums.js:39-41Das Muster „einmal ausführen,

mehrfach lesen" löst das Problem „einmal scannen, N Prozesse konsumieren". Ohne Cache müsste jeder Rollup-Prozess erneut grep + parsen, was massiv IO und CPU verschwendet.

Kapitelzusammenfassung

Kapitel-Reflexion und SelbsttestscanEnumsQ1: Wenn man insaveValueinif (fullKey in defines)die

Konfliktprüfung entfernt, in welchen Szenarien führt das zu Fehlern im Build-Artefakt?:

definesReferenzanalyse枚举名.成员名ist eine globale flache Map, Schlüssel ist📎 scripts/inline-enums.js:98-103, enthält keinen Dateipfad.@vue/compiler-coreNach Entfernen der Konfliktprüfung: Wenn zwei verschiedene Dateien jeweils ein gleichnamiges Enum haben und gleichnamige Member definieren (z. B.@vue/runtime-coreundErrorCodes.__EXTEND_POINT__beide

haben), überschreibt der später Schreibende den früher Schreibenden.defines['ErrorCodes.__EXTEND_POINT__']Konsequenzen:plugin-replaceEs bleibt nur ein Wert übrig, undkann beim Ersetzen die Dateiquelle nicht unterscheiden und ersetztalleErrorCodes.__EXTEND_POINT__Dateien📎 rollup.config.js:222-223durch denselben Wert.

Dadurch wird der Enum-Member-Wert eines der Pakete stillschweigend verfälscht, das Runtime-Verhalten ist fehlerhaft und extrem schwer zu diagnostizieren – weil der Quellcode völlig korrekt aussieht.📎 scripts/inline-enums.js:98-100Genau das ist der Grund, warum der Kommentar betont: „Gleichnamige Enums über Dateien hinweg erlaubt, aber gleichnamige Member nicht".

Die Konfliktprüfung ist der Torwächter, der verhindert, dass die globale Ersetzungstabelle verunreinigt wird.rollup.config.jsQ2: Wenn man inenumPluginim Plugin-Array...resolveReplace()und

die Reihenfolge vertauscht, was passiert?:

ReferenzanalyseenumPluginDie aktuelle Reihenfolge istreplacezuerst,📎 rollup.config.js:331-332danach.transformRollups

-Hook wird in der Reihenfolge des Plugin-Arrays ausgeführt.replaceBei Vertauschungexport enum X { ... }würde zuerst laufen, zu diesem Zeitpunkt sind die Enum-Deklarationen noch in ihrer ursprünglichenreplaceForm.definesverwendetX.Member, umenumPlugin-Referenzen zu ersetzen – aber zu diesem Zeitpunkt sind die Referenzen noch vorhanden, die Ersetzung kann wirksam werden. Das Problem tritt auf, wenns.update(start, end, ...)danach läuft: Es verwendet📎 scripts/inline-enums.js:250-273, um den Deklarationsabschnitt umzuschreiben.replaceAbercodehat bereitsenumPluginmodifiziert, undcodeerhältreplaceDie Ausgabe vonscanEnums, deren Byte-Offsets bereits mit den inrange(basierend auf dem ursprünglichen Quellcode) aufgezeichnetennicht mehr übereinstimmen。

Konsequenz: MagicString schneidet an falschen Offsets, die Syntax des Artefakts wird fehlerhaft. Dies offenbart einen impliziten Vertrag der Plugin-Pipeline:Transformationen, die auf Quellcode-Offsets basieren, müssen zuerst ausgeführt werden, damit nachfolgende Transformationen sicher auf deren Ausgabe fortfahren können.

Q3: verify-treeshaking.jsprüft nur drei String-Sentinels. Wenn ein Refactoring dieisHTMLTaginternen Daten von'html,body,base'in die Array-Form['html','body','base']ändert, was würde das Verifikationsskript tun? Welchen Designfehler offenbart dies?

Referenzauflösung:

Das Verifikationsskript prüft mitprodBuild.includes('html,body,base').📎 scripts/verify-treeshaking.js:33-37Wenn die Daten in ein Array geändert werden, erscheint im minifizierten Artefakt kein kommaverbundener String mehr,includesgibtfalsezurück, die Prüfungbesteht stillschweigend——selbst wennisHTMLTagtatsächlich in das Laufzeitartefakt gelangt ist.

Dies offenbart den inhärenten Mangel der Blacklist-basierten String-Verifikation:Sentinel-Strings sind an die Quellcode-Implementierung gekoppelt; ändert sich die Implementierung, wird die Verifikation ungültig. Sie kann keine „unbekannten Leaks" erkennen, sondern nur „bekannte Leaks, deren String-Form unverändert ist".

〔Design-Inferenz und Architektur-Abwägung〕

Verbesserungsrichtung: Man könnte stattdessen stabilere Identifikatoren prüfen (z. B. FunktionsnamenisHTMLTag), oder auf Quellcode-Ebene per Lint-Regel den Laufzeit-Import von Compiler-Helfern verbieten, statt sich auf Artefakt-Strings zu verlassen. Unter den aktuellen Kostenbeschränkungen sind String-Sentinels jedoch ein „ausreichender und kostengünstiger" Kompromiss.

Enum-Inlining löst „wie man Laufzeit-Overhead zur Build-Zeit eliminiert", das Verifikationsskript löst „wie man bestätigt, dass die Optimierung nicht beschädigt wurde". Aber Build-Artefakte umfassen neben JS noch eine weitere Art von Artefakten, die ebenfalls Pipeline-Verarbeitung benötigen——Typdeklarationsdateien. Das nächste Kapitel betritt die Typartefakt-Pipeline und schaut, wie Vue aus dem Quellcode.d.tsein release-taugliches Typ-Paket generiert und wiedts-testmit Typ-Contract-Tests die Typform der öffentlichen API absichert.

Dieses Kapitel hat zwei Schlüsselskripte der Kompilierungsphase zerlegt. inline-enums.js lokalisiert Enums mit git grep, parst den AST mit Babel, wertet Member mit new Function aus, schreibt Deklarationen präzise mit MagicString um und verwandelt schließlich Enum-Referenzen über die defines-Globalersetzungstabelle in Literale, sodass das Enum-Objekt von Tree-shaking entfernt werden kann. verify-treeshaking.js prüft hingegen nach dem Build das Artefakt mit String-Sentinels, um sicherzustellen, dass drei bekannte Arten von Tree-shaking-Leaks nicht zurückkehren. Beide——einer für „Optimierung", einer für „Verifikation, dass die Optimierung nicht beschädigt wurde"——schützen gemeinsam das Größenversprechen von Vue. Als Nächstes wenden wir uns von der Kompilierungsphase dem Generierungsweg der Typartefakte zu und schauen, wie Vue sicherstellt, dass Quellcode-Typen und Release-Typen strikt übereinstimmen.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 05

Kapitel 5: Typartefakt-Pipeline: Von Quellcode-.d.ts zum release-tauglichen Typ-Paket

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 5 von 14

Im vorherigen Kapitel haben wirinline-enums.jsundverify-treeshaking.jszerlegt: Eines ersetzt Enum-Referenzen durch Literale, damit das Enum-Objekt entfernt werden kann, das andere bestätigt nach dem Build mit String-Sentinels, dass drei bekannte Leaks nicht zurückkehren. Beide schützen gemeinsam das Laufzeit-Größenversprechen von Vue. Aber Build-Artefakte sind nicht nur JS. Wenn der Nutzerimport { ref } from 'vue', hängen die vom Editor angezeigten Typ-Hinweise undtscdie Typprüfung des Nutzercodes alle von einer anderen Art von Artefakten ab——.d.tsDeklarationsdateien. Ist das JS-Artefakt falsch, gibt es Laufzeitfehler; ist das Typartefakt falsch, gibt es auf Nutzerseite bereits zur Kompilierungszeit Fehler, oder schlimmer: Typen driften stillschweigend, Nutzercode kompiliert durch, aber die Typform stimmt nicht mit dem tatsächlichen Laufzeitverhalten überein. Dieses Kapitel verfolgt, wie Vue die in den einzelnen Unterpaketensrcverstreuten Quellcode-Typen zu einem release-tauglichen Typ-Paket aggregiert und mitdts-built-testTyp-Smoke-Tests auf echten Build-Artefakten durchführt.

5.1 Zweistufige Typ-Pipeline: tsc liefert, rollup aggregiert

Intuitives Modell

Stellen Sie sich eine Druckpipeline vor: In der ersten Phase setzt jedes Unterpaket sein eigenes Manuskript (.tsQuellcode) in einseitige Korrekturabzüge (.d.ts); in der zweiten Phase werden Dutzende Korrekturabzüge in Verzeichnisreihenfolge zu einem Buch gebunden (release-taugliche.d.ts) und Kopf- und Fußzeilen vereinheitlicht (Export-Deklarationen).

Ohne diese Pipeline müsste Vue manuell eine Release-Typdatei pflegen; bei jeder Quellcode-Änderung müsste man synchron manuell nachziehen——ein Nährboden für Typ-Drift. Vues Ansatz ist:Typartefakte werden vollständig aus dem Quellcode generiert, niemals handgeschrieben。

Erste Phase: tsconfig.build.json legt den Ausgabebereich fest

tsconfig.build.jsonist die Konfiguration der ersten Phase dieser Pipeline. Sie erbt vom Root-tsconfig.jsonund überschreibt nur build-relevante Optionen.

📎 tsconfig.build.json:3-9

Schlüsseloptionen einzeln zerlegt:

  • declaration: true: Lässt tsc für jede Quelldatei eine entsprechende.d.ts。
  • emitDeclarationOnly: true:generieren, nur Typen, kein JS. JS wird von Rollup verantwortet; tsc ist hier reiner Typ-Extraktor.
  • stripInternal: true: Alle Deklarationen, die mit@internalmarkiert sind, werden aus.d.tsentfernt. Dies ist Vues erste Schleuse zur Kontrolle der öffentlichen API-Oberfläche——interne Implementierungsdetails werden selbst dann nicht in die Release-Typen gelangen, wenn sieexportsind, solange sie mit@internalmarkiert sind.
  • composite: false: Deaktiviert den inkrementellen Build-Modus von project references. Vue braucht hier keine paketübergreifende Inkrementalität; das Abschalten vermeidet zusätzlichen Zustand durch.tsbuildinfo.

includeDie

📎 tsconfig.build.json:10-23

-Liste legt präzise fest, welche Verzeichnisse an der Ausgabe teilnehmen:Beachten Sie, dass hiernur 12 Verzeichnisse aufgelistet sindpackages/。packages-private/、packages/dts-test/、packages/sfc-playground/, nicht das gesamteusw. sind nicht darunter. Das bedeutet: Die Typen privater Pakete und TestpaketeEintritt in die Release-Artefakte. Dies ist eine physische Isolation – nicht durch Konvention, sondern durch Konfiguration.

〔Design-Inferenz und Architektur-Abwägung〕

Warum eine Whitelist statt einer Blacklist? Weil das Hinzufügen neuer Unterpakete im Monorepo der Normalfall ist. Bei einerexcludeBlacklist würde ein neues privates Paket, das vergessen wurde in exclude aufzunehmen, seine Typen stillschweigend in die Release-Artefakte einschleusen. Die Whitelist ist das Gegenteil: Neue Pakete nehmen standardmäßig nicht am Build teil und müssen explizit hinzugefügt werden – das entspricht dem Prinzip der „sicheren Standardwerte".

Nach Ausführung vontsc -p tsconfig.build.json --noChecklanden die Artefakte intemp/packages/<pkg>/src/*.d.ts. Beachten Sie--noCheck: Typprüfung wird übersprungen, nur emit wird ausgeführt. Die Typprüfung übernimmt ein separatestsc --noEmit, die Build-Phase wiederholt die Prüfung nicht, um Zeit zu sparen.

Zweite Phase: rollup.dts.config.js-Aggregation

Die zweite Phase wird vonrollup.dts.config.jsgesteuert. Ihr Einstieg führt zunächst eine Vorabvalidierung durch:

📎 rollup.dts.config.js:15-22

Fallstemp/packagesnicht existiert, bedeutet das, dass die erste Phase nicht ausgeführt wurde; das Skript beendet sich direkt mitprocess.exit(1)und weist darauf hin, zuersttscauszuführen. Dies ist derReihenfolgevertragder Pipeline: Die rollup-Phase ist stark abhängig von den Artefakten der tsc-Phase, beide sind unverzichtbar.

Anschließend werden alle Unterpaketverzeichnisse gelesen und dieTARGETSUmgebungsvariable für Subset-Builds unterstützt:

📎 rollup.dts.config.js:15-22

TARGETSDer Mechanismus erlaubt es, nur die Typen einiger weniger Pakete neu zu bauen, was den Feedback-Zyklus bei der Entwicklungs-Debugging erheblich verkürzt.

Der Kern isttargetPackages.map(...), das für jedes Paket eine Rollup-Konfiguration generiert:

📎 rollup.dts.config.js:23-42

Feldweise Erläuterung:

  • input: ./temp/packages/${pkg}/src/index.d.ts: Der Einstieg ist die in der ersten Phase erzeugte Typdatei, nicht der Quellcode.ts。
  • output.file: packages/${pkg}/dist/${pkg}.d.ts: Die Artefakte landen im jeweiligendist-Verzeichnis jedes Pakets, der Dateiname entspricht dem Paketnamen (z. B.vue.d.ts)。
  • format: 'es': Typdateien einheitlich im ES-Module-Format.
  • plugins: [dts(), patchTypes(pkg), ...(pkg === 'vue' ? [copyMts()] : [])]: Drei Plugins, die ersten beiden gelten für alle Pakete,copyMtsgilt nur für dasvue-Paket.

onwarnDer

📎 rollup.dts.config.js:23-42

-Hook verdient eine gesonderte Erwähnung:UNRESOLVED_IMPORTWährend des dts-Rollups werden alle nicht-relativen Imports standardmäßig externalisiert. Dies führt dazu, dass Rollup-Warnungen ausgibt. Aber das isterwartetes Verhaltenimport { X } from 'some-pkg'– diereturnin Typdateien sollten ohnehin als externe Referenzen erhalten bleiben und nicht mit eingebunden werden. Daher schluckt das Skript für „nicht aufgelöste Imports mit nicht-relativem Pfad" direktwarn。

die Warnung und lässt nur nicht aufgelöste Imports mit relativem Pfad an den Standard-

〔Design-Inferenz und Architektur-Abwägung〕!warning.exporter?.startsWith('.')Hier gibt es eine Feinheit:.prüft, ob der Exporter mit

beginnt. Wenn ein relativer Pfad-Import nicht aufgelöst wird, bedeutet das, dass die Artefakte der ersten Phase unvollständig sind – ein echtes Problem, das gemeldet werden muss. Diese Unterscheidung minimiert das Warnrauschen, ohne echte Fehler zu übersehen.

mermaid
flowchart TD
    src["packages/*/src/*.ts<br/>源码类型"] --> tsc{"tsc -p tsconfig.build.json<br/>--noCheck"}
    tsc -->|"include 白名单命中"| temp["temp/packages/*/src/*.d.ts<br/>单包校样"]
    tsc -->|"不在 include 列表"| skip["不产出<br/>私有包/测试包被隔离"]
    temp --> check{"temp/packages 存在?"}
    check -->|"否"| exit["process.exit(1)<br/>提示先跑 tsc"]
    check -->|"是"| rollup["rollup-plugin-dts<br/>聚合为单文件"]
    rollup --> patch["patchTypes(pkg)<br/>内联导出 + 追加 types/"]
    patch --> vue{"pkg === 'vue'?"}
    vue -->|"是"| mts["copyMts()<br/>写 vue.d.mts"]
    vue -->|"否"| done["packages/pkg/dist/pkg.d.ts"]
    mts --> done

KopierentscDieses Diagramm verankert den Kontrollfluss der beiden Phasen:rollupDie Whitelist voncheckentscheidet, wer in die Pipeline gelangt,patchTypesDascopyMtsvonvueentscheidet, ob fortgefahren werden kann,

ist ein obligatorischer Schritt,

ist der

rollup-plugin-dts-paketspezifische Zweig..d.ts5.2 patchTypes: Die aggregierten Artefakte in eine release-taugliche Form umschreibenexport { A, B, C, ... }Intuitives ModelldefineComponentNachdem

patchTypesDutzende vonin eine Datei zusammengeführt hat, hat das Ergebnis die Form „zuerst eine Reihe von Typen deklarieren, am Ende mit einem riesigeneinheitlich exportieren". Das ist für Menschen schwer lesbar und löst bei manchen Toolchains (z. B. dem

-Aufruf von VitePress) den Fehler aus, dass „abgeleitete Typen nicht ohne Referenz benannt werden können".

patchTypesist dieserrenderChunkNachbearbeitungs-Formungsschritt

📎 rollup.dts.config.js:87-88

  • isExported: „zentraler Export" wird in „Inline-Export vor Ort" umgewandelt, dann werden paketspezifische Typ-Erweiterungen angehängt.Datenstruktur: Zwei Sets und drei Durchläufegibt ein Rollup-Plugin zurück, die Kernlogik liegt imexport { ... }-Hook. Es verwaltet zwei Mengen:
  • shouldRemoveExport: Zeichnet alleursprünglich exportiertenTypnamen auf (aus

-Deklarationen).

Step-by-Step Walkthrough

: Zeichnet alle

📎 rollup.dts.config.js:90-100

Typnamen auf, die aus dem großen Export-Block entfernt werden müssenExportNamedDeclaration(weil sie bereits inline exportiert wurden).Der Verarbeitungsablauf teilt sich in drei Durchläufe (pass 0 / pass 1 / pass 2), ein typisches „erst sammeln, dann umschreiben, zuletzt bereinigen"-Muster.Pass 0: Alle bereits exportierten Typnamen sammeln.export ... from '...'Über die AST-Top-Level-Knoten iterieren; für alleisExported。

, dieexportkein source haben

📎 rollup.dts.config.js:102-125

(also keineVariableDeclaration、TSTypeAliasDeclaration、TSInterfaceDeclaration、TSDeclareFunction、TSEnumDeclaration、ClassDeclaration-Re-Exports sind), wird der local name des Specifiers zuprocessDeclaration。

processDeclarationhinzugefügt.

📎 rollup.dts.config.js:70-85

Pass 1: Deklarationsknoten direkt mit

-Präfix versehen.idÜber Top-Level-Knoten iterieren, für

sechs Deklarationsarten wird_aufgerufen. Die Logik:Drei Schritte:1. Kein

→ direkt zurückgeben (z. B. anonyme Deklaration).shouldRemoveExport2. Name beginnt mitisExported→ überspringen. Dies ist dieprependLeftKonventionexport : Typen mit Unterstrich-Präfix sind interne Hilfstypen und werden nicht exportiert.

3. Den Namen zuVariableDeclarationhinzufügen; falls der Name in

📎 rollup.dts.config.js:104-115

ist (also ursprünglich exportiert wurde), an der Startposition der Deklarationdeclare consteinendeclare const a, b-String einfügen.processDeclarationBeachten Sie, dass derdeclarations[0]-Zweig eine zusätzliche Assertion hat:Wenn einemehrere declarators deklariert (z. B.

), wird direkt ein Fehler geworfen. Weil

📎 rollup.dts.config.js:127-171

nurExportNamedDeclarationverarbeitet, würde ein Multi-declarator zu einer übersehenen Verarbeitung führen. Hier wird

  • schnelles ScheiternshouldRemoveExportstatt stiller Fehler gewählt – Ausdruck defensiver Programmierung.exported === localPass 2: Bereits inline exportierte Typen aus dem großen Export-Block entfernen.export { Foo as Bar }Über
  • iterieren, für jeden Specifier:
  • Falls sein local name inExportNamedDeclarationist und

(ausgenommen

📎 rollup.dts.config.js:172-183

code = s.toString()-Umbenennungsfälle), wird der Specifier entfernt.packages/${pkg}/typesBeim Entfernen wird MagicString präzise eingesetzt: Falls danach noch ein Specifier folgt, bis zum start des nächsten Specifiers löschen; falls es der letzte ist, bis zum end des vorherigen oder zum eigenen start löschen.

〔Design-Inferenz und Architektur-Abwägung〕

Diesestypes/Verzeichnis istmanuell gepflegte Typ-ErweiterungEinstiegspunkt für Typen, die nicht automatisch aus dem Quellcode generiert werden können (z. B. JSX-Global-Erweiterungen, Makro-Typdeklarationen). Es wird in derselben Datei wie die automatisch generierten Typen zusammengeführt, aber die Quellen sind klar getrennt – automatisch generierte oben, manuell erweiterte unten.

Warum muss inline exportiert werden?

Der Kommentar gibt den direkten Grund an:

📎 rollup.dts.config.js:45-51

Im Original heißt es: Alle Typen auf Inline-Export umstellen und aus dem großen Export-Block entfernen, sonst meldet der Aufruf in VitePressdefineComponentden Fehler „the inferred type cannot be named without a reference".

〔Design-Inferenz und Architektur-Abwägung〕

Der Kern dieses Fehlers ist: Wenn TypeScript Typen generiert und ein Typ nur durch „Verweis auf den Export eines anderen Moduls" benannt werden kann, dieser Verweis aber auf der Konsumentenseite nicht sichtbar ist, wird ein Fehler gemeldet. Ein zentraler Export-Block trennt Typnamen von der Deklarationsstelle und verschärft dieses Problem. Inline-Exporte machen jeden Typ an seiner Deklarationsstelle sichtbar und beseitigen diese Indirektionsschicht.

copyMts: Typen für Node ESM/CJS-Dualmodus bereitstellen

copyMtsDas Plugin wirkt nur auf dasvuePaket:

📎 rollup.dts.config.js:196-204

Es schreibt imwriteBundleHook den Inhalt vonvue.d.tsunverändert nachvue.d.mts。

Der Kommentar erklärt den Grund:

📎 rollup.dts.config.js:188-192

Gemäß derpackage.jsonexports-Spezifikation von TypeScript 4.7 müssen, um Typen für Node ESM und CJS gleichzeitig korrekt bereitzustellen,zwei unabhängige Deklarationsdateien existieren. Daher wird beim Buildvue.d.tseinmal alsvue.d.mts。

kopiert

〔Design-Inferenz und Architektur-Abwägung〕package.jsonWarum kopieren statt neu generieren? Weil die Typformen von ESM und CJS vollständig identisch sind; der Unterschied liegt nur in der Dateiendung und imexports-Mapping von

. Kopieren ist die günstigste Lösung und vermeidet einen erneuten Rollup-Durchlauf.

5.3 dts-built-test: Typ-Smoke-Test auf echten Artefakten

Intuitives ModellpatchTypesDie vorherigen beiden Abschnitte stellen sicher, dass Typ-Artefakte generiert werden können und die Form korrekt ist. Aber „generierbar" bedeutet nicht „korrekt generiert". Wennimportbei einem Durchlauf einen Bug hat und einen Export versehentlich löscht, kann das Artefakt immer noch generiert werden, aber der Nutzer stellt beim

dts-built-testfest, dass Typen fehlen.istein Typ-Smoke-Test, der auf echten Build-Artefakten läuftimport: Er testet nicht die Quelltypen, sondernvuedas veröffentlichte

Paket und verifiziert, dass wichtige Typformen nicht regressiert sind.

Datenstruktur: eine minimalisierte Typ-Assertion

📎 packages-private/dts-built-test/src/index.ts:3-6

Der Kern des gesamten Testpakets ist nur eine Datei:

  • Zeilenweise Erläuterung:vueL1: AusdefineComponentwirdimportiert. Beachten Sie, dass hier derPaketnamepackages/vue/dist/vue.d.tsimportiert wird, kein relativer Pfad – es konsumiert das echte Artefakt
  • ._CustomPropsNotErasedL3-6: Definiert eine Komponente
  • mit leeren Props und leerem Setup.// #8376L8: Kommentar
  • , verweist auf ein konkretes Issue.CustomPropsNotErasedL9-12: Exportiert_CustomPropsNotErased, Typ ist{ foo: string }der Schnitttyp von

unddefineComponent.{ foo: string }Dieser Test verifiziert:fooDer Rückgabetyp von。

nach Schnitt mit

, dass diedefineComponent-Eigenschaft nicht gelöscht wird

〔Design-Inferenz und Architektur-Abwägung〕

📎 packages-private/dts-built-test/package.json:1-11

Hintergrundvermutung zu Issue #8376:

  • private: trueDer Rückgabetyp von
  • types: dist/index.d.tswird möglicherweise durch einen Conditional Type oder Mapped Type verarbeitet, wodurch zusätzliche Eigenschaften im Schnitttyp „gelöscht" werden. Dieser Test fixiert dieses Verhalten mit einer minimalen Reproduktion; bei einer Regression wird in der Typprüfungsphase ein Fehler gemeldet.
  • dependenciesPaketkonfiguration: Workspace-Abhängigkeit zeigt auf echte Artefakteworkspace:*Schlüsselfelder:@vue/shared、@vue/reactivity、vue。
: Wird nicht auf npm veröffentlicht.

: Typ-Einstiegspunkt zeigt auf das Build-Artefakt.@vue/sharedIn@vue/reactivitydreivue-Abhängigkeiten:types〔Design-Inferenz und Architektur-Abwägung〕distWarum vonundabhängen? Weil die Typen von

möglicherweise auf die Typen dieser beiden Pakete verweisen. Im Workspace-Modus verlinkt pnpm diese Abhängigkeiten symbolisch auf lokale Pakete, und die

dts-built-test-Felder der lokalen Pakete zeigen auf die Artefakte unter ihren jeweiligensrc/index.ts. So konsumiert die gesamte TestkettetscBuild-Artefaktetsc, nicht Quellcode.

Wie der Test läuft

hat selbst kein Testskript; seinesind die Testfälle. Der Ausführungsweg ist: In CIausführen, um eine Typprüfung dieses Pakets durchzuführen. Bei einer Typform-Regression meldettsceinen Fehler und CI schlägt fehl.

〔Design-Inferenz und Architektur-Abwägung〕

Das Clevere an diesem Design ist: Es kodiert den „Typvertrag" alsdts-built-testkompilierbaren Codedts-test. Keine zusätzliche Assertion-Bibliothek nötig, keine Laufzeit,

  • dts-built-testselbst ist der Test-Runner. Wenn die Typen stimmen, kompiliert es; wenn die Typen falsch sind, schlägt die Kompilierung fehl.Arbeitsteilung mit dts-testBeachten Sie, dass
  • dts-testin diesem Kapitel undim nächsten Kapitel zwei verschiedene Dinge sind:(dieses Kapitel): konsumiert
Build-Artefakte

, verifiziert Typformen auf Veröffentlichungsebene.patchTypes(nächstes Kapitel): konsumiertstripInternalQuelltyptypes/, verifiziert den API-Oberflächenvertrag.dts-built-test〔Design-Inferenz und Architektur-Abwägung〕

Warum braucht es zwei Ebenen? Weil Quelltypen und Artefakttypen inkonsistent sein können.

mermaid
sequenceDiagram
    participant CI as CI 脚本
    participant TSC as tsc (tsconfig.build.json)
    participant Rollup as rollup.dts.config.js
    participant Patch as patchTypes(pkg)
    participant Dist as packages/vue/dist
    participant BuiltTest as dts-built-test

    CI->>TSC: tsc -p tsconfig.build.json --noCheck
    TSC->>TSC: include 白名单过滤
    TSC-->>Rollup: temp/packages/*/src/*.d.ts
    Rollup->>Rollup: existsSync('temp/packages') 校验
    Rollup->>Rollup: rollup-plugin-dts 聚合
    Rollup->>Patch: renderChunk(code, chunk)
    Patch->>Patch: pass0 收集 isExported
    Patch->>Patch: pass1 prependLeft('export ')
    Patch->>Patch: pass2 移除大导出块 specifier
    Patch->>Patch: 追加 packages/vue/types/*
    Patch-->>Rollup: 改写后 code
    Rollup->>Dist: 写 vue.d.ts
    Rollup->>Dist: copyMts 写 vue.d.mts
    CI->>BuiltTest: tsc 类型检查
    BuiltTest->>Dist: import { defineComponent } from 'vue'
    Dist-->>BuiltTest: 类型形状
    BuiltTest-->>CI: 编译通过 / 报错

, das Entfernen vonpatchTypes, das Anhängen desdts-built-test-Verzeichnisses können alle unter der Voraussetzung korrekter Quelltypen Artefakt-Bugs einführen.

bewacht speziell diese letzte Meile.

Die vollständige Zeitsequenz der Typ-Pipeline

patchTypesKopierencode.replace(...)Dieses Sequenzdiagramm verankert die modulübergreifende Zusammenarbeit: CI treibt die beiden Phasen tsc und Rollup an,

1. die drei Durchläufe vonsind die Kernverarbeitung,start/endkonsumiert am Ende die Artefakte zur Verifikation.

2. Designüberlegungen, Fehlerbehebung und Produktions-FallstrickeMagicString kann Mappings erzeugen, sodass umgeschriebene Typdateien weiterhin auf den Quellcode zurückverfolgt werden können. Obwohl der Nutzen von Sourcemaps für Typdateien begrenzt ist, ist Konsistenz eine gute Praxis.

Schnelles Scheitern vs. stilles Fehlertolerieren

patchTypesAn mehreren Stellen verwendetassert:

📎 rollup.dts.config.js:74-74

📎 rollup.dts.config.js:107-108

📎 rollup.dts.config.js:147-148

Diese Assertions werfen sofort einen Fehler, wenn sie auf unerwartete AST-Formen stoßen. Im Vergleich dazuonwarndas stille Verschlucken vonUNRESOLVED_IMPORTinErwartetes Rauschen wird verschluckt, unerwartete Formen führen zu schnellem Scheitern. Das ist die richtige Haltung für Build-Skripte: Lieber den Build fehlschlagen lassen, als Typdateien mit falscher Form zu erzeugen.

Produktions-Fallstricke:_Präfix-Konvention

processDeclarationÜberspringen von Typen, die mit_beginnen:

📎 rollup.dts.config.js:76-78

Das bedeutet, dass jeder exportierte Typ im Quellcode, der mit_beginnt, nicht inline exportiert wird. Wenn ein Typ eigentlich öffentlich sein sollte, aber aufgrund eines Namens, der mit_beginnt, übersprungen wird, stößt der Nutzer auf den Fehler „Typ existiert nicht“.

〔Design-Schlussfolgerungen und Architektur-Abwägungen〕

Der Ansatz zur Fehlersuche bei solchen Problemen: Prüfen Sie zuerst, ob der Typ im Artefaktvue.d.tsnoch im großen Exportblock vorhanden ist, und prüfen Sie dann, ob der Typname im Quellcode mit_beginnt. Dies ist eine implizite Kopplung zwischen Namenskonvention und Tool-Verhalten, die leicht zu Fallstricken führt.

Produktions-Fallstrick: Multi-Declarator-Assertion

📎 rollup.dts.config.js:106-115

Wenn in einem.d.tseindeclare const a, bauftritt, wirft der Build direkt einen Fehler. Dies ist bei handgeschriebenen Typen selten, aber wenn eine von einem Tool generierte Typdatei diese Form verwendet, wird es ausgelöst. Die Fehlermeldung gibt den problematischen Codeausschnitt aus, was die Lokalisierung erleichtert.

Zusammenfassung dieses Kapitels

Dieses Kapitel hat die vollständige Pipeline der Vue-Typartefakte nachverfolgt:

1. Erste Phase (tsc):tsconfig.build.jsonverwendetincludeeine Whitelist, um den Ausgabebereich präzise abzugrenzen,emitDeclarationOnlygibt nur Typen aus,stripInternalentfernt interne Deklarationen. Die Artefakte landen intemp/packages/。

2. Zweite Phase (rollup):rollup.dts.config.jsverwendetrollup-plugin-dts, um die Typen der einzelnen Pakete zu aggregieren,patchTypesschreibt durch drei AST-Durchläufe zentrale Exporte in Inline-Exporte um und hängt manuelle Erweiterungen aus demtypes/-Verzeichnis an.copyMtsFür dasvue-Paket werden zusätzlich.d.mts。

3. generiert. Validierungsphase (dts-built-test): Typ-Smoke-Tests auf den echten Build-Artefakten durchführen, um mit kompilierbarem Code die entscheidenden Typformen festzuschreiben und Typdrift zu verhindern.

Gedanken und Selbsttests zu diesem Kapitel

Q1: Was passiert, wenn man dietsconfig.build.json-Whitelist vonincludein["packages"]ändert (d. h. das gesamte packages-Verzeichnis einschließt)? In welchen Szenarien führt dies zu einer Verschmutzung der veröffentlichten Typen?

Referenzanalyse:

includeNach der Änderung von 12 präzisen Verzeichnissen zu["packages"]nehmen alle Unterpakete (einschließlich allerpackages-privateaußerhalb vonpackages/*) an der tsc-Ausgabe teil.📎 tsconfig.build.json:10-23

Folgenkette:

1. temp/packages/Unter.d.ts。

2. rollup.dts.config.jserscheinen viele zusätzliche Pakete. DasreaddirSync('temp/packages')von📎 rollup.dts.config.js:15-22

3. targetPackagesliest diese zusätzlichen Pakete. Standardmäßig entsprichtpackages/<pkg>/dist/<pkg>.d.ts。📎 rollup.dts.config.js:15-22

allen Paketen, daher wird für jedes Paketdistgeneriert. Verschmutzungsszenario: Wenn ein Paket eigentlich nicht veröffentlicht werden sollte (z. B. ein internes Tool-Paket), erscheint sein Typartefakt unterpackage.json. Wenn dasprivate: truedieses Pakets kein

hat, könnte das Veröffentlichungsskript es mit zu npm veröffentlichen, was zu einem Leck interner Typen führt.

Q2: patchTypesGenau das ist der Wert des Whitelist-Designs: Neue Pakete sind standardmäßig nicht enthalten und müssen explizit hinzugefügt werden, was einem sicheren Standardwert entspricht.processDeclarationIn Pass 1 von_werden Typen, die mitreturnbeginnen, direkt_. Wenn der Typ einer öffentlichen API zufällig mit_InternalTypebeginnt (z. B.

versehentlich exportiert wird), was sieht der Nutzer dann? Wie kann man das untersuchen?:

processDeclarationReferenzanalyse_BeishouldRemoveExportwird bei einem Präfixexport 。📎 rollup.dts.config.js:76-78

direkt zurückgegeben, weder

hinzugefügt nochexport。

vorangestellt. Folgen:shouldRemoveExport1. Dieser Typ erhält kein Inline-

2. Er wird auch nicht aus dem großen Exportblock entfernt (da er nicht inist).3. Daher

ist er weiterhin im großen Exportblockexport { _InternalType }und kann theoretisch weiterhin importiert werden.stripInternalDas Problem ist jedoch: Dastscim großen Exportblock referenziert die Deklarationsposition. Wenn diese Deklaration aus irgendeinem Grund (z. B.

) entfernt wird, referenziert der Exportblock einen nicht existierenden Namen, was zu einem

-Fehler führt.vue.d.tsFehlersuche-Ansatz:export1. Prüfen Sie im Artefakt

, ob der Typ weder an der Deklarationsstelle ein_hat noch im großen Exportblock referenziert wird.

2. Prüfen Sie im Quellcode, ob der Typname mit

beginnt._3. Wenn es sich um ein Namensproblem handelt, reicht eine Umbenennung ohne Unterstrich-Präfix.

Q3: dts-built-testDies legt die implizite Kopplung zwischen Namenskonvention und Tool-Verhalten offen:src/index.tsDas Präfixtypeof _CustomPropsNotErased & { foo: string }bedeutet eigentlich „intern“, aber das Tool behandelt es als „nicht exportieren“ – die beiden Semantiken sind nicht vollständig deckungsgleich.fooDasOmit<typeof _CustomPropsNotErased, never> & { foo: string }von

verwendet den Kreuztyp:

Omit<T, never>, um zu verifizieren, dassnicht gelöscht wird. Wenn man den Kreuztyp inändert, kann der Test dann noch die Regression von #8376 erfassen? Warum?

  • ReferenzanalyseT & { foo: string }erstellt einen neuen Mapping-Typ, derfooalle Eigenschaften von T neu berechnetdefineComponent. Wenn der Bug von #8376 darin besteht, dass „zusätzliche Eigenschaften im Kreuztyp gelöscht werden“, dann:fooUrsprüngliche Schreibweise
  • Omit: direkte Kreuzung,Omitist Teil des Kreuztyps. Wenn die Rückgabetyp-Verarbeitungslogik vonTzusätzliche Eigenschaften in der Kreuzung löscht,{ foo: string }gehtOmitverloren.

📎 packages-private/dts-built-test/src/index.ts:9-12

Schreibweise: Zuerst wirdgemappt und dann mitOmit、Pickgekreuzt. Der Mapping-Prozess von

kann die Typstruktur verändern, sodass die Auslösebedingung des Bugs nicht mehr gilt – selbst wenn der Bug existiert, kann der Test bestehen.

Daher ist die

Minimalitätdts-built-testdes Testfalls entscheidend: Er muss den Auslösepfad des Bugs präzise reproduzieren. Jede zusätzliche Typumwandlung (wiedts-test, sieh, wie Vue mit Typvertragstests die öffentliche API-Oberfläche absichert.

Die drei bilden einen geschlossenen Kreislauf aus „Generieren → Formen → Validieren“, der sicherstellt, dass Quelltyp und veröffentlichter Typ strikt übereinstimmen. Dass das Typ-Paket selbst korrekt ist, bedeutet jedoch nicht, dass die Typform der öffentlichen API fixiert ist. Im nächsten Kapitel gehen wir tiefer inpackages-private/dts-test, um zu sehen, wie über 20.test-d.ts-Dateien mitexpectTypeund anderen Werkzeugen „Typen als API-Vertrag“ in regressionsfähige automatisierte Tests verwandeln.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 06

Kapitel 6: Typvertragstests: Wie dts-test die API-Oberfläche absichert

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 6 von 14

Im vorherigen Kapitel haben wir die Generierungskette der Typdeklarationen verfolgt und gesehen, wie Vue durch Build-Konfiguration und Smoke-Tests sicherstellt, dass „Quelltyp“ und „veröffentlichter Typ“ strikt übereinstimmen. Doch der Typvertrag beschränkt sich nicht auf „ob die Form stimmt“, sondern entscheidender ist, „ob die API-Oberfläche den Erwartungen entspricht“ – welche Typen exportiert werden sollen, welche nicht, und ob generische Constraints präzise sind. Dieses Kapitel geht inpackages-private/dts-test, um zu sehen, wie Vue mit über 20.test-d.ts-Dateien „Typen als API-Vertrag“ in regressionsfähige automatisierte Tests umsetzt.

Das kognitive Modell von Typvertragstests: Die „Spezifikation“ in einen „ausführbaren Vertrag“ verwandeln

dts-testDie Dateien im-Verzeichnis haben ein kontraintuitives Merkmal: Sieerzeugen nahezu kein LaufzeitverhaltendefineComponent.test-d.tsx. Öffnet mandefineComponent({...}), sieht man zahlreichetsc/vue-tsc-Aufrufe, aber sie werden zur Testlaufzeit nie tatsächlich ausgeführt – diese Dateien werden nur vonnoEmit: truetypgeprüft,

📎 packages-private/dts-test/tsconfig.test.json:1-11

um sicherzustellen, dass kein JS erzeugt wird.noEmitDiese Konfiguration ist die „Laufzeitumgebung“ des gesamten Vertragssystems:jsx: preservedeaktiviert die Ausgabe von Artefakten,strictlässt TSX-Syntax für den TypeScript-Compiler zur Analyse bestehen,moduleResolution: bundleraktiviert alle strikten Prüfungen,libentspricht moderner Bundling-Semantik,esnextund führt gleichzeitigdom。ein..test-d.tsxOhne diese Konfiguration würde JSX in。

als Laufzeit-JSX behandelt, und Typassertions verlören ihre Bedeutung.

〔Design-Inferenz und Architekturabwägung〕packages-privateTypentests in ein eigenespackages/vue-Unterpaket auszulagern, statt sie in__tests__vonvuezu stopfen, hat drei Motive: Erstens sind die Abhängigkeiten der Typentests dieveröffentlichungsreifen Typen von(vue/jsx、vue, die.d.ts), nicht interne Quellmodule, und die physische Trennung erzwingt den öffentlichen Einstiegspunkt; zweitens ist dertsc-Check von Typentests deutlich zeitaufwändiger als Laufzeit-Unit-Tests, und ein separates Verzeichnis erleichtert die separate CI-Planung; drittens werden.test-d.tsx-Dateien nicht vom Laufzeit-Sammler von Vitest fälschlich ausgeführt.

Alltagsanalogie: Normale Unit-Tests sind wie „die Maschine einschalten und sehen, ob sie raucht“, während Typvertragstests wie „vor Vertragsunterzeichnung Klausel für Klausel prüfen“ sind – es wird nicht tatsächlich gehandelt, sondern nur bestätigt, dass bei „vom Auftraggeber zu zahlender Betrag“ „Renminbi“ statt „US-Dollar“ steht. Wenn die Vertragsklauseln falsch sind, nützt es nichts, dass die Maschine noch so reibungslos läuft.

utils.d.tsstellt das gesamte Werkzeugset für diese „Vertragsprüfung“ bereit:

📎 packages-private/dts-test/utils.d.ts:7-21

Es gibt nur vier Schlüsselwerkzeuge:expectType<T>(value: T)behauptet, dassvaluegenau vom TypT;expectAssignable<T, T2 extends T>ist; behauptet, dassT2zuweisbar anT;IsUnion<T>ist; prüft, obTein Union-Typ ist;IsAny<T>prüft, obTvom Typanyist. Beachteimport 'vue/jsx'in L5 – es registriert den globalen JSX-Namensraum, sodass<MyComponent />in TSX vom Typsystem alsJSX.Element。

📎 packages-private/dts-test/utils.d.ts:7-21

IsUnionerkannt werden kann. Die Implementierung vonT extends any ? (U extends T ? false : true) : neververdient eine genauere Betrachtung:Tnutzt distributive bedingte Typen; wennextends falseein Union-Typ ist, wird jedes Mitglied unabhängig ausgewertet, und am Endefalseprüft, ob alle Zweigezurückgeben. Dies ist einExistenzbeweis auf Typebeneprops.jjj– er dient dazu, Verträge wie „

muss ein Union-Typ sein und darf nicht zu einer einzigen Signatur zusammengeführt werden“ zu fixieren.defineComponentSzenariogetriebener Walkthrough:

defineComponent.test-d.tsxDie vollständige Kette der Props-Typinferenz inhat 2260 Zeilen und ist der Kern des Vertragssystems. Wir versetzen uns in ein konkretes Szenario:defineComponent({ props: {...}, setup(props) {...} })Der Nutzer schreibtprops, und das Typsystem von Vue muss aus dersetupLaufzeitdeklaration den präzisen Typ desprops-Parameters inableiten. Diese Kette ist der komplexeste Teil des Vue-Typsystems.

Schritt 1: Den „erwarteten Typ“ als Vertragsgrundlage konstruieren

Die Testdatei definiert zuerst dasExpectedProps-Interface und schreibt den Typ, der für jede Props-Deklarationsart abgeleitet werden sollte,explizit fest:

📎 packages-private/dts-test/defineComponent.test-d.tsx:21-53

. Dieses Interface ist die schriftliche Version der „Vertragsklauseln“. Beachte einige subtile Typen:a?: number | undefined(optionale Props mitundefined)、aa: number(hat default, daher nicht optional),aaa: number | null(PropType<number | null>explizit deklariert),aaaa: number | undefined(required: true as constaber der Typ enthältundefined). Diese Unterschiede sind nicht willkürlich geschrieben; jede entspricht einem bestimmten Zweig in derprops-Deklaration.

Schritt 2: Mit verschiedenen DeklarationsartendefineComponent

📎 packages-private/dts-test/defineComponent.test-d.tsx:57-158

„füttern“propsDieses-Objekt ist dieerschöpfende Matrix der Deklarationsarten

  • a: Numberund deckt alle Schreibweisen von Vue props ab:number | undefined
  • aa: { type: Number as PropType<number | undefined>, default: 1 }– Konstruktor-Kurzform, abgeleitet alsnumber
  • aaaa: { type: Number, required: true as const } —— as const– hat default, abgeleitet als nicht optionaltrueverhindert, dassbooleanzu
  • b: { type: String, required: true as true } —— required: trueerweitert wird, und bewahrt den Literaltyp
  • bb: { default: 'hello' }macht die Eigenschaft non-voidtype– kein
  • cc: Array as PropType<string[]>, Typ wird nur über default abgeleitet
  • l: [Date]– explizite TypkonvertierungDate | undefined
  • ll: [Date, Number]– Array-Syntax, abgeleitet alsDate | number | undefined
  • lll: [String, Number]– Multi-Typ-Array, abgeleitet als
– wie oben

required: true as const〔Design-Inferenz und Architekturabwägung〕required: true as true(L70) undas true(L75) existieren nebeneinander als Spuren historischer Entwicklung: Früher verwendete manas const, später stellte man fest, dassallgemeiner ist (es kann gleichzeitig andere Literale im Objekt fixieren), aber die alte Schreibweise blieb erhalten, um Rückwärtskompatibilität zu verifizieren. Das ist der typische Wert von Vertragstests –。

er fixiert gleichzeitig „neue Schreibweise nutzbar“ und „alte Schreibweise ohne Regression“.setup / render / thisSchritt 3: An den drei Positionen

assertierenDies ist das raffinierteste Design des Vertragstests:。

📎 packages-private/dts-test/defineComponent.test-d.tsx:160-217

setup(props)Derselbe Props-Typ muss an drei verschiedenen Konsumpositionen korrekt abgeleitet werdenexpectType<ExpectedProps['x']>(props.x). In

📎 packages-private/dts-test/defineComponent.test-d.tsx:168-170

// @ts-expect-error should included 'undefined'In Kombination mitexpectType<number>(props.aaaa)——wird absichtlich eine fehlschlagende Assertion geschrieben, um mit@ts-expect-errorden Fehler zu schlucken. Dies verifiziert, dassprops.aaaaden Typnicht numberhat (andernfalls würde diese Zeile keinen Fehler werfen,@ts-expect-errorsondern stattdessen fehlschlagen, weil „kein Fehler zum Schlucken“ vorhanden ist). Dies ist die „Reverse-Assertion“-Technik des Typtests.

📎 packages-private/dts-test/defineComponent.test-d.tsx:204-205

// @ts-expect-error props should be readonlyIn Kombination mitprops.a = 1— verifiziert, dass props insetupschreibgeschützt sind. Wenn ein Refactoring versehentlich props veränderbar macht, wirft diese Zeile keinen Fehler mehr,@ts-expect-errorund

render()schlägt fehl.this.$propsInthis.xwird hingegen über die beiden Pfade

📎 packages-private/dts-test/defineComponent.test-d.tsx:221-279

undthisassertiert:this.a = 1L252-276 verifiziert, dass „deklarierte props auch aufthisexponiert werden müssen“, L278-279 verifiziert, dassthis.ceinen Fehler wirft (number(ref(1)props aufthis.d.e.valuesind ebenfalls schreibgeschützt). L281-287 verifiziert das Entpacken des setup-Rückgabewerts:stringist.value)、this.f.gwird entpackt),GT(reactiveist

(verschachtelte refs bleiben erhalten,

ist<MyComponent />branded Typen in

📎 packages-private/dts-test/defineComponent.test-d.tsx:296-322

werden nicht entpackt).<MyComponent>Vierter Schritt: Typvalidierung auf der TSX-Konsumentenseiteclass/style/key/ref/ref_forDer letzte Baustein des Typvertrags ist „wie der Nutzer diese Komponente verwendet“. Die props-Validierung vonin TSX ist ein unabhängiger Typpfad::

📎 packages-private/dts-test/defineComponent.test-d.tsx:337-345

// @ts-expect-error missing required propsHier wird verifiziert, dasswrong prop typesalle deklarierten props akzeptiert, sowieggg="baz"diese eingebauten Attribute. Danach folgtgggReverse-Validierung'foo' | 'bar')。

verifiziert, dass fehlende erforderliche props einen Fehler werfen;

mermaid
flowchart LR
    A["props 声明对象<br/>L57-158"] --> B["defineComponent<br/>泛型推导"]
    B --> C["ExtractPropTypes<br/>运行时声明 → 类型"]
    C --> D["setup(props)<br/>L162-217"]
    C --> E["render() this.$props<br/>L221-279"]
    C --> F["TSX 消费端<br/>L296-345"]
    D --> G["expectType 断言<br/>契约锁定"]
    E --> G
    F --> G
    G --> H{"全部通过?"}
    H -->|是| I["类型契约成立"]
    H -->|否| J["tsc 报错<br/>CI 阻断合并"]

einen Fehler wirft (akzeptiert nurpropsDie gesamte Kette lässt sich mit einem Datenflussdiagramm zusammenfassen:KopierentscDer Schlüssel dieses Diagramms ist:

Dieselbe__typeProps、__typeEmitsDeklaration muss gleichzeitig die Typerwartungen von drei Konsumstellen erfüllen

defineComponent. Jede Abweichung in der Inferenz führt dazu, dasseinen Fehler wirft.Grenzen und Hintertüren:color='white'und bedingte TypverträgeappearanceDie Typinferenz von'outline'hat eine grundlegende Einschränkung:__typePropsLaufzeit-props-Deklarationen können „bedingte Typen“ nicht ausdrücken

__typeProps. Zum Beispiel die Einschränkung „wenn

📎 packages-private/dts-test/defineComponent.test-d.tsx:1803-1836

ConditionalProps, dann musscolorappearancesein“ lässt sich mit Laufzeit-Objektsyntax nicht schreiben. Vue bietet dafürcolor: 'white'und andere „Typ-Hintertüren“.appearance: 'outline': Notausstieg für bedingte props

  • L1823-1824:<Comp color="white" />ist ein Union-Typ: entweder sindcolor: 'white'und
  • L1825-1826:<Comp color="white" appearance="normal" />beide optional, oderappearanceund'outline'
  • L1827:<Comp color="white" appearance="outline" />. Der Test verifiziert:
wirft einen Fehler — alleiniges Angeben von

__typePropserfüllt keinen der beiden Zweige

__typeEmitswirft einen Fehler —

__typeEmitsmusssein:

📎 packages-private/dts-test/defineComponent.test-d.tsx:1838-1885

besteht{ change: [id: number], update: [value: string] }〔Designinferenz und Architekturabwägung〕this.$props.onChange?.(123)Die Designmotivation vononChange?.('123')ist „das Typsystem Einschränkungen ausdrücken zu lassen, die zur Laufzeit nicht ausdrückbar sind“. Es nimmt nicht an der Laufzeit-props-Auflösung teil, sondern ist eine reine Typ-Ebene-Überlagerung. Der Preis ist, dass Nutzer die Konsistenz zwischen Typ und Laufzeitdeklaration manuell wahren müssen — deshalb heißt es „backdoor“ und nicht offizielle API.

📎 packages-private/dts-test/defineComponent.test-d.tsx:1887-1934

: Äquivalenz der beiden emits-Syntaxen{ (e: 'change', id: number): void; (e: 'update', value: string): void }unterstützt zwei Syntaxen, der Testsperrt beide gleichzeitigObjektsyntaxdrückt Parameter mit benannten Tupeln aus. Der Test verifiziert, dassbesteht,

einen Fehler wirft.

Call-Signature-SyntaxdefineEmitsdrückt dies mit Überladungen aus.

__typeRefsDie Testkörper der beiden Syntaxen sind nahezu zeilenweise identisch__typeEl— das ist beabsichtigt: Der Vertrag verlangt, dass beide Schreibweisen

📎 packages-private/dts-test/defineComponent.test-d.tsx:1936-1952

__typeRefsvollständig äquivalentesParentTypverhalten erzeugen.__typeRefs: { child: ComponentInstance<typeof Child> }〔Designinferenz und Architekturabwägung〕refs.child.$refs.fooWarum zwei Syntaxen beibehalten? Die Objektsyntax ähnelt stärker der Schreibweise vonnumber。

📎 packages-private/dts-test/defineComponent.test-d.tsx:1963-1977

__typeEl, die Call-Signature-Syntax ähnelt stärker traditionellen TS-Ereignistypen. Vue muss beide unterstützen und konsistentes Verhalten garantieren. Die „zeilenweise gespiegelte“ Struktur des Tests ist der stärkste Äquivalenzbeweis.undElement: komponentenübergreifende Referenzen und Host-KnotentypenTypeElermöglicht der Elternkomponente, den Typ der Kindkomponenten-ref präzise zu kennen.ElementdeklariertCustomElement, sodass$elzu

inferiert werden kann.

ist subtiler. Der Testkommentar in L1963-1977 benennt die Designabsicht:TypeElHost-Knoten benutzerdefinierter Renderer (TUI, canvas, native) sind kein DOMElement,@vue/runtime-test, daher darf$elnicht auf

eingeschränkt werden. Der Test verwendet das

function syntax w/ runtime propsInterface, um zu verifizieren, dassbeliebige Host-Typen akzeptieren kann.。

📎 packages-private/dts-test/defineComponent.test-d.tsx:1501-1545

〔Designinferenz und Architekturabwägung〕generics aren't supported with object runtime propsDies ist die Typ-Ebene-Garantie dafür, dass Vue 3 benutzerdefinierte Renderer unterstützt. Wäre<Comp3<string>>hart auf

eingeschränkt, könnten Nutzer von Nicht-DOM-Renderern wie

den TypExtractPropTypesnicht korrekt inferieren. Der Vertragstest schützt hier die „Renderer-Unabhängigkeit“.

Gegenseitige Ausschlussbedingungen von generischen Komponenten und Laufzeit-props

@ts-expect-errorDer Abschnitt

@ts-expect-errorsperrt eine wichtige Regel:Generische Komponenten können nicht mit Objekt-Laufzeit-props koexistieren@ts-expect-errorDer Kommentarin L1501 ist eine Vertragsdeklaration. L1525-1535 verifiziert, dass generisches setup + Objekt-props einen Fehler werfen; L1538-1539 verifiziert, dass

📎 packages-private/dts-test/defineComponent.test-d.tsx:1354-1362

einen Fehler wirft. Array-props hingegen erlauben Generics (L1464-1499).// @ts-expect-error missing prop〔Designinferenz und Architekturabwägung〕<Comp msg={123} />Die Ursache dieser Einschränkung ist die Reihenfolge der Typinferenz: Objekt-props benötigen, um zuerst den Typ zu bestimmen, während Generics erst bei der Instanziierung bestimmt werden können — beide kollidieren. Array-props nehmen nicht an der Typextraktion teil, daher kollidieren sie nicht. Der Vertragstest fixiert diese „Typsystem-Einschränkung“ als regressionsfähige Assertion.Designüberlegungen, Fehlererholung und Produktions-FallstrickeexpectType<JSX.Element>(...)Das zweischneidige Schwert von@ts-expect-errorist das Kernwerkzeug des Typvertragstests, hat aber eine tödliche Falle:expectTypeWenn der Code darunter keinen Fehler mehr wirft,

wirft

selbst einen Fehler@ts-expect-error. Das scheint Schutz zu sein, verlangt aber vom Testautor, die „Position des Fehlers“ präzise zu kontrollieren.Betrachten wir diesen Abschnitt:@ts-expect-errorwird inauf die

IsAnyundIsUnion: Existenzbeweis auf Typebene

📎 packages-private/dts-test/defineComponent.test-d.tsx:1991-1993

expectType<IsAny<typeof props.foo>>(false)validiertprops.foonichtany. Dies istumgekehrter Vertrag: Es wird nicht nur gefordert, dass der Typ korrekt ist, sondern auch, dass der Typ nicht zuany」。anydegenerieren darf. ist ein schwarzes Loch des Typsystems; jedesanylässt nachfolgende Assertions bedeutungslos werden.

📎 packages-private/dts-test/defineComponent.test-d.tsx:195-196

expectType<IsUnion<typeof props.jjj>>(true)validiertjjjist ein Union-Typ.jjjdeklariert als((arg1: string) => string) | ((arg1: string, arg2: string) => string), wenn das Typsystem es zu einer einzigen Signatur zusammenführt,IsUniongibtfalsezurück, Test schlägt fehl.

〔Design-Inferenz und Architektur-Abwägung〕

Diese beiden Werkzeuge schützen die „Präzision des Typs“ und nicht die „Korrektheit des Typs“. Ein zuanydegenerierter oder eine zusammengeführte Union – in den meisten Anwendungsszenarien „scheint es zu funktionieren“, aber IDE-Hinweise und Compile-Time-Prüfungen gehen verloren. Vertragstests müssen diese Präzision fixieren.

Impliziter Vertrag der Deklarationsreihenfolge

📎 packages-private/dts-test/defineComponent.test-d.tsx:1784-1801

Dieser Kommentar ist äußerst wichtig:code generated by tsc / vue-tsc, make sure this continues to work so we don't accidentally change the args order of DefineComponent。DefineComponenthat 13 generische Parameter, die Reihenfolge istÖffentlicher Vertrag——vue-tscDer generierte Komponententyp hängt von dieser Reihenfolge ab. Der Test verwendetdeclare const MyButton: DefineComponent<...>und schreibt alle 13 Parameter explizit aus, um die Reihenfolge zu fixieren.

〔Design-Inferenz und Architektur-Abwägung〕

Dies ist der am leichtesten übersehene Vertrag: Die Reihenfolge der generischen Parameter ist kein „Implementierungsdetail“, sondern die „ABI des generierten Codes“. Jeder PR, der die Reihenfolge ändert, führt dazu, dassvue-tscgenerierte.d.tsmit dem Laufzeittyp inkompatibel ist. Vertragstests spielen hier die Rolle des „ABI-Kompatibilitätswächters“.

Dateiübergreifender Vertrag:componentInstance.test-d.tsxErgänzung zu

componentInstance.test-d.tsxhat nur 154 Zeilen, deckt aber alle Eingabeformen desComponentInstanceUtility-Typs ab:

📎 packages-private/dts-test/componentInstance.test-d.tsx:10-40

ComponentInstance<typeof CompSetup>Extrahiert den Instanztyp aus demdefineComponentErgebnis;ComponentInstance<typeof CompFunctional>Extrahiert aus funktionalen Komponenten;ComponentInstance<typeof CompFunction>Extrahiert aus nackten Funktionen. Alle drei müssen dieComponentPublicInstanceBasisklasse ableiten.

📎 packages-private/dts-test/componentInstance.test-d.tsx:71-116

Noch extremer ist das „nackte Objekt ohnedefineComponentWrapper“:CompObjectSetup、CompObjectData、CompObjectNoPropsAlle drei Formen müssen vonComponentInstancekorrekt extrahiert werden können. Besonders L113-114 ist kontraintuitiv:CompObjectNoPropshat keineprops-Deklaration, abercompObjectNoProps.testwird dennoch alsstring | undefinedabgeleitet – dies ist der von derComponentPublicInstanceBasisklasse bereitgestellte Fallback.

📎 packages-private/dts-test/componentInstance.test-d.tsx:143-147

Der#12751-Test in L141 fixiert eine Grenze:__typeEmitsdeklarierte'update:visible'-Ereignis sollte auf der Instanz alscomp['onUpdate:visible'](String-Schlüssel mit Doppelpunkt) exponiert werden, und$propshat den Typ{ 'onUpdate:visible'?: (value?: boolean) => any }. L152-153 validiertcomp['$props']['$props']Fehler – verhindert rekursive Selbstreferenz des Typs.

Zusammenfassung dieses Kapitels

dts-testDas Verzeichnis verwendet über 20.test-d.ts-Dateien, um „Typ als API-Vertrag“ in regressionsfähige automatisierte Tests umzusetzen. Der Kernmechanismus hat drei Ebenen:

1. Werkzeug-Ebene:expectType、expectAssignable、IsUnion、IsAnyBietet Typ-Assertion-Primitive,@ts-expect-errorBietet umgekehrte Assertion-Fähigkeit.

2. Vertrags-Ebene:ExpectedPropsDie Schnittstelle schreibt explizit fest, „welcher Typ abgeleitet werden soll“,propsDie Deklarationsmatrix zählt alle Schreibweisen erschöpfend auf, drei Konsumstellen (setup/render/TSX) werden kreuzvalidiert.

3. Hintertür-Ebene:__typeProps、__typeEmits、__typeRefs、__typeElBietet eine Escape-Luke für Typbeschränkungen, die zur Laufzeit nicht ausgedrückt werden können, und fixiert gleichzeitig die Äquivalenz der beiden emits-Syntaxen.

Denkanstöße und Selbsttests dieses Kapitels

F1: Wenn mandefineComponent.test-d.tsxL168-170@ts-expect-errorlöscht und nurexpectType<number>(props.aaaa)behält, was passiert? Warum würde dieser Test „still fehlschlagen“?

Referenzanalyse:

props.aaaadeklariert als{ type: Number as PropType<number | undefined>, required: true as const }, sein abgeleiteter Typ istnumber | undefined(weilPropType<number | undefined>explizitundefined)。

expectType<number>(props.aaaa)enthält undprops.aaaaerfordert, dassnumbergenaunumber | undefinedist). Da der tatsächliche Typist, würde diese Zeile。@ts-expect-errorselbst einen Fehler melden

Die Aufgabe von@ts-expect-errorist: „Erwartet, dass hier ein Fehler gemeldet wird, und schluckt ihn“.Wenn manprops.aaaalöscht, würde diese Zeile direkt einen Fehler melden, der Test schlägt fehl – es sieht so aus, als wäre er „strenger“. Aber das Problem ist:numberWenn eine Refaktorierung@ts-expect-errortatsächlich zumacht (Bugfix oder Verhaltensänderung), meldet diese Zeile keinen Fehler mehr, und nach dem Löschen von

würde der Test bestehen@ts-expect-error– zu diesem Zeitpunkt kann der Test nicht zwischen „Typ korrekt“ und „Typ falsch, aber zufällig kein Fehler“ unterscheiden.Die Schreibweise mit beibehaltenemistnumber | undefinedbidirektionale Fixierung@ts-expect-error: Es wird sowohl gefordert, dass „der aktuelle TypexpectType<number>ist“ (durchnumberwird dernumber,@ts-expect-error-Fehler geschluckt), als auch, dass „der Typ nichtsein darf“ (wenn er zu。

📎 packages-private/dts-test/defineComponent.test-d.tsx:168-170

Q2: __typePropswird, schlägt es fehl, weil kein Fehler zum Schlucken vorhanden ist). Dies ist die Kerntechnik von Typvertragstests –ConditionalProps„Erwarteter Fehler“ wird verwendet, um zu fixieren, „dass der Typ eine bestimmte Komponente enthalten muss“{ color?: 'normal' | 'primary' | 'secondary' | 'white'; appearance?: 'normal' | 'outline' | 'text' }Der Hintertür-Test (L1803-1836) validiert die Beschränkung des bedingten Union-Typs. Wenn man__typePropsvon einem Union-Typ zu

ändert (d. h. alle Optionen flach klopft), wie würde der Test fehlschlagen? Welche Design-Beschränkung von:

zeigt dies?colorReferenzanalyseappearanceDer flach geklopfte Typ erlaubt jede Kombination voncolor: 'white' + appearance: 'normal'und, einschließlich:

code
// @ts-expect-error
;<Comp color="white" appearance="normal" />

einen Fehler meldet@ts-expect-errorKopieren<Comp color="white" />Wenn der Typ flach geklopft wird, meldet diese Zeile keinen Fehler mehr,@ts-expect-errorschlägt fehl, weil „kein Fehler zum Schlucken vorhanden ist“. Gleichzeitig würde

in L1823-1824 von „Fehler melden“ zu „Bestehen“ wechseln, was ebenfalls__typePropsfehlschlagen lässt.Dies zeigt, dass die Design-Beschränkung von。__typePropsist:PropsEs muss die „Branch-Mutual-Exclusion“-Semantik des Union-Typs bewahrenPrettifyEs ist nicht einfach „Typabdeckung“, sondern „Verwendung des Typsystems, um bedingte Beschränkungen auszudrücken, die Laufzeit-props nicht ausdrücken können“. Wenn bei der ImplementierungOmiteine Mapping-Transformation wie

oder

durchführt, kann dies die Diskriminierbarkeit der Union-Branches zerstören und die Beschränkung unwirksam machen.__typeProps〔Design-Inferenz und Architektur-Abwägung〕CommonProps & ConditionalPropsDeshalb verwendet der Testfall von

Q3: DefineComponentdie einfachsteVNodeProps & AllowedComponentProps & ComponentCustomProps-Kreuzung und nicht den „eleganteren“ Mapping-Typ – jede zusätzliche Typ-Transformation kann Bugs verschleiern.Readonly<ExtractPropTypes<{}>>Die Reihenfolge der 13 generischen Parameter von

wird durch L1784-1801 explizit fixiert. Wenn eine Refaktorierung den 9. Parameter (:

DefineComponent) mit dem 10. Parameter (vue-tsc) vertauscht, welche Downstream-Bereiche wären betroffen? Warum muss der Vertragstest diese Reihenfolge fixieren?<script setup>ReferenzanalysedefineProps / defineEmits,vue-tscDie Reihenfolge der generischen Parameter vonCreateComponentPublicInstance<...>ist die „ABI“ bei der Generierung des Komponententyps. Wenn der Benutzer inschreibt, generierteinen

-Typ ähnlich L1999-2116, wobei die

1. vue-tscPosition.d.tsder generischen Parameter die Bedeutung jedes Typparameters bestimmt.DefineComponentWenn der 9. und 10. Parameter vertauscht werden:VNodeProps & AllowedComponentProps & ComponentCustomPropsDas generierteReadonly<ExtractPropTypes<{}>>füllt die Parameter in der alten Reihenfolge, aberDie Props-Typen der Benutzerkomponente sind alle falsch ausgerichtet。

2. L1786-1800 derdeclare const MyButton: DefineComponent<...>führt direkt zu einem Fehler – weil{}undVNodeProps & ...nicht kompatibel sind.

3. L1999-2116 derErrorMessageTyp (simuliertvue-tscgenerierte Ergebnisse) führt ebenfalls zu einem Fehler.

Der Wert von Vertragstests, die die Reihenfolge fixieren, liegt darin:Sie heben die „Reihenfolge der generischen Parameter" von einem „Implementierungsdetail" zu einem „öffentlichen Vertrag" an. Jeder PR, der die Reihenfolge ändert, lässt L1786-1800 sofort fehlschlagen und verhindert, dass inkompatible Änderungen in ein Release gelangen.

📎 packages-private/dts-test/defineComponent.test-d.tsx:1784-1801

〔Design-Inferenz und Architektur-Abwägung〕

Dies ist der am meisten unterschätzte Wert von Typvertragstests: Sie schützen nicht „ob die Typen korrekt sind", sondern „die Interface-Stabilität des Typsystems". Die Reihenfolge generischer Parameter,@ts-expect-errordie Position vonIsAnyder Rückgabewert von

sind Bestandteile der „Typ-ABI".

Typvertragstests lösen „ob die API-Oberfläche den Erwartungen entspricht". Aber Typen sind nur die Hälfte der Vue-Engineering – die andere Hälfte ist „wie Benutzer das Verhalten dieser APIs in Echtzeit im Browser verifizieren können". Das nächste Kapitel führt in den SFC Playground ein und zeigt, wie Vue Compiler, Runtime und Typsystem in eine browserinterne Echtzeit-Debugging-Umgebung verpackt, sodass Benutzer im Moment der Codeänderung die Kompilierungsartefakte und Laufzeitergebnisse sehen.IsAny/IsUnion), „ob die Reihenfolge generischer Parameter stabil ist" (DefineComponent13 Parameter), „Renderer-Unabhängigkeit" (__typeElnicht aufElementbeschränkt). Sobald diese Einschränkungen durchbrochen werden, driften die IDE-Hinweise auf Benutzerseite,vue-tscdie generierten Typen. Und die Stabilität von Typverträgen muss letztlich der täglichen Debugging-Erfahrung von Entwicklern dienen – im nächsten Kapitel betreten wirpackages-private/sfc-playgroundund sehen, wie ein reiner Frontend-Playground den geschlossenen Kreislauf von SFC-Kompilierung und Echtzeit-Vorschau im Browser vollendet.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 07

Kapitel 7: SFC Playground: Echtzeit-Kompilierung und Debugging-Subsystem im Browser

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 7 von 14

Im vorherigen Kapitel haben wir mit über 20.test-d.tsDateien „Typen als API-Vertrag" in der CI festgenagelt. Aber Typverträge beantworten nur „wie die API-Oberfläche aussieht", sie können nicht beantworten „wie diese SFC tatsächlich kompiliert aussieht" oder „ob die Renderergebnisse im SSR-Modus konsistent sind". Um die letzten beiden Fragen zu beantworten, brauchte das Vue-Team eine Sandbox, die die vollständige Kompilierungspipeline im Browser ausführen kann – das istpackages-private/sfc-playground. Es unterscheidet sich grundlegend von den öffentlichen Paketen unterpackages/:package.jsonin"private": trueund"version": "0.0.0" 📎 packages-private/sfc-playground/package.json:2-4, was bedeutet, dass es niemals auf npm veröffentlicht wird, sondern nur ein offizielles Debugging-Tool ist. In seinen Abhängigkeiten zeigtvueaufworkspace:* 📎 packages-private/sfc-playground/package.json:19, also auf lokale Quellcode-Build-Artefakte statt auf die stabile Version auf npm – was den Playground natürlicherweise zu einer „lebenden Demonstration des aktuellen Commits" macht. Dieses Kapitel konzentriert sich auf drei Fragen: Wie initialisiert der Einstiegspunkt, wie steuert der Header Zustandswechsel, und wie werden Build-Zeit-Konstanten injiziert.

I. Minimalismus des Einstiegspunkts: main.ts und der Initialisierungsvertrag des ReplStore

Intuitives Modell

main.tshat nur 9 Zeilen, wie ein „Selbsttest-Skript beim Start": Bevor die Vue-Anwendung gemountet wird, wird zuerst inwindoweine globale Konfiguration eingefügt, die Vue DevTools mitteilt, „welche App standardmäßig ausgewählt ist". Ohne diesen Schritt würde DevTools beim Öffnen mit mehreren App-Instanzen konfrontiert (der Playground selbst + der im Benutzer-REPL ausgeführte Code) und könnte nicht automatisch fokussieren, was die Debugging-Erfahrung auf manuelles Umschalten degradieren würde.

Datenstruktur und globale Seiteneffekte

main.tsDer Kern voncreateAppist nichtwindowsondern das verschmutzende Schreiben in

📎 packages-private/sfc-playground/src/main.ts:4-7

ts
// @ts-expect-error Custom window property
window.VUE_DEVTOOLS_CONFIG = {
  defaultSelectedAppId: 'repl',
}

Hier gibt es zwei bemerkenswerte Engineering-Details:

〔Design-Inferenz und Architektur-Abwägung〕

1. @ts-expect-errorstatt@ts-ignore:windowDer StandardtypWindow & typeof globalThishat keinVUE_DEVTOOLS_CONFIGFeld. Die Verwendung von@ts-expect-errorbedeutet „Ich weiß, dass hier ein Fehler auftritt, und ich verlange, dass er auftritt" – falls ein zukünftiges@types/*dieses Feld ergänzt,@ts-expect-errorwird wegen „kein Fehler erzeugt" umgekehrt einen Fehler melden und den Autor daran erinnern, den Kommentar zu entfernen. Dies steht in einer Linie mit dem Ansatz der Typvertragstests aus dem vorherigen Kapitel:Absichten mit dem Typsystem schützen, statt Probleme zu verbergen。

〔Design-Inferenz und Architektur-Abwägung〕

2. defaultSelectedAppId: 'repl'Die String-Konvention von: Diese'repl'muss vollständig mit der ID übereinstimmen, die bei der internen App-Erstellung in@vue/replverwendet wird. Es ist ein paketübergreifender Literal-Vertrag, der durch keine Typbeschränkung geschützt wird – sobald@vue/repldie ID ändert, wird die Standardauswahl der Playground-DevTools stillschweigend ungültig.

Step-by-Step: Von HTML zum Mounten

Der Ausführungsfluss ist extrem kurz, aber jeder Schritt hat implizite Einschränkungen:

1. Der Browser lädtindex.html, das<div id="app">enthält (in diesem Material nicht bereitgestellt, abermount('#app')lässt sich rückerschließen).

2. Modulgraph-Auflösung:main.tsDasimport App from './App.vue' 📎 packages-private/sfc-playground/src/main.ts:2am Anfang von@vitejs/plugin-vuelöst die SFC-Kompilierung von

aus.

3. 〔Design-Inferenz und Architektur-Abwägung〕:window.VUE_DEVTOOLS_CONFIGKritische ReihenfolgecreateApp(App).mount('#app') 📎 packages-private/sfc-playground/src/main.ts:9muss vorcreateAppgeschrieben werden. Denn der DevTools-Hook wird innerhalb von

4. mount('#app')registriert, und ein Schreiben der Konfiguration nach dem Mount kann die erste Auswahl nicht mehr beeinflussen.App.vuelöst das Setup vonReplStoreaus und erstellt dannApp.vue(in

mermaid
flowchart TD
    load["浏览器加载 index.html"] --> parse["解析 main.ts 模块图"]
    parse --> sfc["@vitejs/plugin-vue 编译 App.vue"]
    sfc --> setcfg["写入 window.VUE_DEVTOOLS_CONFIG"]
    setcfg --> check{"VUE_DEVTOOLS_CONFIG 已设置?"}
    check -->|是| mount["createApp(App).mount('#app')"]
    check -->|否| devtools["DevTools 无法默认选中 repl"]
    mount --> appsetup["App.vue setup 创建 ReplStore"]
    appsetup --> ready["Playground 就绪"]
    devtools --> mount

Kopieren

main.tsDesign-Überlegungen und FallstrickeDer Minimalismus vonApp.vueist absichtlich:ReplStoreDer Einstiegspunkt übernimmt nur zwei Aufgaben: „globale Seiteneffekt-Injektion + Mounting". Jegliche Geschäftslogik sollte hier nicht auftauchen. Dies ist die Abwägung des Playgrounds als „Debugging-Tool" statt als „Produkt" – es benötigt keine SSR-Kompatibilität, keine mehrfachen Einstiegspunkte, kein Lazy Loading.

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Produktions-Fallstricke:window.VUE_DEVTOOLS_CONFIGistGlobales Singleton. Wenn der Playground in eine andere Seite eingebettet wird, die ebenfalls DevTools verwendet (z. B. iframe-Szenario), überschreibt der später Schreibende den früheren. Da der Playground normalerweise eigenständig deployed wird, wird dieses Risiko akzeptiert.

---

Zwei, Header.vue: computed abgeleiteter Zustand und emit unidirektionaler Datenfluss

Intuitives Modell

Header.vueist das „Kontrollpanel" des Playgrounds – Versionsauswahl, PROD/DEV-Umschaltung, SSR-Schalter, Theme-Umschaltung, Teilen, Herunterladen. Es selbsthält keinen Geschäftszustand, alle Zustände stammen ausprops.storeund booleschen Props, alle Änderungen werden überemitan die Elternkomponente gemeldet. Ohne diese Einschränkung „dumme Komponente + Event-Bubbling" würde der Header zu einem Hotspot verstreuter Zustände werden, und die Seiteneffekte von Versionswechsel und SSR-Umschaltung könnten nicht zentral verwaltet werden.

Datenstruktur- und Feldanalyse

Die Props-Definition des Headers ist der Schlüssel zum Verständnis seiner Verantwortlichkeiten:

📎 packages-private/sfc-playground/src/Header.vue:13-19

ts
const props = defineProps<{
  store: ReplStore
  prod: boolean
  ssr: boolean
  autoSave: boolean
  theme: 'dark' | 'light'
}>()

Die fünf Props teilen sich in zwei Kategorien auf:

  • store: ReplStore: die einzige Zustandscontainer-Referenz, stammt aus@vue/repl. Der Header liest darüberstore.loading、store.vueVersion、store.typescriptVersion, und schreibt direkt instore.vueVersion。
  • vier boolesche/Literal-Props:prod、ssr、autoSave、theme. Sie sindkontrollierter Zustand, der Header liest nur und schreibt nicht, Änderungen müssenemit。

die entsprechende emit-Liste📎 packages-private/sfc-playground/src/Header.vue:20-28:

ts
const emit = defineEmits([
  'toggle-theme',
  'toggle-ssr',
  'toggle-prod',
  'toggle-autosave',
  'reload-page',
])

Beachten Sietoggle-themeobwohl vontoggleDark()internemit, abertoggle-ssr/toggle-prod/toggle-autosaveist im Template direkt$emitdas📎 packages-private/sfc-playground/src/Header.vue:102-118. Diese Mischung ist ein häufiger Vue 3<script setup>-Stil:Bei Bedarf an Seiteneffekten Funktion emit verwenden, bei reiner Weiterleitung Template$emit。

Step-by-Step: Versionsanzeige und -wechsel

Szenario: Benutzer öffnet den Playground, der Header muss die aktuelle Vue-Version anzeigen.

Schritt 1: computed abgeleiteter Anzeigetext

📎 packages-private/sfc-playground/src/Header.vue:30-37

ts
const vueVersion = computed(() => {
  if (store.loading) {
    return 'loading...'
  }
  return store.vueVersion || `@${__COMMIT__}`
})

Hier gibt es drei Prioritätsebenen:loading-Zustand →'loading...'; Benutzer hat explizit Version gewählt →store.vueVersion; andernfalls →@${__COMMIT__}(aktueller Commit-Kurzhash).__COMMIT__ist eine zur Build-Zeit injizierte Konstante, wird im nächsten Abschnitt detailliert beschrieben.

Schritt 2: VersionSelect Zwei-Wege-Bindung

📎 packages-private/sfc-playground/src/Header.vue:88-88

html
<VersionSelect
  :model-value="vueVersion"
  @update:model-value="setVueVersion"
  pkg="vue"
  label="Vue Version"
>

Beachten Sie hierwurde nichtv-modelverwendet, sondern explizit aufgeteilt in:model-value + @update:model-value. Der Grund ist, dassvueVersionein computed ist (schreibgeschützt), nicht direkt zwei-Wege-gebunden werden kann; muss übersetVueVersiondiese Setter-Funktion geschrieben werdenstore.vueVersion:

📎 packages-private/sfc-playground/src/Header.vue:39-41

ts
async function setVueVersion(v: string) {
  store.vueVersion = v
}

function resetVueVersion() {
  store.vueVersion = null
}
〔Design-Schlussfolgerung und Architektur-Abwägung〕

setVueVersionalsasyncdeklariert, aber intern ohneawait– ist das historisches Erbe oder Absicht? Vermutlich zur Angleichung an die asynchrone Ladesemantik vonVersionSelect(Versionswechsel löst Remote-Laden aus), um die Schnittstelle konsistent zu halten.

Schritt 3: TypeScript-Version im Vergleich

📎 packages-private/sfc-playground/src/Header.vue:76-80

html
<VersionSelect
  v-model="store.typescriptVersion"
  pkg="typescript"
  label="TypeScript Version"
/>

Die TypeScript-Version verwendetv-model, weilstore.typescriptVersioneine beschreibbare normale Eigenschaft ist, kein computed-Wrapping benötigt.Dieselbe Komponente verwendet zwei Bindungsarten im selben Template, was genau die intuitive Verkörperung von „kontrolliert vs. unkontrolliert" ist.

Theme-Umschaltung: Kombination von Seiteneffekt und emit

📎 packages-private/sfc-playground/src/Header.vue:58-66

ts
function toggleDark() {
  const cls = document.documentElement.classList
  cls.toggle('dark')
  localStorage.setItem(
    'vue-sfc-playground-prefer-dark',
    String(cls.contains('dark')),
  )
  emit('toggle-theme', cls.contains('dark'))
}

Diese Funktion macht drei Dinge: DOM-Klasse manipulieren, in localStorage persistieren, emit zur Benachrichtigung der Elternkomponente.Beachten Sie, dass sie nicht direktprops.themeändert – weil Props schreibgeschützt sind, die Elternkomponente erst nach Erhalt vontoggle-themeaktualisierttheme, was wiederum den:title-Text im Template steuert📎 packages-private/sfc-playground/src/Header.vue:123。

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Hier gibt es ein subtiles Design:DOM-Klassen-Manipulation und Vue-reaktiver Zustand sind zwei unabhängige Pfade。document.documentElement.classList.toggle('dark')ändert direkt das DOM, währendthemeprop über Vue aktualisiert wird. Wenn beide nicht synchron sind (z. B. Elternkomponente lehnt Aktualisierung ab), zeigt die UI eine Inkonsistenz „Klasse bereits umgeschaltet, aber title-Text unverändert". In der Praxis akzeptiert die Elternkomponente immer das emit, daher tritt das Problem nicht auf.

Versteckte Logik: metaKey-Zweig von copyLink

📎 packages-private/sfc-playground/src/Header.vue:47-56

ts
async function copyLink(e: MouseEvent) {
  if (e.metaKey) {
    resetVueVersion()
    // hidden logic for going to local debug from play.vuejs.org
    window.location.href = 'http://localhost:5173/' + window.location.hash
    return
  }
  await navigator.clipboard.writeText(location.href)
  alert('Sharable URL has been copied to clipboard.')
}

Dies ist eineEntwickler-Hintertür: Aufplay.vuejs.orgCmd gedrückt halten und auf den Teilen-Button klicken, springt zulocalhost:5173(lokaler Dev-Server) und nimmt den aktuellen URL-Hash mit. Der Hash kodiert den vollständigen REPL-Zustand (Quellcode, Version, Optionen), sodass lokales Debugging Online-Probleme reproduzieren kann. Der Kommentar// hidden logic for going to local debug from play.vuejs.org 📎 packages-private/sfc-playground/src/Header.vue:47-56kennzeichnet explizit, dass dies eine absichtlich versteckte Funktion ist.

〔Design-Schlussfolgerung und Architektur-Abwägung〕

resetVueVersion()wird vor dem Sprung aufgerufen, setztstore.vueVersionaufnull, um sicherzustellen, dass lokales Debugging den aktuellen Commit verwendet statt der online ausgewählten Version.

mermaid
flowchart TD
    click["用户点击 Share 按钮"] --> meta{"e.metaKey 按下?"}
    meta -->|是| reset["resetVueVersion() 置 null"]
    reset --> jump["跳转 localhost:5173 + hash"]
    jump --> local["本地 dev server 复现"]
    meta -->|否| copy["navigator.clipboard.writeText(location.href)"]
    copy --> check{"写入成功?"}
    check -->|是| alert["alert 提示已复制"]
    check -->|否| fail["静默失败 (无 catch)"]

Design-Überlegungen und Fallstricke

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Fallstrick 1:navigator.clipboardBerechtigungen und Sicherheitskontext。copyLinkhat kein try/catch📎 packages-private/sfc-playground/src/Header.vue:47-56. Bei Nicht-HTTPS oder wenn der Benutzer die Zwischenablage-Berechtigung verweigert,writeTextwird rejecten, was zu einer unbehandelten Promise-Rejection führt. Der Playground wird über HTTPS deployed, das Risiko wird akzeptiert, aber dies ist eine typische „Produktionsumgebungs-Falle".

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Fallstrick 2:toggleDarkhartkodierter localStorage-Key。'vue-sfc-playground-prefer-dark'ist ein String-Literal, keine Konstantenextraktion. Wenn der Key in Zukunft geändert werden soll, ist eine globale Suche erforderlich.

Fallstrick 3:currentCommitundvueVersionVergleich. Im Template:class="{ active: vueVersion === \@${currentCommit}\ }" 📎 packages-private/sfc-playground/src/Header.vue:88-88Durch String-Verkettung vergleichen. Wenn__COMMIT__die Injektion fehlschlägt (wird zuundefined), wird hier daraus'@undefined', was niemals übereinstimmt. Die Zuverlässigkeit der Build-Zeit-Konstanten-Injektion bestimmt direkt die UI-Korrektheit – genau das ist das Thema des nächsten Abschnitts.

---

Drei, Build-Zeit-Konstanten-Injektion: Die doppelte Verantwortung von __COMMIT__ und copyVuePlugin

Intuitives Modell

vite.config.tsist die „Montagehalle“ des Playgrounds: Es führt zur Build-Zeitgit rev-parseaus, um den Commit-Hash zu erhalten, und macht ihn überdefinezu einer globalen Konstante__COMMIT__; gleichzeitig kopiert es über ein benutzerdefiniertes Plugin die ESM-Browser-Artefakte unterpackages/vue/dist/in das Ausgabeverzeichnis des Playgrounds. Ohne diesen Schritt könnte der Playground die „Vue-Laufzeit des aktuellen Commits“ nicht im Browser laden – er wäre auf die stabile Version von npm angewiesen und würde die Bedeutung einer „Live-Demo“ verlieren.

Datenstruktur und Build-Zeit-Konstanten

📎 packages-private/sfc-playground/vite.config.ts:7-9

ts
const commit = spawnSync('git', ['rev-parse', '--short=7', 'HEAD'])
  .stdout.toString()
  .trim()

spawnSyncführt den git-Befehl synchron aus,--short=7nimmt den 7-stelligen Kurz-Hash. Die synchrone Ausführung ist absichtlich so gewählt:Die Konfigurationsdatei benötigt den Wert voncommitbereits während des Modulladens, asynchron würde die Reihenfolge der Vite-Konfigurationsauflösung durcheinanderbringen.

📎 packages-private/sfc-playground/vite.config.ts:23-26

ts
define: {
  __COMMIT__: JSON.stringify(commit),
  __VUE_PROD_DEVTOOLS__: JSON.stringify(true),
},

defineist VitesTextersetzungsmechanismus: Alle__COMMIT__im Quellcode werden durch das Ergebnis vonJSON.stringify(commit)ersetzt (also durch ein Zeichenkettenliteral mit Anführungszeichen).JSON.stringifyist erforderlich – wenn man direktcommitschreiben würde, würde es nach der Ersetzung zu einem nackten Bezeichnerabc1234werden und als Variablenname statt als Zeichenkette behandelt.

〔Design-Schlussfolgerung und Architektur-Abwägung〕

__VUE_PROD_DEVTOOLS__: trueist eine weitere Schlüsselkonstante: Sie lässt VuesProduktions-Buildebenfalls DevTools-Unterstützung behalten. Standardmäßig entfernt der Produktions-Build den DevTools-Hook, um die Größe zu reduzieren, aber der Playground muss Benutzercode debuggen können, daher wird er zwangsweise aktiviert.

Step-by-Step: Das Artefakt-Verschieben von copyVuePlugin

📎 packages-private/sfc-playground/vite.config.ts:32-63

ts
function copyVuePlugin(): Plugin {
  return {
    name: 'copy-vue',
    generateBundle() {
      const copyFile = (file: string) => {
        const filePath = path.resolve(
          import.meta.dirname,
          '../../packages',
          file,
        )
        const basename = path.basename(file)
        if (!fs.existsSync(filePath)) {
          throw new Error(
            `${basename} not built. ` +
              `Run "nr build vue -f esm-browser" first.`,
          )
        }
        this.emitFile({
          type: 'asset',
          fileName: basename,
          source: fs.readFileSync(filePath, 'utf-8'),
        })
      }

      copyFile(`vue/dist/vue.esm-browser.js`)
      copyFile(`vue/dist/vue.esm-browser.prod.js`)
      copyFile(`vue/dist/vue.runtime.esm-browser.js`)
      copyFile(`vue/dist/vue.runtime.esm-browser.prod.js`)
      copyFile(`server-renderer/dist/server-renderer.esm-browser.js`)
    },
  }
}

Die Kernpunkte einzeln analysiert:

1. generateBundleHook: Wird ausgeführt, nachdem Rollup das Bundle erzeugt hat, aber bevor es auf die Festplatte geschrieben wird. Zu diesem Zeitpunkt kann manemitFilezusätzliche Dateien in die Artefakte einfügen.

2. import.meta.dirname: Die von Node 20.11+ bereitgestellte ESM-Version von__dirname. Der Pfad../../packagesgeht vonpackages-private/sfc-playground/zum Repository-Stamm hinauf und dann inpackages/。

3. Existenzprüfung + explizite Fehlermeldung: Wennvue.esm-browser.jsnicht existiert, wird ein Fehler mit Reparaturanweisung geworfenRun "nr build vue -f esm-browser" first.. Dies istein Musterbeispiel für Developer Experience– die Fehlermeldung sagt einem direkt, wie man es repariert.

4. Fünf Artefakte:vuein Vollversion/Runtime-Version × dev/prod, plusserver-renderer. Diese fünf Dateien sind genau die Kandidatenmenge, die der Playground im Browser dynamisch importiert, entsprechend dem Versionswechsel und dem SSR-Schalter im Header.

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Warum genau diese fünf?Die Vollversion (mit Compiler) wird für das „Runtime-Compile“-Szenario verwendet; die Runtime-Version für das „Precompile“-Szenario; dev/prod entspricht dem PROD/DEV-Schalter im Header; server-renderer entspricht dem SSR-Schalter. Diese fünf Dateien bilden die „Vue-Laufzeitmatrix“ des Playgrounds.

Der vollständige Datenfluss des Versionswechsels

Betrachtet man densetVueVersionim Header zusammen mit den Artefakten von copyVuePlugin:

mermaid
flowchart LR
    user["用户选择版本"] --> setver["setVueVersion(v)"]
    setver --> store["store.vueVersion = v"]
    store --> repl["@vue/repl 内部"]
    repl --> fetch{"版本来源?"}
    fetch -->|"@commit"| local["加载本地 vue.esm-browser.js"]
    fetch -->|"3.4.0"| cdn["从 CDN 加载"]
    local --> compile["浏览器内编译 SFC"]
    cdn --> compile
    compile --> preview["实时预览"]

Beachtet den speziellen Wert@${__COMMIT__}: Er entspricht den lokalen Artefakten, die copyVuePlugin kopiert hat, nicht dem CDN. Deshalb muss der Playground die Browser-Build-Artefakte von Vue hineinkopieren –die Option „This Commit“ benötigt lokale Dateien。

Design-Überlegungen und Stolperfallen

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Stolperfalle 1:spawnSyncFehlerbehandlung. Wenn das aktuelle Verzeichnis kein git-Repository ist (z. B. entpackt aus einem Tarball), gibtspawnSynceinen Exit-Code ungleich null zurück,stdoutist leer,commitwird zu einer leeren Zeichenkette. Dann wird__COMMIT__durch""ersetzt, und im Header wird@${currentCommit}zu'@'. Es gibt keine explizite Fehlerbehandlung.

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Stolperfalle 2:optimizeDeps.exclude: ['@vue/repl'] 📎 packages-private/sfc-playground/vite.config.ts:27-29. Vite bündelt Abhängigkeiten standardmäßig vor, um den Kaltstart zu beschleunigen, aber@vue/replwird ausgeschlossen. Der Grund ist, dass@vue/replintern dynamische Imports und Worker verwendet, und das Vorab-Bündeln diese Mechanismen zerstören würde. Dies ist ein häufiges Problem im Vite-Ökosystem: „Konflikt zwischen Vorab-Bündelung und dynamischem Laden“.

〔Design-Schlussfolgerung und Architektur-Abwägung〕

Stolperfalle 3:script.fsKonfiguration 📎 packages-private/sfc-playground/vite.config.ts:13-19。@vitejs/plugin-vueDie Optionscript.fserlaubt es dem<script>-Block einer SFC, Dateien überfszu lesen. Hier werdenfs.existsSyncundfs.readFileSyncübergeben, um die Auflösung vonimport-Anweisungen in SFCs zu unterstützen (zum Beispiel mussimport x from './foo'prüfen, ob eine Datei existiert).Dies ist der Schlüssel dafür, dass der Playground im Browser eine vollständige Modulauflösung simulieren kann– er injiziert die fs-Fähigkeiten von Node in die Auflösungsphase des Compilers.

---

Design-Überlegung: Die Architektur-Abwägungen des Playgrounds

Betrachtet man die drei Unterabschnitte zusammen, folgt die Architektur des Playgrounds einem klaren Prinzip:Trennung von „Zustand“ und „Nebenwirkungen“, Trennung von „Build-Zeit“ und „Laufzeit“。

  • main.tsführt nur globale Nebenwirkungs-Injektion durch und berührt keinen Business-Zustand.
  • Header.vueist eine reine Präsentationskomponente, der Zustand fließt über Props hinein und über emit hinaus.
  • vite.config.tsverfestigt die Build-Zeit-Information „aktueller Commit“ zu einer Konstante, die zur Laufzeit nur gelesen wird.
〔Design-Schlussfolgerung und Architektur-Abwägung〕

Diese Trennung bringt einen direkten Vorteil:Der Playground kann in jede Vue-Anwendung eingebettet werden(zum Beispiel als eingebettetes Beispiel in einer Dokumentationsseite), solange manstoreund die vier booleschen Props bereitstellt.

Der Preis istZustandsverteilung:storeIn@vue/replist der boolesche Zustand in der Elternkomponente, die DOM-Klasse aufdocument.documentElement, und in localStorage liegt noch eine weitere Kopie. Vier Zustandsorte müssen manuell synchronisiert werden; jede Nichtsynchronisation führt zu UI-Inkonsistenz.

〔Designableitung und Architekturabwägung〕

Eine weitere Abwägung istVerzicht auf SSR-Kompatibilität。main.tsdirekter Zugriff aufwindow,Header.vuedietoggleDarkdirekter Zugriff aufdocument. Playground ist eine reine CSR-Anwendung, serverseitiges Rendering muss nicht berücksichtigt werden.

---

Kapitelzusammenfassung

Dieses Kapitel analysiertpackages-private/sfc-playgrounddie drei Kerndateien:

1. main.ts: 9-zeiliger Einstiegspunkt, der Kern istwindow.VUE_DEVTOOLS_CONFIGdie Injektionsreihenfolge – muss vormounterfolgen.

2. Header.vue: durchcomputedableitenvueVersion, durchemitalle Zustandsänderungen melden.copyLinkdiemetaKeyDer Zweig ist eine versteckte lokale Debug-Hintertür.

3. vite.config.ts:spawnSyncCommit-Hash holen,defineinjizieren__COMMIT__,copyVuePlugindie fünf Vue-Browser-Artefakte in das Playground-Artefaktverzeichnis kopieren.

Der rote Faden durch alle drei istdie Grenze zwischen Build-Zeit-Konstanten und Laufzeit-Zustand:__COMMIT__ist eine schreibgeschützte Build-Zeit-Tatsache,store.vueVersionist eine veränderbare Laufzeit-Auswahl, dasvueVersioncomputed im Header vereinheitlicht beides zu einem Anzeige-String.

Kapitelreflexion und Selbsttest

Q1: Wenn manmain.tsinwindow.VUE_DEVTOOLS_CONFIGdie Zuweisung voncreateApp(App).mount('#app')nach

verschiebt, was passiert? Warum?:window.VUE_DEVTOOLS_CONFIGReferenzanalysecreateAppist die Konfiguration, die Vue DevTools beim Registrieren des Hooks in📎 packages-private/sfc-playground/src/main.ts:4-9。createAppliest.__VUE_DEVTOOLS_GLOBAL_HOOK__registriert sofortdefaultSelectedAppId, zu diesem Zeitpunkt liest DevToolsmountum zu entscheiden, welche App standardmäßig ausgewählt wird. Wenn die Zuweisung später alsreplerfolgt, hat DevTools die erste App-Auswahl bereits abgeschlossen, die Konfiguration wird nicht wirksam, und der Benutzer muss in DevTools manuell zur@vue/replApp wechseln. Noch subtiler: Da

Q2: Header.vueintern ebenfalls eine App erstellt, kann eine späte Zuweisung dazu führen, dass DevTools standardmäßig Playground selbst statt der Benutzer-REPL auswählt; beim Debuggen von Benutzercode ist manuelles Umschalten erforderlich. Dies zeigt die Bedeutung der „Reihenfolge globaler Seiteneffekt-Injektion" in Debug-Tools.toggleDark()Dasprops.themeintoggle-themebearbeitet gleichzeitig DOM-Klasse, localStorage und emit, ändert aber nicht direkttheme. Wenn die Elternkomponente nach Erhalt des

Ereignisses die Aktualisierung der:toggleDark()Prop ablehnt, welche UI-Inkonsistenz tritt auf? Wie lässt sich dies auf Quellcodeebene lokalisieren?📎 packages-private/sfc-playground/src/Header.vue:58-66Referenzanalysedocument.documentElement.classList.toggle('dark')ruft indarkdirekt📎 packages-private/sfc-playground/src/Header.vue:186-186auf, was sofort die.dark navKlasse im DOM ändert und den CSS-Variablenwechsel auslöst (siehe:titledie📎 packages-private/sfc-playground/src/Header.vue:123Regel). Aber derprops.themeText<html>im Template hängt von

Q3: copyVuePluginab; wenn die Elternkomponente nicht aktualisiert, bleibt der title beim alten Wert. Lokalisierungsmethode: In den Browser-DevTools prüfen, ob die Klasse vongenerateBundleund das title-Attribut des Buttons im Widerspruch stehen. Die Grundursache ist, dass „DOM-Seiteneffekt" und „Vue-reaktiver Zustand" zwei unabhängige Pfade nehmen, ohne eine einzige Datenquelle.fs.existsSyncInfs.readFileSyncwird für jede Datei eine

Prüfung durchgeführt, bei Fehlen wird ein Fehler mit Reparaturanweisung geworfen. Wenn man diese Prüfung entfernt und direktaufruft, was passiert in einer CI-Umgebung (ohne vorherigen vue-Build)? Wie würde die Fehlermeldung Entwickler irreführen?fs.readFileSyncReferenzanalyseENOENT: no such file or directory, open '.../packages/vue/dist/vue.esm-browser.js' 📎 packages-private/sfc-playground/vite.config.ts:32-63: Nach Entfernen der Prüfung wirftnr build vue -f esm-browsereinenthrow new Error(\${basename} not built. Run "nr build vue -f esm-browser" first.\). Dieser Fehler teilt dem Entwickler nur mit, dass „die Datei nicht existiert", aber nicht, dass „zuerst

---

ausgeführt werden muss". In einer CI-Umgebung könnte der Entwickler fälschlicherweise auf einen Pfadkonfigurationsfehler, Berechtigungsproblem oder nicht initialisiertes Git-Submodul schließen und viel Zeit mit der Fehlersuche verschwenden. Daspackages-private/template-explorerim Originalcode bindet „Symptom" und „Reparaturaktion" aneinander, ein entscheidendes Detail im Developer-Experience-Design. Dies erklärt auch, warum das Build-Skript von Playground eine klare Abhängigkeitsreihenfolge zum Vue-Kern-Build-Skript haben muss.

Das nächste Kapitel geht in@vue/compiler-domund zeigt, wie Vue die Zwischenprodukte des Compilers (AST, Transformationsergebnisse, Codegenerierung) visualisiert, sodass Entwickler jeden Schritt der Transformation vom Template zur Renderfunktion schrittweise beobachten können. Anders als die „End-to-End-Blackbox" von Playground ist Template Explorer eine „Whitebox-Sonde".@vue/compiler-ssrDamit haben wir gesehen, wie SFC Playground die Kompilierungspipeline in den Browser bringt: Einstiegsinitialisierung, Header-Zustandswechsel und Build-Zeit-Konstanten-Injektion bilden gemeinsam eine in Echtzeit debugbare Sandbox. Doch die Perspektive von Playground ist stets „Kompilierung und Ausführung eines ganzen SFC", sie beantwortet nicht direkt „was der Compiler mit einem bestimmten Template-Ausdruck tatsächlich macht". Das nächste Kapitel betritt Template Explorer und zeigt, wie er

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 08

Zurück nach oben ↑

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 8 von 14

Im vorherigen Kapitel haben wir gesehen, wie der SFC Playground die gesamte Kette von „SFC-Eingabe → Kompilierung im Browser → Echtzeit-Vorschau“ als Blackbox kapselt: Entwickler sehen das endgültige Rendering-Ergebnis, aber nicht, was der Compiler dazwischen tut. Wenn im Template eine benutzerdefinierte Direktive steht oder nach dem Aktivieren von hoistStatic plötzlich eine Reihe von _hoisted_1-Variablen im Output auftaucht, kann der Playground nicht beantworten, „warum der Compiler das so generiert“. Der Template Explorer ist genau gegensätzlich positioniert: Er legt die Kompilierungsartefakte von @vue/compiler-dom und @vue/compiler-ssr, den AST, Fehlermarkierungen sowie die Positionszuordnung von Quellcode zu Artefakt vollständig offen. Sein Kern ist nicht „Ausführen“, sondern „Beobachten“. Dieses Kapitel dreht sich um drei Dateien: index.ts übernimmt den Compiler-Aufruf und die bidirektionale SourceMap-Zuordnung, options.ts verwaltet mit reactive Dutzende von CompilerOptions und treibt die UI an, theme.ts passt das Monaco-Editor-Theme an.

一、Compiler-Aufruf und bidirektionale SourceMap-Zuordnung: index.ts

Intuitives Modell

Der Template Explorer istindex.tswie eine „bidirektionale Übersetzungsmaschine“: Links wird das Template eingegeben, rechts wird die Render-Funktion ausgegeben. Aber sie kann mehr als eine Übersetzungsmaschine – wenn du den Cursor auf eine Zeile links setzt, wird rechts das entsprechende Artefakt hervorgehoben; umgekehrt wird, wenn du den Cursor rechts platzierst, links das entsprechende Template hervorgehoben. Ohne SourceMap-Zuordnung würde dieses Tool zu zwei nebeneinanderliegenden Textfeldern degenerieren, und Entwickler könnten nur mit bloßem Auge vergleichen und keine Kausalkette von „Template-Zeile X → Artefakt-Zeile Y“ herstellen.

Datenstruktur und Speicherlayout

index.tsEnthält keine komplexen Structs, aber einige entscheidende Zustandsvariablen auf Modulebene, die das Verhalten des gesamten Tools bestimmen:

lastSuccessfulCodeundlastSuccessfulMapsind der Cache des Kompilierungsergebnisses📎 packages-private/template-explorer/src/index.ts:74-75. Ersteres ist ein String, letzteres istSourceMapConsumer | undefined. Beachte:lastSuccessfulMapist initialundefined, und wird nur zugewiesen, wenn die Kompilierung erfolgreich war undmapexistiert📎 packages-private/template-explorer/src/index.ts:99-100. Dieserundefined-Zustand ist die Guard-Bedingung für die gesamte spätere Cursor-Zuordnungslogik – wenn die Kompilierung fehlschlägt, wird die Zuordnungsfunktion automatisch still deaktiviert, statt eine Exception zu werfen.

PersistedStateDas Interface definiert die Form des in localStorage und URL-Hash persistierten Zustands📎 packages-private/template-explorer/src/index.ts:26-30:src(Template-Quellcode),ssr(ob SSR-Modus),options(Compiler-Optionen). Hier gibt es ein entscheidendes Design:optionshat den Typ des vollständigenCompilerOptions, aber bei der tatsächlichen Persistierung werden nur „von den Standardwerten abweichende Einträge“ gespeichert; diese Trim-Logik wird inreCompileerledigt.

sharedEditorOptionsSind die von beiden Editoren gemeinsam genutzten Konstruktionsoptionen📎 packages-private/template-explorer/src/index.ts:26-30:fontSize: 14、scrollBeyondLastLine: false、renderWhitespace: 'selection'、minimap.enabled: false. Die Minimap ist deaktiviert, weil Template und Artefakt normalerweise nur ein paar Dutzend Zeilen haben und die Minimap eher horizontalen Platz belegt.

Step-by-Step Walkthrough

Szenario: Der Benutzer öffnet die Seite, gibt<div>{{ msg }}</div>ein und bewegt dann den Cursor.

Erster Schritt: Initialisierung und Zustandswiederherstellung. window.initIst der globale Einstiegspunkt📎 packages-private/template-explorer/src/index.ts:41. Er registriert und aktiviert zuerst das benutzerdefinierte Theme📎 packages-private/template-explorer/src/index.ts:44-45, und versucht dann, den Zustand aus dem URL-Hash oder localStorage wiederherzustellen📎 packages-private/template-explorer/src/index.ts:49-56. Beachte hier die Dekodierungsreihenfolge: zuerstatob, dannescape, danndecodeURIComponent. Wenn das Hash-Parsing fehlschlägt, wird auflocalStorage.getItem('state')zurückgefallen, dann auf{}. Wenn das gesamte JSON.parse fehlschlägt, wird localStorage geleert und eine Warnung ausgegeben📎 packages-private/template-explorer/src/index.ts:57-64。

Nach der Zustandswiederherstellung gibt es ein leicht zu übersehendes Detail:delete persistedState.options?.nodeTransforms 📎 packages-private/template-explorer/src/index.ts:69. Der Kommentar erklärt den Grund – Funktionen können nicht serialisiert werden, daher geht beim PersistierennodeTransformsverloren; wenn bei der Wiederherstellung ein leeres Objekt zurückbleibt, führt das zu anormalem Compiler-Verhalten. Dies ist die klassische Falle beim „Persistieren nicht serialisierbarer Felder“.

Zweiter Schritt: KompilierungskerncompileCode。Dies ist das Herz des gesamten Tools📎 packages-private/template-explorer/src/index.ts:76-106. Er macht zuerstconsole.clear(), wählt dann abhängig vonssrMode.valuessrCompileodercompile 📎 packages-private/template-explorer/src/index.ts:80. Beachte die Aufrufparameter voncompileFn: Spread voncompilerOptions, erzwungenfilename: 'ExampleTemplate.vue'、sourceMap: true, und Injektion desonError-Callbacks zum Sammeln von Fehlern📎 packages-private/template-explorer/src/index.ts:82-89。

Hier gibt es eine Designentscheidung:filenameist hartcodiert auf'ExampleTemplate.vue'. Dieser Wert muss in den späterengeneratedPositionFor-Aufrufen exakt übereinstimmen mit📎 packages-private/template-explorer/src/index.ts:189, sonst liefert die SourceMap-Abfrage ein leeres Ergebnis. Dies ist ein impliziter Vertrag – die beiden Strings müssen übereinstimmen, aber kein Typsystem garantiert das.

Nach Abschluss der Kompilierung werden Fehler in das Marker-Format von Monaco umgewandelt und im Editor gesetzt📎 packages-private/template-explorer/src/index.ts:91-95。formatErrorwandeltCompilerErrorvonlocin MonacosstartLineNumber/startColumn/endLineNumber/endColumn 📎 packages-private/template-explorer/src/index.ts:108-119um. Beachteerrors.filter(e => e.loc)– nur Fehler mit Positionsinformationen werden markiert; Fehler ohneloc(wie globale Konfigurationsfehler) werden nur in der Konsole ausgegeben.

Dritter Schritt: Aufbau der SourceMap.Nach erfolgreicher Kompilierung,lastSuccessfulMap = new SourceMapConsumer(map!) 📎 packages-private/template-explorer/src/index.ts:99, und unmittelbar danach wirdcomputeColumnSpans() 📎 packages-private/template-explorer/src/index.ts:100。computeColumnSpansaufgerufen. Ist eine entscheidende API vonsource-map-js: Sie berechnet die Spaltenbreite jedes Mapping-Segments vorab, sodassgeneratedPositionFordaslastColumn-Feld zurückgibt, das verwendbar ist. Ohne diesen Schritt kann die Rückwärtszuordnung nur die Startspalte lokalisieren und nicht den gesamten Token-Bereich hervorheben.

Vierter Schritt: Bidirektionale Cursor-Zuordnung.Wenn der Benutzer imQuellcode-Editorden Cursor bewegt, wirdeditor.onDidChangeCursorPosition 📎 packages-private/template-explorer/src/index.ts:184ausgelöst. Der Callback ruft nach 100 ms DebouncelastSuccessfulMap.generatedPositionFor({ source: 'ExampleTemplate.vue', line, column: column - 1 }) 📎 packages-private/template-explorer/src/index.ts:188-192auf. Beachtecolumn - 1: Monacos Spaltennummern beginnen bei 1, während die Spaltennummern der SourceMap bei 0 beginnen. Das zurückgegebenepos, falls eslineundcolumnhat, erzeugt im Ausgabe-Editor einen Decorator, der den entsprechenden Bereich hervorhebt📎 packages-private/template-explorer/src/index.ts:194-206, und scrollt zu dieser Position📎 packages-private/template-explorer/src/index.ts:207-210。

Die Rückwärtszuordnung erfolgt inoutput.onDidChangeCursorPosition📎 packages-private/template-explorer/src/index.ts:223. Sie ruftoriginalPositionFor 📎 packages-private/template-explorer/src/index.ts:227-230auf, hat aber eine zusätzliche Guard: Ignoriertpos.line === 1 && pos.column === 0„mock location“📎 packages-private/template-explorer/src/index.ts:231-237. Dieser Guard ist entscheidend – bestimmter vom Compiler generierter Code (wieimportAnweisungen oder Helper-Funktionen) hat keine entsprechende Template-Position, und SourceMap gibt{ line: 1, column: 0 }als Platzhalter zurück. Wenn dies nicht ignoriert wird, führt ein Cursor auf diesen Zeilen fälschlicherweise dazu, dass die erste Zeile des Templates hervorgehoben wird.

Fünfter Schritt: Zustandspersistenz. reCompilelöst nicht nur die Kompilierung aus, sondern ist auch dafür verantwortlich, den aktuellen Zustand in localStorage und URL-Hash zu schreiben📎 packages-private/template-explorer/src/index.ts:121-146. Bei der Persistenz gibt es eine Trim-Logik: Durchlaufen voncompilerOptions, wobei nur Einträge gespeichert werden, die „kein Objekt sind und nicht dem Standardwert entsprechen“📎 packages-private/template-explorer/src/index.ts:125-133. Das erklärt, warumbindingMetadataOptionen dieses Objekttyps nicht persistiert werden – es ist zu komplex, und der Standardwert reicht bereits zur Demonstration aus.

mermaid
flowchart TD
    init["window.init()"] --> restore{"hash 或 localStorage 有状态?"}
    restore -->|是| parse["JSON.parse 成功?"]
    restore -->|否| useDefault["使用默认模板"]
    parse -->|成功| delNodeTrans["delete nodeTransforms"]
    parse -->|失败| clearLS["localStorage.clear() + 警告"]
    delNodeTrans --> createEditor["monaco.editor.create(source)"]
    clearLS --> createEditor
    useDefault --> createEditor
    createEditor --> initOpt["initOptions()"]
    initOpt --> watch["watchEffect(reCompile)"]
    watch --> compileCode["compileCode(source)"]
    compileCode --> chooseFn{"ssrMode.value?"}
    chooseFn -->|true| ssr["ssrCompile(source, opts)"]
    chooseFn -->|false| dom["compile(source, opts)"]
    ssr --> hasMap{"map 存在?"}
    dom --> hasMap
    hasMap -->|是| newSMC["new SourceMapConsumer(map)"]
    hasMap -->|否| skipMap["lastSuccessfulMap 保持 undefined"]
    newSMC --> computeSpan["computeColumnSpans()"]
    computeSpan --> setOutput["output.setValue(code)"]
    skipMap --> setOutput
    compileCode -->|抛异常| catchErr["lastSuccessfulCode = ERROR 注释"]
    catchErr --> setOutput

Designüberlegungen und Stolperfallen im Produktivbetrieb

Warumsource-map-jsstattsource-map? source-mapist die Originalbibliothek von Mozilla, groß und abhängig von WASM (in der neuen Version).source-map-jsist eine reine JS-Implementierung, klein und für Browser-Umgebungen geeignet. Da Template Explorer ein reines Frontend-Tool ist, ist die Wahl vonsource-map-jssinnvoll📎 packages-private/template-explorer/package.json:15。

Wahl der debounce-Verzögerung.Der Standard-debounce des Quellcode-Editors beträgt 300 ms📎 packages-private/template-explorer/src/index.ts:271, während der debounce für Cursorbewegungen 100 ms beträgt📎 packages-private/template-explorer/src/index.ts:215. Dieser Unterschied ist beabsichtigt: Kompilierung ist eine schwere Operation, 300 ms vermeiden häufige Auslösungen; Cursorbewegung ist eine leichte Operation, 100 ms gewährleisten Reaktionsfähigkeit. Aber 100 ms können bei schneller Cursorbewegung immer noch zu Flackern der Hervorhebung führen – ein akzeptabler Kompromiss.

window.initGlobales Mounting vonBeachten Sie, dasswindow.initundwindow.monacobeide global an📎 packages-private/template-explorer/src/index.ts:19-23gehängt werden. Der Grund ist, dass der Monaco-Editor über das CDNloader.jsasynchron geladen wird und nach Abschluss des Ladenswindow.initaufruft. Dieses „globale Callback“-Muster ist die Standardverwendung von Monaco in einer nicht-modularen Umgebung, passt aber schlecht zu modernen ESM-Build-Ansätzen.

---

Zwei: reactive-gesteuertes Optionspanel: options.ts

Intuitives Modell

options.tsEs ist wie ein „Konsolenpanel“: Oben gibt es ein Dutzend Schalter und Radiobuttons, von denen jeder einem Verhalten des Compilers entspricht. Wird ein beliebiger Schalter umgelegt, ändert sich das Kompilierungsergebnis rechts sofort. Ohne dieses Modul könnten Entwickler nur diecompileAufrufparameter im Quellcode ändern und neu kompilieren, ohne die Wirkung verschiedener Optionen in Echtzeit vergleichen zu können.

Datenstruktur und Speicherlayout

options.tsDer Kern von

ssrModesind drei Exporte:ref(false) 📎 packages-private/template-explorer/src/options.ts:5ist eincompilerOptions. Es ist unabhängig voncompile vs ssrCompile, weil der SSR-Modus die Kompilierungsfunktion selbst umschaltet (

defaultOptions), nicht die Kompilierungsoptionen.CompilerOptionsist ein vollständiges📎 packages-private/template-explorer/src/options.ts:5-27Objektmode: 'module'、prefixIdentifiers: false、hoistStatic: false、cacheHandlers: false、scopeId: null、inline: false、ssrCssVars: '{ color }'、compatConfig: { MODE: 3 }、whitespace: 'condense'. Es definiert die Standardwerte aller Optionen, einschließlichbindingMetadata 📎 packages-private/template-explorer/src/options.ts:18-26。

compilerOptions, sowie einesreactive(Object.assign({}, defaultOptions)) 📎 packages-private/template-explorer/src/options.ts:29-31mit 7 Bindungstypen.Object.assign({}, ...)istreactive(defaultOptions). Beachten Sie, dass hiercompilerOptionsfür eine flache Kopie verwendet wird – bei direktemdefaultOptionswürde eine Änderung vonreCompileden Wert von

Step-by-Step Walkthrough

verunreinigen und die Logik „Vergleich mit Standardwert“ in

ungültig machen. AppSzenario: Der Benutzer klickt auf das Kontrollkästchen „hoistStatic“.setupErster Schritt: UI-Rendering.📎 packages-private/template-explorer/src/options.ts:33-35DiessrMode.value、compilerOptions.mode、compilerOptions.prefixIdentifiers-Methode der📎 packages-private/template-explorer/src/options.ts:36-39-Komponente gibt eine Renderfunktion

zurück. Diese Renderfunktion liest reaktive Zustände wie hoistStatic, daher wird die gesamte UI neu gerendert, wenn sich diese Zustände ändern.checkedZweiter Schritt: checked-Bindung des Kontrollkästchens.compilerOptions.hoistStatic && !isSSR 📎 packages-private/template-explorer/src/options.ts:150DashoistStatic-Attribut des Kontrollkästchens istdisabled: isSSR 📎 packages-private/template-explorer/src/options.ts:151. Hier gibt es eine Logik: Im SSR-Modus wird

zwangsweise als nicht ausgewählt angezeigt, da die SSR-Kompilierung statisches Hoisting nicht unterstützt. Gleichzeitig stelltsicher, dass der Benutzer es im SSR-Modus nicht umschalten kann.onChangeDritter Schritt: onChange-Behandlung.📎 packages-private/template-explorer/src/options.ts:152-156Wenn der Benutzer auf das Kontrollkästchen klickt, löste.target.checkedcompilerOptions.hoistStaticaus und weistcompilerOptionsdirektreactivezu. DawatchEffect(reCompile) 📎 packages-private/template-explorer/src/index.ts:266

ist, löst diese Zuweisung Dependency-Tracking aus, was wiederumauslöst und schließlich neu kompiliert.cacheHandlersVierter Schritt: Verknüpfung zwischen Optionen.checkedBeachten Sie, dassusePrefix && compilerOptions.cacheHandlers && !isSSR 📎 packages-private/template-explorer/src/options.ts:166,disabledvon!usePrefix || isSSR 📎 packages-private/template-explorer/src/options.ts:167cacheHandlersistprefixIdentifiersistmode === 'module'. Das bedeutet, dassprefixIdentifiersvonfunctionodercacheHandlersabhängt. Diese Verknüpfung zeigt sich in der UI so: Wenn

scopeIdnicht aktiviert ist und der Modusdisabled: !isModule 📎 packages-private/template-explorer/src/options.ts:182,checked: isModule && compilerOptions.scopeId 📎 packages-private/template-explorer/src/options.ts:183ist, ist das KontrollkästchenisModuledeaktiviert.null 📎 packages-private/template-explorer/src/options.ts:184-189。

Die Verknüpfung von initOptionsist komplexer:createApp(App).mount(document.getElementById('header')!) 📎 packages-private/template-explorer/src/options.ts:232-234. Nur im module-Modus kann scopeId gesetzt werden, und bei onChange wird, wennvuefalse ist, zwangsweisecreateAppgesetzt. Fünfter Schritt: Mounting.@vue/runtime-domruftoptions.tsauf. Beachten Sie, dass hiervueaus dem

mermaid
flowchart LR
    subgraph reactive_state["reactive 状态层"]
        ssrMode["ssrMode: Ref<boolean>"]
        compilerOptions["compilerOptions: reactive(CompilerOptions)"]
    end
    subgraph ui_layer["UI 渲染层 (options.ts)"]
        modeRadio["mode 单选"]
        wsRadio["whitespace 单选"]
        ssrCheck["SSR 复选框"]
        prefixCheck["prefixIdentifiers 复选框"]
        hoistCheck["hoistStatic 复选框"]
        cacheCheck["cacheHandlers 复选框"]
        scopeCheck["scopeId 复选框"]
        inlineCheck["inline 复选框"]
        compatCheck["compatConfig 复选框"]
    end
    subgraph compile_layer["编译层 (index.ts)"]
        watchEffect["watchEffect(reCompile)"]
        compileCode["compileCode()"]
    end
    ssrMode -->|"checked/disabled"| ssrCheck
    ssrMode -->|"isSSR 守卫"| hoistCheck
    ssrMode -->|"isSSR 守卫"| cacheCheck
    compilerOptions -->|"mode"| modeRadio
    compilerOptions -->|"whitespace"| wsRadio
    compilerOptions -->|"prefixIdentifiers"| prefixCheck
    compilerOptions -->|"hoistStatic"| hoistCheck
    compilerOptions -->|"cacheHandlers"| cacheCheck
    compilerOptions -->|"scopeId"| scopeCheck
    compilerOptions -->|"inline"| inlineCheck
    compilerOptions -->|"compatConfig.MODE"| compatCheck
    modeRadio -->|"onChange 赋值"| compilerOptions
    wsRadio -->|"onChange 赋值"| compilerOptions
    ssrCheck -->|"onChange 赋值"| ssrMode
    prefixCheck -->|"onChange 赋值"| compilerOptions
    hoistCheck -->|"onChange 赋值"| compilerOptions
    cacheCheck -->|"onChange 赋值"| compilerOptions
    scopeCheck -->|"onChange 赋值"| compilerOptions
    inlineCheck -->|"onChange 赋值"| compilerOptions
    compatCheck -->|"onChange 赋值"| compilerOptions
    compilerOptions -->|"依赖追踪"| watchEffect
    ssrMode -->|"依赖追踪"| watchEffect
    watchEffect --> compileCode

– weil

Anwendungscode ist und direkt vom vollständigenreactive-Paket abhängen kann.ref? compilerOptionsKopierenreactiveDesignüberlegungen und Stolperfallen im ProduktivbetriebcompilerOptions.hoistStatic = trueWarumcompilerOptions.value.hoistStatic = truestattreactiveverwendet wird:compilerOptions.xxxist ein Objekt mit einem Dutzend Feldern; mit

bindingMetadatakann direktverwendet werden, ohne📎 packages-private/template-explorer/src/options.ts:18-26. Das ist im UI-Code prägnanter. Aber der Preis vonSETUP_CONST、SETUP_REF、SETUP_LET、SETUP_MAYBE_REF、PROPSist, dass Destrukturierung die Reaktivität verliert – im Quellcode gibt es keine Destrukturierung, alles wird überprefixIdentifierszugegriffen, was die korrekte Verwendung ist.$setupDesign der Standardwerte vonprefixIdentifiers. Die Standardwerte von

compatConfigenthalten 7 Bindungen compilerOptions.compatConfig!.MODE = 2 📎 packages-private/template-explorer/src/options.ts:216-220und decken die fünf Typenreactiveab. Dies soll Entwicklern ermöglichen, nach dem Öffnen vonreactivesofort die Auswirkungen verschiedener Bindungstypen auf diecompatConfigZugriffsmethoden im Ergebnis zu sehen. Ohne diesen Standardwert wäre der Effekt vonCompatConfig | undefinedsehr monoton.!Verschachtelte Reaktivität voncompatConfig. Eine solche verschachtelte Zuweisung ist unter

ssrModereaktiv, weilcompilerOptionsverschachtelte Objekte rekursiv proxyt. Beachten Sie jedoch, dass der Typ von ssrModeref,compilerOptionsist, daher wird einereactiveAssertion verwendet. Wennssrnicht in den Standardwerten enthalten wäre, würde dies hier zur Laufzeit abstürzen.compilerOptionsTrennung der Zuständigkeiten vonssrundCompilerOptions.

---

ist

ist

theme.tsWie ein „Skin-Wechsel" für den Editor: Es definiert Farbe und Schriftstil für jeden Syntax-Token. Ohne dieses Modul verwendet Monaco das Standard-vs-darkTheme. Es funktioniert zwar, aber HTML-Tags, Ausdrücke und Direktiven in Vue-Templates sind visuell nicht unterscheidbar, sodass Entwickler wichtige Teile nicht schnell finden können.

Datenstruktur und Speicherlayout

theme.tsExportiert ein Objekt, das der Monaco-IStandaloneThemeDataSchnittstelle entspricht📎 packages-private/template-explorer/src/theme.ts:1-244. Es hat drei Top-Level-Felder:

base: 'vs-dark'Gibt das Basistheme an📎 packages-private/template-explorer/src/theme.ts:2,inherit: trueStellt Regeln dar, die vom Basistheme erben📎 packages-private/template-explorer/src/theme.ts:3. Das bedeutet, dass nur die Unterschiede definiert werden müssen; nicht definierte Tokens fallen zurück aufvs-dark。

rulesIst ein Array, jedes Element enthälttoken(Monacos Token-Name) undforeground/background/fontStyle 📎 packages-private/template-explorer/src/theme.ts:4-235. Dieses Array hat über 50 Einträge und deckt Token-Typen wie number, comment, keyword, string, variable, entity.name.tag usw. ab.

colorsDefiniert die Farben der Editor-UI📎 packages-private/template-explorer/src/theme.ts:236-243:editor.foreground、editor.background、editor.selectionBackground、editor.lineHighlightBackground、editorCursor.foreground、editorWhitespace.foreground。

Step-by-Step Walkthrough

Szenario: Theme beim Laden der Seite registrieren.

Schritt 1: Theme definieren. monaco.editor.defineTheme('my-theme', theme) 📎 packages-private/template-explorer/src/index.ts:44. Dieser Aufruf registrierttheme.tsdas exportierte Objekt im Theme-Register von Monaco unter dem Schlüsselnamen'my-theme'。

Schritt 2: Theme aktivieren. monaco.editor.setTheme('my-theme') 📎 packages-private/template-explorer/src/index.ts:45. Diese Codezeile muss nachdefineThemeaufgerufen werden, sonst wird der Fehler „Theme nicht definiert" ausgelöst.

Schritt 3: Token-Matching.Wenn Monaco Template-Code rendert, tokenisiert es den Code mit dem HTML Language Service und sucht dann anhand des Token-Namens nach Regeln inrules. Zum Beispiel<div>indivwird markiert alsentity.name.tag, passt zuforeground: 'cc6666' 📎 packages-private/template-explorer/src/theme.ts:41-44, wird rot angezeigt.

Designüberlegungen und Stolperfallen im Produktivbetrieb

Waruminherit: true?verwenden? Ohne Vererbung müssten die Farben aller Tokens definiert werden, einschließlich derer, die im Template nicht vorkommen (wiemarkup.heading、meta.diff). Vererbung ermöglicht es, in der Theme-Datei nur die Tokens zu berücksichtigen, die tatsächlich im Template und im JS-Ergebnis vorkommen.

Hierarchisches Matching von Token-Namen.Monacos Token-Matching erfolgt per Präfix-Matching:entity.name.tagpasst zuentity.name.tag.html、entity.name.tag.cssusw. Im Quellcode werden sowohlentity.name.tag 📎 packages-private/template-explorer/src/theme.ts:41-44als auchentity.name.tag.css 📎 packages-private/template-explorer/src/theme.ts:169-172definiert; Letzteres überschreibt Ersteres im CSS-spezifischen Szenario.

colorsArbeitsteilung zwischenrulesund rules.colorssteuert die Farbe des Code-Textes,editor.background: '#1D1F21'steuert die Farben der Editor-UI (Hintergrund, Cursor, ausgewählte Zeile). Beide sind unabhängig, müssen aber visuell aufeinander abgestimmt sein. Im Quellcode sindbase: 'vs-dark'und

---

die Standardhintergründe ähnlich, um visuelle Konsistenz zu wahren.

Designüberlegung: Engineering-Abwägungen bei visuellen Sonden

Der Kernunterschied zwischen Template Explorer und SFC Playground liegt in der „Beobachtungsgranularität". Der Playground beobachtet, „ob ein gesamtes SFC nach der Kompilierung ausgeführt werden kann"; der Template Explorer beobachtet, „zu was ein einzelner Template-Ausdruck kompiliert wird". Dieser Unterschied bestimmt die technische Auswahl der beiden Tools:Die Einführung von SourceMapConsumer ist unvermeidlich.source-map-jsOhne es könnten Entwickler Quellcode und Ergebnis nur mit bloßem Auge vergleichen und keine präzise Zuordnung „Zeile X → Zeile Y" herstellen. Die API von SourceMapConsumer ist jedoch asynchron (neuere Versionen geben ein Promise zurück); im Quellcode wird die synchrone Version verwendet

reactive, um die Aufruflogik zu vereinfachen.Verwaltungsoptionen sind die natürliche Wahl im Vue-Ökosystem.reactiveWenn der Status von über einem Dutzend Optionen mit nativen DOM-Events manuell synchronisiert würde, würde sich die Codemenge verdoppeln.watchEffect(reCompile)Die Abhängigkeitsverfolgung von

automatisiert die Kette „Optionsänderung → Neukompilierung"; window.monacoeine Codezeile erledigt das Abonnieren.window.initMonacos globales Lademodell ist historischer Ballast.

---

Die globale Einbindung von

undindex.tsstammt aus dem AMD-Loader-Design von Monaco. In modernen ESM-Builds wirkt das fehl am Platz, aber Monacos Größe (ca. 5 MB) macht bedarfsgerechtes Laden weiterhin erforderlich.compileCodeZusammenfassung dieses Kapitels@vue/compiler-domTemplate Explorer ist eine „White-Box-Sonde": Er führt das Kompilierungsergebnis nicht aus, sondern zeigt nur den Kompilierungsprozess.@vue/compiler-ssrDurchSourceMapConsumerwirdoptions.tsoderreactiveaufgerufen, mitCompilerOptionseine bidirektionale Zuordnung zwischen Quellcode und Ergebnis hergestellt und über Monacos Decorator-API eine synchronisierte Cursor-Hervorhebung umgesetzt.watchEffectMithoistStaticwirdtheme.tsverwaltet, über

die Neukompilierung angesteuert, und die Verknüpfungen zwischen Optionen (z. B. SSR deaktivierthoistStatic) werden explizit in der UI-Schicht codiert.

Ein benutzerdefiniertes Monaco-Theme sorgt für eine klare visuelle Unterscheidung der Syntax-Tokens von Template und Ergebnis.

Der Kernwert dieses Tools liegt darin, „mit Werkzeugen das Compiler-Verhalten zurückzuverfolgen": Wenn Sie unsicher sind, wasindex.tsmit einem bestimmten Template macht, öffnen Sie den Template Explorer, wechseln Sie Optionen und beobachten Sie Änderungen im Ergebnis. Das ist intuitiver als das Lesen des Compiler-Quellcodes und zuverlässiger als Raten.originalPositionForDenkanstöße und Selbsttest zu diesem Kapitelpos.line === 1 && pos.column === 0Q1: Wenn in{ line: 1, column: 0 }der Mock-Location-Guard (

) vonentfernt würde, in welchem Szenario würde dies zu fehlerhafter Hervorhebung führen? Warum generiert der Compiler eine Zuordnung wie📎 packages-private/template-explorer/src/index.ts:231-237?import { createElementVNode as _createElementVNode } from 'vue'Referenzanalyseexport function render(_ctx, _cache) { ... }: Der Guard befindet sich insource-map-js. Der Compiler fügt bei der Generierung des Ergebnisses Code ein, der keine entsprechende Position im Template hat, z. B. Helper-Import-Anweisungen wie{ line: 1, column: 0 }oder Funktionssignaturen wieoriginalPositionFor. Diese Codeabschnitte haben keine Originalposition in der SourceMap;{ line: 1, column: 0 }, der Code dies als gültige Position betrachtet und dann in der ersten Zeile und ersten Spalte des Quellcode-Editors einen Hervorhebungsdekorator erstellt. Das Ergebnis ist: Der Benutzer klickt auf dieimportZeile des Artefakts, und die erste Zeile des Quellcode-Editors wird fälschlicherweise hervorgehoben, was irreführend ist. Der Kern dieser Wache ist „zwischen echter Zuordnung und Platzhalter-Zuordnung zu unterscheiden“, und{ line: 1, column: 0 }ist dersource-map-jsvereinbarte Sentinel-Wert für „keine Zuordnung“.

Q2: reCompilePersistenzoptionen, wird die Bedingungtypeof val !== 'object' && val !== defaultOptions[key]alle Optionen vom Objekttyp überspringen. WennbindingMetadatavom Benutzer geändert wird (z. B. über die Konsole), geht diese Änderung nach dem Aktualisieren der Seite verloren. Ist das ein Bug oder beabsichtigtes Design? WennbindingMetadatain der Persistenz unterstützt werden soll, welche Probleme müssen gelöst werden?

Referenzanalyse: Die Bedingung befindet sich in📎 packages-private/template-explorer/src/index.ts:129. Dies ist beabsichtigtes Design, aus drei Gründen: Erstens,bindingMetadataDer Wert von istBindingTypesEnum, nach der Serialisierung eine Zahl, und bei der Deserialisierung kann nicht unterschieden werden zwischen „vom Benutzer explizit auf 0 gesetzt“ und „Standardwert“; zweitens,compatConfigist ein verschachteltes Objekt,val !== defaultOptions[key]vergleicht Referenzen, ist immer true, was dazu führt, dass alle Objektoptionen persistiert werden; drittens,nodeTransformsenthält Funktionen, kann nicht serialisiert werden, im Quellcode wird bereits durchdelete persistedState.options?.nodeTransformsbehandelt📎 packages-private/template-explorer/src/index.ts:69. WennbindingMetadataunterstützt werden soll, muss ein tiefer Vergleich implementiert werden (statt Referenzvergleich), und die Serialisierung/Deserialisierung von Enum-Werten muss behandelt werden. Das grundlegendere Problem ist:bindingMetadatahat keinen Bearbeitungseingang in der UI, der Benutzer kann nur über die Konsole ändern, und diese Änderung selbst sollte nicht persistiert werden.

Q3: options.tsincompilerOptionswird mitreactive(Object.assign({}, defaultOptions))erstellt. WennObject.assign({}, defaultOptions)direkt inreactive(defaultOptions)geändert wird, was passiert, wenn der Benutzer die Option umschaltet und dann die Seite aktualisiert? Warum?

Referenzanalyse:Object.assign({}, defaultOptions)ist eine flache Kopie, befindet sich in📎 packages-private/template-explorer/src/options.ts:29-31. Wenn es inreactive(defaultOptions),compilerOptionsgeändert wird, werdendefaultOptionsundhoistStaticauf dasselbe Objekt zeigen. Wenn der BenutzercompilerOptions.hoistStaticauf true umschaltet,defaultOptions.hoistStaticwird true, und gleichzeitig wirdreCompileebenfalls true. Dann vergleicht die Persistenzlogik📎 packages-private/template-explorer/src/index.ts:129inval !== defaultOptions[key], zu diesem Zeitpunkt sindvalunddefaultOptions[key]beide true, die Bedingung ist false, diese Option wird nicht in localStorage gespeichert. Nach dem Aktualisieren der Seite wirddefaultOptionsneu initialisiert alshoistStatic: false, die Änderung des Benutzers geht verloren. Noch schwerwiegender ist, dass nach der Kontamination vondefaultOptionsalle nachfolgenden Logiken vom Typ „mit Standardwert vergleichen“ ungültig werden, was dazu führt, dass die Persistenzfunktion vollständig zusammenbricht. Die Verborgenheit dieses Bugs liegt darin: Innerhalb einer einzelnen Sitzung ist alles normal, erst nach dem Aktualisieren kann er entdeckt werden.

---

Das nächste Kapitel führt inscripts/release.jsein und zeigt, wie Vue mit einem interaktiven Zustandsautomaten den gesamten Ablauf von Versionsnummernaktualisierung, Build, Test, Git-Commit, Tagging und npm publish orchestriert. Anders als das „Beobachten“ des Template Explorers ist release.js „Ausführen“ – es muss den Zustand über mehrere Schritte hinweg pflegen, Fehler-Rollbacks behandeln und ein Gleichgewicht zwischen interaktiver Bestätigung und Automatisierung finden.

Durch den Template Explorer haben wir gelernt, wie der interne Compiler-Zustand – AST, Kompilierungsartefakte, SourceMap – in interaktive visuelle Sonden umgewandelt werden kann, wodurch „warum der Compiler dies so generiert“ von Vermutung zu Beobachtung wird. Diese präzise Kontrolle und Orchestrierung des internen Zustands zeigt sich ebenso im Vue-Release-Prozess: Das nächste Kapitel taucht tief in scripts/release.js ein und zeigt, wie ein über 500 Zeilen langer Zustandsautomat mit parseArgs über ein Dutzend Flags parst, über enquirer interaktiv die Versionsnummer bestätigt und der Reihe nach Build, Test, Git-Commit, Tagging und npm publish auslöst, und enthüllt den vollständigen Zustandsfluss und die Fehler-Rollback-Strategie hinter einem offiziellen Release.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 09

Kapitel 9: Release-Automatisierung: Zustandsautomat und interaktive Orchestrierung von release.js

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 9 von 14

Im vorherigen Kapitel haben wir mithilfe des template-explorers das Compiler-Verhalten zurückverfolgt und die Methodik erlernt, interne Mechanismen mit Werkzeugen zu beobachten. Jetzt richten wir unseren Blick von der Kompilierungszeit auf die Release-Zeit – dies ist der gefährlichste Moment jedes Open-Source-Projekts: Er berührt gleichzeitig vier irreversible externe Systeme: Versionsnummer, Build-Artefakte, Git-Historie und npm registry. Ein fehlerhaftes npm publish kann nicht zurückgenommen werden, ein fehlerhaft gepushter Tag verunreinigt die Abhängigkeitsauflösung aller nachgelagerten Benutzer. Vue core verwendet ein 537 Zeilen langes scripts/release.js, um diese Gefahr zu bändigen – es ist weder ein reines Automatisierungsskript noch eine reine manuelle Checkliste, sondern ein interaktiver Zustandsautomat: An kritischen Knoten wird angehalten und gefragt, an vorhersehbaren Knoten vollautomatisch ausgeführt, und bei einem Fehler in irgendeinem Schritt wird die Versionsnummer auf den Ausgangspunkt zurückgerollt. Dieses Kapitel zerlegt die drei Kernmechanismen dieses Orchestrators: Argumentanalyse und Zustandsinitialisierung, interaktive Versionsentscheidung und CI-Gate, sowie Release-Reihenfolge und Fehler-Rollback.

Argumentanalyse und globale Zustandsinitialisierung

Intuitives Modell

Stellen Sie sichrelease.jsals Bedienfeld einer alten Waschmaschine vor: Der Drehknopf (parseArgsSpeicherlayout von Flags und globalem Zustand

标志位与全局状态的内存布局

〔Design-Inferenz und Architektur-Abwägungen〕

Das Erste, was das Skript nach dem Start tut, ist, die Kommandozeilenargumente in ein strukturiertes Objekt zu parsen. Hier wird das in Node integrierteparseArgsverwendet, nichtyargsodercommander– dies dient dazu, Drittanbieter-Abhängigkeiten zu eliminieren, da das Release-Skript selbst in jeder Umgebung lauffähig sein muss, selbst wennnode_modulesnur halb installiert ist.

📎 scripts/release.js:27-62definiert 10 Optionen, die in vier Kategorien unterteilt werden können:

  • Versionssemantik-Kategorie:preid(Pre-Release-Identifikator, wiealpha/beta/rc)、tag(npm dist-tag)
  • Überspringen-Kategorie:skipBuild、skipTests、skipGit、skipPrompts– diese vier booleschen Schalter bilden den „Automatisierungsgrad“-Regler
  • Ausführungsmodus-Kategorie:dry(Leerlauf),publish(ob lokal direkt veröffentlicht wird),publishOnly(nur veröffentlichen, Version nicht aktualisieren)
  • Ziel-Kategorie:registry(benutzerdefinierte Registry-Adresse)

Beachten Sie, dass der Standardwert vonpublishfalse 📎 scripts/release.js:51-54ist, während andere boolesche Elemente keinen Standardwert haben (d. h.undefined). Diese Asymmetrie ist beabsichtigt:publishDie Semantik vonskipXxxist „ob npm publish lokal ausgeführt wird“, standardmäßig nicht veröffentlichen, die Veröffentlichungsaktion wird an GitHub Actions übergeben; währendundefinedstandardmäßig--skipTestsbedeutet „nicht angegeben“, die nachfolgende Logik wird unterscheiden zwischen „Benutzer hat explizit

übergeben“ und „Benutzer hat nicht übergeben“.📎 scripts/release.js:64-66:

js
const preId = args.preid || semver.prerelease(currentVersion)?.[0]
const isDryRun = args.dry
let skipTests = args.skipTests
const skipBuild = args.skipBuild
const skipPrompts = args.skipPrompts
const skipGit = args.skipGit

KopierenpreIdHier gibt es zwei bemerkenswerte Designentscheidungen. Erstens, die Wertepriorität von📎 scripts/release.js:64-66ist „explizite Angabe auf der Kommandozeile > Ableitung aus der aktuellen Versionsnummer“package.json. Wenn die aktuelle Version von3.5.0-beta.1semver.prereleaseist, dann gibt['beta', 1][0]zurück, nimmt'beta'und erhält--preid beta. Das bedeutet, dass beim kontinuierlichen Veröffentlichen auf dem Beta-Branch nicht jedes MalskipTestseingegeben werden muss. Zweitens,letwird mitconst 📎 scripts/release.js:64-66deklariert, während andererunTestsIfNeededverwenden, weil es in

dynamisch durch CI-Ergebnisse überschrieben wird – dies ist ein „verzögerte Entscheidung“-Statusbit.📎 scripts/release.js:68-83Als Nächstes folgt die Paket-Erkennungslogikpackages/: Lesen despackage.jsonVerzeichnisses, Herausfiltern von Nicht-Verzeichnis-Einträgen, Einträgen ohneprivate: trueund Paketen mitpackages/. Beachten Sie, dass hierpackages-private/gelesen wird, nicht

– letzteres ist ein internes Debug-Paket, das niemals veröffentlicht wird.

📎 scripts/release.js:85-85Der Sortieralgorithmus für die Veröffentlichungsreihenfolge

js
const sortPackagesForPublishing = (packageNames) => [
  ...packageNames.filter(p => p !== 'vue'),
  ...packageNames.filter(p => p === 'vue'),
]

KopierenvueEs platziert das Einstiegspaket📎 scripts/release.js:85-85an das Ende. Der Kommentarvueerklärt den Grund: Wenn zuerst@vue/runtime-coreveröffentlicht wird, können Benutzer eine neue Version vonvueinstallieren, bevor interne Pakete wie

online sind, und npm wird einen Fehler melden, weil keine passende interne Abhängigkeit gefunden wird. Dies ist ein Kompromiss für „Veröffentlichungsatomizität“ im npm-Ökosystem – npm hat keine paketübergreifenden Transaktionen und kann Atomizität nur durch Reihenfolge annähern.

📎 scripts/release.js:111-116Dynamische Konstruktion der Versionsinkrement-Kandidatenmenge

js
const versionIncrements = [
  'patch', 'minor', 'major',
  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : []),
]

KopierenpreIdDies ist eine bedingte Erweiterung: Nur wenn--preidexistiert (d. h. derzeit im Pre-Release-Kanal oder der Benutzer hat explizit3.5.43angegeben), werden die Pre-Release-bezogenen Inkrementtypen zum Menü hinzugefügt. Wenn derzeit eine stabile Versionpreidund keinpatch/minor/majorangegeben ist, hat das Menü nur die drei Einträge3.5.44-0– um zu vermeiden, dass der Benutzer durch Fehlbedienung die stabile Version in eine halbherzige Pre-Release-Version wie

incverwandelt.📎 scripts/release.js:120-120Die Funktionsemver.inckapseltpreIdund übergibttypeof preId === 'string' ? preId : undefinedals dritten Parameter. Hier gibt es eine Typverteidigung:preId– weilstring | undefinedmöglicherweisesemver.incist, währendstring | undefined

erwartet, dient dieser Ternärausdruck der Erfüllung der TS-Typverengung.

📎 scripts/release.js:122-123Ausführungsprimitive: Das Dual-Track-System von run und dryRun

js
const run = async (bin, args, opts = {}) =>
  exec(bin, args, { stdio: 'inherit', ...opts })
const dryRun = async (bin, args, opts = {}) =>
  console.log(pico.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
const runIfNotDry = isDryRun ? dryRun : run

runKopiereninheritsetzt das stdio des Unterprozesses aufdryRun, sodass die Ausgabe von Build/Tests direkt an das Terminal weitergeleitet wird – dies ist entscheidend für lang laufende Builds, der Benutzer kann den Fortschritt in Echtzeit sehen.runIfNotDrygibt nur den Befehl aus, ohne ihn auszuführen.dryRunist eine „Strategieauswahl“: Beim Laden des Moduls wird der Funktionszeiger anrunoderisDryRun。

gebunden, alle nachfolgenden Aufrufpunkte müssen nicht mehr

prüfen 〔Design-Inferenz und Architektur-Abwägungen〕isDryRunDieses Muster „Strategie bei der Initialisierung entscheiden“ ist weniger fehleranfällig als „an jedem Aufrufpunkt entscheiden“: Wenn ein Aufrufpunkt vergisst,runIfNotDryzu prüfen, werden im Dry-Run-Modus tatsächlich Seiteneffekte ausgeführt. Während

mermaid
flowchart TD
    start["node scripts/release.js"] --> parse["parseArgs 解析 10 个选项"]
    parse --> preid{"args.preid 存在?"}
    preid -->|是| use_arg["preId = args.preid"]
    preid -->|否| infer["preId = semver.prerelease(currentVersion)[0]"]
    use_arg --> scan["扫描 packages/ 目录"]
    infer --> scan
    scan --> filter{"是目录 且 有 package.json 且 非 private?"}
    filter -->|否| skip_pkg["排除该包"]
    filter -->|是| keep_pkg["加入 packages 列表"]
    skip_pkg --> build_menu
    keep_pkg --> build_menu
    build_menu{"preId 存在?"} -->|是| full["versionIncrements = patch/minor/major + 4 个 pre*"]
    build_menu -->|否| stable["versionIncrements = patch/minor/major"]
    full --> dispatch{"args.publishOnly?"}
    stable --> dispatch
    dispatch -->|是| publish_only["fnToRun = publishOnly"]
    dispatch -->|否| main_fn["fnToRun = main"]

---

Kopieren

Interaktive Versionsentscheidung und CI-Gate

Intuitives Modell

Diese Phase ist wie eine Flughafensicherheitskontrolle: Zuerst wird Ihr Boarding-Pass überprüft (ob der lokale Commit mit dem Remote synchronisiert ist), dann wird bestätigt, wohin Sie wollen (Versionsnummer), und schließlich wird geprüft, ob Sie die Sicherheitskontrolle bestanden haben (ob CI bestanden wurde). Wenn eine dieser Prüfungen fehlschlägt, wird der gesamte Prozess abgebrochen. Ohne dieses Gate könnte ein nicht gepushter lokaler Commit getaggt und veröffentlicht werden, sodass der Quellcode, der der Version auf npm entspricht, auf GitHub überhaupt nicht existiert – dies ist der am schwersten zu diagnostizierende Veröffentlichungsunfall.

mainSynchronisationsprüfung und VersionsauswahlisInSyncWithRemote() 📎 scripts/release.js:141-141Das Erste, was die Funktion📎 scripts/release.js:337-363tut, istgit rev-parse HEAD. Die Logik dieser Funktion📎 scripts/release.js:348-355ist: den aktuellen Branch-Namen abrufen, die GitHub-API anfordern, um den neuesten Commit-SHA dieses Branches zu erhalten, und mit dem lokalenfalsevergleichen. Wenn sie nicht übereinstimmen, wird ein Bestätigungsdialog📎 scripts/release.js:365-367。

mit roter Warnung angezeigt, damit der Benutzer entscheiden kann, ob fortgefahren werden soll. Wenn die API-Anfrage fehlschlägt (Netzwerkproblem, kein Token), wird direkt

zurückgegeben und

beendet 〔Design-Inferenz und Architektur-Abwägungen〕node scripts/release.js 3.6.0),targetVersionDie Designphilosophie hier ist „Fehler bedeutet Abbruch“: Bei Netzwerkausnahmen wird lieber nicht veröffentlicht, als das Risiko einzugehen, mit unbekanntem Status fortzufahren. Denn die Veröffentlichung ist irreversibel, während die Kosten für ein erneutes Ausführen des Skripts sehr gering sind.📎 scripts/release.js:141-141Die Bestimmung der Versionsnummer erfolgt über zwei Pfade. Wenn der Benutzer ein Positionsargument auf der Kommandozeile übergeben hat (wie📎 scripts/release.js:152-176wird direkt dieser Wert genommencustom. Andernfalls wird das interaktive Menü

aufgerufen: Zuerst wählt der Benutzer den Inkrementtyp, bei Auswahl von📎 scripts/release.js:174wird ein weiteres Eingabefeld angezeigt, in dem der Benutzer die Versionsnummer manuell eingeben kann.

js
targetVersion = release.match(/\((.*)\)/)?.[1] ?? ''

:patch (3.5.44)KopierencustomDas Format des Menüeintrags ist📎 scripts/release.js:164-172。

, diese Regex extrahiert die tatsächliche Versionsnummer aus den Klammern. Wenn der Benutzer📎 scripts/release.js:178-182gewählt hat, wird ein anderer ZweigtargetVersiondurchlaufen. Danach folgt eine „zweite Parsing“-Logikpatch/minorSolche inkrementellen Schlüsselwörter (der Benutzer könnte direkt übergebennode release.js minor), dann wirdincaufgerufen, um es in eine konkrete Versionsnummer umzuwandeln. Schließlich wird mitsemver.validvalidiert📎 scripts/release.js:184-186, ungültige Versionsnummern werfen direkt einen Fehler.

CI-Gate: Die dreistufige Logik von runTestsIfNeeded

Dies ist der komplexeste Kontrollfluss im gesamten Kapitel.📎 scripts/release.js:281-317DasrunTestsIfNeededist tatsächlich eine dreistufige Entscheidungsmaschine:

Zustand eins: Der Benutzer hat explizit--skipTests。skipTestsübergeben, initial auftruegesetzt, der gesamte Funktionskörper wird direkt übersprungen, "Tests skipped." wird ausgegeben📎 scripts/release.js:314-316。

Zustand zwei: Nicht übersprungen, und CI ist bereits bestanden. Das Skript ruftgetCIResult() 📎 scripts/release.js:319-335auf, es fragt die GitHub Actions API ab und prüft, ob ein Workflow-Run namenscimitconclusion === 'success'existiert📎 scripts/release.js:319-335. Falls bestanden, wird der Benutzer gefragt: „CI ist bestanden, lokale Tests überspringen?"📎 scripts/release.js:288-295. Falls der Benutzer--skipPromptsaktiviert hat, werden lokale Tests automatisch übersprungen📎 scripts/release.js:296-298。

Zustand drei: Nicht übersprungen, und CI ist nicht bestanden. Falls--skipPromptsaktiviert ist, wird direkt ein Fehler geworfen📎 scripts/release.js:299-304:

js
throw new Error(
  'CI for the latest commit has not passed yet. ' +
    'Only run the release workflow after the CI has passed.',
)

Falls nicht aktiviert--skipPrompts, dann bleibtskipTestsaufundefined, fällt in den letzten lokalen Test-Zweig📎 scripts/release.js:307-313, führtpnpm run test --run。

aus. Hier gibt es ein subtiles Detail📎 scripts/release.js:285:

js
skipTests ||= isCIPassed

||=ist eine logische Oder-Zuweisung: Nur wennskipTestsfalsy ist (undefinedoderfalse), wirdisCIPassedzugewiesen. Das bedeutet, wenn der Benutzer explizit--skipTests(trueübergeben hat), ändert diese Zeile nichts daran; wenn der Benutzer nichts übergeben hat (undefined), wird es auf das CI-Ergebnis gesetzt. Aber direkt danach wird📎 scripts/release.js:287-298bei bestandener CI erneut zugewiesen – also ist die tatsächliche Wirkung dieser Zeile||=nur „falls CI nicht bestanden, setzeskipTestsauffalse", wodurch der nachfolgendeif (!skipTests)-Zweig lokale Tests ausführt.

〔Design-Inferenz und Architektur-Abwägung〕

Diese Logik macht einen Umweg, im Wesentlichen soll sie ausdrücken: „CI bestanden → lokale Tests können übersprungen werden (aber den Benutzer fragen); CI nicht bestanden → lokale Tests müssen ausgeführt werden (es sei denn, der Benutzer fordert explizit das Überspringen)". Die Schreibweise mit||=plus nachfolgender Überschreibung ist zwar kompakt, aber wenig lesbar, ein typischer Code-Geruch von „Statusbits werden an mehreren Stellen modifiziert".

mermaid
sequenceDiagram
    participant Dev as 开发者
    participant Main as main()
    participant Git as git CLI
    participant GH as GitHub API
    participant Pnpm as pnpm

    Dev->>Main: node scripts/release.js
    Main->>Git: getBranch() / getSha()
    Git-->>Main: branch, sha
    Main->>GH: fetch commits/{branch}
    GH-->>Main: remote sha
    alt sha 不一致
        Main->>Dev: prompt 确认继续?
        Dev-->>Main: yes/no
    end
    Main->>Dev: prompt 选择版本增量
    Dev-->>Main: "patch (3.5.44)"
    Main->>Main: semver.valid 校验
    Main->>GH: getCIResult() 查询 workflow_runs
    GH-->>Main: workflow_runs[]
    alt CI 通过
        Main->>Dev: prompt 跳过本地测试?
        Dev-->>Main: yes
    else CI 未通过
        Main->>Pnpm: run test --run
        Pnpm-->>Main: exit code
    end
    Main->>Main: updateVersions(targetVersion)

Versionsnummer schreiben: Die Iteration von updateVersions

📎 scripts/release.js:377-384DasupdateVersionsmacht zwei Dinge: die Root-package.jsonaktualisieren, dann alle Unterpakete durchlaufen undupdatePackage。updatePackage 📎 scripts/release.js:391-398aufrufen, um JSON zu lesen, dienameundversionumzuschreiben, mitJSON.stringify(pkg, null, 2) + '\n'zurückzuschreiben – beachten Sie das abschließende\n, dies dient dazu, die Datei mit einem Zeilenumbruch am Ende beizubehalten, um zu vermeiden, dass git diff „No newline at end of file" anzeigt.

getNewPackageNameDer ParameterkeepThePackageName 📎 scripts/release.js:105ist standardmäßig

---

, d.h. der Paketname wird nicht geändert. Dieser Parameter existiert, um das Szenario „Umbenennung des Pakets bei Veröffentlichung in einer benutzerdefinierten Registry" zu unterstützen – obwohl alle aktuellen Aufrufstellen den Standardwert übergeben, bietet die Schnittstelle Erweiterbarkeit.

Veröffentlichungsreihenfolge, Idempotenz und Fehler-Rollback

Intuitives ModellupdateVersionsDiese Phase ist wie Dominosteine:

Der erste Stein wird angestoßen (Versionsnummer ändern), die nachfolgenden changelog, lockfile, commit, tag, publish fallen nacheinander um. Wenn mittendrin ein Stein hängen bleibt, muss es einen Mechanismus geben, um die bereits umgefallenen Steine wieder aufzurichten – sonst bleibt das Repository im halbfertigen Zustand „Versionsnummer geändert, aber nicht veröffentlicht" stecken.

Idempotente Veröffentlichung: isPackagePublished und Fehler-Fallback

publishPackage 📎 scripts/release.js:439-489〔Design-Inferenz und Architektur-Abwägung〕📎 scripts/release.js:442-451Das--tagist der Kern der Veröffentlichung. Es bestimmt zuerst den dist-tagalpha/beta/rc: bevorzugt wird derversion.includes('alpha')-Parameter verwendet, andernfalls wird aus demsemver.prerelease-Schlüsselwort in der Versionsnummer abgeleitet. Beachten Sie, dass hier3.5.0-alpha.1,includesstatt

verwendet wird – weil die Versionsnummer die Form📎 scripts/release.js:453-458:

js
if (!isDryRun && (await isPackagePublished(packageName, version))) {
  console.log(pico.yellow(`Skipping already published: ${pkgVersion}`))
  alreadyPublishedPackages.push(pkgVersion)
  return
}

isPackagePublished 📎 scripts/release.js:491-513Vor der Veröffentlichung gibt es eine Idempotenzprüfungnpm view <pkg>@<version> versionKopierentrueführtfalseaus, bei Erfolg wird

zurückgegeben, bei einem E404-ähnlichen Fehler wirdnpm viewzurückgegeben. Der Sinn dieser Prüfung: Der Veröffentlichungsprozess könnte aufgrund von Netzwerkunterbrechungen erneut ausgeführt werden, bereits veröffentlichte Pakete sollten bei erneuter Ausführung nicht noch einmal veröffentlicht werden (npm lehnt doppelte Versionen ab).isPackagePublishedAber die Prüfung selbst kann auch fehlschlagen – zum Beispiel wirft📎 scripts/release.js:507-510aufgrund einer Netzwerk-Zeitüberschreitung einen Nicht-E404-Fehler. In diesem Fall wirft

den Fehler nach obenpnpm publish, was zum Abbruch der gesamten Veröffentlichung führt. Dies ist eine weitere Manifestation von „lieber abbrechen als Risiko eingehen".publishPackageSelbst wenn die Prüfung bestanden wird, kann📎 scripts/release.js:480-488:

js
} catch (e) {
  if (e.message?.match(/previously published/)) {
    console.log(pico.red(`Skipping already published: ${pkgVersion}`))
    alreadyPublishedPackages.push(pkgVersion)
  } else {
    throw e
  }
}

im catch-Block einen zweiten Fallback eingebautpreviously publishedKopieren

Nur wenn

📎 scripts/release.js:412-432übereinstimmt, wird der Fehler geschluckt, alle anderen Fehler werden erneut geworfen. Dies ist „präzise Fehlertoleranz": Nur bei bekannten, sicher ignorierbaren Fehlern wird eine Degradierung durchgeführt.pnpm publishDynamische Zusammensetzung der Veröffentlichungsflags

js
const additionalPublishFlags = []
if (isDryRun) additionalPublishFlags.push('--dry-run')
if (isDryRun || skipGit || process.env.CI)
  additionalPublishFlags.push('--no-git-checks')
if (process.env.CI && !args.registry)
  additionalPublishFlags.push('--provenance')

--no-git-checkssetzt je nach Laufzeitumgebung die zusätzlichen Flags fürpnpm publishzusammen:

--provenanceKopieren📎 scripts/release.js:425-427wird in drei Fällen aktiviert: dry run, git überspringen, oder in CI. Der Grund ist, dass!args.registrystandardmäßig prüft, ob der Arbeitsbereich sauber ist, ob der aktuelle Branch der Release-Branch ist usw., und in CI diese Prüfungen Fehlalarme auslösen.

wird nur in CI und wenn keine benutzerdefinierte Registry angegeben ist aktiviert

. Provenance ist eine Supply-Chain-Sicherheitsfunktion von npm, die die Herkunftsinformationen des Build-Artefakts (welcher Commit, welcher Workflow) signiert und an das Paket anhängt. Aber benutzerdefinierte Registries (wie interne private Registries) unterstützen Provenance normalerweise nicht, daher wurde die Bedingungmainhinzugefügt.📎 scripts/release.js:528-537:

js
fnToRun().catch(err => {
  if (versionUpdated) {
    updateVersions(currentVersion)
  }
  console.error(err)
  process.exit(1)
})

versionUpdatedZurück zum Ende vonfalse 📎 scripts/release.js:24-27KopierenupdateVersionsist eine boolesche Variable auf Modulebene, initialtrue 📎 scripts/release.js:208, wird sofort nach erfolgreichem Aufruf vontrueaufcurrentVersion。

gesetzt. Falls ein nachfolgender Schritt (changelog-Generierung, lockfile-Aktualisierung, git commit, publish) einen Fehler wirft, prüft der catch-Block dieses Flag, und falls

Dieser Rollback ist „Best-Effort": Er rollt nurpackage.jsondie Versionsnummer in zurück, nicht die Changelog-Datei, nicht die Lockfile, nicht den bereits ausgeführten Git-Commit. Wenn der Fehler nach dem Git-Commit auftritt, bleibt im Repository ein Zwischenzustand zurück, in dem „die Versionsnummer zurückgerollt wurde, aber der Commit bereits existiert". Dies ist eine Design-Abwägung – ein vollständiger Rollback würdegit reseterfordern, und das würde andere Änderungen zerstören, die der Benutzer möglicherweise bereits vorgenommen hat. Daher entscheidet sich das Skript dafür, nur die kritischste Versionsnummer zurückzurollen und den Rest dem Benutzer zur manuellen Behandlung zu überlassen.

Beachten SiepublishOnlyden Pfad📎 scripts/release.js:519-526wirdversionUpdatednicht gesetzt, da seine Semantik „nur veröffentlichen, keine Version ändern" lautet – selbst bei einem Fehler ist kein Rollback erforderlich. Wenn jedochtargetVersionexistiert, ruft esupdateVersions 📎 scripts/release.js:519-526auf; schlägt dies fehl, wird die Versionsnummer nicht zurückgerollt. Dies ist ein potenzielles Randproblem, siehe die Denkaufgabe am Ende des Kapitels.

mermaid
flowchart TD
    upd["updateVersions(targetVersion)"] --> flag["versionUpdated = true"]
    flag --> changelog["pnpm run changelog"]
    changelog --> lock["pnpm install --prefer-offline"]
    lock --> gitdiff{"git diff 有输出?"}
    gitdiff -->|是| commit["git add -A && git commit"]
    gitdiff -->|否| nochange["No changes to commit"]
    commit --> pub{"args.publish?"}
    nochange --> pub
    pub -->|是| build["buildPackages()"]
    pub -->|否| push
    build --> publish["publishPackages()"]
    publish --> push["git tag && git push"]
    push --> done["完成"]
    changelog -.->|抛错| rollback["catch: updateVersions(currentVersion)"]
    lock -.->|抛错| rollback
    commit -.->|抛错| rollback
    publish -.->|抛错| rollback
    rollback --> exit["process.exit(1)"]

Veröffentlichungsreihenfolge und spezielle Behandlung des vue-Pakets

publishPackages 📎 scripts/release.js:412-432durchläuft die Ergebnisse vonsortPackagesForPublishing(packages)und ruft nacheinanderpublishPackageauf. Da die Sortierungvueans Ende setzt📎 scripts/release.js:85-85, stellt die gesamte Veröffentlichungssequenz sicher, dass interne Pakete zuerst online gehen.

publishPackageverwendet interncwd: getPkgRoot(pkgName) 📎 scripts/release.js:475, um das Arbeitsverzeichnis in das Unterpaketverzeichnis zu wechseln, sodasspnpm publishdas Unterpaket statt des Root-Pakets veröffentlicht. Der Kommentar📎 scripts/release.js:462-463warnt ausdrücklich: „Nicht zu npm publish ändern" – dennpnpm publishkann dasworkspace:*-Abhängigkeitsprotokoll korrekt verarbeiten und in eine tatsächliche Versionsnummer umwandeln, währendnpm publishdasworkspace:*unverändert beibehalten würde, was zu Installationsfehlern führt.

---

Designüberlegungen

WarumparseArgsstattyargs?verwenden? Das Veröffentlichungsskript ist die „letzte Verteidigungslinie" und muss in jeder Umgebung ausführbar sein. Wenn eine CLI-Bibliothek eines Drittanbieters aufgrund eines beschädigten Abhängigkeitsbaums nicht geladen werden kann, ist der gesamte Veröffentlichungsprozess lahmgelegt. Das in Node integrierteparseArgsist zwar funktional spartanisch (keine Subbefehle, keine automatische Hilfe), aber null Abhängigkeiten, null Risiko.

Warumpublishstandardmäßig auffalse?setzen? Weil die offizielle Veröffentlichung von Vue über GitHub Actions läuft (siehe Hinweis in📎 scripts/release.js:256-263), und das lokale Skript nur für Versionsänderung, Changelog-Generierung, Tag-Erstellung und Push zuständig ist. Das eigentlichenpm publishwird in der CI ausgeführt, um die Provenance-Signierung und kontrollierte Umgebung der CI zu nutzen.--publishDas

-Flag ist ein Notausgang für Maintainer, um im Notfall lokal zu veröffentlichen.Warum wird beim Rollback nur die Versionsnummer zurückgerollt?package.jsonWeil ein vollständiger Rollback verstehen müsste, „welche Änderungen vom Skript und welche vom Benutzer vorgenommen wurden", und dies auf Git-Ebene nicht unterschieden werden kann. Das Skript entscheidet sich, nur das zurückzurollen, bei dem es am sichersten weiß, dass es es geändert hat –

---

die Versionsnummer – und den Rest dem Benutzer zur Beurteilung zu überlassen.

scripts/release.jsKapitelzusammenfassung

1. implementiert mit 537 Zeilen Code eine „interaktive Zustandsmaschine", deren Kerndesign sich in drei Punkten zusammenfassen lässt:Parameter als StrategierunIfNotDry: 10 Flags werden beim Modulladen geparst und in globale Variablen abgeflacht,

2. bindet die Strategie bei der Initialisierung, um fehlende Prüfungen an Aufrufstellen zu vermeiden.Gatekeeping vorgelagert

3. : Synchronisationsprüfung, Versionsvalidierung und CI-Gate werden vor jeglichen Seiteneffekten abgeschlossen, um „alles oder nichts" sicherzustellen.:isPackagePublishedPräzise Fehlertoleranzpreviously publishedVorabprüfung +versionUpdatedFehler-Fallback bilden einen doppelten Idempotenzschutz;

Das Flag ermöglicht einen minimalen Rollback.Dieser Mechanismus bildet einen interessanten Kontrast zum Template Explorer aus dem vorherigen Kapitel: Template Explorer ist „Beobachten" – Visualisierung des internen Compiler-Zustands; release.js ist „Ausführen" – Explizierung jedes Schritts des Veröffentlichungsprozesses. Beide verkörpern dieselbe Ingenieursphilosophie:。

Impliziten Zustand in expliziten Zustand verwandeln, unkontrollierbare Seiteneffekte in kontrollierbare Schritte

Denkaufgaben und Selbsttests dieses Kapitels📎 scripts/release.js:285F1: Wenn manskipTests ||= isCIPasseddasskipTests = isCIPassedzu--skipTestsändert, was passiert, wenn der Benutzer explizit

übergibt und die CI nicht bestanden hat? Warum?Referenzanalyse--skipTests: In der ursprünglichen Logik, wenn der BenutzerskipTestsübergibt, isttrue 📎 scripts/release.js:64-66,||=initialrunTestsIfNeededund ändert es nicht, daher wird📎 scripts/release.js:282inif (!skipTests)die📎 scripts/release.js:314-316-Prüfung als falsch ausgewertet und springt direkt zuskipTests = isCIPassed, um „Tests skipped." auszugeben. Wenn man es zuskipTestsändert, wirdfalsezwangsweise auf📎 scripts/release.js:287gesetzt (CI nicht bestanden), anschließend istif (isCIPassed)in📎 scripts/release.js:299falsch und fällt aufelse if (skipPrompts)in--skipPrompts– wennskipTestsnicht aktiviert ist, bleibtfalseauf📎 scripts/release.js:307-313, und schließlich wird in--skipPromptsder lokale Test ausgeführt. Dies widerspricht der Absicht des Benutzers, „Tests explizit zu überspringen", und wirft in der CI-Umgebung (📎 scripts/release.js:300-303) direkt einen Fehler||=, was die Veröffentlichung abbricht.

Q2: publishOnlyDie Existenz von📎 scripts/release.js:519-526dient genau dazu, die explizite Wahl des Benutzers zu respektieren.targetVersionDer PfadupdateVersionsruftversionUpdatedauf, wennbuildPackagesexistiert, setzt aber nichtpublishPackages. Was passiert, wenn zu diesem Zeitpunkt

oder:publishOnlyeinen Fehler wirft? Ist dieses Design sinnvoll?updateVersions(targetVersion) 📎 scripts/release.js:519-526Referenzanalysepackage.jsonruftversionUpdated = trueauf und ändert die Versionsnummern allerbuildPackages 📎 scripts/release.js:519-526, setzt aber nichtpublishPackages 📎 scripts/release.js:519-526. Wenn anschließendfnToRun().catch 📎 scripts/release.js:528-537oderversionUpdatedeinen Fehler wirft, prüftfalse, obpublishOnlygleichtargetVersionist, und rollt die Versionsnummer nicht zurück. Das Ergebnis ist, dass das Repository im Zustand „Versionsnummer geändert, aber Veröffentlichung fehlgeschlagen" verbleibt. Dieses Design ist unter der ursprünglichen Semantik vonupdateVersions(nur veröffentlichen, keine Version ändern) sinnvoll – denntargetVersionwird normalerweise nicht übergeben und📎 scripts/release.js:519-526wird nicht ausgeführt. Wenn der Benutzer jedochversionUpdated = trueübergibt, weist dieser Pfad eine Rollback-Lücke auf. Die Lösung besteht darin, nachpublishOnlyeinmainhinzuzufügen oder

Q3: isPackagePublished 📎 scripts/release.js:491-513die Rollback-Logik vonnpm viewwiederverwenden zu lassen.npm viewverwendet

, um zu prüfen, ob das Paket bereits veröffentlicht wurde. Was passiert, wenn ein Netzwerk-Timeout dazu führt, dass:isPackagePublishedeinen Nicht-E404-Fehler wirft? Ist dieses Verhalten im CI-Wiederholungsszenario sicher?📎 scripts/release.js:507-510ReferenzanalyseisPackageNotFoundErrorruft im catch-Block📎 scripts/release.js:515-515auf, um den Fehlertyp zu bestimmen. Diese Funktion/E404|No match found|No matching version|notarget/igleicht nurisPackageNotFoundErrorab. Die Nachricht eines Netzwerk-Timeout-Fehlers enthält diese Schlüsselwörter nicht, daher gibtfalse,isPackagePublishedden Fehler erneut aus📎 scripts/release.js:507-510. Dieser Fehler breitet sich nach oben aus bispublishPackage 📎 scripts/release.js:453, was den gesamten Release abbricht. Im Szenario eines CI-Neustarts führt dies dazu, dass „das Paket bereits veröffentlicht wurde, aber aufgrund von Netzwerkfluktuationen abgebrochen wird“ – aber dies ist eine sichere Fehlerrichtung: Ein Abbruch ist besser als eine Fehlentscheidung „nicht veröffentlicht“ und eine erneute Veröffentlichung. Eine erneute Veröffentlichung löst den npm-previously publishedFehler aus, wird von📎 scripts/release.js:491-492abgefangen, verschwendet aber einen Netzwerk-Roundtrip. Daher ist „Netzwerkfehler = Abbruch“ eine konservative, aber korrekte Wahl.

---

Das nächste Kapitel führt in.github/workflows/ein und zeigt, wie GitHub Actions nach dem Push des Tags durch release.js die nachfolgende Build- und Release-Pipeline übernimmt sowie die vollständige Implementierung der CI-Gates.

Damit haben wir gesehen, wie release.js mit einer Zustandsmaschine und interaktiver Orchestrierung das irreversible Release-Risiko minimiert. Aber das Release-Skript selbst ist nur der Ausführende; wer wirklich entscheidet, wann ausgelöst wird und unter welchen Bedingungen freigegeben wird, ist der übergeordnete automatisierte Torwächter. Das nächste Kapitel analysiert das CI/CD-System im Verzeichnis .github/workflows: Wie ci.yml in der PR-Phase die dreifachen Gates lint/typecheck/test ausführt, wie release.yml beim Tag-Push das Release auslöst, wie size-report.yml und size-data.yml Paketgrößen-Regressionen verfolgen und wie autofix.yml Formatprobleme automatisch behebt. Du wirst verstehen, wie Vue mit GitHub Actions Engineering-Standards in eine nicht umgehbare Pipeline gießt.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 10

Kapitel 10: CI/CD-Workflows: Automatisierte Torwächter von PR bis Release

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 10 von 14

Im vorherigen Kapitel haben wir gesehen, wiescripts/release.jsmit einer interaktiven Zustandsmaschine jeden Schritt eines Releases verkettet. Aber dieses Skript hat eine Voraussetzung: Es muss von einer Person oder einem System aktiv aufgerufen werden. Im Vue-core-Repository ist dieser aktive Aufrufer nicht das lokale Terminal eines Maintainers, sondern GitHub Actions. release.js ist der Ausführende, workflows sind der Entscheider – sie bestimmen, welches Ereignis welche Aufgabe auslöst, unter welchen Bedingungen freigegeben und unter welchen Bedingungen blockiert wird. Dieses Kapitel konzentriert sich auf.github/workflows/die vier Dateien im Verzeichnis:ci.yml(PR-Gate und kontinuierliche Vorabveröffentlichung),release.yml(tag-ausgelöste offizielle Veröffentlichung),size-report.yml(Bericht zu Größenregressionen),autofix.yml(automatische Formatkorrektur). Ihr Kern besteht nicht darin, YAML-Syntax auswendig zu lernen, sondern zu erkennen, wie das Vue-Team Engineering-Standards in nicht umgehbare Pipeline-Einschränkungen übersetzt.

I. ci.yml: Dreifache Gates und kontinuierliche Vorabveröffentlichung

Intuitives Modell

Stell dirci.ymlwie eine Flughafensicherheitskontrolle vor. Jeder PR muss durch dieses Gate: lint prüft, ob dein Gepäck verbotene Gegenstände enthält, typecheck bestätigt, dass dein Ausweis echt und gültig ist, test verifiziert, dass du keine Gefahrgüter mitführst. Aber es gibt nicht nur ein Sicherheitsgate – Vue hat hier auch einen „kontinuierlichen Vorabveröffentlichungs“-Kanal eingerichtet, der die Build-Artefakte jedes PR direkt auf pkg-pr-new veröffentlicht, damit Mitwirkende ihre Änderungen in einem echten npm-Installationsszenario validieren können.

Ohne dieses Gate könnte jede Zusammenführung Formatfehler, Typ-Lücken oder Verhaltensregressionen in den main-Branch bringen, und der main-Branch ist die Quelle aller nachfolgenden Releases.

Auslösebedingungen und Nebenläufigkeitskontrolle

ci.ymlDie Auslösekonfiguration von

📎 .github/workflows/ci.yml:2-11

yaml
on:
  push:
    branches:
      - '**'
    tags:
      - '!**'
  pull_request:
    branches:
      - main
      - minor

Hier gibt es zwei entscheidende Designs. Erstens:pushDas Ereignis überwacht alle Branches ('**'), schließt aber mittags: ['!**']explizit alle Tag-Pushes aus. Warum Tags ausschließen? Weil Tag-Pushes vonrelease.ymlseparat behandelt werden. Wennci.ymlebenfalls auf Tags reagieren würde, würden Release-Pipeline und CI-Pipeline doppelt ausgelöst, was Runner-Ressourcen verschwendet und sogar Race Conditions erzeugen kann. Zweitens:pull_requestüberwacht nurmainundminorzwei Branches – das ist Vues Zwei-Branch-Strategie:mainträgt die stabile Version,minorträgt die Vorabveröffentlichungsversion.

📎 .github/workflows/ci.yml:22-22

yaml
concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

Der Ausdruck vongroupverwendetgithub.event.pull_request.number || github.refals Fallback: PR-Ereignisse verwenden die PR-Nummer als Gruppierungsschlüssel, Push-Ereignisse verwenden ref (Branch-Name) als Gruppierungsschlüssel. Das bedeutet, dass mehrere Pushes desselben PR in dieselbe Nebenläufigkeitsgruppe fallen. Undcancel-in-progressist nur bei PR-Ereignissentrue– wenn du drei Commits hintereinander pushst, werden die CI-Läufe der ersten beiden automatisch abgebrochen, und nur der neueste bleibt erhalten.

〔Design-Inferenz und Architektur-Abwägung〕

Die Motivation dieses Designs ist klar: In der PR-Phase pushen Entwickler häufig, die CI-Ergebnisse alter Commits sind bereits bedeutungslos, und deren Abbruch spart viel Runner-Zeit. Aber bei einem Push in den main-Branch darf nicht abgebrochen werden – denn jeder Push auf main könnte die letzte Validierung vor einem Release sein, und ein Abbruch würde eine Validierungslücke erzeugen.

Der Einstieg in die dreifachen Gates: Bedingungsprüfung des test-Jobs

📎 .github/workflows/ci.yml:22-22

yaml
jobs:
  test:
    if: ${{ ! startsWith(github.event.head_commit.message, 'release:') && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) }}
    uses: ./.github/workflows/test.yml

Bedingung enthält zwei logische Und-Zweige (if), und jeder verdient eine Erläuterung.&&Die erste Bedingung

: Wenn die Commit-Nachricht mit! startsWith(github.event.head_commit.message, 'release:'):如果提交信息以 release:Am Anfang, Tests überspringen. Genau das ist das Format der Commit-Nachricht, die release.js im vorherigen Kapitel gepusht hat – release.js hat die vollständigen Tests bereits lokal ausgeführt, CI muss nicht erneut validieren. Dies ist eine Optimierung des „Vertrauens in die vorgelagerte Instanz".

〔Design-Inferenz und Architektur-Abwägung〕

Die zweite Bedingung(github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository): push-Ereignisse führen immer Tests aus; PR-Ereignisse erfordern, dass der PR von einem Fork stammt (head.repo.full_name != github.repository). Warum werden nur PRs von Forks getestet? Weil PRs von Branches im selben Repository normalerweise von Kernmitgliedern erstellt werden und deren Branch-Pushes bereits CI durch push-Ereignisse ausgelöst haben. PRs von Forks lösen jedoch keine push-Ereignisse aus (ein Push in einem Fork benachrichtigt nicht das Upstream-Repository), daher muss dies im PR-Ereignis nachgeholt werden.

Beachten Sieuses: ./.github/workflows/test.yml– dies ist ein Aufruf eines reusable workflow.test.ymlist eine eigenständige Workflow-Datei, die vonci.ymlundrelease.ymlgemeinsam genutzt wird. Diese Wiederverwendung vermeidet die mehrfache Definition von lint/typecheck/test-Schritten in mehreren Workflows.

Kontinuierliche Vorabveröffentlichung: Die Rolle von pkg-pr-new

📎 .github/workflows/ci.yml:25-51

yaml
continuous-release:
  if: github.repository == 'vuejs/core'
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false
    # ... 安装 pnpm、Node.js、依赖 ...
    - name: Build
      run: pnpm build --withTypes
    - name: Release
      run: pnpx pkg-pr-new publish --compact --pnpm './packages/*' --packageManager=pnpm,npm,yarn

continuous-releaseDer Job läuft nur imvuejs/coreHaupt-Repository (if: github.repository == 'vuejs/core'), wird auf Forks nicht ausgeführt. Er erledigt drei Dinge: Build (pnpm build --withTypes, mit Typdeklarationen), dann mitpkg-pr-newalle Pakete unter./packages/*in eine temporäre npm-Registry veröffentlichen.

〔Design-Inferenz und Architektur-Abwägung〕

Der Wert dieses Mechanismus liegt darin: Mitwirkende können in ihrem eigenen Projekt direktnpm installdie Build-Artefakte dieses PRs installieren, um zu verifizieren, ob die Änderung das Problem tatsächlich löst. Dies ist überzeugender als „CI ist grün", weil es das reale Paketkonsumszenario validiert.

Beachten Sie, dass alle Actions auf Commit-SHA festgeschrieben sind (wieactions/checkout@3d3c42e5...), anstatt flüchtige Tags wie@v4zu verwenden. Dies ist eine harte Anforderung der Supply-Chain-Sicherheit – um zu verhindern, dass nach einer Kompromittierung des Action-Repositorys bösartiger Code automatisch eindringt.

ci.yml Kontrollflussdiagramm

mermaid
flowchart TD
    trigger{"事件类型?"}
    trigger -->|"push 到任意分支"| push_check{"提交信息以 release: 开头?"}
    trigger -->|"PR 到 main/minor"| pr_check{"PR 来自 fork?"}

    push_check -->|"是"| skip_test["跳过 test job"]
    push_check -->|"否"| run_test["调用 test.yml"]

    pr_check -->|"是"| run_test
    pr_check -->|"否"| skip_test

    run_test --> test_result{"test.yml 通过?"}
    test_result -->|"否"| block["PR 被阻断"]
    test_result -->|"是"| cont_release{"仓库是 vuejs/core?"}

    cont_release -->|"是"| build["pnpm build --withTypes"]
    cont_release -->|"否"| end_node["结束"]
    build --> publish["pkg-pr-new publish"]
    publish --> end_node

---

Zwei, release.yml: Release-Orchestrierung nach Tag-Push

Intuitives Modell

Wenn manci.ymlals Sicherheitskontrolle bezeichnet,release.ymlist es die Startrampe. Wenn release.js lokal die Versionsnummer aktualisiert, committet, taggt und pusht, zündet das Tag-Push-Ereignis den Motor vonrelease.yml. Es führt zuerst einen vollständigen Testlauf aus (erneute Bestätigung), dann führt es im geschütztenReleaseEnvironmentpnpm release --publishOnlyaus und erstellt schließlich ein GitHub Release.

Ohne es wäre das von release.js gepushte Tag nur eine Git-Referenz, es gäbe keine neue Version auf npm und keine Release-Seite auf GitHub.

Auslösebedingung: Nur Tags werden akzeptiert

📎 .github/workflows/release.yml:3-6

yaml
on:
  push:
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

Es überwacht nur Tag-Pushes im Formatv*. Dies ergänzt sich mitci.ymlvontags: ['!**']– beide sind strikt gegenseitig ausschließend und werden nicht gleichzeitig ausgelöst.

Guard-Bedingungen des Release-Jobs

📎 .github/workflows/release.yml:8-21

yaml
jobs:
  test:
    uses: ./.github/workflows/test.yml

  release:
    if: github.repository == 'vuejs/core'
    needs: [test]
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
    environment: Release

Hier gibt es drei Guard-Ebenen, keine davon ist verzichtbar.

Erste Ebeneif: github.repository == 'vuejs/core': Verhindert versehentliche Release-Auslösung auf Forks. Wenn jemand das Repository forkt und einv1.0.0-Tag pusht, verhindert diese Bedingung die Ausführung des Release-Prozesses.

Zweite Ebeneneeds: [test]: Der Release-Job hängt vom Test-Job ab. Der Test-Job rufttest.ymlauf; wenn die Tests fehlschlagen, startet der Release-Job gar nicht. Dies ist die harte Einschränkung „Tests müssen vor dem Release bestanden werden".

〔Design-Inferenz und Architektur-Abwägung〕

Dritte Ebeneenvironment: Release: Dies ist eine GitHub Environment, für die Deployment-Schutzregeln konfiguriert werden können (z. B. Genehmigung durch bestimmte Personen erforderlich). Das bedeutet, dass selbst wenn ein Tag-Push den Workflow auslöst, der Release-Schritt möglicherweise eine manuelle Genehmigung erfordert – dies ist die letzte Verteidigungslinie gegen irreversible Operationen.

Bezüglich Berechtigungen:contents: writewird zum Erstellen des GitHub Release verwendet,id-token: writefür die npm-Provenance-Authentifizierung (OIDC-Token). Beachten Sie, dass hier keinpackages: writevorhanden ist, da Vue auf npm und nicht auf GitHub Packages veröffentlicht.

Die vollständige Kette der Release-Schritte

📎 .github/workflows/release.yml:37-46

yaml
- name: Install deps
  run: pnpm install --frozen-lockfile

- name: Update npm
  run: npm i -g npm@latest

- name: Build and publish
  id: publish
  run: |
    pnpm release --publishOnly
〔Design-Inferenz und Architektur-Abwägung〕

Die drei Schritte haben jeweils ihre Besonderheiten.--frozen-lockfilestellt sicher, dass die CI-Umgebung strikt gemäß lockfile installiert, sodass Build-Artefakte nicht durch Dependency-Versionsdrift von der lokalen Umgebung abweichen.npm i -g npm@latestdient dem Abrufen der neuesten npm CLI – da Provenance und OIDC-Authentifizierung neuere npm-Versionen erfordern und ältere Versionen diese Funktionen möglicherweise nicht unterstützen.

pnpm release --publishOnlyist der Einstiegspunkt von release.js aus dem vorherigen Kapitel.--publishOnlyDas Flag teilt release.js mit: Interaktive Versionsnummernauswahl überspringen, Git-Commit und Tagging überspringen (da das Tag bereits existiert), nur Build und npm publish ausführen.

GitHub Release erstellen

📎 .github/workflows/release.yml:48-57

yaml
- name: Create GitHub release
  id: release_tag
  uses: yyx990803/release-tag@8cccf7c5aa332d71d222df46677f70f77a8d2dc0 # v1.0.0
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    tag_name: ${{ github.ref }}
    body: |
      For stable releases, please refer to [CHANGELOG.md](...) for details.
      For pre-releases, please refer to [CHANGELOG.md](...) of the `minor` branch.
〔Design-Inferenz und Architektur-Abwägung〕

Hier wirdrelease-tag action。tag_name: ${{ github.ref }}verwendet, das vom Vue-Autor Evan You selbst gepflegt wird, und direkt die Ref des auslösenden Ereignisses verwendet (d. h.refs/tags/v3.x.x). Der Release-Body enthält keine konkreten Änderungen, sondern verweist auf CHANGELOG.md – weil Vues Changelog von conventional-changelog automatisch generiert wird und eine manuelle Pflege des Release-Bodys zu Inkonsistenzen mit dem Changelog führen würde.

release.yml Sequenzdiagramm

mermaid
sequenceDiagram
    participant Dev as "开发者本地"
    participant GH as "GitHub"
    participant Test as "test.yml"
    participant Rel as "release job"
    participant NPM as "npm registry"

    Dev->>GH: "git push origin v3.x.x"
    GH->>Test: "触发 test.yml"
    Test-->>GH: "测试通过"
    GH->>Rel: "needs: [test] 满足"
    Rel->>Rel: "environment: Release 审批"
    Rel->>Rel: "pnpm install --frozen-lockfile"
    Rel->>Rel: "pnpm release --publishOnly"
    Rel->>NPM: "npm publish (OIDC provenance)"
    NPM-->>Rel: "发布成功"
    Rel->>GH: "release-tag 创建 Release"

---

Drei, size-report.yml und autofix.yml: Größenverfolgung und Format-Selbstheilung

size-report.yml: Workflow-übergreifender Größenregressionsbericht

size-report.ymlDie Auslösung ist sehr speziell – sie wird nicht direkt durch push oder PR ausgelöst, sondern durch das Abschlussereignis eines anderen Workflows.

📎 .github/workflows/size-report.yml:3-7

yaml
on:
  workflow_run:
    workflows: ['size data']
    types:
      - completed

workflow_runDas Event lauscht auf den Namensize datades Workflows, der abgeschlossen wurde. Dies ist ein zweistufiges Design:size-data.yml(In diesem Kapitel kein Quellcode bereitgestellt) ist dafür verantwortlich, im PR zu bauen und die Größe zu messen und die Ergebnisse als Artefakt hochzuladen;size-report.ymlnachsize dataAbschluss wird das Artefakt heruntergeladen, ein Bericht generiert und als Kommentar zum PR hinzugefügt.

📎 .github/workflows/size-report.yml:20-23

yaml
if: >
  github.repository == 'vuejs/core' &&
  github.event.workflow_run.event == 'pull_request' &&
  github.event.workflow_run.conclusion == 'success'

Dreifache Absicherung: Hauptrepository, PR-Event, Upstream-Workflow erfolgreich. Wennsize datafehlschlägt, wird der Report-Job nicht ausgeführt – weil keine Daten zum Berichten vorhanden sind.

Der Datenfluss gestaltet sich wie folgt:

📎 .github/workflows/size-report.yml:41-46

yaml
- name: Download Size Data
  uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
  with:
    name: size-data
    run_id: ${{ github.event.workflow_run.id }}
    path: temp/size

Vom Upstream-Workflow-Run wirdsize-datadas Artefakt nachtemp/sizeheruntergeladen. Dann werden parallel die PR-Nummer und der Base-Branch gelesen:

📎 .github/workflows/size-report.yml:48-59

yaml
- parallel:
    - name: Read PR Number
      id: pr-number
      uses: juliangruber/read-file-action@271ff311a4947af354c6abcd696a306553b9ec18 # v1.1.8
      with:
        path: temp/size/number.txt
    - name: Read base branch
      id: pr-base
      uses: juliangruber/read-file-action@271ff311a4947af354c6abcd696a306553b9ec18 # v1.1.8
      with:
        path: temp/size/base.txt

parallelist syntaktischer Zucker von GitHub Actions, der zwei unabhängige Schritte gleichzeitig ausführt.number.txtundbase.txtsindsize-data.ymlMetadatendateien, die beim Messen geschrieben werden.

Anschließend werden die historischen Größendaten des Base-Branches zum Vergleich heruntergeladen:

📎 .github/workflows/size-report.yml:61-69

yaml
- name: Download Previous Size Data
  uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
  with:
    branch: ${{ steps.pr-base.outputs.content }}
    workflow: size-data.yml
    event: push
    name: size-data
    path: temp/size-prev
    if_no_artifact_found: warn

Beachten Sieif_no_artifact_found: warn– wenn der Base-Branch noch keine historischen Daten hat (z. B. ein neuer Branch), schlägt es nicht fehl, sondern warnt nur. Dies stellt sicher, dass der Bericht beim ersten Lauf dennoch generiert wird, nur ohne Vergleichsbasis.

Schließlich wird der Bericht generiert und kommentiert:

📎 .github/workflows/size-report.yml:71-89

yaml
- name: Prepare report
  run: node scripts/size-report.js > size-report.md

- name: Read Size Report
  id: size-report
  uses: juliangruber/read-file-action@271ff311a4947af354c6abcd696a306553b9ec18 # v1.1.8
  with:
    path: ./size-report.md

- name: Create Comment
  uses: actions-cool/maintain-one-comment-backup@fbbc22ad1809c1bcf46f19b58397b6254773588c # backup for v3.0.0
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    number: ${{ steps.pr-number.outputs.content }}
    body: |
      ${{ steps.size-report.outputs.content }}
      <!-- VUE_CORE_SIZE -->
    body-include: '<!-- VUE_CORE_SIZE -->'

scripts/size-report.jsliesttemp/sizeundtemp/size-prevdie Daten unter und generiert einen Markdown-Bericht.maintain-one-comment-backupDie Action verwendetbody-include: '<!-- VUE_CORE_SIZE -->'als Marker, um sicherzustellen, dass pro PR nur ein Größenbericht-Kommentar erhalten bleibt (Aktualisierung statt Anhängen). Beachten Sie den Kommentar in L81, der erklärt, dass das ursprüngliche Action-Repository von GitHub blockiert wurde, daher wurde ein Backup-Repository verwendet und der Commit festgeschrieben.

autofix.yml: Automatische Behebung von Formatproblemen

autofix.ymllöst ein sehr praktisches Problem: Der von Mitwirkenden eingereichte Code entspricht nicht den prettier/eslint-Regeln, CI meldet einen Fehler, und die Mitwirkenden müssen manuellpnpm lint --fixausführen und erneut committen. Dieser Workflow automatisiert diesen Schritt.

📎 .github/workflows/autofix.yml:3-8

yaml
on:
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

Löst bei allen PRs aus, die Nebenläufigkeitssteuerung ist ähnlich wie beici.yml– ein neuer Push im selben PR bricht den alten autofix-Lauf ab.

📎 .github/workflows/autofix.yml:35-41

yaml
- name: Run eslint
  run: pnpm run lint --fix

- name: Run prettier
  run: pnpm run format

- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8

Zuerst wird eslint--fixausgeführt, dann prettier formatiert, und schließlichautofix-ci/actionwerden die geänderten Dateien direkt zurück in den PR-Branch committet. Beachten Sie, dasspnpm run formatselbst ein Formatierungsbefehl ist (kein--fixFlag erforderlich, da das format-Skript internprettier --write)。

[Design-Schlussfolgerungen und Architektur-Abwägungen]

Der Schlüssel dieses Mechanismus liegt darin, dassautofix-ci/actionFixes als PR-Autor committet werden, nicht als Bot. So müssen Mitwirkende nichts weiter tun, und die Formatkorrekturen erscheinen automatisch in ihrem PR. Das bedeutet aber auch, dass autofix fehlschlägt, wenn der Branch des Mitwirkenden Schutzregeln hat (die Bot-Pushes nicht erlauben) – dies ist ein Grenzfall, den Mitwirkende manuell behandeln müssen.

size-report Datenflussdiagramm

mermaid
flowchart LR
    subgraph "size-data.yml (上游)"
        build_pr["构建 PR 分支"] --> measure["测量体积"]
        measure --> artifact_pr["artifact: size-data\n(number.txt, base.txt, 体积数据)"]
    end

    subgraph "size-report.yml (下游)"
        artifact_pr -->|"workflow_run 触发"| download["下载 size-data"]
        download --> read_meta["读取 number.txt / base.txt"]
        read_meta --> download_prev["下载 base 分支历史数据\n(if_no_artifact_found: warn)"]
        download_prev --> gen_report["node scripts/size-report.js"]
        gen_report --> comment["评论到 PR\n(标记: VUE_CORE_SIZE)"]
    end

---

Design-Überlegung: Normen in die Pipeline gießen

Bei der Betrachtung dieser vier Workflows lassen sich mehrere durchgängige Designprinzipien erkennen.

Erstens, Minimierung der Berechtigungen. ci.ymlundautofix.ymldeklarieren beidepermissions: contents: read, nurrelease.ymlbenötigtcontents: writeundid-token: write。size-report.ymlbenötigtpull-requests: writeundissues: writezum Kommentieren. Jeder Workflow erhält nur die Berechtigungen, die er wirklich benötigt.

Zweitens, Supply-Chain-Sicherheit.Alle Drittanbieter-Actions sind auf Commit-SHA festgeschrieben, nicht auf floatende Tags.size-report.ymlDer Kommentar in L81 erklärt sogar direkt, dass nach der Blockierung des ursprünglichen Action-Repositories auf ein Backup-Repository umgestellt und der Commit festgeschrieben wurde – dies ist eine praktische Verteidigung gegen Supply-Chain-Angriffe.

Drittens, Trennung der Zuständigkeiten und Wiederverwendung. test.ymlwird vonci.ymlundrelease.ymlgemeinsam genutzt, um Duplizierung der Testlogik zu vermeiden.size-data.ymlundsize-report.ymlsind getrennt, sodass Messung und Berichterstattung unabhängig voneinander weiterentwickelt werden können.

Viertens, die Wahl der Fehlerrichtung. size-report.ymlDieif_no_artifact_found: warnwählt „Warnen statt Fehlschlagen“, da fehlende historische Daten den PR nicht blockieren sollten. Dierelease.ymlvonneeds: [test]wählt „Testfehler blockiert Release“, da ein Release eine irreversible Operation ist.

Fünftens, Differenzierung der Nebenläufigkeitssteuerung.PR-Events brechen alte Läufe ab (cancel-in-progress: true), push-Events brechen nicht ab (cancel-in-progress: false). Diese Differenz spiegelt die Semantik der beiden Events wider: Alte Commits eines PRs sind bedeutungslos, jeder Commit eines push kann der endgültige Zustand sein.

---

Zusammenfassung dieses Kapitels

Dieses Kapitel analysiert die vier Kern-Workflows des Vue-core-Repositories:

  • ci.yml: PR-Gate + kontinuierliche Vorabveröffentlichung. DurchifBedingungen werden push/PR und fork/gleiches Repository unterschieden, mitconcurrencywerden veraltete PR-Läufe abgebrochen, mitpkg-pr-newwerden installierbare Vorabveröffentlichungspakete veröffentlicht.
  • release.yml: Tag-ausgelöste offizielle Veröffentlichung. Drei Schutzebenen (Repository-Prüfung, needs test, environment-Genehmigung) stellen sicher, dass nur Tags, die Tests bestanden haben und genehmigt wurden, in npm veröffentlicht werden können.
  • size-report.yml: Workflow-übergreifender Größenregressionsbericht. Durchworkflow_run-Event wird das Upstream-size data-Event überwacht, das Artifact heruntergeladen und mit den Daten des Base-Branches verglichen, um als Kommentar zum PR zurückgemeldet zu werden.
  • autofix.yml: Automatische Formatkorrektur. Auf dem PR werden eslint --fix und prettier ausgeführt, und durchautofix-ci/actionwerden die Korrekturen direkt in den PR-Branch zurückcommittet.

Diese vier Workflows bilden gemeinsam eine „nicht umgehbare Pipeline": Code-Standards werden durch autofix automatisch korrigiert, Typen und Tests werden durch ci.yml erzwungen geprüft, Größenregressionen werden durch size-report verfolgt, und die Veröffentlichung wird durch release.yml unter mehrfachen Schutzebenen ausgeführt.

Gedanken und Selbsttest dieses Kapitels

Q1: Wenn inci.ymlder Wert voncancel-in-progressauf konstanttruegeändert wird (d. h. die Bedingunggithub.event_name == 'pull_request'entfernt wird), in welchen Szenarien würde dies zu Problemen führen?

Referenzanalyse:cancel-in-progressKonstantestruebedeutet, dass beim Push auf den main-Branch ein neuer Push die laufende alte CI abbricht. Betrachten wir dieses Szenario: Auf dem main-Branch werden nacheinander zwei PRs gemergt, die CI des ersten PR läuft gerade (mit vollständigem lint/typecheck/test), und der Merge des zweiten PR löst einen neuen CI-Lauf aus. Wenncancel-in-progressauftruegesetzt ist, wird die CI des ersten PR abgebrochen – aber der Code des ersten PR ist bereits auf main, und sein CI-Ergebnis ist entscheidend für die Beurteilung des Gesundheitszustands des main-Branches. Ihn abzubrechen bedeutet, dass ein Teil des Codes auf dem main-Branch nie vollständig validiert wurde. Die Bedingung📎 .github/workflows/ci.yml:22-22vongithub.event_name == 'pull_request'dient genau dazu, dieses Problem zu vermeiden: Nur PR-Events brechen alte Läufe ab, Push-Events brechen niemals ab.

Q2: release.ymlWovor schützen diereleaseundif: github.repository == 'vuejs/core'desenvironment: Release-Jobs in

jeweils? Was passiert, wenn eine davon entfernt wird?:if: github.repository == 'vuejs/core' 📎 .github/workflows/release.yml:14Referenzanalysev3.99.0schützt vor dem Fork-Szenario. Wenn jemand vuejs/core forkt und einenpnpm release --publishOnly-Tag pusht, würde der Workflow ohne diese Bedingung im Fork-Repositoryenvironment: Release 📎 .github/workflows/release.yml:21ausführen. Obwohl das Fork-Repository ohne npm-Token nicht wirklich veröffentlichen kann, würden Runner-Ressourcen verschwendet und möglicherweise irreführende Fehlerbenachrichtigungen erzeugt.ifschützt vor dem Risiko der „automatischen Veröffentlichung nach Tag-Push" – es ermöglicht die Konfiguration einer manuellen Genehmigung, um sicherzustellen, dass selbst wenn ein Tag gepusht wird, die Veröffentlichung eine Bestätigung durch den Maintainer erfordert. Wenn die Bedingungenvironmententfernt wird, verschwendet der Fork Ressourcen; wenn

Q3: size-report.ymlentfernt wird, kann jeder mit Tag-Push-Berechtigung eine Veröffentlichung auslösen, ohne einen letzten manuellen Bestätigungsschritt. Beide sind Schutzebenen auf unterschiedlichen Ebenen und können einander nicht ersetzen.if_no_artifact_found: warnWelche Designphilosophie der Fehlerrichtung spiegeln die Wahl vonrelease.ymlinneeds: [test]bzw. die Wahl von

in:if_no_artifact_found: warn 📎 .github/workflows/size-report.yml:69wider? Was würde passieren, wenn diese beiden Strategien vertauscht würden?failReferenzanalyseneeds: [test] 📎 .github/workflows/release.yml:15wählt „bei fehlenden historischen Daten warnen statt fehlschlagen", weil der Größenbericht eine unterstützende Information ist und keine blockierende Bedingung. Wenn es auf

---

geändert würde, würden neue Branches oder PRs beim ersten Lauf fehlschlagen, weil keine Base-Daten gefunden werden – was offensichtlich unvernünftig ist.scripts/size-report.jswählt „bei Testfehler die Veröffentlichung blockieren", weil die Veröffentlichung eine irreversible Operation ist und die Codequalität sichergestellt werden muss. Wenn vertauscht – size-report schlägt bei fehlenden Daten fehl, release veröffentlicht trotz Testfehler – würde Ersteres zu zahlreichen Fehlalarmen führen, die normale PRs blockieren, und Letzteres würde ungetesteten Code in npm gelangen lassen. Dies spiegelt das Designprinzip der Fehlerrichtung wider: „locker bei unterstützenden Informationen, streng bei irreversiblen Operationen".usage-sizeDas nächste Kapitel wird sich mit dem Kern des Größenbudget-Mechanismus befassen:

wie Größendaten geparst werden, wie Inkremente berechnet werden, wie die Ausgabe formatiert wird, und die Messphilosophie vonscripts/size-report.js– warum Vue sich dafür entscheidet, die „tatsächlich genutzte Größe" statt der „vollständigen Paketgröße" zu messen.scripts/usage-size.jsVom PR-Gate bis zur Tag-Veröffentlichung bilden vier Workflow-Dateien gemeinsam eine nicht umgehbare automatisierte Wächterkette. Aber die Pipeline kann Merges nur blockieren, wenn sie quantifizierbare Beurteilungsgrundlagen besitzt. Das nächste Kapitel wird sich auf Vues engineeringmäßige Governance der Paketgröße als Kernmetrik konzentrieren:

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 11

Zurück nach oben ↑

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 11 von 14

Im vorherigen Kapitel haben wir gesehen, wie Vue mit GitHub Actions Linting, Typprüfung, Tests und Größenverfolgung in eine nicht umgehbare Pipeline integriert, wobei size-report.yml und size-data.yml dafür verantwortlich sind, nach jeder Änderung Größen-Daten zu hinterlassen. Aber die Pipeline führt nur aus; was tatsächlich beantwortet, „um wie viel größer und wo größer", sind die beiden Skripte, die in diesem Kapitel analysiert werden. Der Kernkonflikt des Größenbudgets liegt darin: Die Paketgröße ist eine Metrik, die nur wahrgenommen, aber schwer präzise zugeordnet werden kann. Wenn Benutzer sich beschweren, dass „Vue zu groß ist", müssen die Maintainer drei Fragen beantworten – um wie viel größer? Wo größer? Hat diese Änderung es noch größer gemacht? scripts/size-report.js ist für den Vergleich zuständig, scripts/usage-size.js für die Zuordnung; beide bilden gemeinsam die Messphilosophie des Größenbudgets.

11.1 size-report: Größenunterschiede in eine lesbare Markdown-Tabelle verwandeln

Intuitives Modell

Stellen Sie sich vor, Sie sind Qualitätsprüfer bei einem Logistikunternehmen. Jedes Paket (Build-Artefakt) muss vor dem Versand gewogen werden, und Ihre Aufgabe ist nicht das Wiegen selbst, sondern „das heutige Gewicht" und „das gestrige Gewicht" nebeneinander in eine Tabelle zu setzen und mit fettgedrucktem+2.3 kBzu markieren, welche Pakete schwerer geworden sind. Ohne diese Vergleichstabelle sehen Maintainer nur eine Ansammlung isolierter Zahlen und können nicht beurteilen, ob ein PR eine Größenregression eingeführt hat.

size-report.jsist genau dieser Qualitätsprüfer. Es erzeugt keine Größen-Daten (das ist Aufgabe vonusage-size.jsund den Build-Skripten), es konsumiert nur JSON-Dateien aus zwei Verzeichnissen und generiert einen Markdown-Bericht.

Datenstruktur und Verzeichniskonventionen

Die Kernkonventionen des Skripts verbergen sich in zwei Konstanten. Das aktuelle Datenverzeichnis isttemp/size, das historische Basisverzeichnis isttemp/size-prev。

📎 scripts/size-report.js:23-24

Die Benennung dieser beiden Verzeichnisse ist nicht willkürlich:temp/sizewird vomsize-data.yml-Workflow bei jedem Lauf generiert und als Artefakt hochgeladen📎 .github/workflows/size-data.yml:53-57, währendtemp/size-prevvonsize-report.ymlnach dem Herunterladen des Basis-Artefakts entpackt wird. Der Verzeichnisname selbst ist der Vertrag des Datenflusses.

Das Skript definiert drei Typ-Aliase, die die Struktur der JSON-Dateien präzise beschreiben:

📎 scripts/size-report.js:8-21

SizeResulthat drei numerische Felder:size(unkomprimiert),gzip、brotli。BundleResultfügt darauf basierend dasfile-Feld hinzu, um den Dateinamen anzuzeigen.UsageResultist einRecord, der Schlüssel ist der Preset-Name, der Wert istSizeResult & { name: string }– beachten Sie, dass hier ein zusätzlichesname-Feld vorhanden ist, da die Schlüssel des JSON-Objekts nachObject.valuesverloren gehen und der Name redundant im Wert gespeichert werden muss.

Step-by-Step Walkthrough

Der Hauptablauf ist minimalistisch, nur zwei Schritte plus eine Ausgabe:

📎 scripts/size-report.js:23-38

run()Zuerst wirdrenderFiles()aufgerufen, um die Tabelle der Artefaktdateien zu rendern, dannrenderUsages(), um die Tabelle der Nutzungsszenarien zu rendern, und schließlich wird der in der Modulvariablenoutputakkumulierte String auf einmal nach stdout geschrieben📎 scripts/size-report.js:25. Dieses Muster „Strings akkumulieren und auf einmal ausgeben" vermeidet den Konkatenierungsaufwand mehrfacherprocess.stdout.writeund macht die Ausgabereihenfolge vollständig kontrollierbar.

Erster Schritt: Dateiliste sammeln und Vereinigung bilden.

📎 scripts/size-report.js:44-49

filterFilesfiltert zwei Arten von Dateien heraus: solche, die mit_beginnen (wie_usages.json), und solche, die mit.txtenden (wienumber.txt、base.txt). Diese beiden Dateitypen sind Metadaten, keine Größen-Daten. Dann wird die VereinigungfileListder Dateinamen des aktuellen und des historischen Verzeichnisses gebildet – mitSetzur Deduplizierung. Warum die Vereinigung? Weil eine Datei nur im historischen Verzeichnis existieren kann (dieser Build hat das Artefakt gelöscht) oder nur im aktuellen Verzeichnis (dieser Build hat ein Artefakt hinzugefügt). Beide Fälle müssen im Bericht erscheinen.

Zweiter Schritt: Dateiweiser Vergleich.

📎 scripts/size-report.js:43-75

Für jede Datei in der Vereinigung wird versucht, JSON aus beiden Verzeichnissen zu importieren.importJSONDie Implementierung ist „gibt undefined zurück, wenn die Datei nicht existiert":

📎 scripts/size-report.js:112-115

Hier wird dynamischesimport()in Kombination mitwith: { type: 'json' }Import-Assertions verwendet, nichtfs.readFileSync + JSON.parse. Ersteres wird vom Modul-Loader von Node verarbeitet, letzteres erfordert manuelle Behandlung von Kodierungs- und Parsing-Fehlern. Der Preis für die Wahl vonimport()ist, dass es ein Promise zurückgibt, daher ist das gesamterenderFilesasync.

Der entscheidende Zweig liegt inif (!curr): Wenn das aktuelle Verzeichnis diese Datei nicht enthält, bedeutet das, dass das Artefakt gelöscht wurde; dann wird die Markdown-Durchstreichungs-Syntax~~fileName~~verwendet, um📎 scripts/size-report.js:60-61zu markieren. Andernfalls wird normal eine Zeile gerendert, wobei an jeden numerischen Wert das Ergebnis vongetDiffangehängt wird.

Dritter Schritt: Differenz berechnen.

📎 scripts/size-report.js:124-130

getDiffhat drei vorzeitige Rückkehrpunkte:prev === undefinedgibt einen leeren String zurück (keine Basislinie, kein Vergleich möglich);diff === 0gibt einen leeren String zurück (keine Änderung, kein Rauschen anzeigen); andernfalls wird die fettgedruckte vorzeichenbehaftete Differenz zurückgegeben. Beachten Sie, dassprettyBytes(diff)auch negative Zahlen korrekt behandelt und-1.2 kBausgibt, während die Variablesignnur bei positiven Zahlen ein+。

ergänzt.

📎 scripts/size-report.js:80-103

renderUsagesVierter Schritt: usage-Tabelle rendern.renderFilesDer strukturelle Unterschied zu_usages.jsonist beachtenswert: Es importiert direktObject.values(curr), da die usage-Daten fest in dieser einen Datei liegen.prev?.[usage.name]wandelt das Record in ein Array um und sucht dann übernamedie historischen Daten anhand des Namens – genau deshalb wird das.filter(usage => !!usage)-Feld redundant gespeichert.mapDiese Zeile ist tatsächlich redundant, da

immer ein Array-Element zurückgibt und keinen falsy-Wert erzeugen kann.markdown-tableSchließlich wird die Bibliothek📎 scripts/size-report.js:72-74。

mermaid
flowchart TD
    start["run()"] --> rf["renderFiles()"]
    rf --> read_curr["readdir(temp/size)"]
    rf --> read_prev{"existsSync(temp/size-prev)?"}
    read_prev -->|是| read_prev_dir["readdir(temp/size-prev)"]
    read_prev -->|否| empty_prev["prev = []"]
    read_curr --> union["fileList = Set(curr ∪ prev)"]
    read_prev_dir --> union
    empty_prev --> union
    union --> loop{"遍历 fileList"}
    loop -->|每个 file| import_c["importJSON(currPath)"]
    loop -->|每个 file| import_p["importJSON(prevPath)"]
    import_c --> check_curr{"curr 存在?"}
    check_curr -->|否| deleted["push(~~fileName~~)"]
    check_curr -->|是| render_row["push(fileName, size+diff, gzip+diff, brotli+diff)"]
    deleted --> loop
    render_row --> loop
    loop -->|遍历结束| ru["renderUsages()"]
    ru --> import_u["importJSON(_usages.json)"]
    import_u --> table["markdownTable 渲染"]
    table --> out["process.stdout.write(output)"]

Kopieren

Designüberlegungen und Stolperfallen

〔Design-Schlussfolgerungen und Architektur-Abwägungen〕import()WarumreadFileSync?stattimport()verwenden? Dynamische

filterFilesImport-Assertions für JSON sind die Standardpraxis ab Node 20+ und behandeln nativ das Laden von JSON in ESM-Umgebungen. Der Preis ist, dass sie nicht im synchronen Kontext verwendet werden können und jeder Import vom Modul-Cache erfasst wird – aber in diesem Einmal-Skript ist Caching kein Problem.file[0] !== '_'Die-Prüfung vonreaddirDiese Prüfung geht davon aus, dass der Dateiname nicht leer ist. Wennfile[0]einen leeren String zurückgibt (theoretisch unmöglich), istundefined,undefined !== '_'gleich

Behandlung gelöschter Artefakte.Wenn ein Artefakt gelöscht wird, markiert der Bericht es mit Durchstreichung, anstatt es direkt zu entfernen. Das ist beabsichtigtes Design: Maintainer müssen sehen können, „diese Datei ist verschwunden", statt dass sie stillschweigend aus der Tabelle verschwindet. Würde man sie einfach herausfiltern, könnten Leser fälschlich annehmen, das Artefakt habe nie existiert.

11.2 usage-size: Simulation des Import-Szenarios eines echten Nutzers

Intuitives Modell

size-reportsagt dir „wie groß das vollständige Paket ist", aber das beantwortet nicht die Frage, die Nutzer wirklich interessiert: „Wenn ich nurcreateAppverwende, wie viel Code muss ich tatsächlich herunterladen?" Die Größe des vollständigen Pakets enthält viel Code, den du wahrscheinlich nie brauchst (z. B.defineCustomElement、Transition、KeepAlive)。usage-size.jsbesteht darin, einen „typischen Nutzer" zu spielen: eine virtuelle Einstiegsdatei schreiben, die nur bestimmte APIs importiert, mit Rollup bündeln und sehen, wie groß das endgültige Artefakt ist.

Das ist so, als würde ein Restaurant dir nicht sagen „alle Zutaten in der Küche wiegen insgesamt 50 Kilogramm", sondern „wenn du eine Portion Kung Pao Huhn bestellst, sind die tatsächlich verwendeten Zutaten 300 Gramm".

Datenstruktur: Preset-Array

Die zentrale Datenstruktur des Skripts istpresetsArray, wobei jedes Element ein Nutzungsszenario beschreibt:

📎 scripts/usage-size.js:27-55

PresetDer Typ hat drei Felder:name(Anzeigename),imports(Liste der aus Vue importierten APIs), optionalreplace(zusätzliche Compile-Zeit-Ersetzungen). Fünf Presets decken Nutzungsszenarien vom kleinsten bis zum größten ab:

  • createApp (CAPI only): nur importierencreateApp, und__VUE_OPTIONS_API__ersetzen durch'false', Simulation eines reinen Composition-API-Nutzers📎 scripts/usage-size.js:35-40
  • createApp: nur importierencreateApp, Options API beibehalten📎 scripts/usage-size.js:35-40
  • createSSRApp: SSR-Szenario📎 scripts/usage-size.js:35-40
  • defineCustomElement: Web-Components-Szenario📎 scripts/usage-size.js:35-40
  • overall: sechs Kern-APIs importieren, Simulation eines „voll ausgestatteten" Nutzers📎 scripts/usage-size.js:44-54

Die Einstiegsdatei ist fest auf das runtime-only esm-bundler-Artefakt gesetzt:

📎 scripts/usage-size.js:24-28

Auswahlvue.runtime.esm-bundler.jsstatt der vollständigen Versionvue.esm-bundler.js, weil die Runtime-Version keinen Template-Compiler enthält und damit näher an der tatsächlichen Situation moderner Build-Tool-Nutzer liegt – sie verwenden SFCs zum Vorkompilieren von Templates und benötigen keinen Runtime-Compiler.

Step-by-Step Walkthrough

Erster Schritt: Alle Preset-Bundles parallel erzeugen.

📎 scripts/usage-size.js:62-69

main()Für jedes PresetgenerateBundlePromise erstellen, mitPromise.allparallel ausführen. Parallelität ist hier sicher, weil jedergenerateBundleAufruf unabhängig istrollup(), keinen gemeinsamen Zustand teilt.

Zweiter Schritt: Virtuellen Einstieg konstruieren.

📎 scripts/usage-size.js:94-96

Dies ist der raffinierteste Teil des gesamten Skripts. Es schreibt keine temporäre Datei auf die Festplatte, sondern konstruiert eine virtuelle Modul-IDvirtual:entry, deren Inhalt eine re-export-Anweisung ist:export { createApp } from '/absolute/path/to/vue.runtime.esm-bundler.js'. Beachteentryist ein absoluter Pfad, weil Rollup ihn auflösen können muss.

Dritter Schritt: Rollup-Plugin-Kette konfigurieren.

📎 scripts/usage-size.js:98-121

Die Reihenfolge des Plugin-Arrays ist entscheidend:

1. Benutzerdefiniertusage-size-plugin:resolveIdabfangenvirtual:entrygibt sich selbst zurück,loadgibt virtuellen Inhalt zurück📎 scripts/usage-size.js:101-110. Dies ist das Standardmuster für virtuelle Module in Rollup.

2. nodeResolve(): auflösenvue.runtime.esm-bundler.jsinterne import📎 scripts/usage-size.js:111。

3. replace: Compile-Zeit-Konstanten injizieren📎 scripts/usage-size.js:112-119。

replaceDie Plugin-Konfiguration offenbart den Kernmechanismus des esm-bundler-Artefakts: Es behält__VUE_OPTIONS_API__、__VUE_PROD_DEVTOOLS__und andere Runtime-Flags bei, die vom Build-Tool des Nutzers ersetzt werden. Hier übernimmt das Skript die Ersetzung für den Nutzer:

  • process.env.NODE_ENV → "production": Produktionszweig verwenden
  • __VUE_PROD_DEVTOOLS__ → 'false': devtools-Unterstützung deaktivieren
  • __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ → 'false': ausführliche Hydration-Fehlermeldungen deaktivieren
  • __VUE_OPTIONS_API__ → 'true': Options API standardmäßig beibehalten

Dann...preset.replaceerweitern, damit Presets die Standardwerte überschreiben können.createApp (CAPI only)Das Preset nutzt genau diesen Mechanismus, um__VUE_OPTIONS_API__zu ändern in'false' 📎 scripts/usage-size.js:35-40。

preventAssignment: trueErsetzung verhindernobj.process.env.NODE_ENV = xsolcher Zuweisungsanweisungen📎 scripts/usage-size.js:117。

Vierter Schritt: Generieren, Komprimieren, Messen.

📎 scripts/usage-size.js:123-134

result.generate({})Code erzeugen,output[0].codeabrufen. Dann mit SWC komprimieren:

📎 scripts/usage-size.js:125-130

module: truebedeutet, die Eingabe ist ESM,toplevel: trueerlaubt das Komprimieren von Variablennamen im Top-Level-Scope. Nach der Komprimierung werden drei Metriken berechnet:minified.length(Byte-Länge),gzipSync(minified).length、brotliCompressSync(minified).length。

Beachte, dass hiernode:zlibsynchrone API verwendet wird, nicht die asynchrone Version. In einem Einmal-Skript ist die synchrone API prägnanter, und die Komprimierung selbst ist eine CPU-intensive Operation, sodass Asynchronität keinen Parallelitätsgewinn bringt.

Fünfter Schritt: Ausgabe und Persistenz.

📎 scripts/usage-size.js:62-86

Die Ergebnisse werden zunächst in menschenlesbarem Format auf der Konsole ausgegeben, mitpicoeingefärbt📎 scripts/usage-size.js:62-86. Dann intemp/size/_usages.jsonschreiben, mitObject.fromEntriesdas Array zurück in ein Record umwandeln, Schlüssel ist der Preset-Name📎 scripts/usage-size.js:81-85。

--writeDas Flag steuert, ob zusätzlich das unkomprimierte Bundle jedes Presets auf die Festplatte geschrieben wird📎 scripts/usage-size.js:136-138, zum Debuggen.

mermaid
flowchart LR
    subgraph preset_loop["presets 并行遍历"]
        p1["Preset: createApp"]
        p2["Preset: overall"]
    end
    p1 --> virtual["virtual:entry\n'export { createApp } from ...'"]
    p2 --> virtual
    virtual --> rollup["rollup({ input: virtual:entry })"]
    rollup --> resolve["nodeResolve()\n解析 vue.runtime.esm-bundler.js"]
    resolve --> replace["replace()\n__VUE_OPTIONS_API__ 等"]
    replace --> gen["result.generate()\noutput[0].code"]
    gen --> minify["swc.minify(module, toplevel)"]
    minify --> metrics["size / gzipSync / brotliCompressSync"]
    metrics --> json["_usages.json"]

Designüberlegungen und Stolperfallen

〔Design-Inferenz und Architektur-Abwägungen〕

Warum virtuelle Module statt temporärer Dateien?Temporäre Dateien erfordern die Behandlung von Pfaden, Bereinigung und Konflikten bei gleichzeitigen Schreibvorgängen. Virtuelle Module behalten den Einstiegsinhalt im Speicher, und RollupsresolveId/loadHook unterstützt dieses Muster von Natur aus. Der Preis ist, dass die ID exakt übereinstimmen muss; jeder Tippfehler führt dazu, dass Rollup „Einstieg kann nicht aufgelöst werden" meldet.

replaceDiepreventAssignmentFalle.WennpreventAssignment: true,replacenicht gesetzt wird, ersetzt das Plugin auchprocess.env.NODE_ENV = 'x'solche Zuweisungsanweisungen und erzeugt"production" = 'x'Syntaxfehler. Im Vue-Quellcode existieren tatsächlich Zuweisungen anprocess.env.NODE_ENV(in Testwerkzeugen), daher ist diese Option erforderlich.

__VUE_OPTIONS_API__Wahl des Standardwerts.Das Skript setzt den Standardwert auf'true' 📎 scripts/usage-size.js:116, nicht auf'false'. Dies ist eine konservative Wahl: Wenn der Nutzer nichts konfiguriert, behält Vue die Options-API-Unterstützung bei.createApp (CAPI only)Das Preset überschreibt explizit auf'false', um den Größenvorteil nach dem Deaktivieren zu zeigen. Dieser Vergleich ist selbst Dokumentation für den Nutzer: Er zeigt, „wie viel man spart, wenn man die Options API abschaltet".

ParallelPromise.allFehlersemantik.Wenn das Bündeln eines Presets fehlschlägt,Promise.allwird sofort abgelehnt, andere laufende Bundling-Vorgänge werden nicht abgebrochen (Rollup bietet keinen Abbruchmechanismus). In CI bedeutet dies, dass ein Fehler die Berechnung anderer Presets verschwendet, aber das Skript selbst mit einem Nicht-Null-Exit-Code endet, den CI korrekt erfassen kann.

11.3 Von Daten zur Zugangskontrolle: Wie CI diese Berichte konsumiert

Datenfluss-Panorama

Um diese beiden Skripte zu verstehen, muss man sie in die CI-Pipeline einordnen.size-data.ymlläuft bei Push auf main/minor oder bei PRpnpm run size 📎 .github/workflows/size-data.yml:45, erzeugttemp/sizeVerzeichnis, lädt es dann als Artifact hoch📎 .github/workflows/size-data.yml:53-57。

Für PRs schreibt es zusätzlich zwei Metadatendateien:

📎 .github/workflows/size-data.yml:47-51

number.txtspeichert die PR-Nummer,base.txtspeichert den Namen des Zielbranches. Diese beiden Dateien sind genau diesize-report.jsinfilterFilesherauszufilternden.txtDateien📎 scripts/size-report.js:44-45. Sie existieren, damit der nachgelagertesize-report.ymlweiß, „mit welcher Baseline verglichen werden soll".

Abruf und Vergleich der Baseline

size-report.yml(im vorherigen Kapitel ausführlich beschrieben) ist: dassize-dataArtifact des aktuellen PR herunterladen, das Baseline-Artifact des Zielbranches herunterladen, die Baseline nachtemp/size-preventpacken und dannsize-report.jsausführen, um einen Markdown-Bericht zu erzeugen und als Kommentar zum PR hinzuzufügen.

Hier gibt es eine entscheidende Design-Einschränkung:size-report.jsselbst ist nicht für den Abruf der Baseline verantwortlich, es setzt voraus, dasstemp/size-prevbereits existiert. Falls nicht,existsSync(prevDir)gibt false zurück,previst ein leeres Array📎 scripts/size-report.js:48, alle Diffs sind leere Strings. Dies ist eine elegante Degradierung: Ohne Baseline wird der Bericht trotzdem erzeugt, nur ohne Unterschiede anzuzeigen.

Die Entscheidungslogik der Größen-Zugangskontrolle

〔Design-Inferenz und Architektur-Abwägung〕

Ein häufiges Missverständnis muss geklärt werden:size-report.jsselbst trifft keine Zugangsentscheidung. Es erzeugt nur Berichte, gibt keinen Exit-Code zurück, setzt keine Schwellenwerte. Die eigentliche Zugangskontrolle findet auf der Ebene dessize-report.ymlWorkflows statt – dieser kann einen Schritt enthalten, der die Diff-Werte im Bericht parst und den Job fehlschlagen lässt, wenn ein Schwellenwert überschritten wird.

Dieses Design der „Trennung von Messung und Entscheidung" hat einen tiefen Grund: Das Messskript sollte rein bleiben und nur Fakten produzieren; die Entscheidungslogik sollte auf Workflow-Ebene liegen, da Schwellenwerte je nach Version, Branch und Release-Phase variieren können. Schwellenwerte fest insize-report.jszu kodieren würde seine Wiederverwendbarkeit erschweren.

Design-Überlegungen

Warum braucht das Größenbudget zwei Messgrößen?Die vollständige Paketgröße und die Usage-Größe beantworten unterschiedliche Fragen. Die vollständige Paketgröße ist die „Obergrenze" – sie sagt, wie viel der Nutzer im schlimmsten Fall herunterladen muss. Die Usage-Größe ist der „typische Wert" – sie sagt, wie viel die meisten Nutzer tatsächlich herunterladen. Erst beide zusammen ergeben ein vollständiges Größenbild. Gäbe es nur die vollständige Paketgröße, würden Maintainer dazu neigen, seltene APIs übermäßig zu optimieren; gäbe es nur die Usage-Größe, könnten Größencxplosionen in bestimmten Randfällen übersehen werden.

Die Bedeutung der Doppelmetrik gzip und brotli.Moderne CDNs unterstützen brotli weitgehend, aber nicht in allen Szenarien ist es aktiviert. Beide gleichzeitig zu berichten ermöglicht Maintainern einzuschätzen, „wie die Größe in Umgebungen aussieht, die nur gzip unterstützen". brotli ist typischerweise 15-20% kleiner als gzip, und dieser Unterschied selbst ist wertvolle Information.

Der Stabilitätsvertrag des Datenformats. size-report.jsundusage-size.jssind über JSON-Dateien entkoppelt.usage-size.jsschreibt_usages.json,size-report.jsliest es. Die Feldnamen dieses Vertrags (name、size、gzip、brotli) sind implizit, es gibt keine Schema-Validierung. Wennusage-size.jsFeldnamen ändert und vergisst,size-report.jszu synchronisieren, zeigt der Bericht stillschweigend falsche Daten an. Dies ist die Schwachstelle des aktuellen Designs.

Zusammenfassung dieses Kapitels

Überlegungen und Selbsttests zu diesem Kapitel

Q1: size-report.jsvonfilterFilesfiltert Dateien heraus, die mit_beginnen. Wennusage-size.jsdie Ausgabedatei von_usages.jsoninusages.jsonumbenennt, was passiert?

Referenzauflösung:filterFilesDie Filterbedingung vonfile[0] !== '_' && !file.endsWith('.txt') 📎 scripts/size-report.js:44-45istusages.json. Wenn die Datei in_umbenannt wird, beginnt sie nicht mehr mitfilterFiles, wird vonfileListbehalten, geht in dierenderFiles-Vereinigung ein. Dann wirdimportJSONversuchen, sie als Bundle-Datei zu behandeln:Record<string, UsageResult>kann erfolgreich importiert werden (es ist gültiges JSON), aber ihre Struktur istBundleResultstattcurr?.file, daher istundefined,fileNameein leerer String,curr.sizeist ebenfallsundefined,prettyBytes(undefined)wird einen Fehler werfen oder abnormale Ausgabe erzeugen. Dies führt zum Scheitern der Berichtserzeugung. Die Ursache dieses Problems ist, dassfilterFilesdas Dateinamen-Präfix als Unterscheidungskriterium für „Metadaten vs. Daten" verwendet, statt Verzeichnisstruktur oder explizite Manifeste zu nutzen. Robuster wäre, Usage-Daten in einem Unterverzeichnis abzulegen oder eine explizite Liste von Metadatendateien zu pflegen.

Q2: usage-size.jsInPromise.all(tasks)werden alle Presets parallel gebündelt. Wenn diereplace-Konfiguration eines Presets__VUE_OPTIONS_API__auslässt, was passiert? Warum ist der Standardwert'true'statt'false'?

Referenzauflösung:replaceIn der Plugin-Konfiguration ist__VUE_OPTIONS_API__: 'true'der Standardwert, dann wird...preset.replaceexpandiert, um📎 scripts/usage-size.js:116-118zu überschreiben. Wenn ein Preset die Konfiguration auslässt, verwendet es den Standardwert'true', d.h. Options-API-Unterstützung bleibt erhalten, die Größe wird größer. Der Standardwert'true'ist eine konservative Wahl: Er spiegelt „das tatsächliche Verhalten, wenn der Nutzer nichts konfiguriert" wider. In Vue's esm-bundler-Artefakten ist__VUE_OPTIONS_API__das Standardverhalten, die Options-API beizubehalten (es sei denn, der Nutzer deaktiviert sie explizit). Würde man den Standardwert auf'false'setzen, würden alle nicht explizit konfigurierten Presets eine zu kleine Größe anzeigen und Nutzer irreführen zu glauben, „ohne Konfiguration lässt sich Größe sparen".createApp (CAPI only)Das Preset setzt explizit'false' 📎 scripts/usage-size.js:35-40, genau um „den Gewinn nach explizitem Deaktivieren" zu zeigen und einen Kontrast zum Standardwert zu bilden.

Q3: size-report.jsDasimportJSONvonimport()verwendet dynamischesfs.readFileSyncstatttemp/size-prev. Wenn eine JSON-Datei im

-Verzeichnis beschädigt ist (ungültiges JSON), wie unterscheiden sich die beiden Implementierungen?Referenzauflösungimport(): DynamischesSyntaxErrorwirft beim Parsen ungültigen JSONs einenimportJSON, und dieser Fehler kann nicht von derexistsSync-internenexistsSyncEs wird nur geprüft, ob die Datei existiert, nicht ob der Inhalt gültig ist📎 scripts/size-report.js:112-115. Fehler werden nach oben propagiert anrenderFiles, was dazu führt, dass die gesamte Berichterstellung fehlschlägt. Wenn manfs.readFileSync + JSON.parseverwendet, wird ebenfalls ein Fehler geworfen, aber man kann innerhalb vonimportJSONeinen try-catch-Block verwenden undundefinedzurückgeben, um eine elegante Degradierung zu erreichen. Die aktuelle Implementierung lässt Fehler propagieren, mit der impliziten Annahme, dass „das JSON im Artefakt immer gültig ist“ – diese Annahme gilt in CI-Umgebungen normalerweise, da die Dateien vonusage-size.jsund Build-Skripten generiert werden. Beim lokalen Debuggen jedoch, wenn die JSON-Datei manuell geändert und beschädigt wird, stürzt der Bericht direkt ab, anstatt die Datei zu überspringen. Dies ist eine Designentscheidung, die „der Datenquelle vertraut“.

---

Der Größenbudget-Mechanismus löst die Fragen „was messen“ und „wie vergleichen“, aber er setzt eine Prämisse voraus: Das Build-Artefakt selbst ist reproduzierbar. Das nächste Kapitel führt in die minimale Debug-Sandbox ein:vite-debugWie man mit minimaler Konfiguration eine interaktive Vue-Entwicklungsumgebung startet und wie sie mit lokalen Build-Artefakten interagiert, um einen geschlossenen Kreislauf von Quellcode-Änderungen bis zur Laufzeitvalidierung zu bilden.

Damit ist der Messkreislauf des Größenbudgets klar: size-report.js beantwortet mit dem Verzeichnisvergleich „um wie viel größer“, usage-size.js simuliert mit virtuellen Modulen reale Importszenarien und beantwortet „wo größer“, während die Gate-Entscheidung der Workflow-Ebene überlassen bleibt. Dieser Mechanismus verwandelt Größenregressionen von vagen Beschwerden in nachverfolgbare Daten. Aber Daten können nur sagen, dass ein Problem existiert; um es wirklich zu lokalisieren und zu beheben, braucht man eine minimale Umgebung, die das Problem schnell reproduziert. Das nächste Kapitel führt in packages-private/vite-debug ein und zeigt, wie Vue mit Vite + SFC eine minimalistische Debug-Sandbox aufbaut und „minimale Reproduktion auf echtem Quellcode“ zu einer praktikablen Alltagspraxis macht.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 12

Kapitel 12: Minimale Debug-Sandbox: vite-debug und lokaler Entwicklungskreislauf

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 12 von 14

Im vorherigen Kapitel haben wir den Messkreislauf des Größenbudgets abgeschlossen: size-report.js beantwortet „um wie viel größer“, usage-size.js beantwortet „wo größer“, und die Workflow-Ebene ist für die Gate-Entscheidung verantwortlich. Dieser Mechanismus hat jedoch eine implizite Voraussetzung – das Build-Artefakt selbst ist reproduzierbar. Wenn man feststellt, dass ein Paket ungewöhnlich stark an Größe zunimmt oder ein Laufzeitverhalten nicht den Erwartungen entspricht, braucht man eine minimale Umgebung, die lokalen Quellcode schnell lädt und Änderungen sofort sichtbar macht. packages-private/vite-debug ist diese Umgebung. Sie hat nur vier Dateien und insgesamt weniger als 40 Zeilen Code, bildet aber den Einstieg in die Alltagspraxis „minimale Reproduktion auf echtem Quellcode“ im Vue-core-Repository. Dieses Kapitel zerlegt die Konstruktionslogik dieser Sandbox Datei für Datei und erklärt, warum sie unter packages-private und nicht unter packages liegt.

I. Das Skelett der Sandbox:main.tsundApp.vueminimale Mount-Kette

Intuitives Modell

Wenn man die gesamte Vue-Laufzeit mit einem Motor vergleicht, dann istvite-debugein „nackter Prüfstand“ – ohne Gehäuse, ohne Armaturenbrett, nur mit der minimalen Verkabelung, damit der Motor läuft. Sein Wert liegt nicht in funktionaler Vollständigkeit, sondern darin,alle störenden Variablen auszuschließen: Wenn man vermutet, dass ein Bug im Reaktivitätssystem oder im Renderer liegt, möchte man nicht, dass die Komplexität der Debug-Umgebung selbst zur Rauschquelle wird.

Datenstruktur und Dateilayout

Zuerst der gesamte Inhalt vonmain.ts:

📎 packages-private/vite-debug/main.ts:4-4

ts
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.mount('#app')

Diese sechs Zeilen Code sind das Standardparadigma zum Starten einer Vue-Anwendung, aber jede Zeile hat im Debug-Szenario eine präzise technische Bedeutung:

  • L1Inimport { createApp } from 'vue'von'vue'hängt davon ab, wohin der Modulbezeichnervite.config.tsletztlich aufgelöst wird, vollständig von den Abhängigkeitsdeklarationen inpackage.jsonund
  • L2ab. Dies ist der entscheidende Punkt der gesamten Sandbox – wir werden später sehen, wie er auf lokalen Quellcode zeigt.import App from './App.vue'Das@vitejs/plugin-vuevonApp.vuelöst die SFC-Kompilierungspipeline von<script>、<template>、<style>aus: Vite registriert dieses Plugin beim Start des Dev-Servers; wenn der Browser
  • L4anfordert, zerlegt das Plugin es increateApp(App)drei virtuelle Module, die separat kompiliert werden.app._context、app._instanceDas
  • L6vonapp.mount('#app')erstellt die Anwendungsinstanz; zu diesem Zeitpunkt initialisiert Vue internappund andere Kernfelder, löst aber noch kein Rendering aus.

Dasindex.htmlvonindex.htmlist der eigentliche Startschalter: Es sucht im DOM das Containerelement mit der ID<div id="app"></div>, erstellt die Root-Komponenteninstanz und löst das erste Rendering aus.<script type="module" src="/main.ts"></script>Beachten Sie, dass hier kein Verweis aufapp.mount('#app')vorhanden ist – Vites Konvention ist, dass

im Projektstammverzeichnis als Einstiegs-HTML dient, das

undApp.vueenthält. Obwohl diese Datei nicht in den keyFiles dieses Kapitels steht, ist sie die Voraussetzung dafür, dass

📎 packages-private/vite-debug/App.vue:4-8

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

<style>
button {
  color: red;
}
</style>

Szenariogesteuerter Walkthrough: Die vollständige Kette eines KlicksNun betrachten wir

, den „Versuchsträger“ dieser Sandbox:

@vitejs/plugin-vueKopierenApp.vueIn ein konkretes Szenario übertragen:

  • <script setup>Was passiert, wenn der Benutzer im Browser auf die Schaltfläche klickt?setup()Erster Schritt: SFC-Kompilierungsphase (beim Start des Dev-Servers)ref(0)kompiliertRefImplin drei Teile:.valueDer0。
  • <template>-Block wird in die{{ count }}-Funktion der Komponente kompiliert,_toDisplayString(count.value),@click="count++"der Aufruf gibt einonClick: $event => (count.value++)。
  • <style>-Objekt zurück, dessen<style>anfangs

ist. Derapp.mount-Block wird in eine Renderfunktion kompiliert,

createApp(App)wird umgewandelt inmount('#app')wird die Root-Komponente erstelltComponentInternalInstance, ausgeführtsetup()ergibtcountdie RefImpl, und dann wird die Render-Funktion aufgerufen, um den VNode-Baum zu erzeugen. In der Render-Funktion wirdcount.valuegelesen, wastrackauslöst, Abhängigkeiten zu sammeln – der aktuell aktive Render-Effekt (ReactiveEffect) wird incountindepaufgezeichnet.

Dritter Schritt: Klick-Ereignis (bei Benutzerinteraktion)

Der Browser löst dasclick-Ereignis aus, und der Event-Handler von Vue führtcount.value++aus. Dies ist eine Setter-Operation, dietriggerauslöst: Durchlaufen der incount.depgesammelten Effekte und Planen des erneuten Renderns. Da es sich um eine synchrone Aktualisierung handelt und sie sich nicht in der Batch-Warteschlange befindet, wird der Render-Effekt sofort ausgeführt, die Render-Funktion erneut aufgerufen, ein neuer VNode erzeugt, ein Diff mit dem alten VNode durchgeführt, festgestellt, dass sich der Textinhalt von0zu1geändert hat, und das echte DOM aktualisierttextContent。

Die gesamte Kette kann mit dem folgenden Datenflussdiagramm dargestellt werden:

mermaid
flowchart LR
    subgraph compile["编译期 (Vite Dev Server)"]
        sfc["App.vue"] -->|"@vitejs/plugin-vue"| script["setup() 函数"]
        sfc -->|"@vitejs/plugin-vue"| render["渲染函数"]
        sfc -->|"@vitejs/plugin-vue"| style["CSS 模块"]
    end
    subgraph runtime["运行时 (浏览器)"]
        script -->|"ref(0)"| refimpl["RefImpl { value: 0 }"]
        render -->|"读取 count.value"| track["track() 收集依赖"]
        click["用户点击"] -->|"count.value++"| trigger["trigger() 触发更新"]
        trigger -->|"调度渲染副作用"| rerender["重新执行渲染函数"]
        rerender -->|"diff + patch"| dom["更新真实 DOM"]
    end
    track -.->|"dep 记录 ReactiveEffect"| trigger

Der Schlüssel an diesem Diagramm ist:Es gibt nur zwei Kopplungspunkte zwischen den Compile-Zeit-Artefakten und dem Laufzeitverhalten——ref(0)das zurückgegebene RefImpl-Objekt sowie das Lesen und Schreiben voncount.valuein der Render-Funktion. Das bedeutet, wenn du einen bestimmten Zweig des Reaktivitätssystems debuggen möchtest (zum Beispieltriggerdie Scheduling-Logik in), musst du nur in diesemApp.vuedas entsprechende Lese-/Schreibmuster konstruieren.

Designüberlegung: Warumrefstattreactive?

〔Design-Inferenz und Architektur-Abwägung〕

Die Wahl vonref(0)stattreactive({ count: 0 })als Standardbeispiel impliziert eine Debug-Prioritätsüberlegung:refder.value-Zugriffspfad ist kürzer, beim Aufklappen im DebuggerRefImplkönnen interne Felder wie_value、dep、__v_isRefdirekt gesehen werden, währendreactivedas von einem Proxy-Objekt zurückgegebene Proxy beim Aufklappen in der Konsole Getter auslöst, was die Beobachtung des ursprünglichen Zustands stören kann. Für das Szenario „minimale Reproduktion“ bedeutet eine weniger Proxy-Indirektionsschicht weniger Variablen.

---

Zwei, Alias-Auflösung:vite.config.tsundpackage.jsonwie man'vue'auf den lokalen Quellcode zeigt

Intuitives Modell

vite.config.tshat nur sechs Zeilen, aber es ist das „Routing-Zentrum“ der gesamten Sandbox – es entscheidet, obimport { createApp } from 'vue'in'vue'letztendlich die veröffentlichte Version von npm lädt oder den Quellcode, der sich im Repository in Entwicklung befindet. Ohne die richtige Alias-Konfiguration könnte der Code, den du inApp.vueänderst, möglicherweise überhaupt nicht den Vue-Quellcode auslösen, den du gerade debuggst, und das Debugging wird zu „auf das falsche Ziel schießen“.

Datenstruktur und Auflösungskette

Schauen wir zuerst aufvite.config.ts:

📎 packages-private/vite-debug/vite.config.ts:4-6

ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
})

Hiergibt es keine expliziteresolve.alias-Konfiguration. Wie wird also'vue'zum lokalen Quellcode aufgelöst? Die Antwort liegt inpackage.json:

📎 packages-private/vite-debug/package.json:1-15

json
{
  "name": "vite-debug",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "catalog:",
    "vite": "catalog:",
    "vue": "workspace:*"
  }
}

Der Schlüssel liegt inL13:"vue": "workspace:*". Dies ist die Deklaration des pnpm-Workspace-Protokolls und bedeutet, dassvite-debugvon dem lokalen Paket namensvueim Monorepo abhängt und nicht von der Version in der npm-Registry. pnpm erstellt innode_modules/vueeinen symbolischen Link, der aufpackages/vuezeigt (das Hauptpaketverzeichnis von Vue).

Aber das reicht noch nicht –packages/vueinpackage.jsondasmain/module/exports-Feld zeigt normalerweise aufBuild-Artefakte(wiedist/vue.runtime.esm-bundler.js) und nicht auf den Quellcode untersrc/. Wenn dupackages/runtime-core/src/renderer.tsänderst, aber nicht neu baust, lädt Vite weiterhin die altedist-Datei.

〔Design-Inferenz und Architektur-Abwägung〕

Deshalb wird impackages/vue/package.jsondes Vue-Core-Repositories normalerweise"development"eine bedingte Export-Konfiguration oder ein ähnliches Quellcode-Einstiegsmapping konfiguriert – im Dev-Modus bevorzugt Vitesresolve.conditionsdiedevelopment-Bedingung und lädt dadurchsrc/index.tsstattdist. Dieser Mechanismus ermöglicht esvite-debug, ohne explizite Alias-Konfiguration nach Änderungen am Quellcode sofort per HMR die Wirkung zu sehen.

Szenariogesteuerter Walkthrough: Einimport 'vue'Auflösungsprozess

In das Szenario eintauchen:Wenn der Vite-Dev-Server die Anfrage des Browsers nachmain.tserhält und aufimport { createApp } from 'vue'trifft, wie sieht dann die Auflösungskette aus?

mermaid
flowchart TD
    req["浏览器请求 /main.ts"] --> parse["Vite 解析 import 'vue'"]
    parse --> resolve{"resolve 条件匹配"}
    resolve -->|"development 条件命中"| src_entry["packages/vue/src/index.ts"]
    resolve -->|"仅 production 条件"| dist_entry["packages/vue/dist/vue.runtime.esm-bundler.js"]
    src_entry -->|"源码模块图"| hmr["HMR 监听 src/ 变更"]
    dist_entry -->|"预构建产物"| no_hmr["无源码级 HMR"]
    hmr -->|"修改 renderer.ts"| reload["浏览器热更新"]
    no_hmr -->|"修改 renderer.ts"| stale["仍加载旧产物"]
    reload --> verify["验证行为变更"]
    stale --> rebuild["需手动重新构建"]
    rebuild --> verify

Dieses Flussdiagramm offenbart einen entscheidenden Zweig:Wenn diedevelopment-Bedingung nicht korrekt konfiguriert ist, wird der Browser nach Änderungen am Quellcode nicht hot-updaten, und du gerätst in die Verwirrung „Code geändert, aber Verhalten unverändert“. Die Fehlersuche besteht darin, im Network-Panel der Browser-DevTools den tatsächlichen Ladepfad desvue-Moduls zu prüfen – wenn du dendist/-Pfad siehst, bedeutet das, dass das Quellcode-Einstiegsmapping nicht wirksam ist.

Designüberlegung: Warum nicht invite.config.tsexplizit einen Alias schreiben?

〔Design-Inferenz und Architektur-Abwägung〕

Eine natürliche Frage ist: Warum nicht direkt invite.config.tsschreibenresolve: { alias: { vue: '../../packages/vue/src/index.ts' } }? Das ist zwar intuitiv, hat aber zwei Probleme:

1. Es zerstört Subpfad-Importe: Die öffentliche API von Vue enthältvue/server-renderer、vue/compiler-sfcund andere Subpfade. Wenn nur'vue'selbst aliast wird, laufen Subpfad-Importe weiterhin überdist, was dazu führt, dass einige Module aus dem Quellcode und andere aus den Build-Artefakten stammen, mit inkonsistentem Verhalten.

2. Es umgeht den bedingten Exportmechanismus: In Vuespackage.jsonhat dasexports-Feld bereits ein vollständiges bedingtes Export-Mapping definiert (development/production/browser/nodeusw.), und der Alias würde diesen Mechanismus überschreiben, sodass die Auflösung im Debugging-Umfeld von der im echten Benutzerumfeld abweicht.

Dahervite-debugwähltpackage.jsondie Kombination „Workspace-Protokoll vertrauen + bedingte Exporte“, um die Auflösungskette so nah wie möglich am realen Nutzungsszenario zu halten. Das erklärt auch, warum"vue": "workspace:*"innode_modules/vueerforderlich ist – es ist die Voraussetzung dafür, den pnpm-Symlink auszulösen und Vite dadurchpackages/vuefinden zu lassen.

Produktions-Fallstricke:catalog:Protokoll- und Versionsdrift

Beachtepackage.jsoninL11-L12verwendet das"catalog:"-Protokoll:

json
"@vitejs/plugin-vue": "catalog:",
"vite": "catalog:",

Dies ist eine pnpm-Catalog-Funktion und bedeutet, dass die Versionsnummer zentral über daspnpm-workspace.yaml-Feld incatalogverwaltet wird. Ihre Aufgabe ist es,Versionsdrift zu vermeiden, wenn im Monorepo mehrere Pakete dieselbe Abhängigkeit referenzieren。

〔Design-Inferenz und Architektur-Abwägung〕

Im Debugging-Szenario bringt dies eine versteckte Falle mit sich: Wenn du invite-debugBei einem vermuteten Bug in Vite oder plugin-vue möchte man temporär die Version aktualisieren, um dies zu verifizieren. Direktes Ändern vonpackage.jsonincatalog:ist wirkungslos – man muss die catalog-Definition inpnpm-workspace.yamländern, was alle Pakete betrifft, die diesen catalog verwenden. Die korrekte Vorgehensweise ist, temporär eine explizite Versionsnummer zu verwenden (z. B."vite": "5.0.0"), und nach der Verifizierung wieder aufcatalog:。

---

Drei,packages-privateIsolationsdesign: Warum die Debug-Sandbox nicht veröffentlicht wird

Intuitives Modell

packages-privateDas Verzeichnis ist wie ein „internes Labor" eines Unternehmens – die darin enthaltenen Muster werden nicht extern verkauft, sondern nur für Tests und Demonstrationen verwendet. Es ist physisch vompackagesVerzeichnis isoliert, um zu verhindern, dass Debug-Code versehentlich auf npm veröffentlicht wird.

Drei Schutzschichten des Isolationsmechanismus

Erste Schicht: Verzeichnisisolation

packages-private/vite-debugbefindet sich nicht unterpackages/, währendpnpm-workspace.yamlnormalerweise sowohlpackages/*als auchpackages-private/*als Workspace-Mitglieder deklariert, aber das Veröffentlichungsskript (z. B.scripts/release.js) nur die Pakete unterpackages/durchläuft.

Zweite Schicht:private: true

📎 packages-private/vite-debug/package.json:3

json
"private": true,

Diese Zeile ist eine harte Einschränkung von npm/pnpm: Pakete, die alsprivatemarkiert sind,können niemals durchnpm publishveröffentlicht werden, selbst bei manueller Ausführung wird dies abgelehnt. Dies ist die letzte Verteidigungslinie gegen versehentliche Veröffentlichung.

Dritte Schicht: Keinversion-Feld

Beachten Sie, dasspackage.jsonkeinversion-Feld enthält. Die npm-Spezifikation verlangt, dass veröffentlichbare Paketeversionhaben müssen; Pakete ohne dieses Feld führen beinpm publishzu einem Fehler. Dies ist eine „doppelte Absicherung" – selbst wennprivateversehentlich gelöscht wird, verhindert das fehlendeversionweiterhin die Veröffentlichung.

Designüberlegung: Arbeitsteilung zwischen Debug-Sandbox und Playground

Im Vue-Core-Repository gibt es bereits einen voll funktionsfähigenSFC Playground(in Kapitel 7 diskutiert). Warum wird dann nochvite-debug?

benötigt? 〔Design-Inferenz und Architektur-Abwägung〕

Die Positionierungen der beiden sind grundlegend verschieden:

DimensionSFC Playgroundvite-debug
LaufzeitumgebungIm Browser (Kompilierung ebenfalls im Browser)Node.js + Browser
Quellcode-LadenÜber CDN oder vorgefertigte ArtefakteDirektes Laden des lokalen Quellcodes
Debug-FähigkeitBeschränkt durch Browser-SandboxNode.js-Debugger, Breakpoints verfügbar
Quellcode-ÄnderungNicht unterstütztHMR unterstützt
AnwendungsszenarienKompilierungsausgabe verifizieren, Reproduktionen teilenInternes Laufzeitverhalten debuggen

vite-debugDer Kernwert vonliegt darin, dass es in einer echten Node.js-Umgebung läuft, man kann mitnode --inspecteinen Debugger anhängen, inpackages/reactivity/src/effect.tsBreakpoints setzen und den Erstellungs- und Scheduling-Prozess vonReactiveEffectbeobachten. Dies kann der Playground nicht bieten.

Produktions-Fallstricke: HMR-Grenzen und Zustandsverlust

〔Design-Inferenz und Architektur-Abwägung〕

Bei der Verwendung vonvite-debugzum Debuggen gibt es eine häufige Verwirrung: Nach Änderung desApp.vue-Anfangswerts incountwird der Zähler im Browser nicht zurückgesetzt. Dies liegt daran, dass Vites HMR<script setup>-Blöcke so behandelt, dassder Komponentenzustand beibehalten und nur die Render-Funktion ersetzt wird. Wenn Sie den Zustand vollständig zurücksetzen müssen, müssen Sie die Seite manuell aktualisieren oderApp.vueinimport.meta.hot?.invalidate()hinzufügen, um ein vollständiges Seiten-Refresh zu erzwingen.

Eine weitere Falle: Wenn Sie den Quellcode unterpackages/runtime-core/src/ändern, wird die HMR-Propagierungskette möglicherweise nicht automatisch ausgelöst – weilvite-debugdie HMR-Grenze auf der Ebene vonApp.vuedefiniert ist, während Quellcode-Änderungen unterpackages/durch Vites Modulgraph propagiert werden müssen. Wenn der Browser nach einer Quellcode-Änderung nicht reagiert, prüfen Sie die Vite-Terminalausgabe aufhmr update-Logs; falls keine vorhanden sind, muss möglicherweise der Dev-Server neu gestartet werden.

---

Kapitelzusammenfassung

packages-private/vite-debugMit vier Dateien und weniger als 40 Zeilen Code wird ein vollständiger Debug-Kreislauf aufgebaut:

1. main.tsBietet eine minimale Mount-Kette:createApp(App).mount('#app'), unter Ausschluss jeglicher nicht notwendiger Initialisierungslogik.

2. App.vueAls Experimentträger:ref+ Template-Interpolation + Event-Handling, deckt den Hauptpfad des Reaktivitätssystems ab.

3. vite.config.ts + package.jsonDurch dasworkspace:*-Protokoll und bedingte Exporte wird'vue'auf den lokalen Quellcode aufgelöst, wodurch „Quellcode-Änderung sofort wirksam" erreicht wird.

4. packages-private + private: true+ keinversionDreischichtige Isolation, um sicherzustellen, dass Debug-Code nicht versehentlich veröffentlicht wird.

Die Ingenieursphilosophie dieser Sandbox ist:Die Komplexität der Debug-Umgebung selbst sollte gegen null gehen, die gesamte Komplexität dem zu debuggenden Quellcode überlassen. Wenn Sie inpackages/reactivityauf einen schwer reproduzierbaren Bug stoßen,vite-debugbietet

eine Experimentierplattform, die beliebig geändert und sofort verifiziert werden kann.

Kapitel-Überlegungen und Selbsttestpackage.jsonQ1: Wenn man"vue": "workspace:*"in"vue": "^3.4.0"zuvite-debugändert, was ändert sich im Browser-Verhalten, nachdem manpackages/reactivity/src/ref.tsin

geändert hat? Warum?Referenzanalyse"^3.4.0": Nach der Änderung zupackages/vue 📎 packages-private/vite-debug/package.json:13lädt pnpm die veröffentlichte Version von Vue 3.4.x vom npm registry herunter, anstatt auf das lokaleimport { createApp } from 'vue'zu verlinken. Zu diesem Zeitpunkt wirdnode_modules/.pnpm/vue@3.4.x/node_modules/vue/dist/vue.runtime.esm-bundler.jsaufpackages/reactivity/src/ref.tsaufgelöst, also das vorgefertigte Artefakt. Änderungen anreflösen kein HMR aus, da Vites Modulgraph diese Datei überhaupt nicht enthält. Im Browser läuft weiterhin die npm-Version derworkspace:*-Implementierung. Dieses Experiment verifiziert umgekehrt, dass

Q2: App.vueeine notwendige Bedingung für Quellcode-Level-Debugging ist.<style>Derscoped-Block invite-debughat kein

hinzugefügt. Wenn in dieser Sandbox zwei Komponenteninstanzen gleichzeitig gemountet werden, was passiert mit den Styles? In welcher Beziehung steht dies zum Debug-Ziel von?scopedReferenzanalysebutton { color: red }: Ohne📎 packages-private/vite-debug/App.vue:4-8ist<button>ein globales Stylevite-debug, das auf allescoped-Elemente der Seite wirkt. Wenn zwei Komponenteninstanzen gemountet werden, werden die Buttons beider Instanzen rot. Die Beziehung zum Debug-Ziel besteht darin:data-v-xxxist als „minimale Reproduktion" positioniert, nicht als „Style-Isolationsverifikation". Das Weglassen vonscopedreduziert die zur Kompilierungszeit injiziertescoped-Attributvariable, wodurch die DOM-Struktur im Debugger sauberer wird. Wenn Sie die Kompilierungslogik von@vitejs/plugin-vue-Styles debuggen müssen, sollten Sie explizit

hinzufügen und den generierten Attribut-Injektionscode vonpackages/runtime-core/src/renderer.tsbeobachten.patchQ3: Angenommen, Sie haben in derconsole.log-Funktion von

eine Zeile:

hinzugefügt, aber die Browser-Konsole gibt nichts aus. Bitte listen Sie mindestens drei mögliche Ursachen auf und erklären Sie, wie Sie diese einzeln untersuchen würden.Referenzanalyse。'vue'Ursache eins:distQuellcode-Einstiegspunkt nicht wirksamsrc. Fehlersuche: Im DevTools Network-Panel prüfenvueModul-Ladepfad, wenn er mitdist/beginnt, bedeutet dies, dass der bedingte Export nicht übereinstimmtdevelopmentBedingung📎 packages-private/vite-debug/package.json:13。

Ursache 2:HMR wurde nicht propagiert. Vite's Modulgraph hat die Änderungen vonpackages/runtime-core/src/renderer.tsnicht anvite-debugpropagiert. Fehlersuche: Prüfen, ob das Vite-Terminalhmr updateLogs anzeigt; falls nicht, den Dev-Server neu starten.

Ursache 3:patchFunktion wurde nicht aufgerufen. Wenn die aktuelle Seite keine DOM-Aktualisierung auslöst (z. B. kein Button-Klick),patchwird möglicherweise nur beim ersten Mounten einmal ausgeführt, und das erste Mounten fand statt, bevor duconsole.loghinzugefügt hast. Fehlersuche: Seite neu laden oder inApp.vueeine Aktion hinzufügen, die eine Aktualisierung auslöst.

Ursache 4 (Ergänzung):Build-Cache. Vite's Dependency-Prebuild-Cache (node_modules/.vite) verwendet möglicherweise noch die alte Version. Fehlersuche:node_modules/.vitelöschen und neu starten.

---

Das Größenbudget sagt dir „Das Problem existiert“,vite-debuglässt dich „das Problem selbst reproduzieren“. Aber wenn du versuchst, dieses Sandbox-Muster auf das gesamte Monorepo zu übertragen, stößt du auf eine Reihe von Randbedingungen: Unterschiede bei der Auflösung des Workspace-Protokolls in CI-Umgebungen,catalog:das Upgrade-Dilemma der Versionssperrung,packages-privateundpackagesdie Richtungsbeschränkung der Abhängigkeiten zwischen... Das nächste Kapitel behandelt Architektur-Abwägungen und Fallstrick-Vermeidung und systematisiert die Randbedingungen, die die Monorepo-Engineering in realen Projekten offenlegt.

Damit haben wir den Engineering-Kreislauf von der Größenmessung bis zur minimalen Reproduktion abgeschlossen: vite-debug macht mit minimalistischen vier Dateien „schnelle Validierung am echten Quellcode“ zu einer alltäglich nutzbaren Praxis. Doch wenn du beginnst, dieses System wirklich nachzubauen, wirst du weitere verborgene Abwägungen entdecken – warum muss packages-private physisch von packages getrennt sein? Warum muss das Enum-Inlining vor Rollup abgeschlossen sein? Das nächste Kapitel fasst die entscheidenden Entscheidungspunkte und Produktions-Fallstricke aus den ersten zwölf Kapiteln zusammen und bietet dir eine vollständige Checkliste zur Fallstrick-Vermeidung und Entscheidungsgrundlage.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 13

Kapitel 13: Architektur-Abwägungen und Fallstrick-Vermeidung: Randbedingungen der Monorepo-Engineering

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 13 von 14

Im vorherigen Kapitel haben wir mitpackages-private/vite-debugals Einstieg das Debugging-Paradigma der minimalen Reproduktion am echten Quellcode gemeistert. Wenn solche internen Debug-Pakete immer mehr werden, taucht ein reales Problem auf: Sie teilen sich denselben Workspace mit den offiziell veröffentlichten Paketen – wie stellt man sicher, dass der Release-Prozess sie nicht versehentlich beeinträchtigt? Dieses Kapitel geht tief in die Randbedingungen der Monorepo-Engineering ein, ausgehend vom Doppelverzeichnis-Vertrag vonpackagesundpackages-private, analysiert die defensiven Designentscheidungen hinter den Architektur-Abwägungen und gibt umsetzbare Hinweise zur Fallstrick-Vermeidung.

13.2 Zeitliche eiserne Regel: Enum-Inlining muss vor Rollup ausgeführt werden

Intuitives Modell

Enum-Inlining ist wie „vor dem Verpacken die Etiketten auf den Teilen durch Zahlen ersetzen“. Wenn der Verpackungsarbeiter (Rollup) bereits mit dem Packen begonnen hat und du dann die Etiketten änderst, passen die Teile in der Kiste und die Etiketten nicht mehr zusammen.build.jsverwendetscanEnums() / removeCache()dieses Funktionspaar, um das Inlining strikt vor Rollup einzuklemmen.

Datenstruktur und Lebenszyklus

inline-enums.jsexportiertscanEnums()gibt eineremoveCacheClosure zurück, die Enum-Definitionen im Quellcode scannt und temporäre Dateien für Rollup zur Konsumption generiert📎 scripts/build.js:30-34。build.jsvonrun()verwendettry/finallyum die Cache-Bereinigung sicherzustellen📎 scripts/build.js:81-112:

js
const removeCache = scanEnums()
try {
  // ... buildAll / checkAllSizes / build-dts
} finally {
  removeCache()
}

rollup.config.jsruft auf ModulebeneinlineEnums()auf, um[enumPlugin, enumDefines] 📎 rollup.config.js:47-50zu erhalten, wobeienumPluginin das plugins-Array eingefügt wird📎 rollup.config.js:331-331,enumDefinesund in die Ersetzungstabelle des replace-Plugins aufgenommen wird📎 rollup.config.js:222-223。

Step-by-Step: Der vollständige Lebenszyklus eines Enums in einem Build

1. build.jsvonrun()ruft zuerstscanEnums()auf, scannt die Enum-Definitionen aller Pakete und schreibt sie in den temporären Cache, gibt zurückremoveCache 📎 scripts/build.js:87-87。

2. buildAllstartet mehrere Rollup-Prozesse parallel📎 scripts/build.js:119-121。

3. Jeder Rollup-Prozess führt in der KonfigurationsladephaseinlineEnums()aus, liest den im vorherigen Schritt generierten Cache und erhältenumPluginundenumDefines 📎 rollup.config.js:47-50。

4. enumPluginersetzt in der Transform-Phase Enum-Referenzen im Quellcode durch Literale;enumDefinesergänzt replace und behandelt modulübergreifende Konstantenersetzung📎 rollup.config.js:222-223。

5. Build endet,finallyBlock ruftremoveCache()auf, um temporäre Dateien zu bereinigen📎 scripts/build.js:119-121。

mermaid
flowchart LR
  src["源码 enum 定义"] --> scan["scanEnums()<br/>scripts/inline-enums.js"]
  scan --> cache["临时缓存文件"]
  cache --> inline["inlineEnums()<br/>rollup.config.js"]
  inline --> plugin["enumPlugin<br/>transform 阶段替换"]
  inline --> defines["enumDefines<br/>replace 替换表"]
  plugin --> bundle["Rollup 产物<br/>字面量已内联"]
  defines --> bundle
  bundle --> cleanup["removeCache()<br/>finally 块"]

Designüberlegungen und Fallstricke

〔Design-Inferenz und Architektur-Abwägung〕

Warum nicht ein Rollup-Plugin verwenden, das in der Transform-Phase direkt scannt und verwendet? Weil Enum-Inlining einepaketübergreifende globale Sicht:runtime-corebenötigt – referenzierte Enums können inshareddefiniert sein, ein einzelner Rollup-Prozess sieht nur seinen eigenen Paket-Quellcodebaum und kann keine paketübergreifende Ersetzung durchführen.scanEnums()Vor dem Build einen globalen Cache anzulegen, löst genau dieses Sichtbarkeitsproblem.

Produktions-Fallstrick:removeCache()infinallyzu platzieren bedeutet, dass auch bei einem Fehler mitten im Build bereinigt wird. Aber wenn du beim Debuggen den Prozess manuell unterbrichst (Ctrl+C),finallymöglicherweise nicht ausgeführt wird, und zurückbleibende Cache-Dateien dazu führen, dass der nächste Build veraltete Enums liest. Fehlersuche: Prüfen, ob imtemp/-Verzeichnis zurückbleibende Enum-Cache-Dateien vorhanden sind, manuell löschen und erneut versuchen.

---

13.3 Release-Orchestrator:release.jsdie Skip-Flag-Matrix von

Intuitives Modell

release.jsist wie der Hochzeitsregisseur,skipBuild / skipTests / skipGit / skipPromptsdie vier Schalter sind die Buttons für „Probe überspringen“, „Eid überspringen“, „Fotos überspringen“, „Bestätigung überspringen“. Jeder Button entspricht einem realen Szenario: CI-Umgebungen benötigenskipPrompts, lokales Debugging benötigtskipGit, dringende Hotfixes benötigenskipTests。

Datenstruktur und Standardwerte der Flags

Die vier Skip-Flags werden inparseArgsdeklariert📎 scripts/release.js:39-50und anschließend in lokale Variablen destrukturiert📎 scripts/release.js:64-66:

js
let skipTests = args.skipTests
const skipBuild = args.skipBuild
const skipPrompts = args.skipPrompts
const skipGit = args.skipGit

BeachteskipTestsverwendetletDeklaration, da sie inrunTestsIfNeeded()dynamisch umgeschrieben wird📎 scripts/release.js:281-317。

Step-by-Step: Der vollständige Entscheidungsfluss eines Releases

main()die Ausführungsreihenfolge📎 scripts/release.js:143-279:

1. Remote-Synchronisationsprüfung:isInSyncWithRemote()Vergleicht lokalen HEAD mit dem Remote-Branch-SHA und zeigt bei Abweichung einen Bestätigungsdialog an📎 scripts/release.js:337-363。

2. Versionsauswahl: Ohne Positionsargument wirdversionIncrementsAuswahlmenü angezeigt📎 scripts/release.js:152-176。

3. Testentscheidung:runTestsIfNeeded()ist der Bereich mit der dichtesten skip-Logik📎 scripts/release.js:281-317。

4. Versionsaktualisierung:updateVersions()Durchläuft alle Pakete und schreibtpackage.json 📎 scripts/release.js:377-398。

5. Changelog-Generierung: Ruft aufpnpm run changelog 📎 scripts/release.js:211-212。

6. Git-Commit:skipGitWird bei Wahrheit vollständig übersprungen📎 scripts/release.js:231-240。

7. Veröffentlichung: Nur wennargs.publishwahr ist, wird ausgeführtbuildPackages() + publishPackages() 📎 scripts/release.js:243-246。

runTestsIfNeeded()Die Branch-Logik verdient eine separate Betrachtung:

mermaid
flowchart TD
  entry["runTestsIfNeeded()"] --> skipFlag{"skipTests?"}
  skipFlag -->|是| done["Tests skipped"]
  skipFlag -->|否| ci["getCIResult()"]
  ci --> ciPass{"CI passed?"}
  ciPass -->|是| promptMode{"skipPrompts?"}
  promptMode -->|是| setSkip["skipTests = true"]
  promptMode -->|否| ask["prompt: Skip local tests?"]
  ask --> setSkip2["skipTests = promptSkipTests"]
  ciPass -->|否| noPrompt{"skipPrompts?"}
  noPrompt -->|是| throwErr["throw Error<br/>CI not passed"]
  noPrompt -->|否| runLocal["run('pnpm', ['run','test','--run'])"]
  setSkip --> done
  setSkip2 --> done
  runLocal --> done

Designüberlegungen und Fallstricke

〔Designinferenz und Architekturabwägung〕

skipTestsVerwendetletstattconstDas Design dient dazu, den Optimierungspfad „CI bestanden, lokale Tests automatisch überspringen" zu unterstützen. Dies spart im CI-Release-Szenario erheblich Zeit – GitHub Actions'release.ymlhat bereits vollständige Tests durchlaufen, ein erneuter lokaler Durchlauf wäre reine Verschwendung.

Der versteckte Vertrag der Veröffentlichungsreihenfolge:sortPackagesForPublishingPlatziertvuean letzter Stelle📎 scripts/release.js:85-85, und der Kommentar stellt ausdrücklich klar: „Benutzer dürfen das neue Einstiegspaket nicht installieren, bevor die internen Pakete verfügbar sind." Wenn Sie diese Reihenfolge ändern, könnte der Benutzer beinpm install vue@nexteine Version beziehen, deren Abhängigkeiten noch nicht veröffentlicht sind, was zuERR_MODULE_NOT_FOUND。

Idempotenzschutz:publishPackageRuft vor der VeröffentlichungisPackagePublishedauf, um die Registry zu prüfen📎 scripts/release.js:453-458, fängt bei Veröffentlichungsfehlernpreviously publishedFehler ab und degradiert zum Überspringen von📎 scripts/release.js:480-488. Dadurch kann das Release-Skript sicher wiederholt werden – nach einer Netzwerkunterbrechung schlägt die erneute Ausführung nicht wegen „Paket existiert bereits" vollständig fehl.

Fehler-Rollback:fnToRun().catch()Ruft auf, wennversionUpdatedwahr istupdateVersions(currentVersion)Rollt die Versionsnummer zurück📎 scripts/release.js:528-537. Beachten Sie jedoch: Dies rollt nurpackage.jsondas Versionsfeld inzurück,git commitsetzt bereitsCommits nicht zurückskipGit. Wenn Sie beigit reset。

---

als falsch die Veröffentlichung fehlschlägt, müssen Sie manuell

Designüberlegung: Das gemeinsame Muster der drei AbwägungenBetrachtet man die drei Kernabwägungen dieses Kapitels, teilen sie dieselbe Designphilosophie:。

  • packages-private„Leicht vergessliche Laufzeitprüfungen" in „unmöglich zu umgehende strukturelle Constraints" umwandelnprivatePhysische Isolation: Es wird nicht darauf vertraut, dass der Skriptautor daran denkt,
  • das Feld zu prüfen, sondern der Scan-Bereich schließt es von Natur aus aus.
  • release.jsEnum-Inlining-Vorverlagerung: Es wird nicht darauf vertraut, dass das Rollup-Plugin beim Transform „zufällig" paketübergreifende Enums sieht, sondern vor dem Build ein globaler Cache aufgebaut.skipTests。
Die skip-Matrix: Es wird nicht darauf vertraut, dass der Veröffentlicher daran denkt, „bei bestandener CI keine lokalen Tests auszuführen", sondern das Skript fragt automatisch den CI-Status ab und schreibt

〔Designinferenz und Architekturabwägung〕Der Preis dieses Musters ist:build.jssteigende SkriptkomplexitätprivatePackagesEs müssenrollup.config.jsListen gepflegt werden,release.jsdie Verzeichniserkennungslogik muss dupliziert werden,

---

die Kreuzkombinationen der vier skip-Flags müssen behandelt werden. Doch für ein Repository wie Vue, das mehrmals wöchentlich veröffentlicht, überwiegt der Zuverlässigkeitsgewinn durch strukturelle Constraints bei Weitem die Komplexitätskosten.

Kapitelzusammenfassung

1. packages-privateDieses Kapitel hat ausgehend vom Quellcode drei entscheidende Randbedingungen des Vue-Core-Engineering-Systems herausgearbeitet:packagesDie physische Isolation vonundbuild.jswird durch drei Stellen gemeinsam gewährleistet: Workspace-Glob,release.jsVerzeichniserkennung,📎 pnpm-workspace.yaml:1-3📎 scripts/build.js:153-170📎 scripts/release.js:68-83。

2. FilterungDie Zeitliche Constraint des Enum-InliningsscanEnums() / removeCache()wird durchtry/finallydie📎 scripts/build.js:81-112📎 rollup.config.js:47-50。

3. release.jsStruktur vonerzwungen, die Rollup-Konfiguration konsumiert den Cache auf ModulebeneskipTestsDie skip-Flag-Matrix von📎 scripts/release.js:281-317📎 scripts/release.js:85-85。

dient drei Szenarien: CI-Release, lokales Debugging und Notfall-Hotfix,

die dynamische Umschreibung und die Sortierung der Veröffentlichungsreihenfolge sind die beiden am leichtesten übersehenen versteckten Verträgebuild.jsKapitelüberlegungen und Selbsttestbuild(target)Q1: Wenn man inprivatePackages.includes(target)in derpackagesFunktion diepkgBasePrüfung entfernt und einheitlich

als:build.js:160-164verwendet, in welchen Szenarien würde es Probleme geben?nr build vite-debugReferenzanalysepackages/vite-debugDie Verzeichniserkennung vonpackage.jsonist der einzige Einstiegspunkt, über den private Pakete gebaut werden können. Nach dem Entfernenfs.readFileSyncwirdENOENTunterpackages/nachbuildOptionsgesucht, aber dieses Verzeichnis existiert nicht,rollup.config.js:37-42wirft direktbuild.js. Das verstecktere Problem ist: Wenn in Zukunft jemand unter

Q2: release.jsein gleichnamiges Verzeichnis erstellt, verwendet der Build stillschweigend die Konfiguration des falschen Verzeichnisses, und die Ausgabepfade sowierunTestsIfNeeded()sind alle verschoben. Darüber hinausskipTests ||= isCIPassedhatrelease.js:285eine unabhängige Verzeichniserkennungslogik, beide Stellen müssen synchron geändert werden, sonst entsteht der inkonsistente Zustand „skipPromptshat das Paket gefunden, aber Rollup nicht".else if (skipPrompts)Inthrowvon

,welche Zeile Code (skipPrompts) inskipTests ||= isCIPassedwennisCIPassedwahr ist und CI nicht bestanden hat, welchen Branch würde sie nehmen? Wenn manfalse,skipTestsdenfalsedeselse if (skipPrompts)Branches entfernt, welche Konsequenzen hätte das?Error(release.js:299-304Referenzanalysethrow: Wennif (!skipTests)wahr ist und CI nicht bestanden hat,pnpm run test --runin

Q3: rollup.config.js:55istinlineEnums()aufbuild.js:87bleibt der ursprüngliche Wert (normalerweisescanEnums()). Danach wird derrun()Branch betreten, undinlineEnums()wird geworfen). Wenn man diesesbuildStartentfernt, läuft der Code weiter zum

Branch und führt in einer nicht-interaktiven Umgebung:scanEnums()aus. Dies kann in CI dazu führen, dass Tests aufgrund von Umgebungsunterschieden fehlschlagen, oder schlimmer – die Tests bestehen, aber CI hat tatsächlich nicht bestanden (z. B. lief CI eine andere Test-Teilmenge), und es wird eine nicht vollständig validierte Version veröffentlicht.DasvoninlineEnums()wird auf Modulebene aufgerufen, währendrollup.config.jsdasbuildStartvonbuildAllinnerhalb derbuild.js:119-121Funktion aufgerufen wird. Wenn man die Ausführungszeitpunkte dieser beiden vertauscht (d. h.scanEnums()imremoveCacheHook von Rollup aufrufen lässt), was würde zerstört?

Doppelverzeichnis-Vertrag, Zuordnungsentscheidung von Build-Skripten, Sekundärfilterung von Release-Skripten – diese Mechanismen zusammen definieren die Sicherheitsgrenzen der Monorepo-Industrialisierung. Doch Grenzen sind nicht statisch: Mit der Migration der Build-Tools von Rollup zu Rolldown und der Verschmelzung von Typtests und Laufzeittests werden die bestehenden Abwägungsstrategien vor neuen Herausforderungen stehen. Im nächsten Kapitel werden wir basierend auf dem Änderungsverlauf von 3.0 bis 3.4 die Entwicklungsrichtung der nächsten Generation des Industrialisierungssystems skizzieren.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

CHAPTER 14

Kapitel 14: Zukünftige Entwicklung: Von 3.x zur nächsten Generation des Industrialisierungssystems

Upstream: vuejs/core · Commit @4ab865a8 · Fortschritt: Kapitel 14 von 14

Im vorherigen Kapitel haben wir die „Sicherheitsgrenzen" des Vue-Core-Industrialisierungssystems herausgearbeitet – Doppelverzeichnis-Vertrag, Zuordnungsentscheidung von Build-Skripten, Sekundärfilterung von Release-Skripten. Diese Mechanismen wurden nicht in einem Zug entworfen, sondern in den Iterationen von 3.0 bis 3.4 wiederholt geschliffen. Dieses Kapitel wechselt die Perspektive: Wir schauen nicht mehr darauf, „wie es jetzt aussieht", sondern darauf, „wie es zu dem geworden ist, was es jetzt ist", und leiten daraus ab, wohin die nächste Generation des Industrialisierungssystems steuern wird. Das Quellmaterial dieses Kapitels sind changelogs/CHANGELOG-3.3.md, changelogs/CHANGELOG-3.4.md sowie die package.json im Repository-Stammverzeichnis. Änderungsprotokolle scheinen nur eine Aufzählung von „welcher Bug wurde behoben" zu sein, aber sie sind der ehrlichste Gesundheitsbericht des Industrialisierungssystems: Jeder Commit mit dem Präfix build:, jede Änderung mit dem Präfix types:, jeder Rückzug einer Abhängigkeitsversion legt die Spannungspunkte der aktuellen Architektur offen. Unsere Aufgabe ist es, aus diesen Spannungspunkten die Entwicklungsrichtung herauszulesen. Das Änderungsprotokoll als „Beobachtungsfenster des Industrialisierungssystems" statt als „Funktionsliste" zu betrachten, ist die Kernmethodik dieses Kapitels. Funktionsänderungen sagen uns, was Vue leisten kann, während Änderungen an Build, Typen und CI uns sagen, „wo es weh tut" im Industrialisierungssystem von Vue.

I. Spannungspunkte der Build-Toolchain: Das Migrationspotenzial von Rollup zu Rolldown

Intuitives Modell

Stellen Sie sich die Build-Toolchain als eine Montagelinie vor: Rollup ist der Hauptmontagetisch, esbuild übernimmt das schnelle Schneiden (Transpilieren von TS), terser übernimmt das abschließende Bündeln und Komprimieren. Wenn das Produkt (die Vue-Laufzeit) immer komplexer wird und die Arbeitsschritte am Montagetisch zunehmen, wird der Hauptmontagetisch selbst zum Engpass. Die Positionierung von Rolldown ist der in Rust neu geschriebene Hauptmontagetisch – er soll nicht esbuild ersetzen, sondern Rollup selbst.

Ohne diesen Evolutionsdruck wäre die „Katastrophe", der das System gegenübersteht, nicht ein Absturz, sonderndie lineare Aufblähung der Build-Zeit mit der Anzahl der Pakete: Für jedes zusätzliche Unterpaket muss ein weiterer Rollup-Prozess gestartet, der Enum-Cache erneut durchsucht und eine weitere Runde der dts-Generierung durchlaufen werden.

Datenstrukturen und Abhängigkeitslayout

Betrachten wir zunächst einen statischen Schnappschuss der aktuellen Toolchain.package.jsonDiedevDependenciesvon ist eine präzise „Montagetisch-Liste":

📎 package.json:103-106

code
    "rollup": "^4.63.3",
    "rollup-plugin-dts": "^6.5.1",
    "rollup-plugin-esbuild": "^6.2.1",
    "rollup-plugin-polyfill-node": "^0.13.0",

Hier lassen sich drei Schlüsselfakten ablesen. Erstens ist die Rollup-Hauptversion^4.63.3, was sich in der Reifephase von Rollup 4.x befindet. Zweitens übernimmtrollup-plugin-esbuilddie TS-Transpilierung, was bedeutet, dass Rollup selbst kein TS parst, sondern nur das von esbuild ausgegebene JS verarbeitet. Drittens istrollup-plugin-dtsunabhängig verantwortlich für das.d.tsBundling, was genau die materielle Grundlage für diedts-built-testUnabhängigkeit ist, die im vorherigen Kapitel diskutiert wurde.

Betrachten wir nun die Einstiegsorchestrierung des Build-Skripts:

📎 package.json:8-9

code
    "build": "node scripts/build.js",
    "build-dts": "tsc -p tsconfig.build.json --noCheck && rollup -c rollup.dts.config.js",

build-dtsist „zweistufig": Zuersttsc --noCheckwerden rohe Deklarationsdateien generiert (--noChecküberspringt die Typprüfung und führt nur emit aus), dann werden mitrollup -c rollup.dts.config.jsdie verstreuten.d.tszu einer einzigen Datei gebündelt. Dieses Design selbst ist eine Abhängigkeit von den Fähigkeiten von Rollup –rollup-plugin-dtsbenötigt den Modulgraphen von Rollup, um Typabhängigkeiten zu verfolgen.

Szenariogetrieben: Was einbuild:-Commit offengelegt hat

Die Einträge mit dem Präfixbuild:im Änderungsprotokoll sind direkte Belege für die Spannungspunkte der Build-Toolchain. Wir greifen drei heraus.

Der erste, die minify-Konfigurationsangleichung in 3.4.32:

📎 changelogs/CHANGELOG-3.4.md:84

code
* **build:** use consistent minify options from previous terser config ([789675f](https://github.com/vuejs/core/commit/789675f65d2b72cf979ba6a29bd323f716154a4b))

Die Motivation dieses Commits war „inkonsistente Komprimierungsoptionen nach der Migration von terser zu esbuild minify". Er offenbart einen Zwischenzustand während der Migration: Vue verwendete einst terser zur Komprimierung, wechselte später zu esbuild (was durchdevDependenciesinesbuild: ^0.28.2bestätigt wird), aber die Komprimierungsoptionen wurden nicht vollständig angeglichen, was zu Abweichungen bei der Produktgröße oder im Verhalten führte. Genau das sind die typischen Kosten beim „Austausch von Montagetisch-Komponenten".

Der zweite, der entities-Versionsrückzug in 3.4.38:

📎 changelogs/CHANGELOG-3.4.md:6

code
* **build:** revert entities to 4.5 to avoid runtime resolution errors ([f349af7](https://github.com/vuejs/core/commit/f349af7b65b9f8605d8b7bafcc06c25ab1f2daf0)), closes [#11603](https://github.com/vuejs/core/issues/11603)

entitiesist eine HTML-Entity-Dekodierungsbibliothek, die voncompiler-domabhängt. Der Rückzug auf 4.5 erfolgte, weil die neue Version Probleme bei der Laufzeitanalyse verursachte. Dieser Commit zeigt:Die Abhängigkeitsaktualisierung der Build-Toolchain ist nicht isoliert; ein Versionssprung einer indirekten Abhängigkeit kann bis in das Laufzeitverhalten durchschlagen。

Der dritte, die cjs-Build-Kontamination des server-renderer in 3.4.29:

📎 changelogs/CHANGELOG-3.4.md:155

code
* **build:** fix accidental inclusion of runtime-core in server-renderer cjs build ([11cc12b](https://github.com/vuejs/core/commit/11cc12b915edfe0e4d3175e57464f73bc2c1cb04)), closes [#11137](https://github.com/vuejs/core/issues/11137)

Dies ist die typischste Art von Build-Bug: Im CJS-Format hatserver-rendererversehentlichruntime-corein sein eigenes Produkt eingebunden. Die Ursache ist normalerweise, dass dieexternal-Bestimmung von Rollup im CJS-Format versagt – ESM kann externe Abhängigkeiten statisch durchimport-Anweisungen erkennen, während CJSrequireDynamik ist stärker, leicht zu übersehen. Dieser Commit zeigt direkt auf die Anfälligkeit der Logik in der Rollup-Konfiguration.externalLogik.

Mermaid-Darstellung des Migrationspotenzials

Das folgende Diagramm stellt den Kontrollfluss der aktuellen Build-Pipeline dar und markiert die Knoten, die von der Rolldown-Migration betroffen sein werden:

mermaid
flowchart TD
    start["node scripts/build.js"] --> scan["scanEnums() 全局扫描"]
    scan --> cache_ok{"enum 缓存就绪?"}
    cache_ok -->|否| err_enum["抛出错误 / 中断构建"]
    cache_ok -->|是| build_all["buildAll() 并发启动"]
    build_all --> rollup_proc["每个包一个 Rollup 进程"]
    rollup_proc --> inline["inlineEnums() 顶层调用"]
    inline --> esbuild_plugin["rollup-plugin-esbuild 转译 TS"]
    esbuild_plugin --> external_check{"external 判定"}
    external_check -->|ESM 格式| ext_ok["静态 import 识别成功"]
    external_check -->|CJS 格式| ext_risk["require 动态性导致漏判"]
    ext_risk --> pollution["runtime-core 被打进 server-renderer"]
    ext_ok --> output["产物输出"]
    pollution --> output
    output --> dts["build-dts 两段式生成"]
    dts --> tsc_emit["tsc --noCheck 生成原始 d.ts"]
    tsc_emit --> rollup_dts["rollup-plugin-dts 打包"]
    rollup_dts --> done["构建完成"]
〔Design-Inferenz und Architektur-Abwägungen〕

Der Migrationswert von Rolldown liegt darin: Es ersetzt das Nebenläufigkeitsmodell „ein Prozess pro Paket" durch das Modell „Parallelität innerhalb eines einzelnen Prozesses",scanEnums()Der globale Scan undinlineEnums()Die Ersetzung kann innerhalb derselben Rust-Laufzeit koordiniert werden, und das im vorherigen Kapitel diskutierte Problem der „Nebenläufigkeits-Scan-Race-Condition" wird von Grund auf verschwinden. Aber genau hier liegt der Widerstand gegen die Migration –rollup-plugin-esbuild、rollup-plugin-dtsDiese Plugin-Ökosysteme benötigen eine Kompatibilitätsschicht von Rolldown, undexternalDie Entscheidungslogik muss neu geschrieben werden.

Design-Überlegungen und Fallstricke

Warum wird die Migration nicht auf einen Schlag erfolgen?Betrachten Siepackage.jsonDasenginesFeld:

📎 package.json:61-63

code
  "engines": {
    "node": ">=20.0.0"
  },

Node 20 ist die harte Untergrenze. Rolldown als Rust-natives Modul benötigt entsprechende N-API-Bindings und vorkompilierte Binärverteilung. Sobald es eingeführt wird,pnpm installDie Zeitaufwände, die plattformübergreifende (Windows/macOS/Linux) Binärkompatibilität und die CI-Cache-Strategie müssen neu gestaltet werden. Das ist nicht einfach „eine Abhängigkeit austauschen", sondernEine Neukalibrierung der gesamten Installations-Build-Cache-Kette。

Produktions-Fallstricke:build-dtsDastsc --noCheckIst ein zweischneidiges Schwert. Das Überspringen der Typprüfung beschleunigt das Emit, bedeutet aber, dass.d.tsIn der Generierungsphase keine Typfehler entdeckt werden – Typfehler können nur durchpnpm check(tsc --incremental --noEmit) undtest-dtsAufgefangen werden. Wenn nach der Rolldown-Migration diese beiden Schritte zusammengeführt werden sollen, muss sichergestellt werden, dass die Typprüfung den Build nicht verlangsamt, sonst widerspricht dies der ursprünglichen Absicht von--noCheck.

---

Zwei, der Fusionstrend von Typtests und Laufzeittests

Intuitives Modell

Stellen Sie sich Typtests und Laufzeittests als zwei unabhängige Qualitätskontrollpunkte vor: Einer prüft, ob „die Bedienungsanleitung (.d.ts) korrekt geschrieben ist", der andere prüft, ob „die Maschine (Laufzeit) korrekt läuft". Beide Kontrollpunkte haben eigene Arbeitsplätze, eigene Werkzeuge und eigene Berichte. Der Fusionstrend bedeutet:Können dieselben Testfälle gleichzeitig die Bedienungsanleitung und die Maschine validieren?

Ohne Fusion steht das System vor der KatastropheDrift zwischen Typ und Laufzeitverhalten:.d.tsSagtref()Gibt zurückRef<T>, aber die tatsächlich zurückgegebene Objektform zur Laufzeit hat sich geändert, der Typtest besteht, der Laufzeittest besteht ebenfalls, aber die Kombination beider ist falsch.

Datenstruktur: Das Orchestrierungslayout der Testskripte

package.jsonInscriptsSind die testbezogenen Einträge klar in zwei Gruppen unterteilt:

📎 package.json:19-24

code
    "test": "vitest",
    "test-unit": "vitest --project unit*",
    "test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e --project e2e-browser",
    "test-dts": "run-s build-dts test-dts-only",
    "test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
    "test-coverage": "vitest run --project unit* --coverage",

Die Schlüsselstruktur hier isttest-dtsDasrun-s build-dts test-dts-only– es istSeriell: Zuerst wird.d.tsGebaut, dann werden die Typtests ausgeführt. Undtest-dts-onlyIst intern wiederumZwei unabhängigetscProzesse: Einer führtdts-built-testAus (validiert die Build-Artefakte), einer führtdts-testAus (validiert die Quellcode-Typen).

Beachten Sietest-unitVerwendetvitest --project unit*,test-e2eVerwendetvitest --project e2e --project e2e-browser. Dies zeigt, dass der--project-Mechanismus von Vitest die Tests bereits nach „Unit/E2E/Browser" in verschiedene Projekte aufgeteilt hat.Die physische Grundlage für die Fusion existiert bereits: Der Projekt-Mechanismus von Vitest erlaubt es, verschiedene Testtypen im selben Runner auszuführen.

Szenario-getrieben: Der vollständige Pfad einestypes:Commits

Im Änderungsprotokoll ist die Dichte der Einträge mit dem Präfixtypes:Extrem hoch, was die Komplexität des Typsystems direkt widerspiegelt. Wir verfolgen einen typischen Typ-Fix.

Der ref-Typ-Rückfall in 3.4.37:

📎 changelogs/CHANGELOG-3.4.md:23-24

code
* Revert "fix(types/ref): allow getter and setter types to be unrelated ([#11442](https://github.com/vuejs/core/issues/11442))" ([b1abac0](https://github.com/vuejs/core/commit/b1abac06cdb198bd72f8e614b1f68b92e1c78339))
* Revert "fix(types/ref): correct type inference for nested refs ([#11536](https://github.com/vuejs/core/issues/11536))" ([3a56315](https://github.com/vuejs/core/commit/3a56315f94bc0e11cfbb288b65482ea8fc3a39b4))

Zwei aufeinanderfolgende Reverts, die zwei Typ-Fixes zurückgerollt haben. Beachten Sie, dass diese beiden Fixes in 3.4.35 gerade erst gemergt wurden:

📎 changelogs/CHANGELOG-3.4.md:55

code
* **types/ref:** allow getter and setter types to be unrelated ([#11442](https://github.com/vuejs/core/issues/11442)) ([e0b2975](https://github.com/vuejs/core/commit/e0b2975ef65ae6a0be0aa0a0df43fb887c665251))

📎 changelogs/CHANGELOG-3.4.md:30

code
* **types/ref:** correct type inference for nested refs ([#11536](https://github.com/vuejs/core/issues/11536)) ([536f623](https://github.com/vuejs/core/commit/536f62332c455ba82ef2979ba634b831f91928ba)), closes [#11532](https://github.com/vuejs/core/issues/11532) [#11537](https://github.com/vuejs/core/issues/11537)

Vom Merge in 3.4.35 bis zum Revert in 3.4.37 liegt nur eine Patch-Version dazwischen. Dieser schnelle „Merge-Revert"-Zyklus offenbart ein grundlegendes Dilemma von Typtests:Typtests können validieren, dass „die Typsignatur den Erwartungen entspricht", aber sie können nicht validieren, ob „diese Typsignatur im echten Code gut nutzbar ist".。allow getter and setter types to be unrelatedIn Typtests kann es vollständig bestehen, aber in der tatsächlichen Verwendung wird die Typinferenz vonrefZu locker, was die Typsicherheit des nachgelagerten Codes beeinträchtigt.

Mermaid-Darstellung der Typtest-Fusion

Das folgende Diagramm stellt die aktuelle Trennstruktur von Typtests und Laufzeittests sowie die Zielform nach der Fusion dar:

mermaid
flowchart LR
    subgraph current["当前:分离的两条链路"]
        src["packages/*/src/*.ts"] --> tsc_build["tsc -p tsconfig.build.json --noCheck"]
        tsc_build --> raw_dts["散落的 .d.ts"]
        raw_dts --> rollup_dts["rollup -c rollup.dts.config.js"]
        rollup_dts --> built_dts["打包后的 .d.ts"]
        built_dts --> dts_built_test["dts-built-test/tsconfig.json"]
        src --> dts_test["dts-test/tsconfig.test.json"]
        src --> vitest_unit["vitest --project unit*"]
        dts_built_test --> report_a["类型报告"]
        dts_test --> report_a
        vitest_unit --> report_b["运行时报告"]
    end
    subgraph future["融合目标:单一 runner"]
        src2["源码"] --> vitest_all["vitest --project unit --project dts"]
        vitest_all --> unified["统一报告 + 类型断言"]
    end
    current -.演进.-> future
〔Design-Inferenz und Architektur-Abwägungen〕

Der technische Pfad der Fusion ist höchstwahrscheinlich: Diedts-built-testUnddts-testDietscAufrufe in ein benutzerdefiniertes Vitest-Projekt zu verpacken, sodass Typassertionen in Form vonexpectTypeOfInline in Testdateien eingebettet werden. So kann ein einzigervitestAufruf gleichzeitig Laufzeitassertionen und Typassertionen ausführen, mit einheitlichem Reporting. Aber der Widerstand liegt darin:tscDie Typprüfung von

Ist „vollständig", während die Tests von Vitest „dateiweise" sind – die inkrementellen Strategien beider sind inkompatibel.

Design-Überlegungen und Fallstrickedts-built-testWarumdts-test?Unabhängig vondts-built-testSein muss – wurde im vorherigen Kapitel bereits diskutiert, hier ergänzt aus evolutionärer Perspektive:Validiert(rollup-plugin-dtsBuild-Artefakte.d.ts),dts-testDas gepackteValidiertQuellcode-Typen

📎 changelogs/CHANGELOG-3.4.md:9

code
* **types:** add fallback stub for DOM types when DOM lib is absent ([#11598](https://github.com/vuejs/core/issues/11598)) ([4db0085](https://github.com/vuejs/core/commit/4db0085de316e1b773f474597915f9071d6ae6c6))

Kopierendts-built-test„Fallback-Stub bereitstellen, wenn DOM lib fehlt" – dies ist eine Typkompatibilitätskorrektur auf der Ebene der Build-Artefakte, die nur im Szenario von.d.ts, bei dem gepackte

Konsumiert werden, entdeckt werden kann.Produktions-Fallstricke: Der „Merge-Revert"-Zyklus von Typtests zeigt, dass Änderungen an TypsignaturenEchte nachgelagerte Projektepackages-private/dts-testwird ein repositoryinterner Testfall verwendet, der nicht alle nachgelagerten Nutzungsweisen abdeckt. Wenn der Fusionstrend nur darauf achtet, „zwei Runner zusammenzuführen“, ohne zu lösen, „wie echtes nachgelagertes Feedback eingebracht wird“, ist das nur eine formale Fusion.

---

Drei. Richtungen für feingranulare Optimierung des CI-Caches

Intuitives Modell

Stellen Sie sich den CI-Cache als „Materialbereich“ eines Repositorys vor: Jeder Build muss Rohmaterialien (Abhängigkeiten, Build-Artefakte, Typcache) aus dem Materialbereich entnehmen. Wenn der Materialbereich nur eine große Kiste hat und für jeden Gegenstand die gesamte Kiste durchsucht werden muss, kann selbst eine hohe Cache-Trefferrate nicht schnell sein. Feingranulare Optimierung bedeutet:Die große Kiste in kleine, nach Verwendungszweck kategorisierte Fächer aufteilen。

Ohne feingranularen Cache steht das System vor folgender Katastrophe:Kaskadierende Verstärkung von Cache-Invalidierung: Eine Zeile Quellcode ändern führt dazu, dass der gesamtenode_modules-Cache ungültig wird, CI alle Abhängigkeiten neu installiert und die Build-Zeit von 2 Minuten auf 10 Minuten steigt.

Datenstruktur: Klassifizierung cachefähiger Objekte

Auspackage.jsonlassen sich mehrere Arten cachefähiger „Materialien“ erkennen:

Erste Kategorie: Installationsartefakte von Abhängigkeiten.packageManagerDas Feld legt die pnpm-Version fest:

📎 package.json:4

code
  "packageManager": "pnpm@12.4.2",

pnpm'snode_modulesist eine Symlink-Struktur; gecacht wird der content-addressable Store von pnpm, nicht ein flachesnode_modules. Das bedeutet, der Cache-Schlüssel sollte auf dem Hash vonpnpm-lock.yamlbasieren, nicht aufpackage.json。

Zweite Kategorie: Build-Artefakte.cleanDas Skript offenbart die physischen Speicherorte der Artefakte:

📎 package.json:10

code
    "clean": "rimraf --glob packages/*/dist temp .eslintcache",

packages/*/dist、temp、.eslintcache— diese drei Arten von Artefakten können unabhängig gecacht werden.distist die Build-Ausgabe,tempsind temporäre Dateien (wiebench.json),.eslintcacheist der Lint-Cache.

Dritte Kategorie: Typprüfungs-Cache.checkDas Skript verwendet--incremental:

📎 package.json:15

code
    "check": "tsc --incremental --noEmit",

--incrementalerzeugt.tsbuildinfoDateien; dies ist der inkrementelle Cache der Typprüfung. Wenn diese Datei in CI gecacht wird,tscwird der zweite Lauf von

viel schneller sein.

Szenariogetrieben: CI-Ausführungsfluss eines PRspackages/reactivity/src/ref.tsIn ein typisches Szenario eintauchen: Ein Entwickler ändert

und reicht einen PR ein. Welche Schritte muss CI ausführen, und welche können den Cache treffen?scriptsAussimple-git-hookslässt sich die CI-Ausführungssequenz ableiten (pre-commit's

📎 package.json:48-51

code
  "simple-git-hooks": {
    "pre-commit": "pnpm lint-staged && pnpm check",
    "commit-msg": "node scripts/verify-commit.js"
  },

Kopierenpre-commitLokal läuftlint-stagedmitcheckundlint、check、test-unit、test-dts、size. In CI werden dann

  • lintusw. ausgeführt. Die Cache-Strategie ist für jeden Schritt unterschiedlich:.eslintcache: cache
  • check, Schlüssel basiert auf dem Hash der Quelldateien..tsbuildinfo: cachetsconfig, Schlüssel basiert auf
  • test-unitund dem Quellcode-Hash.
  • test-dts: Vitest hat einen eigenen Cache, aber üblicherweise werden in CI keine Testergebnisse gecacht, sondern nur Abhängigkeiten.build-dts: hängt von den Artefakten vonpackages/*/distab, Cache-Schlüssel basiert auf dem Hash von
  • size: hängt von Build-Artefakten ab, Cache-Schlüssel wie oben.

Mermaid-Darstellung der CI-Cache-Optimierung

mermaid
flowchart TD
    pr["PR 提交"] --> checkout["checkout 代码"]
    checkout --> cache_deps{"pnpm store 缓存命中?"}
    cache_deps -->|是| install_fast["pnpm install --offline"]
    cache_deps -->|否| install_slow["pnpm install 全量下载"]
    install_fast --> lint_step["pnpm lint"]
    install_slow --> lint_step
    lint_step --> cache_eslint{".eslintcache 命中?"}
    cache_eslint -->|是| lint_inc["增量 lint"]
    cache_eslint -->|否| lint_full["全量 lint"]
    lint_inc --> check_step["pnpm check"]
    lint_full --> check_step
    check_step --> cache_tsbuild{".tsbuildinfo 命中?"}
    cache_tsbuild -->|是| check_inc["增量类型检查"]
    cache_tsbuild -->|否| check_full["全量类型检查"]
    check_inc --> test_unit["pnpm test-unit"]
    check_full --> test_unit
    test_unit --> build_dts["pnpm build-dts"]
    build_dts --> cache_dist{"packages/*/dist 命中?"}
    cache_dist -->|是| dts_cached["复用 dts 产物"]
    cache_dist -->|否| dts_rebuild["重新生成 dts"]
    dts_cached --> test_dts["pnpm test-dts-only"]
    dts_rebuild --> test_dts
    test_dts --> size_check["pnpm size"]
    size_check --> done["CI 通过"]
〔Design-Inferenz und Architektur-Abwägungen〕

Der Kernwiderspruch feingranularer Caches istdie Granularität des Cache-Schlüssels: Ist der Schlüssel zu grob (z. B. nur auf Basis des Commit-Hashs), ist die Trefferrate niedrig; ist der Schlüssel zu fein (z. B. auf Basis des Hashs jeder Datei), heben die Kosten für die Schlüsselberechnung den Cache-Nutzen auf. Eine sinnvolle Strategie für Monorepos wie Vue ist „Sharding nach Paket“: Jedespackages/*Unterpaket wird unabhängig gecacht; Änderungen andist,reactivitymachen dencompiler-core-Cache vondistnicht ungültig.

Designüberlegungen und Stolperfallen

Warum muss dassize-Skript in mehrere Unterbefehle aufgeteilt werden?Betrachten Sie diese drei:

📎 package.json:11-14

code
    "size": "run-s \"size-*\" && node scripts/usage-size.js",
    "size-global": "node scripts/build.js vue runtime-dom -f global -p --size",
    "size-esm-runtime": "node scripts/build.js vue -f esm-bundler-runtime",
    "size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",

sizeverwendetrun-s "size-*", um alle Unterbefehle mit dem Präfixsize-seriell auszuführen. Dieses „Präfix-Aggregations“-Muster ermöglicht es, jede Größen-Dimension (global, esm-runtime, esm) unabhängig zu cachen und unabhängig fehlschlagen zu lassen. Wenn man sie zu einem großen Befehl zusammenführt, lässt jede Dimensionsüberschreitung das gesamtesizefehlschlagen, und es lässt sich nicht lokalisieren, welches Problem welche Dimension betrifft.

Produktions-Stolperfallen: Die häufigste Falle bei CI-Caches istCache-Verschmutzung—falsche Artefakte werden gecacht, sodass spätere Builds auf schmutzigen Daten basieren.cleanDas

📎 package.json:10

code
    "clean": "rimraf --glob packages/*/dist temp .eslintcache",

Kopierenpackages/*/distBeachten Sie, dass espackages-private/*/distbereinigt, nichtpackages-private. Das bedeutet, die Artefakte vonpackages-privateliegen nicht im regulären Bereinigungsbereich—wenn CI die Artefakte voncleancacht undpackages-privatesie nicht bereinigt, kann das Problem entstehen, dass „alte Playground-Artefakte gecacht wurden“. Beim Design feingranularer Caches muss

---

separat behandelt werden.

Designüberlegung: Das Engineering-System als ProduktlebenszyklusWenn man die Hinweise der drei Abschnitte verbindet, erkennt man eine klare Hauptlinie:。

Das Engineering-System von Vue bewegt sich von „funktionsfähig“ zu „gut nutzbar“, von „manueller Orchestrierung“ zu „deklarativer Konfiguration“

Die Migration der Build-Toolchain (Rollup → Rolldown) ist eine „leistungsgetriebene“ Entwicklung: Wenn die Anzahl der Pakete bis zu einem gewissen Grad wächst, übersteigen die Kosten prozessbasierter Nebenläufigkeit den Nutzen, und es muss auf ein leichteres Nebenläufigkeitsmodell umgestellt werden.

Die Fusion von Typtests ist eine „konsistenzgetriebene“ Entwicklung: Wenn sich Typ-Signaturen häufiger ändern als das Laufzeitverhalten, werden zwei getrennte Testsätze zur Last, und sie müssen dieselben Testfälle gemeinsam nutzen.

Die Feingranularisierung des CI-Caches ist eine „kostengetriebene“ Entwicklung: Wenn CI-Minuten zum Engpass werden, ist die Verschwendung durch grobgranulare Caches nicht mehr akzeptabel, und es muss nach Verwendungszweck geshardet werden.

〔Design-Inferenz und Architektur-Abwägungen〕Die gemeinsame Einschränkung dieser drei Entwicklungslinien istRückwärtskompatibilitätBREAKING CHANGES. Die Release-Strategie von Vue (sichtbar am

---

-Abschnitt des Changelogs) erlaubt in Minor-Versionen „type-only breaking changes“, aber keine Laufzeit-Breaking-Changes. Das bedeutet, die Entwicklung des Engineering-Systems muss sicherstellen: Egal wie die interne Toolchain ausgetauscht wird, die öffentliche API und das Laufzeitverhalten der Artefakte dürfen sich nicht ändern. Dies ist die harte Grenze aller Entwicklungsentscheidungen.

Zusammenfassung dieses Kapitelspackage.jsonDieses Kapitel geht vom Changelog und

1. aus und ordnet die drei Entwicklungslinien des Engineering-Systems von Vue core:: Die aktuelle Kombination aus Rollup 4.x + esbuild + rollup-plugin-dts zeigt ihre Belastungspunkte inbuild:Commits mit dem Präfix (Minify-Konfigurationsabgleich, Entities-Versionsrücknahme, übersehene CJS-External-Erkennung). Das Potenzial der Rolldown-Migration liegt in der Ablösung von „Multi-Prozess-Konkurrenz" durch „Single-Prozess-Parallelität", der Widerstand kommt vom Plugin-Ökosystem und der plattformübergreifenden Binärverteilung.

2. Typentest-Fusion:test-dtsderrun-s build-dts test-dts-onlyserielle Struktur sowiedts-built-testunddts-testdie dualetscProzesse sind der physische Beweis für die aktuelle getrennte Form. Der technische Pfad zur Fusion nutzt Vitests--projectMechanismus, der Widerstand ist die Inkompatibilität zwischentscVollprüfung und Vitests dateibasierter inkrementeller Teststrategie.

3. CI-Cache-Feingranularisierung:packageManagerpnpm locken,cleandrei Arten von Artefakten bereinigen,checkmit--incremental、sizemit Präfix aggregieren – all dies sind Klassifizierungskriterien für cachebare Objekte. Der Kernwiderspruch ist die Granularität des Cache-Schlüssels, eine sinnvolle Strategie ist „Sharding nach Paket".

Der wichtigste kognitive Wandel ist:Das Engineering-System selbst ist ein Produkt, es hat seine eigenen Nutzer (Contributors), seine eigenen Leistungsmetriken (Build-Zeit, CI-Minuten), seine eigenen Kompatibilitätsbeschränkungen (Artefakt-API unverändert). Es erfordert kontinuierliche Iteration, nicht einmaliges Design.

Gedanken und Selbsttests dieses Kapitels

Q1: package.json:9derbuild-dtsverwendettsc -p tsconfig.build.json --noCheck. Wenn man--noCheckentfernt, welche Kettenreaktionen würde das nach der Rolldown-Migration auslösen?

Referenzanalyse:--noCheckDer Zweck von ist es, die Typprüfung zu überspringen und nur emit durchzuführen. Nach dem Entfernen wirdtscvor der Generierung von.d.tseine vollständige Typprüfung durchführen. Unter der aktuellen Rollup-Architektur macht dies nurbuild-dtslangsamer; aber nach der Rolldown-Migration wird das Problem verstärkt: Rolldowns Kernverkaufsargument ist „Single-Prozess-Parallel-Build", wenn diebuild-dtsPhase eine vollständigetscPrüfung einführt, wird sie zum seriellen Engpass der gesamten Pipeline – alle Paket-Builds müssen auf den Abschluss dieser Prüfung warten. Noch gravierender ist, dasstscTypprüfung single-threaded ist und die Parallelitätsfähigkeiten von Rolldown nicht nutzen kann. Die richtige Vorgehensweise ist,--noCheckbeizubehalten und die Typprüfung an unabhängigepnpm check(package.json:15) undtest-dts(package.json:22) zu übergeben, um Build und Prüfung zu entkoppeln.

Q2: Das Changelog 3.4.37 hat zweitypes/refFixes nacheinander zurückgenommen (CHANGELOG-3.4.md:23-24), während diese beiden Fixes gerade in 3.4.35 gemergt wurden (CHANGELOG-3.4.md:30,55). Wenn Typentests und Laufzeittests bereits fusioniert wären, könnte dieser „Merge-Rollback"-Zyklus vermieden werden? Warum?

Referenzanalyse: Nicht vollständig vermeidbar, aber der Zyklus kann verkürzt werden. Der fusionierte Typentest kann weiterhin nur verifizieren, dass „die Typsignatur der Assertion entspricht", während das Problem bei Fixes wieallow getter and setter types to be unrelateddarin liegt, dass „die Typsignatur zu locker ist und die Typsicherheit nachgelagerter Codes beeinträchtigt" – dies ist ein Problem dernachgelagerten Nutzung, nicht derSignatur selbst. Wo die Fusion den Zyklus verkürzen kann: Wenn Typ-Assertions und Laufzeit-Assertions in derselben Testdatei geschrieben sind, können Entwickler schneller Inkonsistenzen entdecken wie „Typsignatur hat sich geändert, aber Laufzeitverhalten nicht". Um jedoch Rollbacks wirklich zu vermeiden, müsste man Typprüfungen realer nachgelagerter Projekte einführen (z. B.packages-private/dts-testzu einem Testset erweitern, das „nachgelagerte Nutzung simuliert"), was über den Rahmen einer reinen „Runner-Fusion" hinausgeht.

Q3: package.json:10dercleanSkript bereinigtpackages/*/dist, aber nichtpackages-private/*/dist. Wenn CI eine feingranulare Cache-Strategie mit „Sharding nach Paket" verwendet, welche Produktionsfallen bringt diese Asymmetrie?

Referenzanalyse: Die Falle liegt darin, „alte Artefakte vonpackages-privatezu cachen".packages-privateenthältsfc-playground、template-explorerund andere Debug-Tools, deren Build-Artefakte (wiepackages-private/sfc-playground/dist) wenn sie von CI gecacht werden undcleansie nicht bereinigt, entsteht folgendes: Der Quellcode wurde aktualisiert, aber CI verwendet alte Playground-Artefakte wieder, was zu verfälschten Validierungsergebnissen vonbuild-sfc-playground(package.json:39) führt. Noch subtiler ist, dassdev-sfc-prepare(package.json:34) prüft, ob die Artefakte vonpackages-privateexistieren; wenn alte Artefakte gecacht sind, überspringt es den Neuaufbau, sodass Entwickler glauben, die Umgebung sei aktuell. Beim Design feingranularer Caches muss fürpackages-privateentweder ein separater Cache-Schlüssel definiert werden, oder man cached seine Artefakte gar nicht – denn es ist ein Debug-Tool, die Neuerstellungskosten sind niedrig, der Cache-Nutzen gering.

Durch das Beobachtungsfenster des Changelogs haben wir die Belastungspunkte des aktuellen Engineering-Systems identifiziert und daraus mögliche Entwicklungsrichtungen des nächsten Systems abgeleitet. Diese Richtungen sind keine Luftschlösser, sondern aus realen Produktions-Fallstricken und Abwägungen gewachsen. Damit endet die Analyse des Vue-Engineering-Systems in diesem Buch, aber die Erforschung des Engineerings kennt kein Ende – das nächste Kapitel wird als Schlusskapitel die Perspektive von Vue selbst wegziehen und untersuchen, wie diese Erfahrungen auf breitere Engineering-Szenarien übertragen werden können.

Verwandeln Sie jeden Codebase in ein verständliches Buch

Kapitel beendet? Erstellen Sie ein Architekturbuch für Ihr Projekt

Local-First-Architektur mit Tauri 2 + Rust. 100% offline und sicher, kein Code-Upload. Dual-Pane-Lesemodus mit unveränderlichen Commit-Ankern.

⚡ Tauri 2 · Rust Core · 100% Offline & Privat · Getestet mit 1M+ Zeilen

Um ein komplexes Projekt zu verstehen, braucht man nur ein gutes Buch

Automatisch kompiliert von AiReadCode durch Scannen des offiziellen Repositorys mit unveränderlichen Commit-Ankern.

Auf GitHub mit Stern versehen ★ Weitere Bücher durchsuchen →