From f8517eac8868382b80b787e689333a28c95a0a11 Mon Sep 17 00:00:00 2001 From: lemon1825 <83858588+lemon1825@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:32:58 +0900 Subject: [PATCH] fix: normalize Windows project file paths --- .../src/common/file-path.utils.ts | 32 +++++++++++++++++++ .../src/modules/preview/preview.service.ts | 5 +-- .../src/modules/sandbox/sandbox.service.ts | 9 +++--- .../src/modules/session/session.service.ts | 5 +-- 4 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 apps/platform-api/src/common/file-path.utils.ts diff --git a/apps/platform-api/src/common/file-path.utils.ts b/apps/platform-api/src/common/file-path.utils.ts new file mode 100644 index 0000000..2958940 --- /dev/null +++ b/apps/platform-api/src/common/file-path.utils.ts @@ -0,0 +1,32 @@ +import * as path from 'path'; + +/** + * Normalize a user- or agent-supplied file path into a safe repo-relative path. + * + * Handles Windows drive prefixes and backslashes so server-side logic behaves + * consistently for snapshots created from Windows checkouts and cloud Linux runners. + */ +export function normalizeProjectFilePath(filepath: string): string { + return filepath + .replace(/^[A-Za-z]:/, '') + .replace(/\\/g, '/') + .replace(/^\/+/, '') + .split('/') + .filter((segment) => segment && segment !== '.' && segment !== '..') + .join('/'); +} + +/** Resolve a normalized project file path inside a workspace directory. */ +export function resolveProjectFilePath(baseDir: string, filepath: string): string { + const normalizedPath = normalizeProjectFilePath(filepath); + return path.join(baseDir, normalizedPath); +} + + +/** Match a normalized path against a repo-relative target, tolerating accidental absolute prefixes. */ +export function matchesProjectFilePath(filepath: string, expectedPath: string): boolean { + const normalizedPath = normalizeProjectFilePath(filepath); + const normalizedExpectedPath = normalizeProjectFilePath(expectedPath); + + return normalizedPath === normalizedExpectedPath || normalizedPath.endsWith(`/${normalizedExpectedPath}`); +} diff --git a/apps/platform-api/src/modules/preview/preview.service.ts b/apps/platform-api/src/modules/preview/preview.service.ts index 6aefb18..cddfc5a 100644 --- a/apps/platform-api/src/modules/preview/preview.service.ts +++ b/apps/platform-api/src/modules/preview/preview.service.ts @@ -10,6 +10,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as net from 'net'; import { PreviewInstance, PreviewStatus } from './preview.interfaces'; +import { matchesProjectFilePath, resolveProjectFilePath } from '../../common/file-path.utils'; /** Port range reserved for preview dev servers */ const PORT_RANGE_START = 5100; @@ -76,7 +77,7 @@ export class PreviewService implements OnModuleDestroy { // Check if there is a package.json to install deps const hasPackageJson = files.some( - (f) => path.normalize(f.filepath) === 'package.json', + (f) => matchesProjectFilePath(f.filepath, 'package.json'), ); if (hasPackageJson) { @@ -197,7 +198,7 @@ export class PreviewService implements OnModuleDestroy { await fs.mkdir(baseDir, { recursive: true }); for (const file of files) { - const fullPath = path.join(baseDir, file.filepath); + const fullPath = resolveProjectFilePath(baseDir, file.filepath); const dir = path.dirname(fullPath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(fullPath, file.content, 'utf-8'); diff --git a/apps/platform-api/src/modules/sandbox/sandbox.service.ts b/apps/platform-api/src/modules/sandbox/sandbox.service.ts index 4c61742..f189b14 100644 --- a/apps/platform-api/src/modules/sandbox/sandbox.service.ts +++ b/apps/platform-api/src/modules/sandbox/sandbox.service.ts @@ -5,6 +5,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import { BuildRequest, BuildResult } from './sandbox.interfaces'; +import { matchesProjectFilePath, resolveProjectFilePath } from '../../common/file-path.utils'; const execAsync = promisify(exec); @@ -54,7 +55,7 @@ export class SandboxService { // Step 2: Check if package.json exists; if not, skip install const hasPackageJson = request.files.some( - (f) => path.normalize(f.filepath) === 'package.json', + (f) => matchesProjectFilePath(f.filepath, 'package.json'), ); if (hasPackageJson) { @@ -76,8 +77,8 @@ export class SandboxService { // Step 4: TypeScript type-check (only if tsconfig exists) const hasTsConfig = request.files.some( (f) => - path.normalize(f.filepath) === 'tsconfig.json' || - path.normalize(f.filepath) === 'tsconfig.app.json', + matchesProjectFilePath(f.filepath, 'tsconfig.json') || + matchesProjectFilePath(f.filepath, 'tsconfig.app.json'), ); if (hasTsConfig) { @@ -145,7 +146,7 @@ export class SandboxService { files: Array<{ filepath: string; content: string }>, ): Promise { for (const file of files) { - const fullPath = path.join(baseDir, file.filepath); + const fullPath = resolveProjectFilePath(baseDir, file.filepath); const dir = path.dirname(fullPath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(fullPath, file.content, 'utf-8'); diff --git a/apps/platform-api/src/modules/session/session.service.ts b/apps/platform-api/src/modules/session/session.service.ts index f95ff6c..55ca56b 100644 --- a/apps/platform-api/src/modules/session/session.service.ts +++ b/apps/platform-api/src/modules/session/session.service.ts @@ -13,6 +13,7 @@ import { SandboxService } from '../sandbox/sandbox.service'; import { PreviewService } from '../preview/preview.service'; import { DocsService } from '../docs/docs.service'; import { CreateSessionDto, SendMessageDto } from './session.dto'; +import { matchesProjectFilePath, normalizeProjectFilePath } from '../../common/file-path.utils'; /** Shape of SSE events pushed to the client */ export interface SessionSseEvent { @@ -749,7 +750,7 @@ export class SessionService { private detectFramework( files: Array<{ filepath: string; content: string }>, ): 'vite-react' | 'nextjs' | 'static' { - const filenames = files.map((f) => f.filepath.replace(/\\/g, '/')); + const filenames = files.map((f) => normalizeProjectFilePath(f.filepath)); if (filenames.some((f) => f.includes('vite.config'))) { return 'vite-react'; @@ -759,7 +760,7 @@ export class SessionService { } // Check package.json for framework hints - const packageJson = files.find((f) => f.filepath === 'package.json'); + const packageJson = files.find((f) => matchesProjectFilePath(f.filepath, 'package.json')); if (packageJson) { try { const pkg = JSON.parse(packageJson.content);