Skip to content

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 install

Run modes ​

CommandBehavior
npm run parallelOne generation and check cycle, followed by a separate Vite build
npm run parallel-watchInitial 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-firstOne generation and check cycle, with the Vite build running inside afterCheckers
npm run generator-first-watchInitial 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 feedbackRetrieves 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.ts

Try 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 TypeRequest type in src/shared/api/orders/contracts/orders.ts and 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 from pages/orders to pages/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, remove export from 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 in loadOrders.ts — a Biome diagnostic.

MIT License