Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/core/builder/build.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IMiddlewareContribution } from '../../server/interfaces';
import { Request, Response } from 'express';
import { join } from 'path';
import { existsSync } from 'fs';
import { sendFileAllowingDotfiles } from '../../server/utils';

const buildMaps: Record<string, string> = {};
const destMaps: Record<string, string> = {};
Expand Down Expand Up @@ -35,7 +36,7 @@ export default {
if (dest && file) {
const path = join(dest, file);
if (existsSync(path)) {
return res.sendFile(path);
return sendFileAllowingDotfiles(res, path);
}
}

Expand Down
31 changes: 13 additions & 18 deletions src/core/preview/scripting-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import {
mergeGraphicsConfigWithModules,
normalizeIncludeModulesWithGraphics,
} from '../engine/graphics-config';

function sendQuickPackChunk(res: Response, filePath: string): void {
// QuickPack may emit chunks under project temp paths used by smoke workspaces.
// The path is resolved by the loader, not by raw URL-to-file joining.
res.sendFile(filePath, { dotfiles: 'allow' });
}
import { sendFileAllowingDotfiles } from '../../server/utils';

/**
* 动态预览的共享资源路由。
Expand Down Expand Up @@ -53,7 +48,7 @@ export const scriptingRoutes = [
const relPath = decodeURIComponent(rawPath.substring('/external'.length));
const resourcePath = join(facet.engineDistRoot, 'external', relPath);
if (await pathExists(resourcePath) && (await stat(resourcePath)).isFile()) {
res.sendFile(resourcePath, { dotfiles: 'allow' });
sendFileAllowingDotfiles(res, resourcePath);
} else {
next();
}
Expand Down Expand Up @@ -206,7 +201,7 @@ export const scriptingRoutes = [
const file = info?.library?.['.js'];
if (file && await pathExists(file) && (await stat(file)).isFile()) {
res.set('Cache-Control', 'no-store');
res.sendFile(file, { dotfiles: 'allow' });
sendFileAllowingDotfiles(res, file);
} else {
console.warn(`[Preview Server] Plugin script not found: ${relPath}`);
next();
Expand All @@ -226,7 +221,7 @@ export const scriptingRoutes = [
relPath = decodeURIComponent(relPath);
const resourcePath = join(facet.engineDistRoot, relPath);
if (await pathExists(resourcePath) && (await stat(resourcePath)).isFile()) {
res.sendFile(resourcePath, { dotfiles: 'allow' });
sendFileAllowingDotfiles(res, resourcePath);
} else {
next();
}
Expand Down Expand Up @@ -343,7 +338,7 @@ export const scriptingRoutes = [
const { default: scripting } = await import('../../core/scripting');
const effectBinPath = join(scripting.projectPath, 'temp', 'asset-db', 'effect', 'effect.bin');
if (await pathExists(effectBinPath) && (await stat(effectBinPath)).isFile()) {
res.sendFile(effectBinPath);
sendFileAllowingDotfiles(res, effectBinPath);
} else {
next();
}
Expand Down Expand Up @@ -426,7 +421,7 @@ export const scriptingRoutes = [
if (packResource.type === 'json') {
res.json(packResource.json);
} else if (packResource.type === 'chunk') {
sendQuickPackChunk(res, packResource.chunk.path);
sendFileAllowingDotfiles(res, packResource.chunk.path);
} else {
console.warn(`[Preview Server] Unknown pack resource type for ${fullUrl}:`, packResource);
next(new Error('Unknown pack resource type'));
Expand All @@ -446,7 +441,7 @@ export const scriptingRoutes = [
try {
const packResource = await facet.loadPackResource(url);
if (packResource.type === 'chunk') {
sendQuickPackChunk(res, packResource.chunk.path);
sendFileAllowingDotfiles(res, packResource.chunk.path);
} else if (packResource.type === 'json') {
res.json(packResource.json);
} else {
Expand Down Expand Up @@ -501,7 +496,7 @@ export const scriptingRoutes = [
}

if (await pathExists(resourcePath) && (await stat(resourcePath)).isFile()) {
res.sendFile(resourcePath, { dotfiles: 'allow' });
sendFileAllowingDotfiles(res, resourcePath);
} else {
console.warn(`[Preview Server] Engine resource NOT FOUND on disk: ${resourcePath}`);
next();
Expand Down Expand Up @@ -538,7 +533,7 @@ export const scriptingRoutes = [
}
}
if (await pathExists(resourcePath) && (await stat(resourcePath)).isFile()) {
return res.sendFile(resourcePath, { dotfiles: 'allow' });
return sendFileAllowingDotfiles(res, resourcePath);
}
}
next();
Expand All @@ -550,7 +545,7 @@ export const scriptingRoutes = [
const relPath = req.path.substring('/static/web'.length);
const resourcePath = join(GlobalPaths.workspace, 'static', 'web', relPath);
if (await pathExists(resourcePath) && (await stat(resourcePath)).isFile()) {
res.sendFile(resourcePath);
sendFileAllowingDotfiles(res, resourcePath);
} else {
console.warn(`[Preview Server] Static resource not found: ${resourcePath}`);
next();
Expand All @@ -566,12 +561,12 @@ export const scriptingRoutes = [
if (relPath.startsWith('/extras/')) {
const extraPath = join(GlobalPaths.workspace, 'node_modules', '@cocos', 'systemjs', 'dist', relPath);
if (await pathExists(extraPath) && (await stat(extraPath)).isFile()) {
return res.sendFile(extraPath);
return sendFileAllowingDotfiles(res, extraPath);
}
}
const resourcePath = join(facet.systemJsHomeDir, relPath);
if (await pathExists(resourcePath) && (await stat(resourcePath)).isFile()) {
res.sendFile(resourcePath);
sendFileAllowingDotfiles(res, resourcePath);
} else {
console.warn(`[Preview Server] SystemJS resource not found: ${resourcePath}`);
next();
Expand All @@ -594,7 +589,7 @@ export const scriptingRoutes = [
}

if (await pathExists(finalPath) && (await stat(finalPath)).isFile()) {
res.sendFile(finalPath, { dotfiles: 'allow' });
sendFileAllowingDotfiles(res, finalPath);
} else {
next();
}
Expand Down
54 changes: 53 additions & 1 deletion src/core/preview/test/scripting-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const mockGetModules = jest.fn();
const mockGetConfigPath = jest.fn();
const mockPathExists = jest.fn();
const mockReadJSON = jest.fn();
const mockStat = jest.fn();
const mockWaitForProgrammingFacet = jest.fn();

jest.mock('../../engine', () => ({
Engine: {
Expand All @@ -18,10 +20,20 @@ jest.mock('../../configuration', () => ({
jest.mock('fs-extra', () => ({
pathExists: mockPathExists,
readJSON: mockReadJSON,
stat: jest.fn(),
stat: mockStat,
readFile: jest.fn(),
}));

jest.mock('../../scripting/programming/FacetInstance', () => ({
waitForProgrammingFacet: mockWaitForProgrammingFacet,
}));

jest.mock('../../scripting', () => ({
__esModule: true,
default: { projectPath: '/workspace/.hidden-project' },
}));

import { join } from 'path';
import { scriptingRoutes } from '../scripting-routes';

describe('preview scripting routes', () => {
Expand All @@ -30,6 +42,10 @@ describe('preview scripting routes', () => {
mockGetModules.mockReturnValue(['base', 'custom-pipeline']);
mockGetConfigPath.mockResolvedValue('E:/project/cocos.config.json');
mockPathExists.mockResolvedValue(true);
mockStat.mockResolvedValue({ isFile: () => true });
mockWaitForProgrammingFacet.mockResolvedValue({
systemJsHomeDir: '/workspace/.hidden',
});
});

it('normalizes disk graphics settings when serving engine modules', async () => {
Expand Down Expand Up @@ -58,4 +74,40 @@ describe('preview scripting routes', () => {

expect(res.json).toHaveBeenCalledWith(['base', 'legacy-pipeline']);
});

it('allows files under a dot-prefixed directory', async () => {
const route = scriptingRoutes.find((item) => item.url instanceof RegExp && item.url.source === '^\\/scripting\\/systemjs');
const res = {
sendFile: jest.fn(),
};
const next = jest.fn();

expect(route).toBeDefined();

await route!.handler({ path: '/scripting/systemjs/example.js' } as any, res as any, next);

expect(res.sendFile).toHaveBeenCalledWith(
'/workspace/.hidden/example.js',
{ dotfiles: 'allow' },
);
expect(next).not.toHaveBeenCalled();
});

it('serves effect settings from a dot-prefixed project path', async () => {
const route = scriptingRoutes.find((item) => item.url === '/scripting/engine/effect-settings');
const res = {
sendFile: jest.fn(),
};
const next = jest.fn();

expect(route).toBeDefined();

await route!.handler({} as any, res as any, next);

expect(res.sendFile).toHaveBeenCalledWith(
join('/workspace/.hidden-project', 'temp', 'asset-db', 'effect', 'effect.bin'),
{ dotfiles: 'allow' },
);
expect(next).not.toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion src/core/scene/scene.scripting.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { basename, join } from 'path';
import ejs from 'ejs';
import { GlobalPaths } from '../../global';
import { scriptingRoutes } from '../preview/scripting-routes';
import { sendFileAllowingDotfiles } from '../../server/utils';
import { pathExists } from 'fs-extra';

export default {
Expand Down Expand Up @@ -57,7 +58,7 @@ export default {
const effectBinPath = join(scripting.projectPath, 'temp', 'cli', 'asset-db', 'effect', 'effect.bin');
if (await pathExists(effectBinPath)) {
res.setHeader('Content-Type', 'application/octet-stream');
res.sendFile(effectBinPath);
sendFileAllowingDotfiles(res, effectBinPath);
} else {
res.status(404).send('effect.bin not found');
}
Expand Down
11 changes: 11 additions & 0 deletions src/server/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import net from 'net';
import type { Response } from 'express';


/**
* 获取当前系统可用端口
Expand All @@ -25,3 +27,12 @@ export async function getAvailablePort(preferredPort: number): Promise<number> {
});
});
}

/**
* 以允许点目录/点文件的方式返回文件。
* express/send 默认忽略路径中以 `.` 开头的段(dotfiles: 'ignore'),
* 项目或资源位于点目录(如 ~/.projects/x)下时会被误判 404,统一走此 helper。
*/
export function sendFileAllowingDotfiles(res: Response, filePath: string): void {
res.sendFile(filePath, { dotfiles: 'allow' });
}
Loading