Skip to content

PluginModular ​

Collects information about a set of files or folders and generates a single combined file. Use it when you need a central module based on the project structure: a page registry, widget map, list of public entry points, or route or import configuration.

Unlike PluginReexport, this plugin does not create a file in every matched folder. Instead, it writes one targetFile per configuration.

In watch mode, the plugin responds only to files being added or deleted, ignoring changes to file content.

ts
entries: Array<string>

An array of glob patterns for finding files and folders within rootDir

ts
entryKind: 'file' | 'dir'

Determines how matches for entries are processed

ts
targetFile: string

The absolute path of the file to generate

ts
template: (params: {
  modules: Array<{
    entryName: string;
    entryPath: string;
    moduleName: string;
    relativePath: string;
    entryFileName: string;
    rootRelativePath: string;
    files: Array<{
      ext: string;
      name: string;
      fileName: string;
      entryPath: string;
      relativePath: string;
      rootRelativePath: string;
    }>;
  }>;
  rootDir: string;
  targetFile: string;
  targetFolder: string;
}) => string;

A template function that returns the target file content

modules fields ​

ts
entryPath

The absolute path of the matched file or folder

ts
entryFileName

The full name of the matched file or folder, including the extension

ts
entryName

The name from path.parse(entryPath).name, without the extension

ts
moduleName

The name of the matched entry’s parent folder

ts
relativePath

The path from targetFolder to the matched entry, without the extension

ts
rootRelativePath

The path from rootDir to the matched entry, including the extension

ts
files

For entryKind: 'dir', the immediate child files and folders; for 'file', an empty array

Example: Central entry points for feature modules

The plugin collects index.ts files from different feature modules into a single file with namespace exports. Each export name comes from the parent folder

ts
new PluginModular([
  {
    entries: ['features/*/index.ts'],
    entryKind: 'file',
    targetFile: path.resolve(rootDir, 'features.generated.ts'),
    template({ modules }) {
      return modules
        .map(({ moduleName, relativePath }) => `export * as ${moduleName} from './${relativePath}';`)
        .join('\n');
    },
  },
]),
Run Sandbox
USER: features/orders/index.ts was created with content:
ts
export const featureName = 'orders';
USER: features/profile/index.ts was created with content:
ts
export const featureName = 'profile';
ARFA Generator shows logs:
txt
[ARFA-GEN] [write] features.generated.ts
[ARFA-GEN] finished in 0ms
ARFA Generator generated files:
ts
// this file is generated

export * as orders from './features/orders/index';
export * as profile from './features/profile/index';

Example: Page types

The plugin creates types or runtime objects describing each page’s store and actions, where present

ts
new PluginModular([
  {
    entries: ['pages/*'],
    entryKind: 'dir',
    targetFile: path.resolve(rootDir, 'modularPages.ts'),
    template({ modules }) {
      return modules
        .map(({ entryName: pageName, files }) => ({
          pageName,
          storePath: files.find(({ fileName }) => fileName === 'store.ts')?.relativePath,
          actionsPath: files.find(({ fileName }) => fileName === 'actions')?.relativePath,
        }))
        .map(
          ({ pageName, actionsPath, storePath }) => `export declare const ${pageName}: {
  store: ${storePath ? `typeof import('./${storePath}').PageStore` : 'never'};
  actions: ${actionsPath ? `typeof import('./${actionsPath}')` : 'never'};
};`
        )
        .join('\n\n');
    },
  },
]),
Run Sandbox
USER: pages/orders/index.ts was created with content:
ts
export const pageName = 'orders';
USER: pages/orders/store.ts was created with content:
ts
export class PageStore {}
USER: pages/orders/actions/index.ts was created with content:
ts
export async function createOrder() {}
USER: pages/profile/index.ts was created with content:
ts
export const pageName = 'profile';
ARFA Generator shows logs:
txt
[ARFA-GEN] [write] modularPages.ts
[ARFA-GEN] finished in 0ms
ARFA Generator generated files:
ts
// this file is generated

export declare const orders: {
  store: typeof import('./pages/orders/store').PageStore;
  actions: typeof import('./pages/orders/actions');
};

export declare const profile: {
  store: never;
  actions: never;
};

Example: Feature module routes

The plugin finds feature modules with a routes.ts file and combines their routes. Adding routes.ts includes the module automatically

ts
new PluginModular([
  {
    entries: ['features/*'],
    entryKind: 'dir',
    targetFile: path.resolve(rootDir, 'routes.generated.ts'),
    template({ modules }) {
      const routeModules = modules.flatMap(({ entryName, files }) =>
        files
          .filter(({ fileName }) => fileName === 'routes.ts')
          .map(({ relativePath }) => ({ name: `${entryName}Routes`, relativePath }))
      );
      const imports = routeModules
        .map(({ name, relativePath }) => `import { routes as ${name} } from './${relativePath}';`)
        .join('\n');
      const routes = routeModules.map(({ name }) => `  ...${name},`).join('\n');

      return `${imports}\n\nexport const routes = [\n${routes}\n];`;
    },
  },
]),
Run Sandbox
USER: features/orders/model.ts was created with content:
ts
export const orders = [];
USER: features/orders/routes.ts was created with content:
ts
export const routes = [{ path: '/orders' }];
USER: features/profile/model.ts was created with content:
ts
export const profile = {};
ARFA Generator shows logs:
txt
[ARFA-GEN] [write] routes.generated.ts
[ARFA-GEN] finished in 0ms
ARFA Generator generated files:
ts
// this file is generated

import { routes as ordersRoutes } from './features/orders/routes';

export const routes = [
  ...ordersRoutes,
];

Example: Feature module manifest

Generates a typed description of each feature module’s capabilities based on the files in its directory

ts
new PluginModular([
  {
    entries: ['features/*'],
    entryKind: 'dir',
    targetFile: path.resolve(rootDir, 'featureManifest.generated.ts'),
    template({ modules }) {
      const manifest: Record<string, { hasApi: boolean; hasRoutes: boolean; hasMessages: boolean }> = {};

      for (const { entryName, files } of modules) {
        const fileNames = new Set(files.map(({ fileName }) => fileName));

        manifest[entryName] = {
          hasApi: fileNames.has('api.ts'),
          hasRoutes: fileNames.has('routes.ts'),
          hasMessages: fileNames.has('messages.ts'),
        };
      }

      return `export const featureManifest = ${JSON.stringify(manifest, null, 2)} as const;`;
    },
  },
]),
Run Sandbox
USER: features/orders/api.ts was created with content:
ts
export async function getOrders() {}
USER: features/orders/routes.ts was created with content:
ts
export const routes = [{ path: '/orders' }];
USER: features/orders/messages.ts was created with content:
ts
export const messages = {};
USER: features/profile/routes.ts was created with content:
ts
export const routes = [{ path: '/profile' }];
ARFA Generator shows logs:
txt
[ARFA-GEN] [write] featureManifest.generated.ts
[ARFA-GEN] finished in 0ms
ARFA Generator generated files:
ts
// this file is generated

export const featureManifest = {
  "orders": {
    "hasApi": true,
    "hasRoutes": true,
    "hasMessages": true
  },
  "profile": {
    "hasApi": false,
    "hasRoutes": true,
    "hasMessages": false
  }
} as const;

MIT License