diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index b40842ce5ed..2bc92d60fec 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -22,7 +22,10 @@ jobs: uses: ./.github/actions/setup-nodejs with: # The default is build without typechecking, we want both - build-command: pnpm build + build-command: pnpm build --summarize + + - name: Report build critical path + run: node scripts/turbo-critical-path.mjs >> "$GITHUB_STEP_SUMMARY" unit-test: name: Unit tests diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index ce1fda701f8..721b6554d78 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -149,7 +149,11 @@ jobs: if: fromJSON(steps.ci-filter.outputs.results).ci || fromJSON(steps.ci-filter.outputs.results).e2e uses: ./.github/actions/setup-nodejs with: - build-command: ${{ fromJSON(steps.ci-filter.outputs.results).ci && 'pnpm build' || 'pnpm turbo run build --filter=@n8n/playwright-janitor' }} + build-command: ${{ fromJSON(steps.ci-filter.outputs.results).ci && 'pnpm build --summarize' || 'pnpm turbo run build --filter=@n8n/playwright-janitor' }} + + - name: Report build critical path + if: fromJSON(steps.ci-filter.outputs.results).ci + run: node scripts/turbo-critical-path.mjs >> "$GITHUB_STEP_SUMMARY" - name: Run format check if: fromJSON(steps.ci-filter.outputs.results).ci diff --git a/packages/frontend/@n8n/chat/vite.config.mts b/packages/frontend/@n8n/chat/vite.config.mts index ea20577945d..5c6d3f0b7ad 100644 --- a/packages/frontend/@n8n/chat/vite.config.mts +++ b/packages/frontend/@n8n/chat/vite.config.mts @@ -22,7 +22,8 @@ export default mergeConfig( compiler: 'vue3', autoInstall: true, }), - dts(), + // The bundle pass emits the same declarations as the lib pass — skip the ~12s duplicate run. + ...(includeVue ? [] : [dts()]), { name: 'rename-css-file', closeBundle() { diff --git a/packages/frontend/@n8n/i18n/src/index.ts b/packages/frontend/@n8n/i18n/src/index.ts index df1db3e994c..8f3dc011661 100644 --- a/packages/frontend/@n8n/i18n/src/index.ts +++ b/packages/frontend/@n8n/i18n/src/index.ts @@ -2,6 +2,7 @@ import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow'; import { ref } from 'vue'; import { createI18n } from 'vue-i18n'; +import type { I18n } from 'vue-i18n'; import englishBaseText from './locales/en.json'; import type { BaseTextKey, LocaleMessages, INodeTranslationHeaders } from './types'; @@ -14,7 +15,11 @@ import { export type * from './types'; -export const i18nInstance = createI18n({ +// Widened from the inferred type: vue-i18n otherwise resolves every t() call against a +// schema of all ~7.7k en.json keys, costing tens of seconds of typecheck in each consumer. +// Key safety is enforced at the baseText(key: BaseTextKey) boundary instead. +type LooseSchema = Record; +export const i18nInstance: I18n = createI18n({ legacy: false, locale: 'en', fallbackLocale: 'en', diff --git a/scripts/turbo-critical-path.mjs b/scripts/turbo-critical-path.mjs new file mode 100644 index 00000000000..2ca0c5d4412 --- /dev/null +++ b/scripts/turbo-critical-path.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/** + * Reports the build-time-weighted critical path of a Turborepo run, from a + * `turbo run --summarize` summary file. The critical path is the + * dependency chain with the largest total duration — the lower bound on wall + * time no amount of parallelism can beat. Use it to spot tasks worth + * shrinking or edges worth removing. + * + * Usage: node scripts/turbo-critical-path.mjs [path-to-summary.json] + * Defaults to the newest file in .turbo/runs/. Prints markdown; never fails. + */ +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const TOP_N = 10; + +function newestRunSummary() { + const dir = join(process.cwd(), '.turbo', 'runs'); + const files = readdirSync(dir) + .filter((f) => f.endsWith('.json')) + .map((f) => join(dir, f)) + .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs); + if (files.length === 0) throw new Error(`no run summaries in ${dir}`); + return files[0]; +} + +function fmt(seconds) { + return seconds >= 100 ? `${Math.round(seconds)}s` : `${seconds.toFixed(1)}s`; +} + +try { + const file = process.argv[2] ?? newestRunSummary(); + const summary = JSON.parse(readFileSync(file, 'utf8')); + + const tasks = (summary.tasks ?? []) + .filter((t) => t.execution?.startTime && t.execution?.endTime) + .map((t) => ({ + id: t.taskId, + dur: (t.execution.endTime - t.execution.startTime) / 1000, + deps: t.dependencies ?? [], + cached: t.cache?.status === 'HIT', + })); + if (tasks.length === 0) throw new Error(`no executed tasks in ${file}`); + + const byId = new Map(tasks.map((t) => [t.id, t])); + const memo = new Map(); + function heaviestChain(id) { + if (memo.has(id)) return memo.get(id); + const task = byId.get(id); + if (!task) return { total: 0, path: [] }; + // turbo graphs are acyclic, so plain recursion terminates + let best = { total: 0, path: [] }; + for (const dep of task.deps) { + const chain = heaviestChain(dep); + if (chain.total > best.total) best = chain; + } + const result = { total: best.total + task.dur, path: [...best.path, id] }; + memo.set(id, result); + return result; + } + + let critical = { total: 0, path: [] }; + for (const t of tasks) { + const chain = heaviestChain(t.id); + if (chain.total > critical.total) critical = chain; + } + + const cpu = tasks.reduce((sum, t) => sum + t.dur, 0); + const wall = + (Math.max(...summary.tasks.map((t) => t.execution?.endTime ?? 0)) - + Math.min( + ...summary.tasks.filter((t) => t.execution?.startTime).map((t) => t.execution.startTime), + )) / + 1000; + const cachedCount = tasks.filter((t) => t.cached).length; + + const label = (t) => `\`${t.id}\`${t.cached ? ' (cache hit)' : ''}`; + + const lines = [ + `### Turbo critical path — \`${summary.execution?.command ?? 'run'}\``, + '', + `Wall ${fmt(wall)} · task CPU ${fmt(cpu)} · parallelism ${(cpu / wall).toFixed(1)}× · ${tasks.length} tasks (${cachedCount} cache hits)`, + '', + `**Critical path: ${fmt(critical.total)}** (${((critical.total / wall) * 100).toFixed(0)}% of wall time)`, + '', + ...critical.path.map((id) => `- ${fmt(byId.get(id).dur)} ${label(byId.get(id))}`), + '', + `**Slowest tasks**`, + '', + ...tasks + .slice() + .sort((a, b) => b.dur - a.dur) + .slice(0, TOP_N) + .map((t) => `- ${fmt(t.dur)} ${label(t)}`), + '', + ]; + console.log(lines.join('\n')); +} catch (error) { + // Reporting only — never break the build over it. + console.log(`turbo-critical-path: skipped (${error.message})`); +} diff --git a/turbo.json b/turbo.json index e2b6e1d3e0e..8faa43c8118 100644 --- a/turbo.json +++ b/turbo.json @@ -27,7 +27,9 @@ "env": ["RELEASE"] }, "typecheck": { - "dependsOn": ["^typecheck", "^build"] + // Typechecks are all --noEmit and read upstream types from dist (^build) or source + // (tsconfig paths) — depending on ^typecheck would only serialize the pipeline. + "dependsOn": ["^build"] }, "format": {}, "format:check": {},