From 52760e713c571e1486a46d5e9d11a4d3f5666805 Mon Sep 17 00:00:00 2001 From: liurenfeng94-ops <279598012+liurenfeng94-ops@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:25:39 +0800 Subject: [PATCH] Add configurable default save name command --- package.json | 36 +++++++++++++++++++-------------- scripts/generate-contributes.ts | 7 +++++++ src/appState.ts | 10 ++++++--- src/configuration.ts | 3 +++ src/constants.ts | 1 + src/saveName.ts | 25 +++++++++++++++++++++++ src/storage.ts | 25 +++++++++++++++++++++++ test/save-name.test.ts | 18 +++++++++++++++++ 8 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 src/saveName.ts create mode 100644 test/save-name.test.ts diff --git a/package.json b/package.json index ddfb78b..24b7164 100644 --- a/package.json +++ b/package.json @@ -52,53 +52,59 @@ ], "order": 0 }, + "tsperf.tracer.saveNameCommand": { + "type": "string", + "default": "", + "description": "Optional shell command used to generate the default save name. The first line of stdout is used; empty output or command failures fall back to \"default\".", + "order": 1 + }, "tsperf.tracer.traceCmd": { "type": "string", "default": "npx tsc --noEmit --generateTrace ${traceDir}", "description": "command to generate tsc traces", - "order": 1 + "order": 2 }, "tsperf.tracer.typescriptPath": { "type": "string", "default": "", "description": "Path to TypeScript. Must be specified if 'Use TypeScript from' is 'Use tracer TypeScript path setting'", - "order": 2 + "order": 3 }, "tsperf.tracer.benchmarkIterations": { "type": "number", "default": 3, "description": "Higher values reduce variance but increase benchmarking time", - "order": 3 + "order": 4 }, "tsperf.tracer.restartTsserverOnIteration": { "type": "boolean", "default": false, "description": "Restart tsserver on each iteration to avoid caching influincing measurements", - "order": 4 + "order": 5 }, "tsperf.tracer.allIdentifiers": { "type": "boolean", "default": false, "description": "Benchmark all allIdentifiers or only the first of each statement", - "order": 5 + "order": 6 }, "tsperf.tracer.enableRealtimeMetrics": { "type": "boolean", "default": true, "description": "Create diagnostics from tsserver", - "order": 6 + "order": 7 }, "tsperf.tracer.enableTraceMetrics": { "type": "boolean", "default": true, "description": "Create diagnostics from trace data", - "order": 7 + "order": 8 }, "tsperf.tracer.fileBrowserExecutable": { "type": "string", "default": "", "description": "command to open your preferred file browser. Use the ${traceDir} substitution variable", - "order": 8 + "order": 9 }, "tsperf.tracer.traceTimeThresholds": { "type": "object", @@ -125,7 +131,7 @@ "error": -1 }, "description": "todo", - "order": 9, + "order": 10, "markdownDescription": "# Trace Time Thresholds\n\nTrigger diagnostics from trace files when check time in ms exceeds these thresholds.\n\n* -1 will disable diagnostics of that severity\n" }, "tsperf.tracer.traceTypeThresholds": { @@ -153,7 +159,7 @@ "error": -1 }, "description": "todo", - "order": 10, + "order": 11, "markdownDescription": "# Trace Type Thresholds\n\nTrigger diagnostics from trace files when the number of types created locally in this check call exceeds these thresholds.\n\n* -1 will disable diagnostics of that severity\n" }, "tsperf.tracer.traceTotalTypeThresholds": { @@ -181,14 +187,14 @@ "error": -1 }, "description": "todo", - "order": 11, + "order": 12, "markdownDescription": "# Trace Total Type Thresholds\n\nTrigger diagnostics from trace files when the number of types created in this check call stack exceeds these thresholds.\n\n* -1 will disable diagnostics of that severity\n" }, "tsperf.tracer.traceDiagnosticsRelative": { "type": "boolean", "default": false, "description": "Use measurements relative to the average for trace diagnostics", - "order": 12 + "order": 13 }, "tsperf.tracer.traceTimeRelativeThresholds": { "type": "object", @@ -215,7 +221,7 @@ "error": -1 }, "description": "todo", - "order": 13 + "order": 14 }, "tsperf.tracer.traceTypeRelativeThresholds": { "type": "object", @@ -242,7 +248,7 @@ "error": -1 }, "description": "todo", - "order": 14 + "order": 15 }, "tsperf.tracer.traceTotalTypeRelativeThresholds": { "type": "object", @@ -269,7 +275,7 @@ "error": -1 }, "description": "todo", - "order": 15 + "order": 16 } } }, diff --git a/scripts/generate-contributes.ts b/scripts/generate-contributes.ts index 59f33f1..196dee3 100755 --- a/scripts/generate-contributes.ts +++ b/scripts/generate-contributes.ts @@ -126,6 +126,13 @@ const orderedConfigurationProperties: Partial postMessage({ message: 'projectOpen', name })) watchT('savePath', (path) => { @@ -138,7 +138,11 @@ export async function initAppState(extensionContext: vscode.ExtensionContext) { workspacePath.value = getWorkspacePath() projectName.value = getProjectName() - saveName.value = 'default' + void openDefaultSaveName() +} + +async function openDefaultSaveName() { + saveName.value = await getDefaultSaveName() } const triggers: Partial void | Promise), remoteHandler: (arg: any) => void }>> = {} diff --git a/src/configuration.ts b/src/configuration.ts index 3193be0..0539d3b 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -9,6 +9,7 @@ const currentConfig = { benchmarkIterations: 3, restartTsserverOnIteration: false, allIdentifiers: false, + saveNameCommand: '', // eslint-disable-next-line no-template-curly-in-string traceCmd: 'npx tsc --noEmit --generateTrace ${traceDir}', traceTimeThresholds: { info: 1, warning: -1, error: -1 }, @@ -56,6 +57,7 @@ const configValidate = { benchmarkIterations: isNumber, restartTsserverOnIteration: isBoolean, allIdentifiers: isBoolean, + saveNameCommand: isString, traceCmd: isString, traceTimeThresholds: isThresholds, traceTypeThresholds: isThresholds, @@ -78,6 +80,7 @@ const configHandlers = { benchmarkIterations: noop, restartTsserverOnIteration: noop, allIdentifiers: noop, + saveNameCommand: noop, traceCmd: noop, traceTimeThresholds: noop, traceTypeThresholds: noop, diff --git a/src/constants.ts b/src/constants.ts index fc97b3b..fa893cd 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,6 +6,7 @@ export const configKeys = [ 'benchmarkIterations', 'restartTsserverOnIteration', 'allIdentifiers', + 'saveNameCommand', 'traceCmd', 'traceTimeThresholds', 'traceTypeThresholds', diff --git a/src/saveName.ts b/src/saveName.ts new file mode 100644 index 0000000..600f052 --- /dev/null +++ b/src/saveName.ts @@ -0,0 +1,25 @@ +import { isAbsolute, normalize } from 'node:path' + +export const DEFAULT_SAVE_NAME = 'default' + +export function normalizeSaveName(value: string) { + const trimmed = value.trim() + if (!trimmed) + return DEFAULT_SAVE_NAME + + const rawParts = trimmed.split(/[\\/]+/).filter(Boolean) + if (rawParts.includes('..')) + return DEFAULT_SAVE_NAME + if (/^(?:[a-z]:[\\/]|\\\\)/i.test(trimmed)) + return DEFAULT_SAVE_NAME + + const normalized = normalize(trimmed).replaceAll(/\\/g, '/') + if (isAbsolute(normalized)) + return DEFAULT_SAVE_NAME + + const parts = normalized.split('/').filter(Boolean) + if (!parts.length || parts.includes('..')) + return DEFAULT_SAVE_NAME + + return parts.join('/') +} diff --git a/src/storage.ts b/src/storage.ts index aa95859..cab15a9 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,12 +1,17 @@ +import { exec } from 'node:child_process' import { readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { env } from 'node:process' +import { promisify } from 'node:util' import * as vscode from 'vscode' import { traceData } from '../shared/src/traceData' import { getCurrentConfig } from './configuration' import { log } from './logger' +import { DEFAULT_SAVE_NAME, normalizeSaveName } from './saveName' import { state } from './appState' +const execAsync = promisify(exec) + export function getProjectName(): string { if (state.projectName.value) return state.projectName.value @@ -26,6 +31,26 @@ export function getProjectName(): string { return state.projectName.value } +export async function getDefaultSaveName(): Promise { + const command = getCurrentConfig().saveNameCommand.trim() + if (!command) + return DEFAULT_SAVE_NAME + + try { + const { stdout } = await execAsync(command, { + cwd: state.workspacePath.value || getWorkspacePath(), + timeout: 5000, + windowsHide: true, + }) + const [firstLine = ''] = stdout.trim().split(/\r?\n/) + return normalizeSaveName(firstLine) + } + catch (e) { + log(`saveNameCommand failed: ${e}`) + return DEFAULT_SAVE_NAME + } +} + export function getSavePath(): string { return state.savePath.value = join(state.projectPath.value, state.saveName.value) } diff --git a/test/save-name.test.ts b/test/save-name.test.ts new file mode 100644 index 0000000..9d1827f --- /dev/null +++ b/test/save-name.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { DEFAULT_SAVE_NAME, normalizeSaveName } from '../src/saveName' + +describe('normalizeSaveName', () => { + it('uses default for empty, absolute, or parent-traversal names', () => { + expect(normalizeSaveName('')).toBe(DEFAULT_SAVE_NAME) + expect(normalizeSaveName(' ')).toBe(DEFAULT_SAVE_NAME) + expect(normalizeSaveName('/tmp/trace')).toBe(DEFAULT_SAVE_NAME) + expect(normalizeSaveName('C:\\trace')).toBe(DEFAULT_SAVE_NAME) + expect(normalizeSaveName('../trace')).toBe(DEFAULT_SAVE_NAME) + expect(normalizeSaveName('feature/../trace')).toBe(DEFAULT_SAVE_NAME) + }) + + it('allows nested relative save names', () => { + expect(normalizeSaveName(' main ')).toBe('main') + expect(normalizeSaveName('feature/perf-trace')).toBe('feature/perf-trace') + }) +})