531 lines
22 KiB
TypeScript
531 lines
22 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { createRequire } from 'node:module';
|
|
import {
|
|
mkdir,
|
|
mkdtemp,
|
|
realpath,
|
|
readFile,
|
|
rename,
|
|
rm,
|
|
symlink,
|
|
utimes,
|
|
writeFile,
|
|
} from 'node:fs/promises';
|
|
import { join, relative } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
createStaticProjectPackage,
|
|
ProjectPackageError,
|
|
} from '@electron/services/project-packager';
|
|
import { createCodingProjectConfigV2 } from '@electron/coding-projects/project-config';
|
|
import type { ProjectType } from '../../shared/project-config';
|
|
|
|
const openRaceHook = vi.hoisted(() => ({
|
|
beforeOpen: null as null | ((path: string) => Promise<void>),
|
|
}));
|
|
|
|
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
|
return {
|
|
...actual,
|
|
open: async (...args: Parameters<typeof actual.open>) => {
|
|
await openRaceHook.beforeOpen?.(String(args[0]));
|
|
return await actual.open(...args);
|
|
},
|
|
};
|
|
});
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const AdmZip = require('adm-zip') as typeof import('adm-zip');
|
|
|
|
const tempDirectories: string[] = [];
|
|
const projectId = '11111111-1111-4111-8111-111111111111';
|
|
|
|
async function createProject(projectType: ProjectType = 'interactive_ai_app'): Promise<string> {
|
|
const projectPath = await mkdtemp(join(tmpdir(), 'makelore-project-packager-'));
|
|
tempDirectories.push(projectPath);
|
|
await mkdir(join(projectPath, 'src'), { recursive: true });
|
|
await mkdir(join(projectPath, 'public'), { recursive: true });
|
|
await mkdir(join(projectPath, '.makelore'), { recursive: true });
|
|
await writeFile(
|
|
join(projectPath, '.makelore', 'project.json'),
|
|
JSON.stringify(createCodingProjectConfigV2('2026-08-09T00:00:00.000Z', projectType)),
|
|
'utf8',
|
|
);
|
|
await writeFile(
|
|
join(projectPath, 'package.json'),
|
|
JSON.stringify({
|
|
name: 'space-cleaner',
|
|
private: true,
|
|
packageManager: 'npm@10.9.2',
|
|
devDependencies: { vite: '^7.0.0' },
|
|
}),
|
|
'utf8',
|
|
);
|
|
await writeFile(
|
|
join(projectPath, 'package-lock.json'),
|
|
JSON.stringify({
|
|
name: 'space-cleaner',
|
|
lockfileVersion: 3,
|
|
requires: true,
|
|
packages: {},
|
|
}),
|
|
'utf8',
|
|
);
|
|
await writeFile(join(projectPath, 'index.html'), '<div id="app"></div>', 'utf8');
|
|
await writeFile(join(projectPath, 'src', 'main.ts'), 'console.log("ready")\n', 'utf8');
|
|
await writeFile(join(projectPath, 'public', 'icon.txt'), 'icon\n', 'utf8');
|
|
return await realpath(projectPath);
|
|
}
|
|
|
|
async function createNestedSource(projectPath: string, directory = 'games/Alice 的游戏'): Promise<string> {
|
|
const source = join(projectPath, directory);
|
|
await mkdir(join(source, 'src'), { recursive: true });
|
|
await writeFile(join(source, 'package.json'), JSON.stringify({
|
|
name: 'selected-inner-game', packageManager: 'npm@10.9.2',
|
|
scripts: { dev: 'vite --host 0.0.0.0 --port 5179' }, devDependencies: { vite: '^7.0.0' },
|
|
}));
|
|
await writeFile(join(source, 'package-lock.json'), JSON.stringify({ name: 'selected-inner-game', lockfileVersion: 3, packages: {} }));
|
|
await writeFile(join(source, 'index.html'), '<main>selected inner game</main>');
|
|
await writeFile(join(source, 'src', 'main.ts'), 'console.log("inner source")\n');
|
|
return source;
|
|
}
|
|
|
|
async function saveSourceEntry(projectPath: string, directory: string, patch: Record<string, unknown> = {}): Promise<void> {
|
|
const configPath = join(projectPath, '.makelore', 'project.json');
|
|
const config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
await writeFile(configPath, JSON.stringify({ ...config, projectId }));
|
|
await writeFile(join(projectPath, '.makelore', 'work-entry.json'), JSON.stringify({
|
|
version: 1, projectId, projectPath, url: 'http://127.0.0.1:5179/',
|
|
launch: { kind: 'vite', directory, args: [] }, ...patch,
|
|
}));
|
|
}
|
|
|
|
async function archiveTarget(): Promise<string> {
|
|
const directory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(directory);
|
|
return join(directory, 'project.zip');
|
|
}
|
|
|
|
async function removeOuterSource(projectPath: string): Promise<void> {
|
|
await Promise.all(['package.json', 'package-lock.json', 'index.html', 'src', 'public']
|
|
.map(name => rm(join(projectPath, name), { recursive: true, force: true })));
|
|
}
|
|
|
|
afterEach(async () => {
|
|
openRaceHook.beforeOpen = null;
|
|
await Promise.all(
|
|
tempDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe('project packager', () => {
|
|
it.each([false, true])('packages the saved inner source at zip root using only outer project configuration (inner custom config=%s)', async (innerConfig) => {
|
|
const projectPath = await createProject();
|
|
const source = await createNestedSource(projectPath);
|
|
if (innerConfig) {
|
|
await mkdir(join(source, '.makelore'));
|
|
await writeFile(join(source, '.makelore', 'project.json'), JSON.stringify(
|
|
createCodingProjectConfigV2('2026-08-09T00:00:00.000Z', 'custom', '22222222-2222-4222-8222-222222222222'),
|
|
));
|
|
}
|
|
await mkdir(join(projectPath, 'knowledge'));
|
|
await mkdir(join(projectPath, 'sibling-assets'));
|
|
await writeFile(join(projectPath, 'knowledge', 'notes.txt'), 'private knowledge');
|
|
await writeFile(join(projectPath, 'sibling-assets', 'cover.png'), 'unrelated image');
|
|
await writeFile(join(projectPath, 'private.txt'), 'outer private file');
|
|
await saveSourceEntry(projectPath, 'games/Alice 的游戏');
|
|
const outerConfig = await readFile(join(projectPath, '.makelore', 'project.json'));
|
|
const savedEntry = await readFile(join(projectPath, '.makelore', 'work-entry.json'));
|
|
const archivePath = await archiveTarget();
|
|
|
|
const summary = await createStaticProjectPackage({ projectPath, archivePath });
|
|
|
|
const archive = new AdmZip(archivePath);
|
|
expect(archive.getEntries().map(entry => entry.entryName)).toEqual([
|
|
'index.html', 'niancode.yml', 'package-lock.json', 'package.json', 'src/main.ts',
|
|
]);
|
|
expect(archive.readAsText('index.html')).toBe('<main>selected inner game</main>');
|
|
expect(JSON.parse(archive.readAsText('package.json')).name).toBe('selected-inner-game');
|
|
expect(summary.manifest.project_type).toBe('interactive_ai_app');
|
|
expect(summary.fileCount).toBe(5);
|
|
expect(await readFile(join(projectPath, '.makelore', 'project.json'))).toEqual(outerConfig);
|
|
expect(await readFile(join(projectPath, '.makelore', 'work-entry.json'))).toEqual(savedEntry);
|
|
expect(await readFile(join(projectPath, 'private.txt'), 'utf8')).toBe('outer private file');
|
|
});
|
|
|
|
it('discovers a unique nested standard Vite source without saving or rewriting project metadata', async () => {
|
|
const projectPath = await createProject();
|
|
await removeOuterSource(projectPath);
|
|
await createNestedSource(projectPath);
|
|
const outerConfig = await readFile(join(projectPath, '.makelore', 'project.json'));
|
|
const archivePath = await archiveTarget();
|
|
|
|
await createStaticProjectPackage({ projectPath, archivePath });
|
|
|
|
expect(new AdmZip(archivePath).readAsText('index.html')).toBe('<main>selected inner game</main>');
|
|
expect(new AdmZip(archivePath).getEntry('games/Alice 的游戏/index.html')).toBeNull();
|
|
expect(await readFile(join(projectPath, '.makelore', 'project.json'))).toEqual(outerConfig);
|
|
await expect(readFile(join(projectPath, '.makelore', 'work-entry.json'))).rejects.toMatchObject({ code: 'ENOENT' });
|
|
});
|
|
|
|
it('rejects multiple nested candidates without choosing a game or producing an archive', async () => {
|
|
const projectPath = await createProject();
|
|
await removeOuterSource(projectPath);
|
|
await createNestedSource(projectPath, 'first');
|
|
await createNestedSource(projectPath, 'second');
|
|
const archivePath = await archiveTarget();
|
|
|
|
await expect(createStaticProjectPackage({ projectPath, archivePath }))
|
|
.rejects.toMatchObject({ code: 'PROJECT_SOURCE_UNCONFIRMED' });
|
|
await expect(readFile(archivePath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
});
|
|
|
|
it.each([undefined, 'vite && node custom-dev.js'])('retains root Vite package compatibility for dev script %s', async (dev) => {
|
|
const projectPath = await createProject();
|
|
const packagePath = join(projectPath, 'package.json');
|
|
const packageJson = JSON.parse(await readFile(packagePath, 'utf8'));
|
|
await writeFile(packagePath, JSON.stringify({ ...packageJson, ...(dev ? { scripts: { dev } } : {}) }));
|
|
const archivePath = await archiveTarget();
|
|
|
|
await createStaticProjectPackage({ projectPath, archivePath });
|
|
|
|
expect(new AdmZip(archivePath).readAsText('index.html')).toBe('<div id="app"></div>');
|
|
expect(JSON.parse(new AdmZip(archivePath).readAsText('package.json'))).toEqual({
|
|
...packageJson, ...(dev ? { scripts: { dev } } : {}),
|
|
});
|
|
});
|
|
|
|
it('reports a missing lock in the saved source without falling back to the valid root or another game', async () => {
|
|
const projectPath = await createProject();
|
|
const source = await createNestedSource(projectPath, 'selected');
|
|
await createNestedSource(projectPath, 'other');
|
|
await saveSourceEntry(projectPath, 'selected');
|
|
await rm(join(source, 'package-lock.json'));
|
|
const archivePath = await archiveTarget();
|
|
|
|
await expect(createStaticProjectPackage({ projectPath, archivePath }))
|
|
.rejects.toMatchObject({ code: 'PROJECT_FILE_MISSING', message: expect.stringContaining('package-lock.json') });
|
|
await expect(readFile(archivePath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
});
|
|
|
|
it.each(['traversal', 'copied-project-id', 'copied-root', 'symlink'] as const)('ignores an unsafe %s saved directory and never packages its files', async (kind) => {
|
|
const projectPath = await createProject();
|
|
const outside = await createProject();
|
|
await writeFile(join(outside, 'external-secret.txt'), 'external private data');
|
|
let directory = '.makelore/copied-source';
|
|
let patch: Record<string, unknown> = {};
|
|
if (kind === 'traversal') directory = relative(projectPath, outside).split('\\').join('/');
|
|
else if (kind === 'symlink') {
|
|
directory = '.makelore/linked-source';
|
|
await symlink(outside, join(projectPath, directory), 'junction');
|
|
} else {
|
|
await createNestedSource(projectPath, directory);
|
|
patch = kind === 'copied-project-id'
|
|
? { projectId: '22222222-2222-4222-8222-222222222222' }
|
|
: { projectPath: outside };
|
|
}
|
|
await saveSourceEntry(projectPath, directory, patch);
|
|
const archivePath = await archiveTarget();
|
|
|
|
await createStaticProjectPackage({ projectPath, archivePath });
|
|
|
|
const archive = new AdmZip(archivePath);
|
|
expect(archive.readAsText('index.html')).toBe('<div id="app"></div>');
|
|
expect(archive.getEntries().map(entry => entry.entryName)).toEqual([
|
|
'index.html', 'niancode.yml', 'package-lock.json', 'package.json', 'public/icon.txt', 'src/main.ts',
|
|
]);
|
|
expect(archive.readAsText('src/main.ts')).toBe('console.log("ready")\n');
|
|
});
|
|
|
|
it('rejects a saved source ancestor replaced by a symlink between scan and streamed file reads', async () => {
|
|
const projectPath = await createProject();
|
|
const source = await createNestedSource(projectPath, 'games/selected');
|
|
await saveSourceEntry(projectPath, 'games/selected');
|
|
const outside = await createProject();
|
|
await createNestedSource(outside, 'selected');
|
|
await writeFile(join(outside, 'selected', 'src', 'main.ts'), 'external private data');
|
|
const archivePath = await archiveTarget();
|
|
let replaced = false;
|
|
openRaceHook.beforeOpen = async path => {
|
|
if (replaced || path !== join(source, 'src', 'main.ts')) return;
|
|
replaced = true;
|
|
await rename(join(projectPath, 'games'), join(projectPath, 'games-original'));
|
|
await symlink(outside, join(projectPath, 'games'), 'junction');
|
|
};
|
|
|
|
await expect(createStaticProjectPackage({ projectPath, archivePath }))
|
|
.rejects.toMatchObject({ code: 'PROJECT_SYMLINK_UNSUPPORTED' });
|
|
expect(replaced).toBe(true);
|
|
await expect(readFile(archivePath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
});
|
|
|
|
it('generates a deterministic static manifest zip and excludes local or sensitive files', async () => {
|
|
const projectPath = await createProject();
|
|
await mkdir(join(projectPath, 'node_modules', 'vite'), { recursive: true });
|
|
await mkdir(join(projectPath, '.git'), { recursive: true });
|
|
await mkdir(join(projectPath, 'dist'), { recursive: true });
|
|
for (const directory of ['.ssh', '.aws', '.gnupg', '.kube', '.docker', '.niancode', '.opencode', 'secrets']) {
|
|
await mkdir(join(projectPath, directory), { recursive: true });
|
|
await writeFile(join(projectPath, directory, 'credential'), 'private', 'utf8');
|
|
}
|
|
await writeFile(join(projectPath, 'node_modules', 'vite', 'index.js'), 'generated', 'utf8');
|
|
await writeFile(join(projectPath, '.git', 'config'), 'private', 'utf8');
|
|
await writeFile(join(projectPath, 'dist', 'index.html'), 'built', 'utf8');
|
|
await writeFile(join(projectPath, '.makelore', 'local-state.json'), '{}', 'utf8');
|
|
await writeFile(join(projectPath, '.env'), 'TOKEN=secret', 'utf8');
|
|
await writeFile(join(projectPath, '.env.example'), 'TOKEN=replace-me', 'utf8');
|
|
await writeFile(join(projectPath, '.npmrc'), '//registry/:_authToken=secret', 'utf8');
|
|
await writeFile(join(projectPath, 'debug.log'), 'local log', 'utf8');
|
|
await writeFile(join(projectPath, 'niancode.yml'), 'runtime: compose\n', 'utf8');
|
|
for (const filename of [
|
|
'.netrc',
|
|
'_netrc',
|
|
'.git-credentials',
|
|
'id_rsa',
|
|
'id_dsa',
|
|
'id_ecdsa',
|
|
'id_ed25519',
|
|
'service-account.json',
|
|
'service_account.prod.json',
|
|
'firebase-adminsdk-project.json',
|
|
'application_default_credentials.json',
|
|
'terraform.tfstate',
|
|
'terraform.tfstate.backup',
|
|
'production.tfvars',
|
|
'secrets.auto.tfvars.json',
|
|
'signing.ppk',
|
|
'auth-key.p8',
|
|
]) {
|
|
await writeFile(join(projectPath, filename), 'private', 'utf8');
|
|
}
|
|
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
const firstArchive = join(outputDirectory, 'first.zip');
|
|
const secondArchive = join(outputDirectory, 'second.zip');
|
|
|
|
const first = await createStaticProjectPackage({ projectPath, archivePath: firstArchive });
|
|
await utimes(join(projectPath, 'src', 'main.ts'), new Date(), new Date());
|
|
const second = await createStaticProjectPackage({ projectPath, archivePath: secondArchive });
|
|
|
|
expect(await readFile(firstArchive)).toEqual(await readFile(secondArchive));
|
|
expect(first.sha256).toBe(second.sha256);
|
|
expect(first.archiveBytes).toBe(second.archiveBytes);
|
|
expect(first.fileCount).toBe(6);
|
|
expect(first.excludedPaths).toEqual(expect.arrayContaining([
|
|
'.env',
|
|
'.env.example',
|
|
'.git/',
|
|
'.git-credentials',
|
|
'.gnupg/',
|
|
'.makelore/',
|
|
'.niancode/',
|
|
'.npmrc',
|
|
'.opencode/',
|
|
'.ssh/',
|
|
'.aws/',
|
|
'.docker/',
|
|
'.kube/',
|
|
'.netrc',
|
|
'_netrc',
|
|
'debug.log',
|
|
'dist/',
|
|
'firebase-adminsdk-project.json',
|
|
'id_rsa',
|
|
'node_modules/',
|
|
'production.tfvars',
|
|
'secrets.auto.tfvars.json',
|
|
'secrets/',
|
|
'service-account.json',
|
|
'terraform.tfstate.backup',
|
|
]));
|
|
|
|
const archive = new AdmZip(firstArchive);
|
|
const entries = archive.getEntries();
|
|
expect(entries.map((entry) => entry.entryName)).toEqual([
|
|
'index.html',
|
|
'niancode.yml',
|
|
'package-lock.json',
|
|
'package.json',
|
|
'public/icon.txt',
|
|
'src/main.ts',
|
|
]);
|
|
expect(archive.readAsText('niancode.yml')).toBe(
|
|
'schema_version: 1\n'
|
|
+ 'project_type: interactive_ai_app\n'
|
|
+ 'kind: web\n'
|
|
+ 'runtime: static\n'
|
|
+ 'build:\n'
|
|
+ ' preset: vite\n'
|
|
+ ' package_manager: npm\n'
|
|
+ ' entry: index.html\n',
|
|
);
|
|
expect(first.manifest.project_type).toBe('interactive_ai_app');
|
|
expect(entries.every((entry) => entry.header.time.getFullYear() === 1980)).toBe(true);
|
|
await expect(readFile(join(projectPath, 'niancode.yml'), 'utf8')).resolves.toBe('runtime: compose\n');
|
|
});
|
|
|
|
it.each(['mini_game', 'mini_program'] as const)(
|
|
'normalizes a legacy %s project to the canonical manifest type',
|
|
async (legacyProjectType) => {
|
|
const projectPath = await createProject();
|
|
await writeFile(
|
|
join(projectPath, '.makelore', 'project.json'),
|
|
JSON.stringify({
|
|
...createCodingProjectConfigV2('2026-08-09T00:00:00.000Z', 'interactive_ai_app'),
|
|
projectType: legacyProjectType,
|
|
}),
|
|
'utf8',
|
|
);
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
const archivePath = join(outputDirectory, 'project.zip');
|
|
|
|
const summary = await createStaticProjectPackage({ projectPath, archivePath });
|
|
|
|
expect(summary.manifest.project_type).toBe('interactive_ai_app');
|
|
expect(new AdmZip(archivePath).readAsText('niancode.yml'))
|
|
.toContain('project_type: interactive_ai_app\n');
|
|
},
|
|
);
|
|
|
|
it('rejects custom projects before validating Vite source files', async () => {
|
|
const projectPath = await mkdtemp(join(tmpdir(), 'makelore-custom-packager-'));
|
|
tempDirectories.push(projectPath);
|
|
await mkdir(join(projectPath, '.makelore'), { recursive: true });
|
|
await writeFile(
|
|
join(projectPath, '.makelore', 'project.json'),
|
|
JSON.stringify(createCodingProjectConfigV2('2026-08-09T00:00:00.000Z', 'custom')),
|
|
'utf8',
|
|
);
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
|
|
await expect(createStaticProjectPackage({
|
|
projectPath,
|
|
archivePath: join(outputDirectory, 'project.zip'),
|
|
})).rejects.toMatchObject<ProjectPackageError>({
|
|
code: 'PROJECT_TYPE_UNPUBLISHABLE',
|
|
message: '自定义项目暂未配置发布方式,请新建交互式 AI 应用项目',
|
|
});
|
|
});
|
|
|
|
it('rejects a project that is not a locked npm Vite project', async () => {
|
|
const projectPath = await createProject();
|
|
await writeFile(
|
|
join(projectPath, 'package.json'),
|
|
JSON.stringify({
|
|
name: 'space-cleaner',
|
|
packageManager: 'pnpm@10.0.0',
|
|
devDependencies: { vite: '^7.0.0' },
|
|
}),
|
|
'utf8',
|
|
);
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
|
|
await expect(createStaticProjectPackage({
|
|
projectPath,
|
|
archivePath: join(outputDirectory, 'project.zip'),
|
|
})).rejects.toMatchObject<ProjectPackageError>({
|
|
code: 'PACKAGE_MANAGER_UNSUPPORTED',
|
|
message: '当前仅支持带 package-lock.json 的 npm 项目',
|
|
});
|
|
});
|
|
|
|
it('returns a precise validation error when a required root file is missing', async () => {
|
|
const projectPath = await createProject();
|
|
await rm(join(projectPath, 'index.html'));
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
|
|
await expect(createStaticProjectPackage({
|
|
projectPath,
|
|
archivePath: join(outputDirectory, 'project.zip'),
|
|
})).rejects.toMatchObject<ProjectPackageError>({
|
|
code: 'PROJECT_FILE_MISSING',
|
|
message: '项目根目录缺少 index.html',
|
|
});
|
|
});
|
|
|
|
it('does not follow a linked directory outside the project root', async () => {
|
|
const projectPath = await createProject();
|
|
const externalPath = await mkdtemp(join(tmpdir(), 'makelore-project-external-'));
|
|
tempDirectories.push(externalPath);
|
|
await writeFile(join(externalPath, 'secret.txt'), 'must not be archived', 'utf8');
|
|
await symlink(externalPath, join(projectPath, 'linked-external'), 'junction');
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
|
|
await expect(createStaticProjectPackage({
|
|
projectPath,
|
|
archivePath: join(outputDirectory, 'project.zip'),
|
|
})).rejects.toMatchObject<ProjectPackageError>({
|
|
code: 'PROJECT_SYMLINK_UNSUPPORTED',
|
|
});
|
|
});
|
|
|
|
it('rejects a parent directory replaced by a junction after the pre-open identity check', async () => {
|
|
const projectPath = await createProject();
|
|
const externalPath = await mkdtemp(join(tmpdir(), 'makelore-project-external-'));
|
|
tempDirectories.push(externalPath);
|
|
await writeFile(join(externalPath, 'main.ts'), 'console.log("external secret")\n', 'utf8');
|
|
const sourcePath = join(projectPath, 'src');
|
|
const sourceFilePath = join(sourcePath, 'main.ts');
|
|
let replaced = false;
|
|
openRaceHook.beforeOpen = async (path) => {
|
|
if (replaced || path !== sourceFilePath) return;
|
|
replaced = true;
|
|
await rename(sourcePath, join(projectPath, 'src-original'));
|
|
await symlink(externalPath, sourcePath, 'junction');
|
|
};
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
|
|
await expect(createStaticProjectPackage({
|
|
projectPath,
|
|
archivePath: join(outputDirectory, 'project.zip'),
|
|
})).rejects.toMatchObject<ProjectPackageError>({
|
|
code: 'PROJECT_SYMLINK_UNSUPPORTED',
|
|
});
|
|
expect(replaced).toBe(true);
|
|
});
|
|
|
|
it('fails before allocating an oversized source package', async () => {
|
|
const projectPath = await createProject();
|
|
await writeFile(join(projectPath, 'large.bin'), Buffer.alloc(32));
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
|
|
await expect(createStaticProjectPackage({
|
|
projectPath,
|
|
archivePath: join(outputDirectory, 'project.zip'),
|
|
limits: {
|
|
maxFiles: 20,
|
|
maxSourceBytes: 16,
|
|
maxArchiveBytes: 1024,
|
|
},
|
|
})).rejects.toMatchObject<ProjectPackageError>({ code: 'PROJECT_TOO_LARGE' });
|
|
});
|
|
|
|
it('rejects an oversized required root file before reading it into memory', async () => {
|
|
const projectPath = await createProject();
|
|
await writeFile(join(projectPath, 'package-lock.json'), Buffer.alloc(512, 0x20));
|
|
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
|
|
tempDirectories.push(outputDirectory);
|
|
|
|
await expect(createStaticProjectPackage({
|
|
projectPath,
|
|
archivePath: join(outputDirectory, 'project.zip'),
|
|
limits: {
|
|
maxFiles: 20,
|
|
maxSourceBytes: 256,
|
|
maxArchiveBytes: 1024,
|
|
},
|
|
})).rejects.toMatchObject<ProjectPackageError>({ code: 'PROJECT_TOO_LARGE' });
|
|
});
|
|
});
|