Document that package.json aliases may target only direct subfolders of src #21

Merged
temeddix merged 27 commits from alias-targets into main 2026-10-18 14:37:50 +00:00
15 changed files with 103 additions and 65 deletions
Showing only changes of commit bfa8269801 - Show all commits
+4 -1
View File
@@ -17,6 +17,9 @@ as skipped instead of silently passing.
`codex`/`claude`/`npx` binaries and a fake Gitea through
`protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that
for changes to the phase flow ([[bot-agents-runtime-no-build]]).
- `src/testing.ts` holds the shared test helpers; tests follow
- `src/testing.ts` holds the shared test helpers and is the `#testing` file
alias. Every test imports it as `#testing`, never `./testing.ts`, and it
re-exports the source helpers `tests/` needs, since `tests/` may not use
`../`. Tests follow
[[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in
`tests/`).
+52 -42
View File
@@ -1,75 +1,85 @@
---
description: Shared code comes through # aliases in package.json imports, each src/ subfolder's entry is index.ts, barrels keep imports few; never ../ out of a directory
description: An alias owns its target (folder index.ts = folder and below, any other file = that file); no overlap; outside an area only the exact alias reaches it; ../ is banned in every file; no folder names in the rule
---
## `#` aliases
## `#` aliases own their targets
Shared code is imported through Node subpath imports declared in
`package.json` `imports`, never by a relative path that climbs out of a
directory.
`package.json` `imports`. Node, TypeScript, Vite, Vite+'s config bundler, and
Oxlint's plugin loader all resolve them natively, so `.ts` source runs
unbuilt. `vp pack` inlines them, so they never reach package consumers.
**Targets (user rule, 2026-10-16):** an alias may point ONLY at a direct
subfolder of `src/`: its `index.ts` (`"#helpers": "./src/helpers/index.ts"`)
or a `#x/*` pattern inside that one folder. Never alias anything outside
`src/` (`scripts/`, root files), `src/` itself (no `#plugin` for
`./src/index.ts`), or a deeper folder (`./src/a/b/`). Code that would need
such an alias uses a relative import instead; `scripts/` and
`vite.config.ts` import each other relatively. Layer rules
(`enforce-layer-order`) must still pass, so reorder entries if needed.
One rule for every file, never tied to folder or file names (user rule
2026-10-17; it replaces the earlier "aliases only point at direct `src/`
subfolders" rule and the "import-check checks only aliased folders" scope):
- **An alias may point anywhere** and owns its target. An alias to a
folder's `index.ts` owns that folder and everything below it; an alias to
any other file owns only that file.
- **No overlap.** No file may be owned by 2 aliases: no alias inside another
alias's area, and no 2 aliases with the same target. import-check's
`enforce-layer-order` reports it once at the top of every file 2 aliases
claim. Fix by merging the aliases or moving files apart.
- **Outside an area, only the exact alias reaches it.** A deeper alias
specifier (`#a/one.ts`, `no-barrel-bypass`) and a relative path into the
area (`./a/one.ts`, `no-relative-bypass`; `./b/f.ts` to a file alias,
`prefer-alias-import`, fixed to the alias) are bypasses.
- **Inside an area, relative imports of siblings and subfolders are free.**
- **`../` is banned in every file**, owned or not (`no-parent-import`).
Anything above a file needs an alias.
- **Files no alias owns** need no `index.ts` and no alias, and import `./`
siblings and subfolders freely.
- Unchanged: no `/*` subpath patterns, the key order of `imports` is the
layer order `enforce-layer-order` checks (add a new alias at the right
layer), and entry files are `index.ts` only.
```jsonc
"imports": {
"#helpers": "./src/helpers/index.ts",
"#components/*": "./src/components/*"
"#a": "./a/index.ts", // owns a/**
"#f": "./b/f.ts", // owns only b/f.ts
"#a-sub": "./a/sub/index.ts", // no: inside #a
"#a2": "./a/index.ts" // no: same target as #a
}
```
```ts
import { typeNameTail } from "#helpers"; // yes
import { typeNameTail } from "../helpers/ast.ts"; // no: import/no-relative-parent-imports
// in main.ts, which no alias owns
import { a } from "#a"; // yes
import { one } from "#a/one.ts"; // no: deeper alias specifier
import { one } from "./a/one.ts"; // no: relative path into #a
import { f } from "#f"; // yes
import { f } from "./b/f.ts"; // no: relative path to a file alias
import { g } from "./b/g.ts"; // yes: unowned
// in a/one.ts, owned by #a
import { two } from "./two.ts"; // yes: sibling
import { deep } from "./sub/deep.ts"; // yes: subfolder of the same area
import { x } from "../x.ts"; // no: no ../ anywhere
```
Node, TypeScript, Vite, Vite+'s config bundler, and Oxlint's plugin loader
all resolve them natively, so `.ts` source runs unbuilt. `vp pack` inlines them,
so they never reach package consumers.
- Deno's trailing-slash mappings (`"#components/"`) don't exist in Node; they
become patterns, `"#components/*": "./src/components/*"`.
- Aliases point at `.ts` files only. There is no `#package` alias
([[ts-only-files-and-imports]]).
- In repos linted by import-check, the key order of `imports` is the layer
order `enforce-layer-order` checks; add a new alias at the right layer.
- Lint `jsPlugins` entries in `vite.config.ts` stay path strings, even when
the file has an alias.
- Keep test-only helpers in their own alias (e.g. `#testing`) so they never
land in a bundle.
## `index.ts` entries and barrels
## Barrels
- Every `src/` subfolder's entry file is `index.ts`, never `mod.ts` (decided
2026-10-12; import-check's `enforce-mod-file` requires it only in folders a
`#` alias owns). Folders outside `src/`, such as `scripts/`, have none.
- A directory used from outside exposes its API through its `index.ts`
barrel, and callers import the barrel (`#ui`), not files inside it
(`no-barrel-bypass`).
- An area used from outside exposes its API through its alias target, and
callers import the alias (`#ui`), not files inside it.
- Barrels are also the standard fix for `import/max-dependencies`: one barrel
import replaces many direct ones ([[lint-overrides-policy]]).
- `import/no-cycle` is on; a barrel must not create a cycle.
Relative imports inside one directory (`./sibling.ts`) are fine, with the
`.ts` extension written out.
Relative imports are written with the `.ts` extension.
## `scripts/` is flat (user rule, 2026-10-16)
- `scripts/` has no subfolders and no `index.ts`. Barrels and `index.ts`
entries exist only inside `src/`; delete any `index.ts` outside it.
- `scripts/` has no subfolders and no `index.ts`.
- Each tool is one file, `scripts/<name>.ts`, imported directly by file:
`import { decorators } from "./scripts/decorators.ts";` in
`vite.config.ts`.
`vite.config.ts`. No alias owns `scripts/`, so these `./` imports are free.
- Code in a former `scripts/<tool>/` folder is merged or renamed into flat
`scripts/<name>.ts` files. Non-TS assets (HTML, images, fixtures) move to
where they belong (next to the tool's output, `.agents/`, or a gitignored
scratch dir). A subfolder that is really an app moves under `src/` as an
aliased folder.
- import-check 0.10.0+ checks folder layout only where the importer or the
target sits in a `#`-aliased folder, so `vite.config.ts` →
`./scripts/x.ts` passes without any override. Remove overrides that only
dodged import-check.
scratch dir). A subfolder that is really an app moves under an alias.
+6 -3
View File
@@ -11,9 +11,7 @@ practice.
off for `src/rules/index.ts`, the rule registry, which must import every
rule. It is not a precedent for apps.
- **`scripts/**`:** a narrow override is allowed when a tooling script truly
needs it, e.g. `no-console` in a CLI script. Never one that only dodges
import-check (e.g. `no-relative-bypass` for `vite.config.ts`); import-check
ignores folders no `#` alias owns.
needs it, e.g. `no-console` in a CLI script.
- **Generated files:** list them in `ignorePatterns`, never `overrides`.
- **Anything else needs the user's approval first.** When the user is away,
don't add it: refactor, or leave the violation failing and log the question.
@@ -45,3 +43,8 @@ pedantic hits in 274 files, of which `max-lines-per-function` 206,
`max-lines` 145, `max-classes-per-file` 59, `max-dependencies` 45 (median 14
imports, worst 29). The user chose refactoring over raising limits or
matching memona's old hugoalh limits (2048 lines per file, no function limit).
Alias rule: an alias may point anywhere and owns its target (a folder's
`index.ts` owns the folder and below, any other file owns only itself); no
two aliases may own the same file, and `../` is banned everywhere
([[imports-aliases-barrels]]).
+6
View File
@@ -30,3 +30,9 @@ The `imports` key order can carry meaning: import-check's
Refresh the lockfile with `npm install` (or `npm install
--package-lock-only` for a version bump) and commit it with the change.
Alias rule: an alias may point anywhere and owns its target (a folder's
`index.ts` owns the folder and everything below it; any other file owns only
itself). No two aliases may own the same file, outside an area only the exact
alias reaches it, and `../` is banned in every file
([[imports-aliases-barrels]]).
+7 -4
View File
@@ -1,5 +1,5 @@
---
description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry; scripts/ has no tests
description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry (through its # alias, never ../); scripts/ has no tests
---
Decided 2026-10-14 for all repos.
@@ -11,9 +11,12 @@ Decided 2026-10-14 for all repos.
config needed. Packages publish only `dist/`, so colocated tests cost nothing
in the tarball.
- **Regression and integration tests** live in the top-level `tests/` folder
and use only the public entry point through a relative import
(`../src/index.ts`; no alias may target `src/` itself, see
[[imports-aliases-barrels]]), never internals. `vp test tests` runs just these.
and use only the public entry point, imported through its `#` alias (e.g.
`#plugin` for `./src/plugin/index.ts`), never internals and never `../`,
which is banned in every file. `vp test tests` runs just these
([[imports-aliases-barrels]]). In bot-agents, `tests/` imports only
`#testing` (`./src/testing.ts`), which re-exports the few source helpers
those tests need.
- **No in-source tests** (`import.meta.vitest`).
- **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not
production code (`project-lint.ts`, `decorators.ts`, `lit-check.ts`,
+8 -4
View File
@@ -19,10 +19,14 @@ dependency. Keep the sources to erasable syntax and `.ts` imports, which
type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as
`src/*.test.ts`; repository-level tests live in `tests/`.
`src/` is flat, so `package.json` has no `imports` aliases and modules, tests,
and `scripts/` import each other relatively. If one is ever added, it may point
only at a direct subfolder of `src/` (its `index.ts`, or a `#x/*` pattern
inside it); never at `src/` itself, a deeper folder, or anything outside `src/`.
An alias in `package.json` `imports` owns its target: a folder's `index.ts`
owns that folder and everything below it, any other file owns only that file.
No file may be owned by 2 aliases. Outside an area, only the exact alias
reaches it (no deeper `#` specifier, no relative path); inside it, relative
imports of siblings and subfolders are free. `../` is banned in every file
(`import/no-relative-parent-imports`). The one alias is `#testing`
(`./src/testing.ts`): every test imports it by alias, and `tests/` reaches
`src/` only through it. Other `src/` files import `./` siblings freely.
`scripts/` is flat: one file per tool, no subfolders and no `index.ts`
(barrels live only in `src/`), imported directly by file path.
2
+3
View File
@@ -3,6 +3,9 @@
"private": true,
"description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.",
"type": "module",
"imports": {
"#testing": "./src/testing.ts"
},
"devDependencies": {
"@eslint/core": "^0.17.0",
"@types/node": "^24.19.2",
+2
View File
@@ -203,6 +203,8 @@ const rules: DummyRuleMap = {
],
// The explicit-types template's extras.
"import/no-cycle": "error",
// `../` is banned in every file; anything above a file needs a `#` alias.
"import/no-relative-parent-imports": "error",
"typescript/consistent-type-imports": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/no-non-null-assertion": "error",
+1 -1
View File
@@ -9,7 +9,7 @@ import {
retryInvalidToken,
stageCodexAuth,
} from "./auth.ts";
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "./testing.ts";
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing";
test("retries once after an invalid refresh token", async () => {
let attempts = 0;
+1 -1
View File
@@ -10,7 +10,7 @@ import {
readProfileFile,
} from "./isolation.ts";
import type { RuntimeProfile } from "./runtime.ts";
import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "./testing.ts";
import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "#testing";
const profile = (uid: number, home: string): RuntimeProfile => ({
index: uid - 20_000,
+1 -1
View File
@@ -6,7 +6,7 @@ import { isNotFound } from "./errors.ts";
import { postRuntime } from "./post.ts";
import type { PostContext } from "./post.ts";
import type { RuntimeManifest, RuntimeOutcome } from "./runtime.ts";
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "./testing.ts";
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing";
const context: PostContext = {
apiUrl: "https://gitea.example/api/v1",
+1 -1
View File
@@ -7,7 +7,7 @@ import { assert, expect, test } from "vite-plus/test";
import { output } from "./command.ts";
import { prepareRuntime } from "./runtime.ts";
import type { RuntimeProfile } from "./runtime.ts";
import { IS_ROOT, makeTempDir } from "./testing.ts";
import { IS_ROOT, makeTempDir } from "#testing";
const MASTER_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8";
const ENCRYPTED = "v1:ICEiIyQlJicoKSor:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA";
+8 -1
View File
@@ -1,4 +1,6 @@
// Test support: the current user's IDs and a fresh temporary directory.
// Test support: the current user's IDs and a fresh temporary directory, plus
// the source helpers that the repository-level tests in tests/ need. Those
// tests sit outside src/ and reach it only through the `#testing` alias.
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -21,3 +23,8 @@ export const IS_WINDOWS: boolean = process.platform === "win32";
export function makeTempDir(): Promise<string> {
return mkdtemp(join(tmpdir(), "bot-agents-test-"));
}
export { output } from "./command.ts";
export { isNotFound } from "./errors.ts";
export { isMapping } from "./json.ts";
export type { JsonObject } from "./json.ts";
+1 -1
View File
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
import { expect, test } from "vite-plus/test";
import { output } from "../src/command.ts";
import { output } from "#testing";
// Each phase runs as `node src/<phase>.ts` with no build step. Node loads the
// whole import graph, stripping types, before the phase reads its first
+2 -5
View File
@@ -4,11 +4,8 @@ import process from "node:process";
import { assert, expect, test } from "vite-plus/test";
import { parse } from "yaml";
import { output } from "../src/command.ts";
import { isNotFound } from "../src/errors.ts";
import { isMapping } from "../src/json.ts";
import type { JsonObject } from "../src/json.ts";
import { makeTempDir } from "../src/testing.ts";
import { isMapping, isNotFound, makeTempDir, output } from "#testing";
import type { JsonObject } from "#testing";
const mapping = (value: unknown, at: string): JsonObject => {
assert(isMapping(value), at);