feat!: standalone operations layout, drop the .generated suffix - #8
Merged
Conversation
Add a second Angular output layout in which every operation is a top-level `defineOperation(...)` constant in its own file under `rest/<group>/`, re-exported by a `rest/<group>.operations.generated.ts` barrel. `both` also emits the per-tag class, built from `ops.<method>.withInjector()`, with the `Params`/`Error` interfaces re-exported so type imports keep resolving across layouts. The default `services` output is byte-identical. Runtime: one `OperationImpl` class backs both forms. Standalone `.observable()`/`.resource()` resolve DI per call from `options.injector` or the current injection context and throw a dev-only message with NG0203 as `cause` otherwise; `.request()` is pure unless given an injector. `.withInjector()` and the free `withInjector(record)` return today's `RequestFn`; `requestFactory` is sugar over them. `validateRest` accepts either form via `Resourceful`. Generator: `Layout` option across config, napi, JS wrapper, CLI and config file; per-operation and barrel paths in the plan; reserved words such as `delete` are declared under an alias and exported by name; `default` is rejected with `E_POLICY_VIOLATION`/`reserved-identifier`; two operations resolving to one method name in a group are rejected. Tests: Rust unit tests, labelled layout snapshots, consumer type proof, CLI tests, and a runtime spec that loads the template under Node (`@angular/compiler` dev dependency, `templates/package.json` for ESM). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJqAgGwh52GqqMx1rfPzGn
Emitted files are now `model.ts`, `rest/<group>.rest.ts` and, for the standalone layouts, `rest/<group>/<method>.ts` with the barrel at `rest/<group>/index.ts` so a group is imported by its directory. The runtime templates were already unsuffixed, every file carries the do-not-edit banner, and the writer never used the suffix to find its own output, so the marker only lengthened import paths. Docs state the contract instead: the output directory belongs to the generator, is committed or ignored as one unit, and is deleted before regenerating after removals since stale files are never cleaned up. BREAKING CHANGE: consumer import paths lose the `.generated` segment, e.g. `./rest/pet.rest.generated` becomes `./rest/pet.rest` and `./rest/pet.operations.generated` becomes `./rest/pet`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJqAgGwh52GqqMx1rfPzGn
The operations layout emits `rest/<group>/<method>.ts`, two levels deep, which the flat two-row tree could not show. Build the tree recursively, render directories as toggle buttons with `aria-expanded`, and keep the folded set across regenerations, unfolding only when the selected file falls back into a folded directory. Add `layout` to the accepted config options and the default JSONC template. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`layout` now mirrors `emit`: a list of `services` and/or `operations` (`--layout services,operations` on the CLI, comma-separated and repeatable). Listing both replaces the old `layout: 'both'`. The core rejects an empty list and `operations` without the `angular` target; the CLI and the wrapper share one allow-list and one list validator, and the browser entry exports `Layout`. Runtime: every `defineOperation` call carries `/* @__PURE__ */`, so esbuild and Rollup drop operations a barrel namespace import never touches. `.request()`, `.observable()` and `.resource()` agree that an explicit `options.injector` wins over a `withInjector()` binding, and are arrow properties so detached methods keep working. A bound `.resource()` hands its options to `httpResource` untouched, so the resource lives in the caller's injection context, as it did under `requestFactory`. Planner: reject an operation named `index` (it would overwrite the barrel, `reserved-identifier`), two method names sharing a kebab file stem, a method name with no letters or digits, and two groups sharing a file stem (`naming-resolution`). The barrel and the bound class derive their import specifiers from the naming helpers instead of re-parsing planned paths; the `default` rejection names the real reason (`ops.default`). Tests: unit tests for `emit_bound_service` and the new rejections, an `index-method-name` fixture with success and failure snapshots, runtime spec cases for detached methods, injector precedence and the resource lifetime. Docs: standalone-operation example and layouts bullet in the README, a layouts card on the landing page, the new rejections on the limitations and diagnostics pages, `Config.layout` as a list in the node-api reference. BREAKING CHANGE: `layout` is an array; `layout: 'both'` becomes `layout: ['services', 'operations']`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
layout: ['operations']emits onedefineOperation(...)constant per operation underrest/<group>/<method>.tswith arest/<group>/index.tsbarrel, so an app imports and bundles only the operations it uses.layout: ['services', 'operations']adds the per-tag class bound from the barrel.['services'](the default) is byte-identical to today's output.OperationImplbacks both forms:.observable()/.resource()resolve DI per call fromoptions.injectoror the current injection context (dev-only message with NG0203 ascause),.request()is pure,.withInjector()and the freewithInjector(record)return the boundRequestFn. EverydefineOperationcall is/* @__PURE__ */so barrel namespace imports tree-shake under esbuild and Rollup..generatedsuffix from emitted files:model.ts,rest/<group>.rest.ts,rest/<group>/<method>.ts.layoutaccepted in the config editor.defaultandindexmethod names underoperations, two method names or two groups sharing a kebab file stem, and method names with no letters or digits.Breaking changes
.generatedsegment:./rest/pet.rest.generated→./rest/pet.rest,./model.generated→./model.E_POLICY_VIOLATION/naming-resolution) instead of silently overwriting output.layout: Array<'services' | 'operations'>(default['services']);--layouton the CLI.Verification
cargo test --all-targets,cargo clippy,bun run test(385),bun run lintastro check, vitest, Playwright e2e against the locally built engineNote on the Docs check
The Docs workflow builds the playground against the published
@avsystem/openapi-ng@0.5.1, whose wrapper rejects the newlayoutoption, so its Playwright run fails on this PR by construction. It goes green with the version bump that follows the release.🤖 Generated with Claude Code