Guides: Monorepos and workspace tables: Check which apps and packages of a workspace may use which, and make each app list the packages that it imports. # Monorepos and workspace tables > Check which apps and packages of a workspace may use which, and make each app list the packages that it imports. This guide checks the architecture of a monorepo with one config at its root. A table says which apps and packages may use which. A second rule makes each app list the packages that it imports. ## Before you start * Read [Getting started](/archangel/start/getting-started/). * Your monorepo uses workspaces of npm, pnpm or Yarn, so that each workspace package is linked in `node_modules`. The example has two apps and two packages: * archangel.config.ts * package.json * apps/ * api/ * package.json * src/ * index.ts * web/ * package.json * src/ * index.ts * packages/ * contract/ * package.json * src/ * index.ts * ui/ * package.json * src/ * index.ts ## How archangel sees a workspace package An app imports a package of the workspace by its name, for example `@acme/ui`. The package manager links `node_modules/@acme/ui` to `packages/ui`. Archangel follows the link, as Node and TypeScript do. So the import goes to a file in `packages/ui`, and a rule about parts judges it as an import of that file. Note For this reason, an externals set cannot hold the name of a workspace package. Archangel refuses the config. Use a part for the folder of the package. ## Write the workspace table 1. Write one config at the root of the monorepo, with one part for each app and each package: archangel.config.ts ```ts import { defineConfig } from '@enhance-eng/archangel'; export default defineConfig({ parts: { web: 'apps/web/**', api: 'apps/api/**', ui: 'packages/ui/**', contract: 'packages/contract/**', }, rules: ({ mayImport, rule }) => [ mayImport('workspace', { web: ['ui', 'contract'], api: ['contract'], ui: [], contract: [], }, { because: 'An app uses packages. A package never uses an app, and the contract depends on nothing.', }), rule('declared-packages', { files: 'apps/*/src/**', mayNotImport: { packages: ['*'] }, dependency: 'undeclared', because: 'Each app lists what it uses in its own package.json, so it builds alone.', examples: { rejected: [{ 'apps/web/package.json': '{ "name": "@acme/web" }', 'apps/web/src/a.ts': "import 'react';", }], }, }), ], }); ``` The table `workspace` says: * The web app may use the UI kit and the contract. * The API may use only the contract. * The UI kit and the contract may use no other workspace. 2. Check the monorepo: Terminal ```ansi $ npx archangel check error[may-import]: api may not import ui, through "@acme/ui": api may import only contract, and itself --> apps/api/src/index.ts:2:23 | 2 | import { Price } from '@acme/ui'; | ^^^^^^^^^^ api may import only contract, and itself | = help: remove the import, or move the code to a file whose row permits the import = note: rule: workspace.api = note: because: An app uses packages. A package never uses an app, and the contract depends on nothing. error[may-not-import]: apps/api/src/index.ts may not import "zod", because files in "apps/*/src/**" may not import any package that apps/api/package.json does not list --> apps/api/src/index.ts:3:19 | 3 | import { z } from 'zod'; | ^^^^^ remove the import, or move the code to files that may import it | = note: rule: declared-packages = note: because: Each app lists what it uses in its own package.json, so it builds alone. checked 5 files and 7 imports: 2 problems For what a code means, run `archangel docs may-import` or `archangel docs may-not-import`. ``` The check finds two problems. The first is from the table: the API imports the UI kit. The second is from the rule `declared-packages`, in the next section. Here is the API file: apps/api/src/index.ts ```ts import type { Order } from '@acme/contract'; import { Price } from '@acme/ui'; import { z } from 'zod'; const order = z.object({ id: z.string(), total: z.number() }); export function parseOrder(body: unknown): Order { Price({ cents: 0 }); return order.parse(body); } ``` ## Make each app list its packages In a monorepo, a package that one workspace installs is often visible to all of them. An app can then import a package that its own `package.json` does not list. It works on your laptop and fails when the app builds alone. The rule `declared-packages` refuses this. It uses two options: * `mayNotImport: { packages: ['*'] }` names each outside package. * `dependency: 'undeclared'` limits the rule to packages that the nearest `package.json` of the importing file does not list. The API imports `zod`. The root `package.json` installs it, but `apps/api/package.json` does not list it: apps/api/package.json ```json { "name": "@acme/api", "private": true, "type": "module", "main": "src/index.ts", "dependencies": {"@acme/contract": "*", "fastify": "5.6.0"} } ``` The API also imports `@acme/ui`, which its `package.json` does not list either. The rule does not refuse it, because the import goes to a file of the repository, not to an outside package. The table judges it. To find packages that are only in `devDependencies`, use `dependency: 'dev'`. ## Prove the rules Terminal ```ansi $ npx archangel test pass declared-packages rejected #1 apps/web/package.json, apps/web/src/a.ts 1 examples, none failed; 0 rules without a rejected example 4 table rows need no written example; `archangel rules` shows what each covers ``` The example of `declared-packages` writes its own `package.json`. A rule with `dependency` needs files that you write, because the text of the manifest decides the result. One-line examples do not work here. Terminal ```ansi $ npx archangel rules workspace 4 rows archangel.config.ts:11 because: An app uses packages. A package never uses an app, and the contract depends on nothing. workspace.web: web may import only ui, contract, and itself 1 file · 2 imports judged · 0 problems · 0 exceptions workspace.api: api may import only contract, and itself 1 file · 2 imports judged · 1 problem · 0 exceptions workspace.ui: ui may import only itself 1 file · 0 imports judged · 0 problems · 0 exceptions workspace.contract: contract may import only itself 1 file · 0 imports judged · 0 problems · 0 exceptions workspace.every-file-has-a-kind: a file in no row is not checked here; add `files` to check it 4 files · 0 problems declared-packages archangel.config.ts:20 because: Each app lists what it uses in its own package.json, so it builds alone. files in apps/*/src/** may not import * that the nearest package.json does not list 2 files · 1 import judged · 1 problem · 0 exceptions Not judged by any rule: 0 files, 0 imports between repository files. Outside packages that no rule names: none. ``` ## One config or many Archangel uses the nearest config at or above the folder where it starts. One config at the root can check all workspaces in one run, and a table can say how they depend on each other. A config in a workspace sees only that workspace. Use one root config for the rules between workspaces. ## Next steps * [Import options](/archangel/reference/import-options/) lists `dependency`, `specifier`, `instance` and `within`. * [Next.js](/archangel/guides/nextjs/) shows how to use a preset for one app, with its `root`.