akanjs 2.0.0-rc.6 → 2.0.0

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.
Files changed (86) hide show
  1. package/base/primitiveRegistry.ts +28 -2
  2. package/cli/application/application.command.ts +11 -3
  3. package/cli/application/application.runner.ts +17 -1
  4. package/cli/guidelines/databaseModule/databaseModule.instruction.md +1 -1
  5. package/cli/guidelines/modelConstant/modelConstant.instruction.md +5 -5
  6. package/cli/guidelines/modelDocument/modelDocument.instruction.md +34 -61
  7. package/cli/guidelines/modelService/modelService.instruction.md +1 -1
  8. package/cli/index.js +9321 -19222
  9. package/cli/library/library.runner.ts +14 -13
  10. package/cli/package/package.runner.ts +31 -6
  11. package/cli/package/package.script.ts +2 -2
  12. package/cli/templates/app/page/_index.tsx +200 -79
  13. package/cli/templates/app/page/_layout.tsx +0 -1
  14. package/cli/templates/app/public/favicon.ico.template +0 -0
  15. package/cli/templates/app/public/logo.png.template +0 -0
  16. package/cli/templates/module/__Model__.Zone.tsx +1 -1
  17. package/cli/templates/module/__model__.document.ts +1 -1
  18. package/cli/templates/workspaceRoot/.gitignore.template +1 -11
  19. package/cli/templates/workspaceRoot/biome.json.template +16 -0
  20. package/cli/workspace/workspace.command.ts +7 -9
  21. package/cli/workspace/workspace.runner.ts +3 -13
  22. package/cli/workspace/workspace.script.ts +24 -9
  23. package/client/csrTypes.ts +1 -1
  24. package/constant/fieldInfo.ts +1 -1
  25. package/constant/serialize.ts +7 -1
  26. package/devkit/capacitor.base.config.ts +1 -1
  27. package/devkit/capacitorApp.ts +5 -1
  28. package/devkit/commandDecorators/argMeta.ts +28 -14
  29. package/devkit/commandDecorators/command.ts +41 -15
  30. package/devkit/commandDecorators/commandBuilder.ts +78 -42
  31. package/devkit/commandDecorators/helpFormatter.ts +7 -4
  32. package/devkit/dependencyScanner.ts +121 -15
  33. package/devkit/executors.ts +35 -23
  34. package/devkit/frontendBuild/cssCompiler.ts +9 -3
  35. package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +2 -1
  36. package/devkit/lint/no-deep-internal-import.grit +25 -0
  37. package/devkit/lint/no-import-external-library.grit +1 -0
  38. package/devkit/mobile/mobileTarget.ts +48 -8
  39. package/devkit/scanInfo.ts +4 -1
  40. package/devkit/src/capacitorApp.ts +277 -0
  41. package/devkit/transforms/barrelImportsPlugin.ts +6 -0
  42. package/fetch/client/fetchClient.ts +1 -0
  43. package/fetch/client/httpClient.ts +13 -1
  44. package/package.json +37 -31
  45. package/server/akanServer.ts +21 -7
  46. package/server/hmr/clientScript.ts +8 -5
  47. package/server/resolver/resolver.contract.fixture.ts +1 -1
  48. package/test/index.ts +14 -0
  49. package/test/signalTest.preload.ts +10 -0
  50. package/test/signalTestRuntime.ts +126 -0
  51. package/test/testServer.ts +130 -25
  52. package/ui/Constant/Doc.tsx +696 -0
  53. package/ui/Constant/Mermaid.tsx +149 -0
  54. package/ui/Constant/index.ts +6 -0
  55. package/ui/Constant/schemaDoc.ts +324 -0
  56. package/ui/Field.tsx +0 -1
  57. package/ui/Portal.tsx +2 -0
  58. package/ui/System/CSR.tsx +6 -5
  59. package/ui/System/SSR.tsx +1 -1
  60. package/ui/System/SelectLanguage.tsx +1 -1
  61. package/ui/index.ts +1 -0
  62. package/ui/styles.css +0 -2
  63. package/webkit/bootCsr.tsx +8 -5
  64. package/base/test-globals.d.ts +0 -4
  65. package/cli/templates/app/common/commonLogic.ts +0 -12
  66. package/cli/templates/app/common/index.ts +0 -10
  67. package/cli/templates/app/public/favicon.ico +0 -0
  68. package/cli/templates/app/public/icons/icon-128x128.png +0 -0
  69. package/cli/templates/app/public/icons/icon-144x144.png +0 -0
  70. package/cli/templates/app/public/icons/icon-152x152.png +0 -0
  71. package/cli/templates/app/public/icons/icon-192x192.png +0 -0
  72. package/cli/templates/app/public/icons/icon-256x256.png +0 -0
  73. package/cli/templates/app/public/icons/icon-384x384.png +0 -0
  74. package/cli/templates/app/public/icons/icon-48x48.png +0 -0
  75. package/cli/templates/app/public/icons/icon-512x512.png +0 -0
  76. package/cli/templates/app/public/icons/icon-72x72.png +0 -0
  77. package/cli/templates/app/public/icons/icon-96x96.png +0 -0
  78. package/cli/templates/app/public/logo.svg +0 -70
  79. package/cli/templates/app/public/manifest.json.template +0 -67
  80. package/cli/templates/app/srvkit/backendLogic.ts +0 -12
  81. package/cli/templates/app/srvkit/index.ts +0 -10
  82. package/cli/templates/app/ui/UiComponent.ts +0 -16
  83. package/cli/templates/app/ui/index.ts +0 -10
  84. package/cli/templates/app/webkit/frontendLogic.ts +0 -12
  85. package/cli/templates/app/webkit/index.ts +0 -10
  86. package/cli/templates/module/index.tsx +0 -44
@@ -1,9 +1,15 @@
1
+ import { builtinModules } from "node:module";
1
2
  import * as path from "node:path";
2
3
  import ignore from "ignore";
4
+ import ts from "typescript";
3
5
  import type { App, Lib, Pkg } from "./commandDecorators";
4
6
  import { FileSys } from "./fileSys";
5
7
  import type { PackageJson, TsConfigJson } from "./types";
6
8
 
9
+ const testFileRegex = /\.(?:test|spec)\.[cm]?[tj]sx?$/;
10
+ const builtinModuleSet = new Set([...builtinModules, ...builtinModules.map((mod) => `node:${mod}`)]);
11
+ const stripShebang = (source: string) => source.replace(/^#!.*(?:\r?\n|$)/, "");
12
+
7
13
  export class TypeScriptDependencyScanner {
8
14
  #fileDependencies = new Map<string, string[]>();
9
15
  #fileRuntimeDependencies = new Map<string, string[]>();
@@ -57,6 +63,47 @@ export class TypeScriptDependencyScanner {
57
63
  };
58
64
  }
59
65
 
66
+ async getPackageBuildDependencies(
67
+ projectName: string,
68
+ ): Promise<{ npmDeps: string[]; npmDevDeps: string[]; missingDeps: string[] }> {
69
+ const runtimeDeps = new Set<string>();
70
+ const devDeps = new Set<string>();
71
+ const sourceFiles = await this.#findTypeScriptFiles(this.directory, {
72
+ excludeBuildFiles: true,
73
+ excludeTestFiles: true,
74
+ });
75
+ const cssFiles = await this.#findCssFiles(this.directory);
76
+
77
+ for (const filePath of sourceFiles) {
78
+ const fileContent = await FileSys.readText(filePath);
79
+ const { imports, typeImports } = this.#extractImports(fileContent, filePath);
80
+ this.#addNormalizedImports(runtimeDeps, imports, projectName);
81
+ this.#addNormalizedImports(devDeps, typeImports, projectName);
82
+ }
83
+
84
+ for (const filePath of cssFiles) {
85
+ const fileContent = await FileSys.readText(filePath);
86
+ this.#addNormalizedImports(runtimeDeps, this.#extractCssPluginImports(fileContent), projectName);
87
+ }
88
+
89
+ const buildFilePath = path.join(this.directory, "build.ts");
90
+ if (await FileSys.fileExists(buildFilePath)) {
91
+ const fileContent = await FileSys.readText(buildFilePath);
92
+ const { imports, typeImports } = this.#extractImports(fileContent, buildFilePath);
93
+ this.#addNormalizedImports(devDeps, [...imports, ...typeImports], projectName);
94
+ }
95
+
96
+ for (const dep of runtimeDeps) devDeps.delete(dep);
97
+
98
+ const rootDeps = { ...this.rootPackageJson.dependencies, ...this.rootPackageJson.devDependencies };
99
+ const missingDeps = [...runtimeDeps, ...devDeps].filter((dep) => !rootDeps[dep]).sort();
100
+ return {
101
+ npmDeps: [...runtimeDeps].sort(),
102
+ npmDevDeps: [...devDeps].sort(),
103
+ missingDeps,
104
+ };
105
+ }
106
+
60
107
  async getImportSets<DepSets extends Set<string>[]>(depSets: DepSets): Promise<DepSets> {
61
108
  const fileDependencies = await this.getDependencies();
62
109
  return this.#getImportSetsFromDependencies(depSets, fileDependencies);
@@ -101,11 +148,35 @@ export class TypeScriptDependencyScanner {
101
148
  return this.#fileDependencies;
102
149
  }
103
150
 
104
- async #findTypeScriptFiles(directory: string): Promise<string[]> {
151
+ async #findTypeScriptFiles(
152
+ directory: string,
153
+ {
154
+ excludeBuildFiles = false,
155
+ excludeTestFiles = false,
156
+ }: { excludeBuildFiles?: boolean; excludeTestFiles?: boolean } = {},
157
+ ): Promise<string[]> {
105
158
  const files: string[] = [];
106
159
  const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
107
160
 
108
161
  const glob = new Bun.Glob("**/*.{ts,tsx}");
162
+ for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
163
+ if (skipDirs.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`))) continue;
164
+ if (excludeBuildFiles && filePath === "build.ts") continue;
165
+ if (excludeTestFiles && testFileRegex.test(filePath)) continue;
166
+
167
+ const fullPath = path.join(directory, filePath);
168
+ const relativePath = path.relative(this.workspaceRoot, fullPath);
169
+ if (this.ig.ignores(relativePath)) continue;
170
+
171
+ files.push(fullPath);
172
+ }
173
+ return files;
174
+ }
175
+
176
+ async #findCssFiles(directory: string): Promise<string[]> {
177
+ const files: string[] = [];
178
+ const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
179
+ const glob = new Bun.Glob("**/*.css");
109
180
  for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
110
181
  if (skipDirs.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`))) continue;
111
182
 
@@ -160,27 +231,29 @@ export class TypeScriptDependencyScanner {
160
231
 
161
232
  #extractImports(source: string, filePath: string) {
162
233
  const transpiler = filePath.endsWith(".tsx") ? this.#tsxTranspiler : this.#tsTranspiler;
234
+ const scanSource = stripShebang(source);
163
235
  const imports = new Set(
164
236
  transpiler
165
- .scanImports(source)
237
+ .scanImports(scanSource)
166
238
  .map((imp) => imp.path)
167
239
  .filter(Boolean),
168
240
  );
169
241
  const typeImports = new Set<string>();
170
- const typeOnlyImportRegex = /\bimport\s+type\s+[\s\S]*?\s+from\s*["']([^"']+)["']/g;
171
- for (const match of source.matchAll(typeOnlyImportRegex)) {
172
- const importPath = match[1];
173
- if (importPath && !imports.has(importPath)) typeImports.add(importPath);
174
- }
175
242
 
176
- const namedTypeOnlyImportRegex = /\bimport\s+{([^}]*)}\s+from\s*["']([^"']+)["']/g;
177
- for (const match of source.matchAll(namedTypeOnlyImportRegex)) {
178
- const namedImports = match[1]
179
- ?.split(",")
180
- .map((specifier) => specifier.trim())
181
- .filter(Boolean);
182
- const importPath = match[2];
183
- if (importPath && namedImports?.every((specifier) => specifier.startsWith("type ")) && !imports.has(importPath)) {
243
+ const sourceFile = ts.createSourceFile(filePath, scanSource, ts.ScriptTarget.Latest, true);
244
+ for (const statement of sourceFile.statements) {
245
+ if (!ts.isImportDeclaration(statement)) continue;
246
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
247
+
248
+ const importPath = statement.moduleSpecifier.text;
249
+ const namedBindings = statement.importClause?.namedBindings;
250
+ const isNamedTypeOnlyImport =
251
+ namedBindings &&
252
+ ts.isNamedImports(namedBindings) &&
253
+ namedBindings.elements.length > 0 &&
254
+ namedBindings.elements.every((element) => element.isTypeOnly);
255
+
256
+ if ((statement.importClause?.isTypeOnly || isNamedTypeOnlyImport) && !imports.has(importPath)) {
184
257
  typeImports.add(importPath);
185
258
  }
186
259
  }
@@ -188,6 +261,39 @@ export class TypeScriptDependencyScanner {
188
261
  return { imports: [...imports], typeImports: [...typeImports] };
189
262
  }
190
263
 
264
+ #extractCssPluginImports(source: string) {
265
+ const imports = new Set<string>();
266
+ const pluginRegex = /@plugin\s+(?:url\()?["']([^"')]+)["']\)?/g;
267
+ for (const match of source.matchAll(pluginRegex)) {
268
+ const importPath = match[1];
269
+ if (importPath) imports.add(importPath);
270
+ }
271
+ return [...imports];
272
+ }
273
+
274
+ #addNormalizedImports(deps: Set<string>, imports: string[], projectName: string) {
275
+ for (const importPath of imports) {
276
+ const dep = this.#normalizePackageImport(importPath, projectName);
277
+ if (dep) deps.add(dep);
278
+ }
279
+ }
280
+
281
+ #normalizePackageImport(importPath: string, projectName: string): string | null {
282
+ if (
283
+ importPath.startsWith(".") ||
284
+ importPath.startsWith("/") ||
285
+ importPath.startsWith("#") ||
286
+ importPath.startsWith("bun:") ||
287
+ builtinModuleSet.has(importPath)
288
+ )
289
+ return null;
290
+
291
+ const parts = importPath.split("/");
292
+ const packageName = importPath.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
293
+ if (!packageName || packageName === projectName || importPath.startsWith(`${projectName}/`)) return null;
294
+ return packageName;
295
+ }
296
+
191
297
  generateDependencyGraph(): string {
192
298
  let graph = "Dependency Graph:\n\n";
193
299
 
@@ -18,11 +18,10 @@ import {
18
18
  validatePageSourceFile,
19
19
  validateSubRoutePageKey,
20
20
  } from "akanjs/common";
21
- import chalk from "chalk";
22
- import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
23
-
24
21
  import { $ } from "bun";
22
+ import chalk from "chalk";
25
23
  import ts from "typescript";
24
+ import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
26
25
  import { FileSys } from "./fileSys";
27
26
  import { getDirname } from "./getDirname";
28
27
  import { Linter } from "./linter";
@@ -58,10 +57,7 @@ const parseEnvFile = (envPath: string): Record<string, string> => {
58
57
  if (separatorIndex <= 0) continue;
59
58
  const key = normalized.slice(0, separatorIndex).trim();
60
59
  let value = normalized.slice(separatorIndex + 1).trim();
61
- if (
62
- (value.startsWith('"') && value.endsWith('"')) ||
63
- (value.startsWith("'") && value.endsWith("'"))
64
- ) {
60
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
65
61
  value = value.slice(1, -1);
66
62
  }
67
63
  env[key] = value;
@@ -1244,29 +1240,45 @@ export class PkgExecutor extends Executor {
1244
1240
  this.#scanInfo = scanInfo;
1245
1241
  return scanInfo;
1246
1242
  }
1243
+ #toDependencyMap(
1244
+ rootPackageJson: PackageJson,
1245
+ dependencies: string[] = [],
1246
+ devDependencies: string[] = [],
1247
+ ): Pick<PackageJson, "dependencies" | "devDependencies"> {
1248
+ const rootDeps = { ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies };
1249
+ const dependencyNames = [...new Set(dependencies)].sort();
1250
+ const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort();
1251
+ const missingDeps = [...dependencyNames, ...devDependencyNames].filter((dep) => !rootDeps[dep]).sort();
1252
+ if (missingDeps.length > 0)
1253
+ throw new Error(`Missing dependency versions in root package.json: ${missingDeps.join(", ")}`);
1254
+
1255
+ return {
1256
+ dependencies: Object.fromEntries(dependencyNames.map((dep) => [dep, rootDeps[dep]])),
1257
+ devDependencies: Object.fromEntries(devDependencyNames.map((dep) => [dep, rootDeps[dep]])),
1258
+ };
1259
+ }
1260
+ async updatePackageJsonDependencies(
1261
+ dependencies: string[] = [],
1262
+ devDependencies: string[] = [],
1263
+ ): Promise<PackageJson> {
1264
+ const [rootPackageJson, pkgJson] = await Promise.all([this.workspace.getPackageJson(), this.getPackageJson()]);
1265
+ const dependencyMaps = this.#toDependencyMap(rootPackageJson, dependencies, devDependencies);
1266
+ const newPkgJson = {
1267
+ ...pkgJson,
1268
+ ...dependencyMaps,
1269
+ };
1270
+ await this.writeJson("package.json", newPkgJson);
1271
+ return newPkgJson;
1272
+ }
1247
1273
  async generateDistPackageJson(dependencies: string[] = [], devDependencies: string[] = []): Promise<PackageJson> {
1248
1274
  const [rootPackageJson, pkgJson] = await Promise.all([this.workspace.getPackageJson(), this.getPackageJson()]);
1249
- const { dependencies: rootDependencies = {}, devDependencies: rootDevDependencies = {} } = rootPackageJson;
1250
- const rootDeps = { ...rootDependencies, ...rootDevDependencies };
1275
+ const dependencyMaps = this.#toDependencyMap(rootPackageJson, dependencies, devDependencies);
1251
1276
  const distPkgJson: PackageJson = {
1252
1277
  ...pkgJson,
1253
1278
  type: "module",
1254
1279
  exports: { ...pkgJson.exports, ".": { import: "./index.ts", types: "./index.ts", default: "./index.ts" } },
1255
1280
  engines: { bun: ">=1.3.13" },
1256
- dependencies: Object.fromEntries(
1257
- dependencies.map((dep) => {
1258
- const rootVersion = rootDeps?.[dep];
1259
- if (!rootVersion) this.logger.warn(`Package ${dep} is not found in root package.json`);
1260
- return [dep, rootVersion];
1261
- }),
1262
- ),
1263
- devDependencies: Object.fromEntries(
1264
- devDependencies.map((dep) => {
1265
- const rootVersion = rootDeps?.[dep];
1266
- if (!rootVersion) this.logger.warn(`Package ${dep} is not found in root package.json`);
1267
- return [dep, rootVersion];
1268
- }),
1269
- ),
1281
+ ...dependencyMaps,
1270
1282
  };
1271
1283
  await Promise.all([this.dist.writeJson("package.json", distPkgJson), this.writeJson("package.json", distPkgJson)]);
1272
1284
  return distPkgJson;
@@ -8,6 +8,8 @@ import { CssImportResolver } from "./cssImportResolver";
8
8
 
9
9
  const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"] as const;
10
10
  const NON_SOURCE_EXT_RE = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
11
+ const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
12
+ const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
11
13
 
12
14
  interface CssDiscovery {
13
15
  cssPaths: string[];
@@ -88,7 +90,7 @@ export class CssCompiler {
88
90
 
89
91
  while (queue.length > 0) {
90
92
  const filePath = queue.shift();
91
- if (!filePath || sourceFiles.has(filePath) || filePath.includes("node_modules")) continue;
93
+ if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath)) continue;
92
94
  sourceFiles.add(filePath);
93
95
 
94
96
  let content: string;
@@ -125,7 +127,7 @@ export class CssCompiler {
125
127
  }
126
128
  if (NON_SOURCE_EXT_RE.test(spec)) continue;
127
129
  const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
128
- if (!resolved || sourceFiles.has(resolved) || resolved.includes("node_modules")) continue;
130
+ if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved)) continue;
129
131
  queue.push(resolved);
130
132
  }
131
133
  }
@@ -221,7 +223,7 @@ export class CssCompiler {
221
223
  await Promise.all(
222
224
  dirs.map(async (dir) => {
223
225
  for await (const file of glob.scan({ cwd: dir, absolute: true })) {
224
- if (file.includes("node_modules")) continue;
226
+ if (isIgnoredNodeModuleSource(file)) continue;
225
227
  files.add(file);
226
228
  }
227
229
  }),
@@ -271,6 +273,10 @@ function isSourceFile(filePath: string) {
271
273
  return SOURCE_EXTS.includes(path.extname(filePath) as (typeof SOURCE_EXTS)[number]);
272
274
  }
273
275
 
276
+ export function isIgnoredNodeModuleSource(filePath: string): boolean {
277
+ return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
278
+ }
279
+
274
280
  function getPageKeyBasePath(pageKey: string, basePaths: string[]): string | null {
275
281
  const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
276
282
  const segments = normalized.split("/");
@@ -308,7 +308,8 @@ class IncrementalBuilder {
308
308
  }
309
309
 
310
310
  #shouldRebuildCsr() {
311
- return process.env.AKAN_DEV_CSR_REBUILD === "1";
311
+
312
+ return true;
312
313
  }
313
314
 
314
315
  static async create() {
@@ -0,0 +1,25 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ or {
5
+ JsModuleSource() as $source where {
6
+ $source <: within JsImport(),
7
+ not $filename <: r".*\.(?:test|spec)\.tsx?",
8
+ $source <: r"\"@(?:apps|libs)/[^/]+/[^/]+/.+\"",
9
+ not $source <: r"\"@apps/[^/]+/env/env\.client\"",
10
+ register_diagnostic(
11
+ span = $source,
12
+ message = "@apps and @libs imports should only reference the first two path segments after the alias."
13
+ )
14
+ },
15
+ JsModuleSource() as $source where {
16
+ $source <: within JsImport(),
17
+ not $filename <: r".*\.(?:test|spec)\.tsx?",
18
+ $filename <: r".*apps/akasys/lib/projectBuild/[^/]+\.(?:constant|dictionary|document|service|signal|store)\.ts|.*apps/akasys/lib/projectBuild/[^/]+\.(?:Template|Unit|Util|View|Zone)\.tsx",
19
+ $source <: r"\"\.\./\.\./.*\"",
20
+ register_diagnostic(
21
+ span = $source,
22
+ message = "projectBuild module files should not import from two or more parent directories."
23
+ )
24
+ }
25
+ }
@@ -11,6 +11,7 @@ JsModuleSource() as $source where {
11
11
  r"\"@apps/.*",
12
12
  r"\"@libs/.*",
13
13
  r"\"@pkgs/.*",
14
+ r"\"bun:test\"",
14
15
  r"\"react.*"
15
16
  },
16
17
  register_diagnostic(
@@ -16,23 +16,63 @@ export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]
16
16
  return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
17
17
  };
18
18
 
19
+ export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
20
+ const config = await app.getConfig();
21
+ const targetNames = Object.keys(config.mobile.targets);
22
+ if (targetNames.length > 1) return targetNames;
23
+ const basePaths = [...config.basePaths];
24
+ if (basePaths.length > 1) return basePaths;
25
+ if (targetNames.length > 0) return targetNames;
26
+ return basePaths;
27
+ };
28
+
29
+ const resolveMobileTargetByBasePath = (
30
+ targets: ResolvedMobileTarget[],
31
+ basePath: string,
32
+ ): ResolvedMobileTarget | undefined => {
33
+ const normalizedBasePath = basePath.replace(/^\/+|\/+$/g, "");
34
+ const byBasePath = targets.find((target) => target.config.basePath?.replace(/^\/+|\/+$/g, "") === normalizedBasePath);
35
+ if (byBasePath) return byBasePath;
36
+ const [template] = targets;
37
+ if (!template) return undefined;
38
+ return {
39
+ name: template.name,
40
+ config: {
41
+ ...template.config,
42
+ basePath: normalizedBasePath,
43
+ },
44
+ };
45
+ };
46
+
19
47
  export const resolveMobileTargets = async (
20
48
  app: App,
21
49
  selection: MobileTargetSelection,
22
50
  ): Promise<ResolvedMobileTarget[]> => {
51
+ const config = await app.getConfig();
23
52
  const targets = await getMobileTargets(app);
24
53
  if (targets.length === 0) throw new Error(`No mobile targets configured for ${app.name}`);
25
54
  if (!selection) {
26
- if (targets.length === 1) return targets;
27
- throw new Error(
28
- `Multiple mobile targets found for ${app.name}. Pass --target <${targets.map((t) => t.name).join("|")}|all>.`,
29
- );
55
+ const choices = await getMobileTargetChoices(app);
56
+ if (choices.length === 1) return resolveMobileTargets(app, choices[0]);
57
+ throw new Error(`Multiple mobile targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
58
+ }
59
+ if (selection === "all") {
60
+ if (Object.keys(config.mobile.targets).length > 1) return targets;
61
+ const basePaths = [...config.basePaths];
62
+ if (basePaths.length > 1) {
63
+ return basePaths.flatMap((basePath) => {
64
+ const resolved = resolveMobileTargetByBasePath(targets, basePath);
65
+ return resolved ? [resolved] : [];
66
+ });
67
+ }
68
+ return targets;
30
69
  }
31
- if (selection === "all") return targets;
32
70
  const target = targets.find((candidate) => candidate.name === selection);
33
- if (!target)
34
- throw new Error(`Mobile target '${selection}' was not found. Available: ${targets.map((t) => t.name).join(", ")}`);
35
- return [target];
71
+ if (target) return [target];
72
+ const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
73
+ if (basePathTarget && config.basePaths.has(selection.replace(/^\/+|\/+$/g, ""))) return [basePathTarget];
74
+ const choices = await getMobileTargetChoices(app);
75
+ throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
36
76
  };
37
77
 
38
78
  export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
@@ -91,8 +91,11 @@ const moduleUiFileTypes = {
91
91
  scalar: new Set(["Template", "Unit"]),
92
92
  } satisfies Record<ModuleKind, Set<string>>;
93
93
  const testFilePattern = /\.(test|spec)\.(ts|tsx)$/;
94
+ const rootSignalTestFilePattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(ts|tsx)$/;
94
95
 
95
96
  const isAllowedTestFile = (filename: string) => testFilePattern.test(filename);
97
+ const isAllowedLibRootFile = (filename: string) =>
98
+ libRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
96
99
  const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
97
100
  path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
98
101
 
@@ -117,7 +120,7 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
117
120
  }
118
121
 
119
122
  libRoot.files
120
- .filter((filename) => !libRootAllowedFiles.has(filename))
123
+ .filter((filename) => !isAllowedLibRootFile(filename))
121
124
  .forEach((filename) => {
122
125
  addViolation(path.join("lib", filename), "unsupported lib root file");
123
126
  });