Demo: Order Management
This small order management interface demonstrates @arfa-tools/generator as a layer for generation, checks, and LLM feedback in a typical React + Vite project. This example uses the FSD 2.1 architecture.
Preview
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')],
}),
],
});
Download
shell
npx degit dkazakov8/arfa-generator/examples/orders react-example
cd react-example
npm installRun modes
| Command | Behavior |
|---|---|
npm run parallel | One generation and check cycle, followed by a separate Vite build |
npm run parallel-watch | Initial generation, then the generator watcher and Vite dev server with HMR run in parallel. In most cases, there are two HMR updates: the first after a file is edited, and the second after the generator writes new files. In this mode, the generator is unaware of build errors and does not include them in the unified report |
npm run generator-first | One generation and check cycle, with the Vite build running inside afterCheckers |
npm run generator-first-watch | Initial generation and build, then only the generator watches for changes; each subsequent build runs in afterCheckers. HMR is not available in this mode. Instead, a custom plugin reloads the page once, after the entire cycle, including the build, has completed. Build errors are included in the report |
npm run feedback | Retrieves the latest report in Markdown format (useful for LLM agents) |
Generated files
ARFA Generator creates and updates the following files (the list was retrieved using 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.tsTry triggering errors
Run npm run generator-first-watch and make one change at a time. Retrieve the latest report with npm run feedback. Undo each change before trying the next experiment.
- Rename the
TypeRequesttype insrc/shared/api/orders/contracts/orders.tsand all its usages — a validation schema generation error. - Import the orders page from the customers page — a violation of page boundaries.
- Import any page from a component in
shared/ui— a violation of the shared layer’s boundaries. - In
App.tsx, change the import frompages/orderstopages/orders/ui/OrdersPage— a violation of the page’s public API. - In
loadOrders.ts, rename the exported async function while keeping the file name — a re-export validation error. - In
OrderStatusBadge/messages.ts, removeexportfrom the messages declaration — a translation processing error. - In
App.tsx, import a module that does not exist — a Vite build error. - Reference an undeclared variable in a TypeScript file — a tsc diagnostic.
- Add
debugger;inside a function inloadOrders.ts— a Biome diagnostic.