@akanjs/devkit 3.2.0-rc.5 → 3.2.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -131,6 +131,8 @@
131
131
  - The dev builder draws the graph on request through a new `build-graph` builder RPC, and keeps it until the next save.
132
132
  - It sits behind the same gate as the other `/_akan` devtools routes, and `akan tunnel` / `akan start --share` refuse it.
133
133
 
134
+ - 2d3b958: `ApiReportPolicy.check` counts a changeset that a branch adds in place of a similar one it deletes. git's rename detection paired the two, so the added changeset was not listed and its `minor` bump did not satisfy the check.
135
+ - 465ccb9: `@akanjs/devkit/apiReport/apiReport` renders a package's public API, one report per entry point, from its declaration emit. The akanjs repository now commits that report for `akanjs` and checks it in CI: a removed export or a changed declaration needs a `minor` changeset that says how to upgrade.
134
136
  - 0504209: `akan build <app> --env <env>` builds for an environment other than the workspace `.env`'s, the way the desktop and mobile builds already do. Before, the shell's `AKAN_PUBLIC_ENV` lost to the `.env`, so a new workspace could only build `local` without editing it. The local-build notice, the image guard and the README now say what a `local` build changes in a built server: no endpoint rate limits, and local defaults for Redis and the database mode. They no longer say the dev routes are served, which a built server never does.
135
137
  - bf872ce: A variable the shell sets now outranks the workspace `.env` when `akan` builds or runs an app, as dotenv and Bun's own `.env` loading do: `AKAN_PUBLIC_ENV=main akan build <app>` builds `main` where the `.env` says `local`. The order is `--env` (for `AKAN_PUBLIC_ENV`), then the process environment (the shell, and the `.env.local` and `.env.<NODE_ENV>` Bun loads into it), then the `.env`, then the defaults; an empty export counts as unset. The bundles, the bundled server env, `akan.env.json` and the Dockerfile still take one set of values. Where an exported value outranks the `.env`, `akan build` now warns with both values instead of the old warning that listed the values it ignored, and the server-env message, the `--env` help, the local-build notice, the README and the CLI reference state the new order.
136
138
 
@@ -229,19 +231,26 @@
229
231
  - b101fcf: `akan remove-application`, `remove-library`, `remove-package`, `remove-module` and `remove-scalar` no longer delete a target they picked themselves. Left out, or named but not in the workspace, the target is refused in a terminal or not, with the choices and the line to rerun, such as `` Missing app: this command deletes files and never picks its target, so rerun it as `akan remove-application <app>` (choices: pokedex). `` Before, `akan remove-application` in a one-app workspace removed `apps/<app>` without asking, and without a terminal `akan remove-scalar <name>` removed the scalar from the only app or lib. A terminal is no longer offered a select for these commands. Other commands still take the only app, or the only app or lib without a terminal, as before. A command declares this with `destructive: true` on its target.
230
232
  - 44149b1: `akan remove-module` now removes a service module named exactly, as in `akan remove-module pokedex:_billing` for `apps/pokedex/lib/_billing`. Before, no command could remove a service module: `remove-module` left out every `lib/_<name>` folder and refused one by name with `Invalid module name: _billing`. A service module left out or misnamed gets the same refusal as any target of a remove command, and its choices now list the service modules. `create-view`, `create-unit`, `create-template` and `create-crud-page` still take only a model module. A command opts in to service modules by taking `ModuleOrService` instead of `Module`.
231
233
  - 4fcf44f: A new `rename-key` workflow renames a slice key or a Filter query key everywhere it is spelled. That covers the key itself, every name derived from it (`initMemoInPublic`, `memoListInPublic`, `queryByName`) in the app and every lib, and its module dictionary entry. It is the same rename the editor extension's F2 runs, so agents get it through `plan_workflow` / `apply_workflow` and the CLI through `akan workflow plan rename-key --app <app> --module <module> --slice <key> --to <newKey>` (or `--query <key>`). The plan lists each file it will change. A use that could also be another model's is left alone, and `akan verify` then reports it.
234
+ - 627dde4: The route JS budget stops failing deploy builds: a plain `akan build` reports an overage, `akan build --check-budget` fails on it, and CI runs that for `apps/minimal` and `apps/akan`, which now commit their `akan.budget.json`. The rc npm publish, which calls CI, skips that job, so an app's budget never holds a release.
235
+
236
+ - **Report by default:** a plain `akan build`, the one a deploy runs, prints a route over its entry or with none, or an unreadable `akan.budget.json`, as a warning under the route table and passes, as `akan verify` does. `--quiet` drops the rest of the summary but still prints the warning.
237
+ - **`--check-budget`** fails the build on an overage, a route with no entry (a new page records its entry with `--update-budget` in the same change), an unreadable file, or an app with no file, naming every overage and missing entry in one failure.
238
+ - **`--raise-budget`** re-records every route at its measured size and names the entries it raised, for the change that grows a route on purpose. An overage now names it with the app's own name, along with the file and section to edit by hand. `--check-budget`, `--update-budget` and `--raise-budget` exclude each other.
239
+ - **`System` counts as the SSR shell:** the generated root layout imports `System` whole because it serves SSR pages, the CSR shell and MCP App views. The measure now reads that import as `System/SSR.tsx` and leaves out `System`'s CSR and MCP App providers wherever an app imports `System`, since no server page renders them; every client entry is still built. On `apps/minimal` the median route drops from 342KB to 307.5KB brotli and the largest, `/:lang/explore`, from 499.1KB to 473.3KB. On `apps/akan` most routes drop about 1KB, because their other client entries load the providers' dependencies anyway.
240
+
232
241
  - 9a9e1fa: A server page from `akan build` now preloads its client modules from `<head>`, so a cold load stops waiting on chains of imports.
233
242
 
234
243
  - **What is preloaded:** the rsc client and the chunks it imports, the chunks every client reference row (`I` row) in the shell's flight names, and the importmap vendors those chunks import, each with its own chunks. A client URL that only the page's own text carries is never preloaded. Before, only the rsc client and the locale dictionary were preloaded. The browser found the rest one import level at a time, after the document finished.
235
- - **Measured on `apps/minimal`:** `/en/explore` preloads 45 modules and its flight names 18 client chunks, all of them preloaded. `/en/lab/buttons` preloads 112 for 85. Preloading every client component each route's page and layouts import would have meant 237 modules for `/en/explore`, so the preloads follow the flight instead.
244
+ - **Measured on `apps/minimal`:** `/en/explore` preloads 64 modules and its flight names 37 client chunks, all of them preloaded. `/en/lab/buttons` preloads 165 for 137. Preloading every client component each route's page and layouts import would have meant 274 modules for `/en/explore`, so the preloads follow the flight instead.
236
245
  - **A boundary that resolves after the shell** still loads its chunks when its flight rows arrive. `akan start` preloads nothing new, because a dev page loads its client code from the SSR dev registry.
237
246
  - `routes-manifest.json` carries the preload table as `modulePreloads`: the runtime chunks, each vendor's chunks, and the vendors each browser chunk imports.
238
247
 
239
- - 9a9e1fa: `akan build` measures the JavaScript each server route loads, and fails a route that outgrows the budget the app commits.
248
+ - 9a9e1fa: `akan build` measures the JavaScript each server route loads, and reports a route that outgrows the budget the app commits; `akan build --check-budget` fails on it.
240
249
 
241
- - **The measure:** the runtime, every client module statically reachable from the route's page and layouts, and their vendors, counted in the brotli bytes the server sends. A chunk loaded by a dynamic `import()` is left out, and so is the locale dictionary. A new `Measuring route JS` step names the largest route, and the summary lists the ten largest with their brotli and raw sizes. On `apps/minimal` the largest is `/:lang/explore` at 499.1KB brotli (1.3MB raw).
242
- - **The budget:** `akan build <app> --update-budget` writes `apps/<app>/akan.budget.json` with the environment it built for and one `routeJsBrotliKB` entry per route. After that, `akan build` fails a route that passes its entry by more than the larger of 1% and 4KB, as the bundle size gate does, and names the route, its bytes and its limit, so CI stops the growth. The slack absorbs the `AKAN_PUBLIC_*` values a build inlines, so a budget recorded for one environment holds for another, and a failure says when the file was recorded for another one. The update adds a new route, lowers an entry its route now fits under and drops a route that is gone. It never raises an entry: to raise one, edit it by hand in the change that grows the route.
243
- - **Where it fails:** `akan build` fails on an overage or an unreadable file, and `--update-budget` rewrites an unreadable file. `akan verify` warns of each route over its entry and of an unreadable file, one line each, but never fails on them. A mobile or desktop build skips the step.
244
- - **Opt-in:** an app without the file gets the report and nothing fails. A route without an entry is reported, not failed. `akan sync` accepts `akan.budget.json` at an app root.
250
+ - **The measure:** the runtime, every client module statically reachable from the route's page and layouts, and their vendors, counted in the brotli bytes the server sends. A chunk loaded by a dynamic `import()` is left out, and so is the locale dictionary. A new `Measuring route JS` step names the largest route, and the summary lists the ten largest with their brotli and raw sizes. On `apps/minimal` the largest is `/:lang/explore` at 473.3KB brotli (1.2MB raw).
251
+ - **The budget:** `akan build <app> --update-budget` writes `apps/<app>/akan.budget.json` with the environment it built for and one `routeJsBrotliKB` entry per route. After that, a route that passes its entry by more than the larger of 1% and 4KB, as the bundle size gate allows, is an overage, named with its bytes and its limit. The slack absorbs the `AKAN_PUBLIC_*` values a build inlines, so a budget recorded for one environment holds for another, and an overage says when the file was recorded for another one. The update adds a new route, lowers an entry its route now fits under and drops a route that is gone. It never raises an entry: `--raise-budget` or a hand edit does, in the change that grows the route.
252
+ - **Where it fails:** only `akan build --check-budget`, on an overage, a route with no entry, an unreadable file or a missing one. A plain `akan build` warns of each but a missing file under its route table, and `--update-budget` rewrites an unreadable file. `akan verify` warns of each route over its entry or with none and of an unreadable file, one line each, but never fails on them. A mobile or desktop build skips the step.
253
+ - **Opt-in:** an app without the file gets the report and nothing fails, unless `--check-budget` asks for it. A route without an entry is reported, and fails only `--check-budget`. `akan sync` accepts `akan.budget.json` at an app root.
245
254
 
246
255
  - 9055634: Partial RSC commit can now patch a real page, and it patches cleanly. It is still off by default behind `AKAN_PUBLIC_RSC_PARTIAL_COMMIT=1`. With the flag on:
247
256
 
@@ -384,7 +393,9 @@
384
393
 
385
394
  - d2b2291: A failed workflow no longer reports success to its caller. `akan workflow plan` exits 1 when the plan has an error, `akan workflow apply` when the apply or its `--dry-run` fails, and `akan workflow validate` when its status is `failed` (which includes `passed-with-baseline-blockers`); all three exited 0 before. A dry run of a plan with an error, such as `add-field --type Number`, reports `failed` and lists no changed or generated files, where it used to list the plan's edits under "Automatically Modified". Over MCP, `plan_workflow` names `apply_workflow` in `next` and `approval.canApplyWith` only for a plan without an error, and `apply_workflow` names `run_validation` only when the apply did not fail; a refused plan also drops its "apply this plan" and "validate the apply report" recommendations. The workflows that write into an existing module (`add-field`, `add-enum-field`, `add-mutation`, `add-slice`, `create-ui`, `rename-key`) now refuse a service module, or any folder without its `<module>.constant.ts`, at plan time and again at apply. The `workflow-module-not-database` error lists the target's database modules and points at `create-module`, and for a field at `create-scalar`. Before, `add-field` on a service module passed its plan and dry run and failed only at apply, and `create-ui` wrote a misnamed `_<service>.View.tsx` into it. The `workflow-module-missing` error now lists only database modules too, says a name that matches a service module (with or without its `_`) is one instead of asking "Did you mean \_<service>?", and points at another app or library only when it holds a database module of that name.
386
395
  - adcdf3b: `akan workflow plan` and the `plan_workflow` MCP tool refuse a module the target app or library lacks, for every workflow that writes into an existing module (`add-field`, `add-enum-field`, `add-mutation`, `add-slice`, `create-ui`, `rename-key`). The `workflow-module-missing` error names the module, lists the modules the target has, and points at the module whose name differs only in case or by a leading `_`, at a scalar of that name, or at the app or library that holds it; only when none exists does it point at `create-module`. Before, the plan passed and its dry run reported `passed`. `add-mutation` used to plan a missing module with a warning so that a `create-module` plan could precede it; it now refuses too, so plan it after applying `create-module`. Every workflow now refuses an unknown `app` at plan time, listing the workspace's apps and libraries. `apply` and `--dry-run` check the target again before anything else, so a plan written earlier for a module that is gone fails instead of passing. `plan_workflow` now inspects the MCP server's workspace rather than the process directory.
396
+ - Updated dependencies [bd198af]
387
397
  - Updated dependencies [bc792ab]
398
+ - Updated dependencies [31f7039]
388
399
  - Updated dependencies [61fbd7d]
389
400
  - Updated dependencies [b3f85fa]
390
401
  - Updated dependencies [a707354]
@@ -426,9 +437,12 @@
426
437
  - Updated dependencies [4561152]
427
438
  - Updated dependencies [73e833b]
428
439
  - Updated dependencies [b24fef3]
440
+ - Updated dependencies [465ccb9]
429
441
  - Updated dependencies [513e48d]
430
442
  - Updated dependencies [5cd1fc3]
431
443
  - Updated dependencies [f47cc82]
444
+ - Updated dependencies [7f0de74]
445
+ - Updated dependencies [c0b84d0]
432
446
  - Updated dependencies [1f93507]
433
447
  - Updated dependencies [5d3cb3e]
434
448
  - Updated dependencies [9a9e1fa]
@@ -0,0 +1,120 @@
1
+ import ts from "typescript";
2
+
3
+ export type ApiPrintMode = "export" | "declare";
4
+
5
+ export class ApiDeclarationPrinter {
6
+ static readonly #reportedTags = ["deprecated", "internal"];
7
+ static readonly #printer = ts.createPrinter({ removeComments: true, newLine: ts.NewLineKind.LineFeed });
8
+ static readonly #headPattern =
9
+ "^((?:abstract |const )?(?:class|function|interface|type|enum|namespace|module|const|let|var) )";
10
+
11
+ static isNamed(
12
+ node: ts.Node,
13
+ ): node is
14
+ | ts.ClassDeclaration
15
+ | ts.FunctionDeclaration
16
+ | ts.InterfaceDeclaration
17
+ | ts.TypeAliasDeclaration
18
+ | ts.EnumDeclaration
19
+ | ts.ModuleDeclaration {
20
+ return (
21
+ ts.isClassDeclaration(node) ||
22
+ ts.isFunctionDeclaration(node) ||
23
+ ts.isInterfaceDeclaration(node) ||
24
+ ts.isTypeAliasDeclaration(node) ||
25
+ ts.isEnumDeclaration(node) ||
26
+ (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name))
27
+ );
28
+ }
29
+
30
+ static prepared(statement: ts.Statement): ts.Statement {
31
+ const kept = (ts.canHaveModifiers(statement) ? (ts.getModifiers(statement) ?? []) : []).filter(
32
+ (modifier) =>
33
+ modifier.kind !== ts.SyntaxKind.ExportKeyword &&
34
+ modifier.kind !== ts.SyntaxKind.DefaultKeyword &&
35
+ modifier.kind !== ts.SyntaxKind.DeclareKeyword,
36
+ );
37
+ const stripped = ts.canHaveModifiers(statement)
38
+ ? (ts.factory.replaceModifiers(statement as ts.HasModifiers, kept) as ts.Statement)
39
+ : statement;
40
+ return ts.isClassDeclaration(stripped) ? ApiDeclarationPrinter.#withoutPrivateMembers(stripped) : stripped;
41
+ }
42
+
43
+ static print(
44
+ statement: ts.Statement,
45
+ sourceFile: ts.SourceFile,
46
+ { localName, name, mode }: { localName: string; name: string; mode: ApiPrintMode },
47
+ ) {
48
+ const tags = ApiDeclarationPrinter.#tagsOf(ts.getOriginalNode(statement));
49
+ const node = ApiDeclarationPrinter.#withSortedUnions(ApiDeclarationPrinter.prepared(statement), sourceFile);
50
+ const printed = ApiDeclarationPrinter.#printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
51
+ const tagLine = tags.length ? `/** ${tags.map((tag) => `@${tag}`).join(" ")} */\n` : "";
52
+ if (mode === "export" && name === "default") return `${tagLine}export default ${printed}`;
53
+ const renamed =
54
+ localName === name
55
+ ? printed
56
+ : printed.replace(
57
+ new RegExp(`${ApiDeclarationPrinter.#headPattern}${ApiDeclarationPrinter.#escape(localName)}(?![\\w$])`),
58
+ `$1${name}`,
59
+ );
60
+ const typeOnly = ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement);
61
+ const keyword = mode === "export" ? (typeOnly ? "export " : "export declare ") : typeOnly ? "" : "declare ";
62
+ return `${tagLine}${keyword}${renamed}`;
63
+ }
64
+
65
+ static printType(node: ts.TypeNode, sourceFile: ts.SourceFile) {
66
+ return ApiDeclarationPrinter.#printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
67
+ }
68
+
69
+ //? A union's members print in the order the checker first met them, which moves when unrelated files change.
70
+ static #withSortedUnions(statement: ts.Statement, sourceFile: ts.SourceFile) {
71
+ const result = ts.transform(statement, [
72
+ (context) => (root) => {
73
+ const visit = (node: ts.Node): ts.Node => {
74
+ const visited = ts.visitEachChild(node, visit, context);
75
+ if (!ts.isUnionTypeNode(visited)) return visited;
76
+ const sorted = visited.types
77
+ .map((type) => ({ type, key: ApiDeclarationPrinter.printType(type, sourceFile) }))
78
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
79
+ .map(({ type }) => type);
80
+ return context.factory.updateUnionTypeNode(visited, context.factory.createNodeArray(sorted));
81
+ };
82
+ return ts.visitNode(root, visit) as ts.Statement;
83
+ },
84
+ ]);
85
+ const [transformed = statement] = result.transformed;
86
+ result.dispose();
87
+ return transformed;
88
+ }
89
+
90
+ static #withoutPrivateMembers(declaration: ts.ClassDeclaration) {
91
+ const members = declaration.members.filter(
92
+ (member) =>
93
+ ts.isConstructorDeclaration(member) ||
94
+ !(
95
+ (member.name && ts.isPrivateIdentifier(member.name)) ||
96
+ (ts.canHaveModifiers(member) &&
97
+ ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword))
98
+ ),
99
+ );
100
+ if (members.length === declaration.members.length) return declaration;
101
+ return ts.factory.updateClassDeclaration(
102
+ declaration,
103
+ declaration.modifiers,
104
+ declaration.name,
105
+ declaration.typeParameters,
106
+ declaration.heritageClauses,
107
+ members,
108
+ );
109
+ }
110
+
111
+ static #tagsOf(node: ts.Node) {
112
+ const target = ts.isVariableStatement(node) ? (node.declarationList.declarations[0] ?? node) : node;
113
+ const names = ts.getJSDocTags(target).map((tag) => tag.tagName.text);
114
+ return ApiDeclarationPrinter.#reportedTags.filter((tag) => names.includes(tag));
115
+ }
116
+
117
+ static #escape(value: string) {
118
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
+ }
120
+ }