mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Plan N2 / spec §10 / §21.3.2 / §16 Phase 7.
apps/daemon/src/plugins/atoms/code-import.ts ships the daemon-side
implementation of the SKILL.md fragment landed in §3.M4. Given a
repo path + a project cwd, the runner walks the tree once, writes
`<cwd>/code/index.json`, and never re-walks until the next
explicit re-import.
The walker:
- Honours OD_CODE_IMPORT_BUDGET_MS (default 60s) and stops cleanly
when the budget is exhausted, recording every unwalked
directory under skipped[] with reason='budget-exceeded'.
- Skips the canonical no-go folders: node_modules, .git, .next,
.svelte-kit, dist, build, out, coverage, .vercel, .vscode,
.turbo, .cache, .pnpm-store, .parcel-cache, .nuxt, .astro.
Each entry lands in skipped[] with reason='directory-skiplist'.
- Marks symlinks as skipped (reason='symlink') without following.
- Records files larger than largeFileBytes (default 1 MiB) in
skipped[] AND in files[] (so the agent sees they exist), but
omits their imports[] to keep memory bounded.
- Detects framework: next (app vs pages router), sveltekit, astro,
remix, vite, cra, custom, unknown.
- Detects package manager from lockfile presence (pnpm-lock.yaml
> yarn.lock > bun.lockb > package-lock.json > unknown).
- Detects style system: tailwind / styled-components / emotion /
css.
- Lightweight import edge extraction (regex) for ts/tsx/js/jsx;
documented as heuristic in the SKILL.md fragment.
Daemon tests: 1528 → 1534 (+6 cases on plugins-code-import:
next-app router + tailwind + pnpm detection, skiplist enforcement,
symlink skip, large-file imports omission, persisted JSON layout,
non-directory repoPath rejection).
Co-authored-by: Tom Huang <1043269994@qq.com>
96 lines
4.4 KiB
TypeScript
96 lines
4.4 KiB
TypeScript
// Phase 7 entry slice / spec §10 / §21.3.2 — code-import runner.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile, symlink } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { runCodeImport } from '../src/plugins/atoms/code-import.js';
|
|
|
|
let repo: string;
|
|
let cwd: string;
|
|
|
|
beforeEach(async () => {
|
|
const tmp = await mkdtemp(path.join(os.tmpdir(), 'od-code-import-'));
|
|
repo = path.join(tmp, 'repo');
|
|
cwd = path.join(tmp, 'cwd');
|
|
await mkdir(repo, { recursive: true });
|
|
await mkdir(cwd, { recursive: true });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(path.dirname(repo), { recursive: true, force: true });
|
|
});
|
|
|
|
describe('runCodeImport', () => {
|
|
it('walks a Next.js app-router repo + records framework / package-manager / style', async () => {
|
|
await writeFile(path.join(repo, 'package.json'), JSON.stringify({
|
|
name: 'fixture',
|
|
dependencies: { next: '15', react: '18' },
|
|
devDependencies: { tailwindcss: '4', typescript: '5' },
|
|
}));
|
|
await writeFile(path.join(repo, 'pnpm-lock.yaml'), '');
|
|
await mkdir(path.join(repo, 'app'), { recursive: true });
|
|
await writeFile(path.join(repo, 'app', 'page.tsx'),
|
|
`import { useState } from 'react';\nimport { Button } from '@/components/Button';\nexport default function Page(){return null}\n`);
|
|
await mkdir(path.join(repo, 'components'), { recursive: true });
|
|
await writeFile(path.join(repo, 'components', 'Button.tsx'),
|
|
`import clsx from 'clsx';\nexport const Button = ({children}: {children: any}) => <button>{children}</button>;\n`);
|
|
|
|
const index = await runCodeImport({ repoPath: repo, cwd });
|
|
expect(index.framework).toBe('next');
|
|
expect(index.packageManager).toBe('pnpm');
|
|
expect(index.styleSystem).toBe('tailwind');
|
|
expect(index.routes).toEqual({ kind: 'next-app' });
|
|
expect(index.files.map((f) => f.path).sort()).toEqual([
|
|
'app/page.tsx',
|
|
'components/Button.tsx',
|
|
'package.json',
|
|
]);
|
|
const page = index.files.find((f) => f.path === 'app/page.tsx');
|
|
expect(page?.imports).toEqual(['react', '@/components/Button']);
|
|
});
|
|
|
|
it('skips node_modules / .git / dist via the skiplist + records reasons', async () => {
|
|
await writeFile(path.join(repo, 'package.json'), JSON.stringify({ name: 'f' }));
|
|
for (const skip of ['node_modules', '.git', 'dist']) {
|
|
await mkdir(path.join(repo, skip), { recursive: true });
|
|
await writeFile(path.join(repo, skip, 'noop.ts'), 'export const x = 1;\n');
|
|
}
|
|
await writeFile(path.join(repo, 'index.ts'), 'export {};\n');
|
|
const index = await runCodeImport({ repoPath: repo, cwd });
|
|
expect(index.files.map((f) => f.path).sort()).toEqual(['index.ts', 'package.json']);
|
|
const skipped = index.skipped.filter((s) => s.reason === 'directory-skiplist').map((s) => s.path).sort();
|
|
expect(skipped).toEqual(['.git', 'dist', 'node_modules']);
|
|
});
|
|
|
|
it('marks symlinks as skipped without following them', async () => {
|
|
await writeFile(path.join(repo, 'real.ts'), 'export const x = 1;\n');
|
|
await symlink('real.ts', path.join(repo, 'link.ts'));
|
|
const index = await runCodeImport({ repoPath: repo, cwd });
|
|
expect(index.files.map((f) => f.path)).toContain('real.ts');
|
|
expect(index.skipped.some((s) => s.reason === 'symlink' && s.path === 'link.ts')).toBe(true);
|
|
});
|
|
|
|
it('records large files but skips their imports[]', async () => {
|
|
const big = Buffer.alloc(5 * 1024, 0x20).toString('utf8') + "\nimport 'foo';\n";
|
|
await writeFile(path.join(repo, 'huge.ts'), big);
|
|
const index = await runCodeImport({ repoPath: repo, cwd, largeFileBytes: 1024 });
|
|
const huge = index.files.find((f) => f.path === 'huge.ts');
|
|
expect(huge).toBeDefined();
|
|
expect(huge?.imports).toBeUndefined();
|
|
expect(index.skipped.some((s) => s.reason === 'large-file' && s.path === 'huge.ts')).toBe(true);
|
|
});
|
|
|
|
it('persists code/index.json under the project cwd', async () => {
|
|
await writeFile(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
await runCodeImport({ repoPath: repo, cwd });
|
|
const indexPath = path.join(cwd, 'code', 'index.json');
|
|
const json = JSON.parse(await readFile(indexPath, 'utf8'));
|
|
expect(json.files.map((f: { path: string }) => f.path)).toEqual(['a.ts']);
|
|
});
|
|
|
|
it('throws when repoPath is not a directory', async () => {
|
|
await expect(runCodeImport({ repoPath: path.join(repo, 'no-such-dir'), cwd }))
|
|
.rejects.toThrow(/not a directory/);
|
|
});
|
|
});
|