@akanjs/devkit 3.0.0-alpha.6 → 3.0.0-alpha.60
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 +15 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -1
- package/aiEditor.ts +1 -1
- package/akanConfig/akanConfig.test.ts +47 -10
- package/akanConfig/akanConfig.ts +23 -16
- package/akanContext.ts +53 -11
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +0 -1
- package/artifact/implicitRootLayout.ts +2 -2
- package/biome.base.json +336 -0
- package/biomeBase.ts +9 -0
- package/executors.test.ts +87 -5
- package/executors.ts +26 -6
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/buildRouteClient.test.ts +28 -2
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/cssCompiler.ts +122 -11
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/frontendBuild.test.ts +88 -1
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +20 -4
- package/frontendBuild/styleGuard.test.ts +15 -0
- package/frontendBuild/styleGuard.ts +17 -0
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/getCredentials.ts +1 -3
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +2 -2
- package/integration/devStabilityHarness.ts +1 -5
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-return-in-store-action.grit +35 -0
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/linter.ts +17 -12
- package/package.json +4 -4
- package/qualityScanner.test.ts +52 -0
- package/qualityScanner.ts +48 -37
- package/repoIdentity.ts +42 -0
- package/routeSourceValidator.test.ts +41 -0
- package/routeSourceValidator.ts +2 -44
- package/scanInfo.ts +21 -20
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
package/biome.base.json
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.8/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"formatter": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"indentStyle": "space",
|
|
11
|
+
"indentWidth": 2,
|
|
12
|
+
"lineWidth": 120
|
|
13
|
+
},
|
|
14
|
+
"linter": {
|
|
15
|
+
"enabled": true,
|
|
16
|
+
"rules": {
|
|
17
|
+
"preset": "recommended",
|
|
18
|
+
"suspicious": {
|
|
19
|
+
"noConsole": {
|
|
20
|
+
"level": "error",
|
|
21
|
+
"options": {
|
|
22
|
+
"allow": ["assert", "error", "info", "warn"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"noArrayIndexKey": "off",
|
|
26
|
+
"noShadowRestrictedNames": "off",
|
|
27
|
+
"noUnnecessaryConditions": {
|
|
28
|
+
"level": "warn"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"correctness": {
|
|
32
|
+
"noUnusedFunctionParameters": "off",
|
|
33
|
+
"noUnusedImports": {
|
|
34
|
+
"level": "error",
|
|
35
|
+
"fix": "safe"
|
|
36
|
+
},
|
|
37
|
+
"useParseIntRadix": "off",
|
|
38
|
+
"useExhaustiveDependencies": "off",
|
|
39
|
+
"useHookAtTopLevel": "off"
|
|
40
|
+
},
|
|
41
|
+
"nursery": {
|
|
42
|
+
"useSortedClasses": {
|
|
43
|
+
"level": "error",
|
|
44
|
+
"fix": "safe",
|
|
45
|
+
"options": {
|
|
46
|
+
"attributes": ["classList"],
|
|
47
|
+
"functions": ["cn"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"a11y": "off",
|
|
52
|
+
"complexity": {
|
|
53
|
+
"noStaticOnlyClass": "off"
|
|
54
|
+
},
|
|
55
|
+
"style": {
|
|
56
|
+
"useTemplate": {
|
|
57
|
+
"level": "warn",
|
|
58
|
+
"fix": "safe"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"domains": {
|
|
63
|
+
"project": "recommended",
|
|
64
|
+
"react": "recommended",
|
|
65
|
+
"test": "recommended",
|
|
66
|
+
"types": "all"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"javascript": {
|
|
70
|
+
"parser": {
|
|
71
|
+
"unsafeParameterDecoratorsEnabled": true
|
|
72
|
+
},
|
|
73
|
+
"formatter": {
|
|
74
|
+
"quoteStyle": "double"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"assist": {
|
|
78
|
+
"enabled": true,
|
|
79
|
+
"actions": {
|
|
80
|
+
"source": {
|
|
81
|
+
"organizeImports": "on"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"overrides": [
|
|
86
|
+
{
|
|
87
|
+
"includes": [
|
|
88
|
+
"**/env.client.local.ts",
|
|
89
|
+
"**/env.client.debug.ts",
|
|
90
|
+
"**/env.client.develop.ts",
|
|
91
|
+
"**/env.client.main.ts",
|
|
92
|
+
"**/env.client.testing.ts",
|
|
93
|
+
"**/env.server.local.ts",
|
|
94
|
+
"**/env.server.develop.ts",
|
|
95
|
+
"**/env.server.main.ts",
|
|
96
|
+
"**/env.server.testing.ts",
|
|
97
|
+
"**/env.server.type.ts",
|
|
98
|
+
"apps/*/lib/cnst.ts",
|
|
99
|
+
"apps/*/lib/dict.ts",
|
|
100
|
+
"apps/*/lib/db.ts",
|
|
101
|
+
"apps/*/lib/srv.ts",
|
|
102
|
+
"apps/*/lib/st.ts",
|
|
103
|
+
"apps/*/lib/sig.ts",
|
|
104
|
+
"apps/*/lib/useClient.ts",
|
|
105
|
+
"apps/*/lib/useServer.ts",
|
|
106
|
+
"apps/*/client.ts",
|
|
107
|
+
"apps/*/server.ts",
|
|
108
|
+
"apps/*/lib/*/index.tsx",
|
|
109
|
+
"libs/*/lib/cnst.ts",
|
|
110
|
+
"libs/*/lib/dict.ts",
|
|
111
|
+
"libs/*/lib/db.ts",
|
|
112
|
+
"libs/*/lib/srv.ts",
|
|
113
|
+
"libs/*/lib/st.ts",
|
|
114
|
+
"libs/*/lib/sig.ts",
|
|
115
|
+
"libs/*/lib/useClient.ts",
|
|
116
|
+
"libs/*/lib/useServer.ts",
|
|
117
|
+
"libs/*/client.ts",
|
|
118
|
+
"libs/*/server.ts",
|
|
119
|
+
"libs/*/index.ts",
|
|
120
|
+
"libs/*/lib/*/index.tsx",
|
|
121
|
+
"apps/*/lib/__lib/**",
|
|
122
|
+
"libs/*/lib/__lib/**"
|
|
123
|
+
],
|
|
124
|
+
"linter": { "enabled": false },
|
|
125
|
+
"formatter": { "enabled": false },
|
|
126
|
+
"assist": { "enabled": false }
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"includes": [
|
|
130
|
+
"apps/**/*.ts",
|
|
131
|
+
"apps/**/*.tsx",
|
|
132
|
+
"libs/**/*.ts",
|
|
133
|
+
"libs/**/*.tsx",
|
|
134
|
+
"!**/*.test.ts",
|
|
135
|
+
"!**/*.test.tsx",
|
|
136
|
+
"!**/*.spec.ts",
|
|
137
|
+
"!**/*.spec.tsx",
|
|
138
|
+
"!**/*.constant.ts",
|
|
139
|
+
"!**/common/**",
|
|
140
|
+
"!**/env/**"
|
|
141
|
+
],
|
|
142
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-throw-raw-error.grit"]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"includes": [
|
|
146
|
+
"{apps,libs}/**/page/**/*.ts",
|
|
147
|
+
"{apps,libs}/**/page/**/*.tsx",
|
|
148
|
+
"{apps,libs}/**/*.Unit.tsx",
|
|
149
|
+
"{apps,libs}/**/*.View.tsx"
|
|
150
|
+
],
|
|
151
|
+
"plugins": [
|
|
152
|
+
"./node_modules/@akanjs/devkit/lint/no-import-client-functions.grit",
|
|
153
|
+
"./node_modules/@akanjs/devkit/lint/no-use-client-in-server.grit"
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"includes": ["{apps,libs}/**/page/**/*.ts", "{apps,libs}/**/page/**/*.tsx"],
|
|
158
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/non-scalar-props-restricted.grit"]
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"includes": [
|
|
162
|
+
"{apps,libs}/**/*.constant.ts",
|
|
163
|
+
"{apps,libs}/**/*.document.ts",
|
|
164
|
+
"{apps,libs}/**/*.service.ts",
|
|
165
|
+
"{apps,libs}/**/*.store.ts"
|
|
166
|
+
],
|
|
167
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-js-private-class-method.grit"]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"includes": ["{apps,libs}/**/*.store.ts"],
|
|
171
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-return-in-store-action.grit"]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"includes": ["{apps,libs}/**/*.signal.ts"],
|
|
175
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-redeclare-predefined-endpoint.grit"]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"includes": ["{apps,libs}/**/*.Util.tsx", "{apps,libs}/**/*.Zone.tsx"],
|
|
179
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-model-type-in-util-zone.grit"]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"includes": ["{apps,libs}/**/ui/**/*.tsx", "!**/*.test.tsx", "!**/*.spec.tsx"],
|
|
183
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-async-component-in-ui.grit"]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"includes": ["{apps,libs}/**/*.tsx", "!**/*.test.tsx", "!**/*.spec.tsx"],
|
|
187
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-unpublished-form-setter.grit"]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"includes": [
|
|
191
|
+
"{apps,libs}/**/*.ts",
|
|
192
|
+
"{apps,libs}/**/*.tsx",
|
|
193
|
+
"!**/*.test.ts",
|
|
194
|
+
"!**/*.test.tsx",
|
|
195
|
+
"!**/*.spec.ts",
|
|
196
|
+
"!**/*.spec.tsx"
|
|
197
|
+
],
|
|
198
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-init-fetch-in-client.grit"]
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"includes": [
|
|
202
|
+
"**/ui/**/*.ts",
|
|
203
|
+
"**/ui/**/*.tsx",
|
|
204
|
+
"**/webkit/**/*.ts",
|
|
205
|
+
"**/webkit/**/*.tsx",
|
|
206
|
+
"**/common/**/*.ts",
|
|
207
|
+
"**/page/**/*.tsx",
|
|
208
|
+
"**/*.constant.ts",
|
|
209
|
+
"**/*.store.ts",
|
|
210
|
+
"**/*.Template.tsx",
|
|
211
|
+
"**/*.Unit.tsx",
|
|
212
|
+
"**/*.Util.tsx",
|
|
213
|
+
"**/*.View.tsx",
|
|
214
|
+
"**/*.Zone.tsx",
|
|
215
|
+
"!**/*.test.ts",
|
|
216
|
+
"!**/*.test.tsx",
|
|
217
|
+
"!**/*.spec.ts",
|
|
218
|
+
"!**/*.spec.tsx"
|
|
219
|
+
],
|
|
220
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-bang-comment-in-client.grit"]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"includes": [
|
|
224
|
+
"{apps,libs}/**/*.constant.ts",
|
|
225
|
+
"{apps,libs}/**/*.dictionary.ts",
|
|
226
|
+
"{apps,libs}/**/*.document.ts",
|
|
227
|
+
"{apps,libs}/**/*.service.ts",
|
|
228
|
+
"{apps,libs}/**/*.signal.ts",
|
|
229
|
+
"{apps,libs}/**/*.store.ts",
|
|
230
|
+
"{apps,libs}/**/*.Template.tsx",
|
|
231
|
+
"{apps,libs}/**/*.Unit.tsx",
|
|
232
|
+
"{apps,libs}/**/*.Util.tsx",
|
|
233
|
+
"{apps,libs}/**/*.View.tsx",
|
|
234
|
+
"{apps,libs}/**/*.Zone.tsx"
|
|
235
|
+
],
|
|
236
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-deep-internal-import.grit"]
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"includes": [
|
|
240
|
+
"{apps,libs}/**/page/**/*.ts",
|
|
241
|
+
"{apps,libs}/**/page/**/*.tsx",
|
|
242
|
+
"{apps,libs}/**/index.ts",
|
|
243
|
+
"{apps,libs}/**/index.tsx",
|
|
244
|
+
"{apps,libs}/**/cnst.ts",
|
|
245
|
+
"{apps,libs}/**/db.ts",
|
|
246
|
+
"{apps,libs}/**/dict.ts",
|
|
247
|
+
"{apps,libs}/**/option.ts",
|
|
248
|
+
"{apps,libs}/**/sig.ts",
|
|
249
|
+
"{apps,libs}/**/srv.ts",
|
|
250
|
+
"{apps,libs}/**/st.ts",
|
|
251
|
+
"{apps,libs}/**/*.constant.ts",
|
|
252
|
+
"{apps,libs}/**/*.dictionary.ts",
|
|
253
|
+
"{apps,libs}/**/*.document.ts",
|
|
254
|
+
"{apps,libs}/**/*.service.ts",
|
|
255
|
+
"{apps,libs}/**/*.signal.ts",
|
|
256
|
+
"{apps,libs}/**/*.signal.test.ts",
|
|
257
|
+
"{apps,libs}/**/*.service.test.ts",
|
|
258
|
+
"{apps,libs}/**/*.store.ts",
|
|
259
|
+
"{apps,libs}/**/*.Template.tsx",
|
|
260
|
+
"{apps,libs}/**/*.Unit.tsx",
|
|
261
|
+
"{apps,libs}/**/*.Util.tsx",
|
|
262
|
+
"{apps,libs}/**/*.View.tsx",
|
|
263
|
+
"{apps,libs}/**/*.Zone.tsx"
|
|
264
|
+
],
|
|
265
|
+
"plugins": [
|
|
266
|
+
"./node_modules/@akanjs/devkit/lint/no-import-external-library.grit",
|
|
267
|
+
"./node_modules/@akanjs/devkit/lint/no-deep-internal-import.grit"
|
|
268
|
+
]
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"includes": [
|
|
272
|
+
"apps/**/*.tsx",
|
|
273
|
+
"libs/**/*.tsx",
|
|
274
|
+
"apps/**/ui/**/*.ts",
|
|
275
|
+
"libs/**/ui/**/*.ts",
|
|
276
|
+
"apps/**/webkit/**/*.ts",
|
|
277
|
+
"libs/**/webkit/**/*.ts",
|
|
278
|
+
"apps/**/page/**/*.ts",
|
|
279
|
+
"libs/**/page/**/*.ts",
|
|
280
|
+
"apps/**/common/**/*.ts",
|
|
281
|
+
"libs/**/common/**/*.ts",
|
|
282
|
+
"apps/**/*.store.ts",
|
|
283
|
+
"libs/**/*.store.ts",
|
|
284
|
+
"apps/**/*.constant.ts",
|
|
285
|
+
"libs/**/*.constant.ts",
|
|
286
|
+
"!**/*.test.ts",
|
|
287
|
+
"!**/*.test.tsx",
|
|
288
|
+
"!**/*.spec.ts",
|
|
289
|
+
"!**/*.spec.tsx"
|
|
290
|
+
],
|
|
291
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-import-server-in-client.grit"]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"includes": [
|
|
295
|
+
"apps/**/*.document.ts",
|
|
296
|
+
"libs/**/*.document.ts",
|
|
297
|
+
"apps/**/*.dictionary.ts",
|
|
298
|
+
"libs/**/*.dictionary.ts",
|
|
299
|
+
"apps/**/*.service.ts",
|
|
300
|
+
"libs/**/*.service.ts",
|
|
301
|
+
"apps/**/*.signal.ts",
|
|
302
|
+
"libs/**/*.signal.ts",
|
|
303
|
+
"apps/**/srvkit/**/*.ts",
|
|
304
|
+
"libs/**/srvkit/**/*.ts",
|
|
305
|
+
"apps/**/common/**/*.ts",
|
|
306
|
+
"libs/**/common/**/*.ts",
|
|
307
|
+
"apps/**/*.constant.ts",
|
|
308
|
+
"libs/**/*.constant.ts",
|
|
309
|
+
"!**/*.test.ts",
|
|
310
|
+
"!**/*.test.tsx",
|
|
311
|
+
"!**/*.spec.ts",
|
|
312
|
+
"!**/*.spec.tsx"
|
|
313
|
+
],
|
|
314
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-import-client-in-server.grit"]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"includes": [
|
|
318
|
+
"apps/**/*.ts",
|
|
319
|
+
"apps/**/*.tsx",
|
|
320
|
+
"libs/**/*.ts",
|
|
321
|
+
"libs/**/*.tsx",
|
|
322
|
+
"!**/*.test.ts",
|
|
323
|
+
"!**/*.test.tsx",
|
|
324
|
+
"!**/*.spec.ts",
|
|
325
|
+
"!**/*.spec.tsx"
|
|
326
|
+
],
|
|
327
|
+
"plugins": [
|
|
328
|
+
"./node_modules/@akanjs/devkit/lint/no-raw-palette-class.grit",
|
|
329
|
+
"./node_modules/@akanjs/devkit/lint/no-arbitrary-color.grit",
|
|
330
|
+
"./node_modules/@akanjs/devkit/lint/no-daisyui-legacy-class.grit",
|
|
331
|
+
"./node_modules/@akanjs/devkit/lint/no-inline-color.grit",
|
|
332
|
+
"./node_modules/@akanjs/devkit/lint/no-interpolated-arbitrary-class.grit"
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
}
|
package/biomeBase.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** `extends` target for a workspace `biome.json`; Biome resolves it through node_modules. */
|
|
2
|
+
export const biomeBaseConfig = "@akanjs/devkit/biome.base.json";
|
|
3
|
+
|
|
4
|
+
// Biome moves rules between groups across minors — `noUnnecessaryConditions` sat in `nursery` at 2.4 and moved to
|
|
5
|
+
// `suspicious` at 2.5 — and the stale position is a hard "unknown key" error, not a warning. A workspace whose
|
|
6
|
+
// Biome disagrees with the shipped base config therefore fails to load it at all, which is why the version is
|
|
7
|
+
// pinned here instead of resolved to latest at create time. Bump this and `biome.base.json` in one commit, and run
|
|
8
|
+
// `biome migrate --write` in the workspace root and in `pkgs/@akanjs/devkit` so both configs move together.
|
|
9
|
+
export const biomeVersion = "2.5.8";
|
package/executors.test.ts
CHANGED
|
@@ -31,7 +31,6 @@ const rootPackageJson = (extra: Partial<PackageJson> = {}): PackageJson => ({
|
|
|
31
31
|
react: "19.0.0",
|
|
32
32
|
"react-dom": "19.0.0",
|
|
33
33
|
"react-server-dom-webpack": "19.0.0",
|
|
34
|
-
sharp: "1.0.0",
|
|
35
34
|
lodash: "4.0.0",
|
|
36
35
|
},
|
|
37
36
|
devDependencies: {
|
|
@@ -149,8 +148,10 @@ describe("Executor filesystem helpers", () => {
|
|
|
149
148
|
"AI Development Guide",
|
|
150
149
|
);
|
|
151
150
|
expect(await readFile(path.join(root, "workspace/docs/GENERATED.md"), "utf8")).toContain("Generated Akan Files");
|
|
151
|
+
// Rules and their plugin registrations live in the package's base config, so a framework release reaches an
|
|
152
|
+
// existing workspace on `bun update` — the workspace file only extends it and scopes its own files.
|
|
152
153
|
expect(await readFile(path.join(root, "workspace/biome.json"), "utf8")).toContain(
|
|
153
|
-
"
|
|
154
|
+
'"extends": ["@akanjs/devkit/biome.base.json"]',
|
|
154
155
|
);
|
|
155
156
|
});
|
|
156
157
|
|
|
@@ -620,6 +621,64 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
620
621
|
});
|
|
621
622
|
});
|
|
622
623
|
|
|
624
|
+
describe("root layout source validation during page key discovery", () => {
|
|
625
|
+
const makeRouteValidationApp = async (appName: string, config: string, files: Record<string, string>) => {
|
|
626
|
+
const root = await makeTempRoot();
|
|
627
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
628
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
629
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
630
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
631
|
+
await mkdir(path.join(root, "apps", appName), { recursive: true });
|
|
632
|
+
await writeFile(path.join(root, `apps/${appName}/akan.config.ts`), config);
|
|
633
|
+
for (const [file, source] of Object.entries(files)) {
|
|
634
|
+
const filePath = path.join(root, `apps/${appName}/page`, file);
|
|
635
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
636
|
+
await writeFile(filePath, source);
|
|
637
|
+
}
|
|
638
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
639
|
+
return AppExecutor.from(workspace, appName);
|
|
640
|
+
};
|
|
641
|
+
const wsConnectLayout = [
|
|
642
|
+
"export const wsConnect = false;",
|
|
643
|
+
"export default function Layout({ children }) { return children; }",
|
|
644
|
+
"",
|
|
645
|
+
].join("\n");
|
|
646
|
+
|
|
647
|
+
test("accepts wsConnect on a configured base-path root layout", async () => {
|
|
648
|
+
process.env.AKAN_PUBLIC_BASE_PATHS = "web,admin";
|
|
649
|
+
const app = await makeRouteValidationApp(
|
|
650
|
+
"base-path-root-ws",
|
|
651
|
+
[
|
|
652
|
+
"export default {",
|
|
653
|
+
' routes: [{ basePath: "web", domains: {} }, { basePath: "admin", domains: {} }],',
|
|
654
|
+
"};",
|
|
655
|
+
"",
|
|
656
|
+
].join("\n"),
|
|
657
|
+
{ "admin/_layout.tsx": wsConnectLayout },
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
await expect(app.getPageKeys({ refresh: true })).resolves.toEqual(["./admin/_layout.tsx"]);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("rejects wsConnect on a nested layout", async () => {
|
|
664
|
+
const app = await makeRouteValidationApp(
|
|
665
|
+
"nested-layout-ws",
|
|
666
|
+
'export default { routes: [{ basePath: "admin", domains: {} }] };\n',
|
|
667
|
+
{ "admin/users/_layout.tsx": wsConnectLayout },
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(/unsupported export "wsConnect"/);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("accepts wsConnect on a grouped root layout", async () => {
|
|
674
|
+
const app = await makeRouteValidationApp("grouped-root-ws", "export default {};\n", {
|
|
675
|
+
"(docs)/_layout.tsx": wsConnectLayout,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await expect(app.getPageKeys({ refresh: true })).resolves.toEqual(["./(docs)/_layout.tsx"]);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
623
682
|
test("accepts metadata route exports during page key discovery", async () => {
|
|
624
683
|
const root = await makeTempRoot();
|
|
625
684
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
@@ -758,6 +817,29 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
758
817
|
expect(offsetMinimalStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8286");
|
|
759
818
|
expect(offsetMinimalStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8286");
|
|
760
819
|
});
|
|
820
|
+
|
|
821
|
+
test("pins the start command to development so an ambient NODE_ENV cannot pick the production router", async () => {
|
|
822
|
+
const root = await makeTempRoot();
|
|
823
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
824
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
825
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
826
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
827
|
+
await mkdir(path.join(root, "apps/ambient"), { recursive: true });
|
|
828
|
+
await writeFile(path.join(root, "apps/ambient/akan.config.ts"), "export default {};\n");
|
|
829
|
+
|
|
830
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
831
|
+
process.env.NODE_ENV = "production";
|
|
832
|
+
try {
|
|
833
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
834
|
+
const started = await AppExecutor.from(workspace, "ambient").prepareCommand("start");
|
|
835
|
+
expect(started.env.NODE_ENV).toBe("development");
|
|
836
|
+
// The builder runs in this process and bakes NODE_ENV into the dev bundles it emits.
|
|
837
|
+
expect(process.env.NODE_ENV).toBe("development");
|
|
838
|
+
} finally {
|
|
839
|
+
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
840
|
+
else process.env.NODE_ENV = originalNodeEnv;
|
|
841
|
+
}
|
|
842
|
+
});
|
|
761
843
|
});
|
|
762
844
|
|
|
763
845
|
describe("PkgExecutor package generation", () => {
|
|
@@ -771,7 +853,7 @@ describe("PkgExecutor package generation", () => {
|
|
|
771
853
|
exports: { "./extra": { import: "./extra.ts" } },
|
|
772
854
|
peerDependencies: { react: "19.0.0" },
|
|
773
855
|
peerDependenciesMeta: { react: { optional: true } },
|
|
774
|
-
optionalDependencies: {
|
|
856
|
+
optionalDependencies: { "@sample/native": "1.0.0" },
|
|
775
857
|
});
|
|
776
858
|
|
|
777
859
|
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
@@ -781,12 +863,12 @@ describe("PkgExecutor package generation", () => {
|
|
|
781
863
|
expect(distPackageJson).toMatchObject({
|
|
782
864
|
name: "@sample/tool",
|
|
783
865
|
type: "module",
|
|
784
|
-
engines: { bun: ">=1.
|
|
866
|
+
engines: { bun: ">=1.4.0" },
|
|
785
867
|
dependencies: { lodash: "4.0.0" },
|
|
786
868
|
devDependencies: { typescript: "6.0.0" },
|
|
787
869
|
peerDependencies: { react: "19.0.0" },
|
|
788
870
|
peerDependenciesMeta: { react: { optional: true } },
|
|
789
|
-
optionalDependencies: {
|
|
871
|
+
optionalDependencies: { "@sample/native": "1.0.0" },
|
|
790
872
|
});
|
|
791
873
|
expect(distPackageJson.exports?.["."]).toEqual({
|
|
792
874
|
import: "./index.ts",
|
package/executors.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { pathToFileURL } from "node:url";
|
|
|
23
23
|
import type { AkanPlugin, AkanSyncContext, PluginRuntimeContext } from "akanjs";
|
|
24
24
|
import {
|
|
25
25
|
capitalize,
|
|
26
|
+
getPageSourceFileViolation,
|
|
26
27
|
isRouteSourceFile,
|
|
27
28
|
Logger,
|
|
28
29
|
parseRouteModuleKey,
|
|
@@ -39,9 +40,11 @@ import {
|
|
|
39
40
|
upsertAgentBlock,
|
|
40
41
|
} from "./agentsIndex";
|
|
41
42
|
import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
|
|
43
|
+
import { getRootBoundarySegments, isRootBoundarySegments } from "./artifact/implicitRootLayout";
|
|
42
44
|
import { FileSys } from "./fileSys";
|
|
43
45
|
import { getDirname } from "./getDirname";
|
|
44
46
|
import { Linter } from "./linter";
|
|
47
|
+
import { resolveRepoName } from "./repoIdentity";
|
|
45
48
|
import { AppInfo, LibInfo, PkgInfo, WorkspaceInfo } from "./scanInfo";
|
|
46
49
|
import { Spinner } from "./spinner";
|
|
47
50
|
// Type-only: the implementation is loaded on demand in `getTypeChecker` to keep `typescript` out of
|
|
@@ -232,7 +235,7 @@ export class Executor {
|
|
|
232
235
|
);
|
|
233
236
|
});
|
|
234
237
|
proc.on("exit", (code, signal) => {
|
|
235
|
-
if (
|
|
238
|
+
if (code || signal)
|
|
236
239
|
reject(
|
|
237
240
|
new CommandExecutionError({
|
|
238
241
|
command,
|
|
@@ -341,7 +344,7 @@ export class Executor {
|
|
|
341
344
|
);
|
|
342
345
|
});
|
|
343
346
|
proc.on("exit", (code, signal) => {
|
|
344
|
-
if (
|
|
347
|
+
if (code || signal)
|
|
345
348
|
reject(
|
|
346
349
|
new CommandExecutionError({
|
|
347
350
|
command: modulePath,
|
|
@@ -788,7 +791,7 @@ export class WorkspaceExecutor extends Executor {
|
|
|
788
791
|
static #execs = new Map<string, WorkspaceExecutor>();
|
|
789
792
|
static fromRoot({
|
|
790
793
|
workspaceRoot = process.cwd(),
|
|
791
|
-
repoName =
|
|
794
|
+
repoName = resolveRepoName(workspaceRoot),
|
|
792
795
|
}: {
|
|
793
796
|
workspaceRoot?: string;
|
|
794
797
|
repoName?: string;
|
|
@@ -1005,6 +1008,16 @@ export class SysExecutor extends Executor {
|
|
|
1005
1008
|
const path = this.type === "app" ? `apps/${this.name}/lib` : `libs/${this.name}/lib`;
|
|
1006
1009
|
return await this.workspace.getDirInModule(path, this.name);
|
|
1007
1010
|
}
|
|
1011
|
+
async getPageConventionViolations(): Promise<{ relativePath: string; reason: string }[]> {
|
|
1012
|
+
if (!(await this.exists("page"))) return [];
|
|
1013
|
+
const violations: { relativePath: string; reason: string }[] = [];
|
|
1014
|
+
for (const entry of await this.getAllFiles("**/*", { cwd: this.getPath("page") })) {
|
|
1015
|
+
const relativePath = entry.split(path.sep).join("/");
|
|
1016
|
+
const reason = getPageSourceFileViolation(relativePath);
|
|
1017
|
+
if (reason) violations.push({ relativePath: `page/${relativePath}`, reason });
|
|
1018
|
+
}
|
|
1019
|
+
return violations;
|
|
1020
|
+
}
|
|
1008
1021
|
|
|
1009
1022
|
#scanInfo: AppInfo | LibInfo | null = null;
|
|
1010
1023
|
hasScanInfo() {
|
|
@@ -1363,6 +1376,12 @@ export class AppExecutor extends SysExecutor {
|
|
|
1363
1376
|
this.cp("public", `${this.dist.cwdPath}/public`, { dereference: true }),
|
|
1364
1377
|
]);
|
|
1365
1378
|
} else await this.removeDir(".akan");
|
|
1379
|
+
//? `akan start` is the dev server, and it must say so rather than inherit an answer. Bun auto-loads the
|
|
1380
|
+
//? workspace `.env`, so NODE_ENV=production can reach this process without ever being exported in a shell —
|
|
1381
|
+
//? and a dev server that believes it is production serves from the production route cache, whose every
|
|
1382
|
+
//? route throws because a dev artifact carries no routes manifest. Written into `process.env` and not only
|
|
1383
|
+
//? into the child env because the builder runs here and bakes NODE_ENV into the bundles it emits.
|
|
1384
|
+
if (type === "start") process.env.NODE_ENV = "development";
|
|
1366
1385
|
const devPort = type === "start" ? (await this.getDevPort()).toString() : undefined;
|
|
1367
1386
|
const env = this.getCommandEnv({
|
|
1368
1387
|
AKAN_COMMAND_TYPE: type,
|
|
@@ -1451,7 +1470,8 @@ export class AppExecutor extends SysExecutor {
|
|
|
1451
1470
|
);
|
|
1452
1471
|
}
|
|
1453
1472
|
if (!owner) owners.set(routeId, { absPath, fromLib });
|
|
1454
|
-
const
|
|
1473
|
+
const layoutSegments = getRootBoundarySegments(key);
|
|
1474
|
+
const isRootLayout = layoutSegments !== null && isRootBoundarySegments(layoutSegments, akanConfig.basePaths);
|
|
1455
1475
|
const routeSource = await Bun.file(absPath).text();
|
|
1456
1476
|
const validator = await AppExecutor.#getRouteSourceValidator();
|
|
1457
1477
|
if (parsed.kind === "overrides") validator.validateOverridesSourceExports(routeSource, absPath);
|
|
@@ -1700,7 +1720,7 @@ export class LibExecutor extends SysExecutor {
|
|
|
1700
1720
|
#akanConfig: AkanLibConfig | null = null;
|
|
1701
1721
|
override async getConfig({ refresh }: { refresh?: boolean } = {}) {
|
|
1702
1722
|
if (this.#akanConfig && !refresh) return this.#akanConfig;
|
|
1703
|
-
this.#akanConfig = await AkanLibConfig.from(this);
|
|
1723
|
+
this.#akanConfig = await AkanLibConfig.from(this, { bustImportCache: refresh });
|
|
1704
1724
|
return this.#akanConfig;
|
|
1705
1725
|
}
|
|
1706
1726
|
}
|
|
@@ -1806,7 +1826,7 @@ export class PkgExecutor extends Executor {
|
|
|
1806
1826
|
default: "./index.ts",
|
|
1807
1827
|
},
|
|
1808
1828
|
},
|
|
1809
|
-
engines: { bun: ">=1.
|
|
1829
|
+
engines: { bun: ">=1.4.0" },
|
|
1810
1830
|
...dependencyMaps,
|
|
1811
1831
|
};
|
|
1812
1832
|
await Promise.all([this.dist.writeJson("package.json", distPkgJson), this.writeJson("package.json", distPkgJson)]);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { FormSetterScanner } from "./formSetterScanner";
|
|
4
|
+
import type { SourceFileInfo } from "./qualityScanner";
|
|
5
|
+
|
|
6
|
+
const fileOf = (file: string, content: string): SourceFileInfo => ({
|
|
7
|
+
file,
|
|
8
|
+
absolutePath: `/tmp/${file}`,
|
|
9
|
+
content,
|
|
10
|
+
sourceFile: ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const scan = (content: string) =>
|
|
14
|
+
new FormSetterScanner().scan([fileOf("apps/demo/lib/task/Task.Template.tsx", content)]);
|
|
15
|
+
|
|
16
|
+
describe("FormSetterScanner", () => {
|
|
17
|
+
test("a setter passed by reference publishes, so nothing is reported", () => {
|
|
18
|
+
expect(
|
|
19
|
+
scan(`export const General = () => (
|
|
20
|
+
<Field.Text value={taskForm.title} onChange={st.do.setTitleOnTask} />
|
|
21
|
+
);
|
|
22
|
+
`),
|
|
23
|
+
).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("every wrapper shape is counted once per field, whatever the wrapper was for", () => {
|
|
27
|
+
const warnings = scan(`export const General = () => (
|
|
28
|
+
<>
|
|
29
|
+
<Field.Text onChange={st.do.setTitleOnTask} />
|
|
30
|
+
<Field.ToggleSelect onChange={(type) => { st.do.setTypeOnTask(type); }} />
|
|
31
|
+
<Field.Phone onChange={(phone) => st.do.setPhoneOnTask(formatPhone(phone))} />
|
|
32
|
+
<Field.Parent
|
|
33
|
+
onChange={(project) => {
|
|
34
|
+
st.do.setProjectOnTask(project);
|
|
35
|
+
if (project) st.do.addMembersOnTask(project.members ?? []);
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
expect(warnings).toHaveLength(1);
|
|
43
|
+
expect(warnings[0]?.rule).toBe("akan.agent.unpublished-form-setter");
|
|
44
|
+
expect(warnings[0]?.scope).toBe("agent");
|
|
45
|
+
expect(warnings[0]?.message).toContain("setPhoneOnTask, setProjectOnTask, setTypeOnTask");
|
|
46
|
+
expect(warnings[0]?.locations).toHaveLength(3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a nested path write is unannotatable by design and is not counted", () => {
|
|
50
|
+
expect(
|
|
51
|
+
scan(`export const Rows = ({ idx }: { idx: number }) => (
|
|
52
|
+
<Field.Text onChange={(title) => st.do.writeOnTask(\`payments.\${idx}.title\`, title)} />
|
|
53
|
+
);
|
|
54
|
+
`),
|
|
55
|
+
).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a zero-parameter handler is a button setting a constant, not a form control", () => {
|
|
59
|
+
expect(
|
|
60
|
+
scan(`export const Actions = () => (
|
|
61
|
+
<Button onClick={() => st.do.setStatusOnTask("done")}>Done</Button>
|
|
62
|
+
);
|
|
63
|
+
`),
|
|
64
|
+
).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a non-setter action behind a wrapper is somebody else's rule", () => {
|
|
68
|
+
expect(
|
|
69
|
+
scan(`export const Filters = () => (
|
|
70
|
+
<Select onChange={(ids) => st.do.setQueryArgsOfTaskInSelf(ids)} />
|
|
71
|
+
);
|
|
72
|
+
`),
|
|
73
|
+
).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("only .tsx is scanned", () => {
|
|
77
|
+
const content = `export const set = (v: string) => <X onChange={(t) => st.do.setTitleOnTask(t)} />;\n`;
|
|
78
|
+
expect(new FormSetterScanner().scan([fileOf("apps/demo/lib/task/task.store.ts", content)])).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|