diff --git a/packages/browser/src/client/snapshot.ts b/packages/browser/src/client/snapshot.ts index d37eadf635a3..b0d43a0e40c6 100644 --- a/packages/browser/src/client/snapshot.ts +++ b/packages/browser/src/client/snapshot.ts @@ -11,11 +11,11 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment { } readSnapshotFile(filepath: string): Promise { - return rpc().readFile(filepath) + return rpc().readSnapshotFile(filepath) } saveSnapshotFile(filepath: string, snapshot: string): Promise { - return rpc().writeFile(filepath, snapshot, true) + return rpc().saveSnapshotFile(filepath, snapshot) } resolvePath(filepath: string): Promise { @@ -27,10 +27,6 @@ export class BrowserSnapshotEnvironment implements SnapshotEnvironment { } removeSnapshotFile(filepath: string): Promise { - return rpc().removeFile(filepath) - } - - async prepareDirectory(dirPath: string): Promise { - await rpc().createDirectory(dirPath) + return rpc().removeSnapshotFile(filepath) } } diff --git a/packages/snapshot/src/manager.ts b/packages/snapshot/src/manager.ts index 31482627be2d..756ea8b52c8c 100644 --- a/packages/snapshot/src/manager.ts +++ b/packages/snapshot/src/manager.ts @@ -3,6 +3,7 @@ import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './ty export class SnapshotManager { summary: SnapshotSummary = undefined! + resolvedPaths = new Set() extension = '.snap' constructor(public options: Omit) { @@ -26,7 +27,9 @@ export class SnapshotManager { ) }) - return resolver(testPath, this.extension) + const path = resolver(testPath, this.extension) + this.resolvedPaths.add(path) + return path } resolveRawPath(testPath: string, rawPath: string) { diff --git a/packages/snapshot/src/port/utils.ts b/packages/snapshot/src/port/utils.ts index 522f11604866..69e02e1c323c 100644 --- a/packages/snapshot/src/port/utils.ts +++ b/packages/snapshot/src/port/utils.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import { dirname, join } from 'pathe' import naturalCompare from 'natural-compare' import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format' import { @@ -128,13 +127,6 @@ function printBacktickString(str: string): string { return `\`${escapeBacktickString(str)}\`` } -export async function ensureDirectoryExists(environment: SnapshotEnvironment, filePath: string) { - try { - await environment.prepareDirectory(join(dirname(filePath))) - } - catch { } -} - export function normalizeNewlines(string: string) { return string.replace(/\r\n|\r/g, '\n') } @@ -157,7 +149,6 @@ export async function saveSnapshotFile( if (skipWriting) return - await ensureDirectoryExists(environment, snapshotPath) await environment.saveSnapshotFile( snapshotPath, content, @@ -175,7 +166,6 @@ export async function saveSnapshotFileRaw( if (skipWriting) return - await ensureDirectoryExists(environment, snapshotPath) await environment.saveSnapshotFile( snapshotPath, content, diff --git a/packages/snapshot/src/types/environment.ts b/packages/snapshot/src/types/environment.ts index c2dc09e0714d..450274fb7c14 100644 --- a/packages/snapshot/src/types/environment.ts +++ b/packages/snapshot/src/types/environment.ts @@ -3,7 +3,6 @@ export interface SnapshotEnvironment { getHeader(): string resolvePath(filepath: string): Promise resolveRawPath(testPath: string, rawPath: string): Promise - prepareDirectory(dirPath: string): Promise saveSnapshotFile(filepath: string, snapshot: string): Promise readSnapshotFile(filepath: string): Promise removeSnapshotFile(filepath: string): Promise diff --git a/packages/ui/client/components/views/ViewEditor.vue b/packages/ui/client/components/views/ViewEditor.vue index 9de5001cfd6e..957fde0f2161 100644 --- a/packages/ui/client/components/views/ViewEditor.vue +++ b/packages/ui/client/components/views/ViewEditor.vue @@ -24,7 +24,7 @@ watch(() => props.file, draft.value = false return } - code.value = await client.rpc.readFile(props.file.filepath) || '' + code.value = await client.rpc.readTestFile(props.file.filepath) || '' serverCode.value = code.value draft.value = false }, @@ -116,7 +116,7 @@ watch([cm, failed], ([cmValue]) => { async function onSave(content: string) { hasBeenEdited.value = true - await client.rpc.writeFile(props.file!.filepath, content) + await client.rpc.saveTestFile(props.file!.filepath, content) serverCode.value = content draft.value = false } diff --git a/packages/ui/client/composables/client/static.ts b/packages/ui/client/composables/client/static.ts index d044ae0e61bc..113af2310291 100644 --- a/packages/ui/client/composables/client/static.ts +++ b/packages/ui/client/composables/client/static.ts @@ -46,21 +46,26 @@ export function createStaticClient(): VitestClient { return { code: id, source: '', + map: null, } }, - readFile: async (id) => { - return Promise.resolve(id) - }, onDone: noop, onCollected: asyncNoop, onTaskUpdate: noop, writeFile: asyncNoop, rerun: asyncNoop, updateSnapshot: asyncNoop, - removeFile: asyncNoop, - createDirectory: asyncNoop, resolveSnapshotPath: asyncNoop, snapshotSaved: asyncNoop, + onAfterSuiteRun: asyncNoop, + onCancel: asyncNoop, + getCountOfFailedTests: () => 0, + sendLog: asyncNoop, + resolveSnapshotRawPath: asyncNoop, + readSnapshotFile: asyncNoop, + saveSnapshotFile: asyncNoop, + readTestFile: asyncNoop, + removeSnapshotFile: asyncNoop, } as WebSocketHandlers ctx.rpc = rpc as any as BirpcReturn diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 542ada2721fe..c9c8959b0d94 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -127,6 +127,9 @@ export function parseSingleV8Stack(raw: string): ParsedStack | null { // normalize Windows path (\ -> /) file = resolve(file) + if (method) + method = method.replace(/__vite_ssr_import_\d+__\./g, '') + return { method, file, diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 6e7e63b0d9bf..df830dda504b 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -69,25 +69,36 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit resolveSnapshotRawPath(testPath, rawPath) { return ctx.snapshot.resolveRawPath(testPath, rawPath) }, - removeFile(id) { - return fs.unlink(id) - }, - createDirectory(id) { - return fs.mkdir(id, { recursive: true }) + async readSnapshotFile(snapshotPath) { + if (!ctx.snapshot.resolvedPaths.has(snapshotPath) || !existsSync(snapshotPath)) + return null + return fs.readFile(snapshotPath, 'utf-8') }, - async readFile(id) { - if (!existsSync(id)) + async readTestFile(id) { + if (!ctx.state.filesMap.has(id) || !existsSync(id)) return null return fs.readFile(id, 'utf-8') }, + async saveTestFile(id, content) { + // can save only already existing test file + if (!ctx.state.filesMap.has(id) || !existsSync(id)) + return + return fs.writeFile(id, content, 'utf-8') + }, + async saveSnapshotFile(id, content) { + if (!ctx.snapshot.resolvedPaths.has(id)) + return + await fs.mkdir(dirname(id), { recursive: true }) + return fs.writeFile(id, content, 'utf-8') + }, + async removeSnapshotFile(id) { + if (!ctx.snapshot.resolvedPaths.has(id) || !existsSync(id)) + return + return fs.unlink(id) + }, snapshotSaved(snapshot) { ctx.snapshot.add(snapshot) }, - async writeFile(id, content, ensureDir) { - if (ensureDir) - await fs.mkdir(dirname(id), { recursive: true }) - return await fs.writeFile(id, content, 'utf-8') - }, async rerun(files) { await ctx.rerunFiles(files) }, diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index 3f96018b2d82..cf7e7d1a744e 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -21,10 +21,11 @@ export interface WebSocketHandlers { resolveSnapshotRawPath(testPath: string, rawPath: string): string getModuleGraph(id: string): Promise getTransformResult(id: string): Promise - readFile(id: string): Promise - writeFile(id: string, content: string, ensureDir?: boolean): Promise - removeFile(id: string): Promise - createDirectory(id: string): Promise + readSnapshotFile(id: string): Promise + readTestFile(id: string): Promise + saveTestFile(id: string, content: string): Promise + saveSnapshotFile(id: string, content: string): Promise + removeSnapshotFile(id: string): Promise snapshotSaved(snapshot: SnapshotResult): void rerun(files: string[]): Promise updateSnapshot(file?: File): Promise diff --git a/packages/vitest/src/integrations/browser/server.ts b/packages/vitest/src/integrations/browser/server.ts index 7dd2ac36b431..151b396ad8af 100644 --- a/packages/vitest/src/integrations/browser/server.ts +++ b/packages/vitest/src/integrations/browser/server.ts @@ -8,6 +8,7 @@ import { resolveApiServerConfig } from '../../node/config' import { CoverageTransform } from '../../node/plugins/coverageTransform' import type { WorkspaceProject } from '../../node/workspace' import { MocksPlugin } from '../../node/plugins/mocks' +import { resolveFsAllow } from '../../node/plugins/utils' export async function createBrowserServer(project: WorkspaceProject, options: UserConfig) { const root = project.config.root @@ -44,7 +45,13 @@ export async function createBrowserServer(project: WorkspaceProject, options: Us config.server = server config.server.fs ??= {} - config.server.fs.strict = false + config.server.fs.allow = config.server.fs.allow || [] + config.server.fs.allow.push( + ...resolveFsAllow( + project.ctx.config.root, + project.ctx.server.config.configFile, + ), + ) return { resolve: { diff --git a/packages/vitest/src/node/create.ts b/packages/vitest/src/node/create.ts index ffe60a021b62..a0b9dec101e8 100644 --- a/packages/vitest/src/node/create.ts +++ b/packages/vitest/src/node/create.ts @@ -17,6 +17,8 @@ export async function createVitest(mode: VitestRunMode, options: UserConfig, vit ? resolve(root, options.config) : await findUp(configFiles, { cwd: root } as any) + options.config = configPath + const config: ViteInlineConfig = { logLevel: 'error', configFile: configPath, diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 465471b11b25..4cd4d47d6ee9 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -12,7 +12,7 @@ import { GlobalSetupPlugin } from './globalSetup' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' import { MocksPlugin } from './mocks' -import { deleteDefineConfig, hijackVitePluginInject, resolveOptimizerConfig } from './utils' +import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow, resolveOptimizerConfig } from './utils' import { VitestResolver } from './vitestResolver' export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('test')): Promise { @@ -87,6 +87,9 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t open, hmr: false, preTransformRequests: false, + fs: { + allow: resolveFsAllow(getRoot(), testConfig.config), + }, }, } diff --git a/packages/vitest/src/node/plugins/utils.ts b/packages/vitest/src/node/plugins/utils.ts index 8b0a038a9a92..122d0eb78984 100644 --- a/packages/vitest/src/node/plugins/utils.ts +++ b/packages/vitest/src/node/plugins/utils.ts @@ -1,6 +1,7 @@ import { builtinModules } from 'node:module' -import { version as viteVersion } from 'vite' +import { searchForWorkspaceRoot, version as viteVersion } from 'vite' import type { DepOptimizationOptions, ResolvedConfig, UserConfig as ViteConfig } from 'vite' +import { dirname } from 'pathe' import type { DepsOptimizationOptions, InlineConfig } from '../../types' export function resolveOptimizerConfig(_testOptions: DepsOptimizationOptions | undefined, viteOptions: DepOptimizationOptions | undefined, testConfig: InlineConfig) { @@ -84,3 +85,9 @@ export function hijackVitePluginInject(viteConfig: ResolvedConfig) { } } } + +export function resolveFsAllow(projectRoot: string, rootConfigFile: string | false | undefined) { + if (!rootConfigFile) + return [searchForWorkspaceRoot(projectRoot)] + return [dirname(rootConfigFile), searchForWorkspaceRoot(projectRoot)] +} diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 97dc7639a234..fef1b2299349 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -10,7 +10,7 @@ import { CSSEnablerPlugin } from './cssEnabler' import { SsrReplacerPlugin } from './ssrReplacer' import { GlobalSetupPlugin } from './globalSetup' import { MocksPlugin } from './mocks' -import { deleteDefineConfig, hijackVitePluginInject, resolveOptimizerConfig } from './utils' +import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow, resolveOptimizerConfig } from './utils' import { VitestResolver } from './vitestResolver' interface WorkspaceOptions extends UserWorkspaceConfig { @@ -69,6 +69,12 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp open: false, hmr: false, preTransformRequests: false, + fs: { + allow: resolveFsAllow( + project.ctx.config.root, + project.ctx.server.config.configFile, + ), + }, }, test: { env, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58ee55b81ae5..9e81230aa2b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1834,6 +1834,15 @@ importers: specifier: workspace:* version: link:../../packages/vitest + test/restricted: + devDependencies: + jsdom: + specifier: ^22.1.0 + version: 22.1.0 + vitest: + specifier: workspace:* + version: link:../../packages/vitest + test/run: devDependencies: vite: @@ -6765,7 +6774,7 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - '@types/node': 20.5.0 + '@types/node': 18.16.19 playwright-core: 1.28.0 dev: true @@ -8922,7 +8931,7 @@ packages: resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==} dependencies: '@types/jsonfile': 6.1.1 - '@types/node': 18.16.19 + '@types/node': 20.5.0 dev: true /@types/fs-extra@9.0.13: @@ -9312,7 +9321,7 @@ packages: /@types/ws@8.5.5: resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} dependencies: - '@types/node': 18.16.19 + '@types/node': 20.5.0 dev: true /@types/yargs-parser@21.0.0: diff --git a/test/restricted/package.json b/test/restricted/package.json new file mode 100644 index 000000000000..6a7c5e6c1afe --- /dev/null +++ b/test/restricted/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitest/test-restricted", + "private": true, + "scripts": { + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "devDependencies": { + "jsdom": "^22.1.0", + "vitest": "workspace:*" + } +} diff --git a/test/restricted/src/math.js b/test/restricted/src/math.js new file mode 100644 index 000000000000..b99ad1b10661 --- /dev/null +++ b/test/restricted/src/math.js @@ -0,0 +1,3 @@ +export function multiply(a, b) { + return a * b +} diff --git a/test/restricted/tests/basic.spec.js b/test/restricted/tests/basic.spec.js new file mode 100644 index 000000000000..712aa95767fa --- /dev/null +++ b/test/restricted/tests/basic.spec.js @@ -0,0 +1,7 @@ +import { expect, it } from 'vitest' +import { multiply } from '../src/math' + +it('2 x 2 = 4', () => { + expect(multiply(2, 2)).toBe(4) + expect(multiply(2, 2)).toBe(Math.sqrt(16)) +}) diff --git a/test/restricted/vitest.config.ts b/test/restricted/vitest.config.ts new file mode 100644 index 000000000000..7ccc9dc48994 --- /dev/null +++ b/test/restricted/vitest.config.ts @@ -0,0 +1,29 @@ +import { resolve } from 'pathe' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + { + // simulates restrictive FS + name: 'restrict-fs', + config() { + return { + server: { + fs: { + allow: [ + resolve(__dirname, 'src'), + ], + }, + }, + } + }, + }, + ], + test: { + environment: 'jsdom', + include: ['tests/**/*.spec.{js,ts}'], + setupFiles: [ + './vitest.setup.js', + ], + }, +}) diff --git a/test/restricted/vitest.setup.js b/test/restricted/vitest.setup.js new file mode 100644 index 000000000000..69ad9ac10420 --- /dev/null +++ b/test/restricted/vitest.setup.js @@ -0,0 +1 @@ +globalThis.SOME_TEST_VARIABLE = '3'