Demo: Order Management
Этот небольшой интерфейс управления заказами показывает @arfa-tools/generator как слой генерации, проверок и LLM feedback в типовом React + Vite-проекте. В данном примере используется FSD 2.1 архитектура.
Предпросмотр
builder
format.ts
generator.ts
generatorFirst.ts
parallel.ts
reloadPlugin.ts
src
biome.json
index.html
package.json
tsconfig.json
vite.config.ts
ts
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
CheckerBoundaries,
CheckerCli,
FileGenerator,
PluginReexport,
PluginTranslations,
PluginValidators,
type TypeMessage,
} from '@arfa-tools/generator';
import { createMessages } from '../src/shared/i18n/createMessages.ts';
import { format } from './format.ts';
const filename = fileURLToPath(import.meta.url);
const rootDir = path.resolve(path.dirname(filename), '..');
const sourceDir = path.resolve(rootDir, 'src');
const exclude = /(^_|^index\.ts$|^messages\.ts$|\.css$|\.d\.ts$|\.md$|package\.json$)/;
const headerTemplate = `// biome-ignore-all assist/source/organizeImports: This file is auto-generated\n\n`;
function upperFirst(value: string): string {
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
}
function parseMessages(filePath: string): Array<TypeMessage> {
const source = fs.readFileSync(filePath, 'utf-8');
const content = source.replace(/^[\s\S]*?export\s+const\s+messages\s*=\s*/, 'return ');
if (content === source) throw new Error(`Messages export not found in: ${filePath}`);
const messages = new Function('createMessages', content)(createMessages);
return Object.values(messages);
}
export const generator = new FileGenerator({
rootDir: sourceDir,
headerTemplate,
historyPath: path.resolve(rootDir, '.llm/history.jsonl'),
format,
plugins: [
new PluginTranslations([
{
entries: ['**/messages.ts'],
targetFile: path.resolve(sourceDir, 'shared/i18n/usedTranslations.json'),
languages: ['en'],
baseLanguage: 'en',
getKey({ name }) {
return name;
},
parseMessages,
async translate() {
return '';
},
},
]),
new PluginValidators([
{
entries: ['shared/api/*/contracts'],
exclude,
targetFolder: path.resolve(sourceDir, 'shared/api/validators'),
tsconfigPath: path.resolve(rootDir, 'tsconfig.json'),
requestDeclaration: 'TypeRequest',
responseDeclaration: 'TypeResponse',
formatContent({ schema, targetPath }) {
return `export const ${path.parse(targetPath).name} = ${JSON.stringify(schema, null, 2)} as const;\n`;
},
},
]),
new PluginReexport([
{
entries: ['pages/*'],
exclude,
noPackage: true,
fileNameTemplate: () => 'index.ts',
template({ folderName }) {
const pageName = `${upperFirst(folderName)}Page`;
return `export { ${pageName} } from './ui/${pageName}';`;
},
},
{
entries: ['shared/api/*'],
exclude: /(^validators$|^index\.ts$)/,
noPackage: true,
fileNameTemplate: () => 'index.ts',
template({ files }) {
return files
.filter(({ ext }) => ext === '.ts')
.map(({ name }) => `export type * from './${name}';`)
.join('\n');
},
},
{
entries: ['pages/*/api'],
exclude,
noPackage: true,
fileNameTemplate: () => 'index.ts',
template({ files }) {
return files.map(({ name }) => `export { ${name} } from './${name}';`).join('\n');
},
validateContent({ name, content }) {
return content.includes(`export async function ${name}(`);
},
},
{
entries: ['shared/lib/*', 'shared/i18n'],
exclude,
noPackage: true,
fileNameTemplate: () => 'index.ts',
template({ files }) {
return files
.filter(({ ext }) => ['.ts', '.tsx', '.json'].includes(ext))
.map(({ name, ext, fileName }) =>
ext === '.json'
? `export { default as ${name} } from './${fileName}';`
: `export * from './${name}';`
)
.join('\n');
},
},
{
entries: ['pages/*/ui/*'],
exclude,
onlyPackage: true,
fileNameTemplate({ folderName }) {
return `${upperFirst(folderName)}.tsx`;
},
template: () => '',
},
{
entries: ['shared/ui/*'],
exclude,
fileNameTemplate: () => 'index.ts',
template({ files }) {
return files
.filter(({ ext }) => ['.ts', '.tsx'].includes(ext))
.map(({ name }) => `export * from './${name}';`)
.join('\n');
},
},
]),
],
checkers: [
new CheckerBoundaries({
tsconfigPath: path.resolve(rootDir, 'tsconfig.json'),
extensions: ['.ts', '.tsx'],
rules: [
{
entries: ['app/**'],
allow: {
imports: [
'pages/*/index.ts',
'shared/api/*/index.ts',
'shared/i18n/index.ts',
'shared/lib/*/index.ts',
'shared/ui/*/index.ts',
],
importsInsideScope: 'all',
external: true,
},
},
{
entries: ['pages/**'],
allow: {
imports: [
'shared/api/*/index.ts',
'shared/i18n/index.ts',
'shared/lib/*/index.ts',
'shared/ui/*/index.ts',
],
importsInsideScope: 'sameTopLevel',
external: true,
},
},
{
entries: ['shared/**'],
allow: {
imports: [],
importsInsideScope: 'all',
external: true,
},
},
],
}),
new CheckerCli({
name: 'biome',
getCommand({ changedFiles }) {
if (changedFiles.length === 0) return false;
return [
path.resolve(rootDir, 'node_modules/.bin/biome'),
'check',
'--no-errors-on-unmatched',
...changedFiles.map(({ relativePath }) => relativePath),
];
},
}),
new CheckerCli({
name: 'tsc',
getCommand: () => [path.resolve(rootDir, 'node_modules/.bin/tsc')],
}),
],
});
Скачивание
shell
npx degit dkazakov8/arfa-generator/examples/orders react-example
cd react-example
npm installРежимы запуска
| Команда | Поведение |
|---|---|
npm run parallel | Один проход генерации и проверок, затем отдельная сборка Vite |
npm run parallel-watch | Первоначальная генерация, затем параллельная работа вотчера генератора и dev-сервера Vite с HMR. В большинстве случаев будет 2 HMR-обновления: первый после редактирования файла, второй — после записи генератором новых файлов. Обратите внимание, что в данном случае генератор не знает об ошибках при сборке и не выводит их в едином отчете |
npm run generator-first | Один проход генерации и проверок со сборкой Vite внутри afterCheckers |
npm run generator-first-watch | Первоначальная генерация и сборка, затем watch только генератора; каждая следующая сборка вызывается в afterCheckers. В этом случае не работает HMR, а обновление страницы делается кастомным плагином, который перезагружает страницу только 1 раз — после завершения всего цикла, включая сборку. Ошибки сборки попадают в отчет |
npm run feedback | Получение последнего отчета в Markdown-формате (удобно для LLM-агентов) |
Сгенерированные файлы
ARFA Generator создает и обновляет следующие файлы (список получен с помощью getControlledFiles):
text
shared/i18n/usedTranslations.json
shared/api/validators/shared/api/customers/contracts/customers.ts
shared/api/validators/shared/api/invoices/contracts/invoices.ts
shared/api/validators/shared/api/orders/contracts/orders.ts
shared/api/validators/shared/api/orders/contracts/updateOrderStatus.ts
pages/customers/index.ts
pages/invoices/index.ts
pages/orders/index.ts
shared/api/customers/index.ts
shared/api/invoices/index.ts
shared/api/orders/index.ts
pages/customers/api/index.ts
pages/invoices/api/index.ts
pages/orders/api/index.ts
shared/i18n/index.ts
shared/lib/dates/index.ts
shared/lib/money/index.ts
shared/lib/timing/index.ts
pages/customers/ui/CustomerStatusBadge/package.json
pages/invoices/ui/InvoiceStatusBadge/package.json
pages/orders/ui/OrderStatusBadge/package.json
shared/ui/Button/package.json
shared/ui/Button/index.ts
shared/ui/DataTable/package.json
shared/ui/DataTable/index.ts
shared/ui/EmptyState/package.json
shared/ui/EmptyState/index.ts
shared/ui/MoneyText/package.json
shared/ui/MoneyText/index.tsПопробуйте вызвать ошибки
Запустите npm run generator-first-watch и вносите изменения по одному. Последний отчёт можно получить командой npm run feedback. Перед следующим экспериментом отменяйте предыдущую правку.
- Переименуйте тип
TypeRequestвsrc/shared/api/orders/contracts/orders.tsвместе с его использованиями — ошибка генерации схем валидации. - Импортируйте страницу заказов из страницы клиентов — нарушение границ между страницами.
- Импортируйте любую страницу из компонента в
shared/ui— нарушение границ слоя shared. - В
App.tsxзамените импорт изpages/ordersнаpages/orders/ui/OrdersPage— нарушение публичного API страницы. - В
loadOrders.tsпереименуйте экспортируемую async-функцию, сохранив имя файла — ошибка валидации реэкспорта. - В
OrderStatusBadge/messages.tsуберитеexportу объявления messages — ошибка обработки переводов. - В
App.tsxукажите несуществующий модуль в импорте — ошибка сборки Vite. - Обратитесь к необъявленной переменной в TypeScript-файле — диагностика tsc.
- Добавьте
debugger;в тело функции вloadOrders.ts— диагностика Biome.