初始化项目版本

This commit is contained in:
Axhub Make
2026-07-29 16:04:39 +08:00
commit 4305a1082b
2629 changed files with 760590 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
createAnnotationSourceMarkdownPlugin,
preprocessAnnotationSourceMarkdown,
resolveAnnotationMarkdownPath,
} from './annotationSourceMarkdown';
const tempRoots: string[] = [];
function createProjectRoot(): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'annotation-source-markdown-'));
tempRoots.push(root);
fs.mkdirSync(path.join(root, 'src/prototypes/order-review/docs/nested'), { recursive: true });
fs.writeFileSync(path.join(root, 'src/prototypes/order-review/index.tsx'), 'export default function Demo() { return null; }\n', 'utf8');
return root;
}
function writeSource(projectRoot: string, source: unknown): string {
const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
fs.writeFileSync(sourcePath, `${JSON.stringify(source, null, 2)}\n`, 'utf8');
return sourcePath;
}
afterEach(() => {
vi.restoreAllMocks();
for (const root of tempRoots.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});
describe('annotation source markdown preprocessing', () => {
it('inlines directory markdownPath content without creating Make-specific edit URLs', () => {
const projectRoot = createProjectRoot();
const mdPath = path.join(projectRoot, 'src/prototypes/order-review/docs/prd-03-status.md');
fs.writeFileSync(mdPath, '# PRD 03\n\n状态说明', 'utf8');
const sourcePath = writeSource(projectRoot, {
documentVersion: 1,
format: 'axhub-annotation-source',
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
markdownMap: {},
assetMap: {},
directory: {
nodes: [
{
type: 'markdown',
id: 'prd-03-status',
title: 'PRD 03 | 状态',
markdownPath: 'docs/prd-03-status.md',
},
],
},
});
const result = preprocessAnnotationSourceMarkdown({
projectRoot,
sourceFilePath: sourcePath,
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
mode: 'serve',
});
const node = result.source.directory.nodes[0];
expect(node).toMatchObject({
type: 'markdown',
markdown: '# PRD 03\n\n状态说明',
markdownPath: 'docs/prd-03-status.md',
});
expect(node).not.toHaveProperty('markdownEditUrl');
expect(result.watchFiles).toEqual([mdPath]);
});
it('does not expose edit URLs when preprocessing for export builds', () => {
const projectRoot = createProjectRoot();
fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md'), '# Exported PRD', 'utf8');
const sourcePath = writeSource(projectRoot, {
documentVersion: 1,
format: 'axhub-annotation-source',
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
markdownMap: {},
assetMap: {},
directory: {
nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/prd.md' }],
},
});
const result = preprocessAnnotationSourceMarkdown({
projectRoot,
sourceFilePath: sourcePath,
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
mode: 'build',
});
expect(result.source.directory.nodes[0]).toMatchObject({
markdown: '# Exported PRD',
markdownPath: 'docs/prd.md',
});
expect(result.source.directory.nodes[0]).not.toHaveProperty('markdownEditUrl');
});
it('recursively inlines markdownPath documents nested in directory folders', () => {
const projectRoot = createProjectRoot();
fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/nested/prd.md'), '# Nested PRD', 'utf8');
const sourcePath = writeSource(projectRoot, {
documentVersion: 1,
format: 'axhub-annotation-source',
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
markdownMap: {},
assetMap: {},
directory: {
nodes: [
{
type: 'folder',
id: 'docs',
title: '文档',
children: [
{ type: 'markdown', id: 'nested-prd', title: 'Nested PRD', markdownPath: 'docs/nested/prd.md' },
],
},
],
},
});
const result = preprocessAnnotationSourceMarkdown({
projectRoot,
sourceFilePath: sourcePath,
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
mode: 'serve',
});
expect(result.source.directory.nodes[0].children[0]).toMatchObject({
type: 'markdown',
markdownPath: 'docs/nested/prd.md',
markdown: '# Nested PRD',
});
expect(result.source.directory.nodes[0].children[0]).not.toHaveProperty('markdownEditUrl');
});
it.each([
['docs/prd.md', 'src/prototypes/order-review/docs/prd.md'],
['docs/nested/prd.md', 'src/prototypes/order-review/docs/nested/prd.md'],
])('allows safe prototype-relative markdownPath %s', (markdownPath, expectedProjectRelativePath) => {
const projectRoot = createProjectRoot();
const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');
const resolved = resolveAnnotationMarkdownPath(projectRoot, sourcePath, markdownPath);
expect(resolved).toMatchObject({
ok: true,
projectRelativePath: expectedProjectRelativePath,
});
});
it.each([
'/abs.md',
'../secret.md',
'docs/../../secret.md',
'docs//prd.md',
'docs/%2e%2e/secret.md',
])('rejects unsafe markdownPath %s', (markdownPath) => {
const projectRoot = createProjectRoot();
const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');
const resolved = resolveAnnotationMarkdownPath(projectRoot, sourcePath, markdownPath);
expect(resolved.ok).toBe(false);
});
it('falls back to existing inline markdown when the referenced file is missing and warns in dev', () => {
const projectRoot = createProjectRoot();
const sourcePath = writeSource(projectRoot, {
documentVersion: 1,
format: 'axhub-annotation-source',
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
markdownMap: {},
assetMap: {},
directory: {
nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/missing.md', markdown: '旧内容' }],
},
});
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const result = preprocessAnnotationSourceMarkdown({
projectRoot,
sourceFilePath: sourcePath,
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
mode: 'serve',
});
expect(result.source.directory.nodes[0]).toMatchObject({
markdown: '旧内容',
markdownPath: 'docs/missing.md',
});
expect(result.source.directory.nodes[0]).not.toHaveProperty('markdownEditUrl');
expect(warn).toHaveBeenCalledWith(expect.stringContaining('docs/missing.md'));
});
it('loads annotation-source.json as an inlined JavaScript module in Vite', async () => {
const projectRoot = createProjectRoot();
fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md'), '# Vite PRD', 'utf8');
const sourcePath = writeSource(projectRoot, {
documentVersion: 1,
format: 'axhub-annotation-source',
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
markdownMap: {},
assetMap: {},
directory: {
nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/prd.md' }],
},
});
const watched: string[] = [];
const plugin = createAnnotationSourceMarkdownPlugin(projectRoot, { mode: 'serve' });
const transform = plugin.transform as (
this: { addWatchFile: (file: string) => void },
code: string,
id: string,
) => string | Promise<string> | null;
const code = await transform.call(
{ addWatchFile: (file: string) => watched.push(file) },
fs.readFileSync(sourcePath, 'utf8'),
sourcePath,
);
expect(() => JSON.parse(String(code))).not.toThrow();
expect(code).not.toContain('export default');
expect(code).toContain('# Vite PRD');
expect(code).not.toContain('markdownEditUrl');
expect(watched).toEqual([path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md')]);
});
});