@akanjs/cli 2.3.11-rc.9 → 2.3.12-rc.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.
@@ -721,6 +721,7 @@ var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
721
721
  var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
722
722
  var NATIVE_RUNTIME_PACKAGES = ["sharp"];
723
723
  var DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"];
724
+ var MOBILE_RUNTIME_PACKAGES = ["firebase"];
724
725
  var DATABASE_MODE_RUNTIME_PACKAGES = {
725
726
  single: [],
726
727
  multiple: ["@libsql/client", "bullmq", "ioredis", "protobufjs"],
@@ -730,6 +731,7 @@ var AKAN_RUNTIME_PACKAGES = new Set([
730
731
  ...SSR_RUNTIME_PACKAGES,
731
732
  ...NATIVE_RUNTIME_PACKAGES,
732
733
  ...DEFAULT_BACKEND_RUNTIME_PACKAGES,
734
+ ...MOBILE_RUNTIME_PACKAGES,
733
735
  ...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat()
734
736
  ]);
735
737
  var DEFAULT_AKAN_IMAGE_CONFIG = {
@@ -1013,11 +1015,20 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
1013
1015
  return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
1014
1016
  }
1015
1017
  getMissingDatabaseModeDependencySpecs(databaseMode = this.defaultDatabaseMode) {
1018
+ return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
1019
+ }
1020
+ getMobileRuntimePackages() {
1021
+ return [...MOBILE_RUNTIME_PACKAGES];
1022
+ }
1023
+ getMissingMobileDependencySpecs() {
1024
+ return this.#getMissingDependencySpecs(this.getMobileRuntimePackages());
1025
+ }
1026
+ #getMissingDependencySpecs(libs) {
1016
1027
  const rootDependencies = {
1017
1028
  ...this.rootPackageJson.dependencies,
1018
1029
  ...this.rootPackageJson.devDependencies
1019
1030
  };
1020
- return this.getDatabaseModeRuntimePackages(databaseMode).filter((lib) => !rootDependencies[lib]).map((lib) => {
1031
+ return libs.filter((lib) => !rootDependencies[lib]).map((lib) => {
1021
1032
  const version = this.#resolveProductionDependencyVersion(lib);
1022
1033
  if (!version)
1023
1034
  throw new Error(`Dependency ${lib} not found in package.json`);
@@ -2691,6 +2702,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
2691
2702
  throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
2692
2703
  }
2693
2704
  }
2705
+ function validateOverridesSourceExports(source, filePath) {
2706
+ const sourceFile = ts3.createSourceFile(filePath, source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
2707
+ const fail = (message) => {
2708
+ throw new Error(`[route-convention] ${message}: ${filePath}`);
2709
+ };
2710
+ let defaultOverride = null;
2711
+ for (const statement of sourceFile.statements) {
2712
+ if (ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression))
2713
+ continue;
2714
+ if (ts3.isImportDeclaration(statement))
2715
+ continue;
2716
+ if (ts3.isInterfaceDeclaration(statement) || ts3.isTypeAliasDeclaration(statement))
2717
+ continue;
2718
+ if (ts3.isExportAssignment(statement) && !statement.isExportEquals) {
2719
+ defaultOverride = statement;
2720
+ continue;
2721
+ }
2722
+ fail(`_overrides.tsx may only contain imports and a single "export default override({ ... })"`);
2723
+ }
2724
+ if (!defaultOverride)
2725
+ fail(`_overrides.tsx must "export default override({ ... })"`);
2726
+ const expression = defaultOverride.expression;
2727
+ if (!ts3.isCallExpression(expression) || !ts3.isIdentifier(expression.expression) || expression.expression.text !== "override")
2728
+ fail(`_overrides.tsx default export must be a call to "override", e.g. "export default override({ Modal: BrandModal })"`);
2729
+ }
2694
2730
 
2695
2731
  class Executor {
2696
2732
  static verbose = false;
@@ -3714,7 +3750,11 @@ class AppExecutor extends SysExecutor {
3714
3750
  throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
3715
3751
  }
3716
3752
  const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
3717
- validateRouteSourceExports(await Bun.file(absPath).text(), absPath, parsed.kind, { rootLayout: isRootLayout });
3753
+ const routeSource = await Bun.file(absPath).text();
3754
+ if (parsed.kind === "overrides")
3755
+ validateOverridesSourceExports(routeSource, absPath);
3756
+ else
3757
+ validateRouteSourceExports(routeSource, absPath, parsed.kind, { rootLayout: isRootLayout });
3718
3758
  pageKeys.push(key);
3719
3759
  }
3720
3760
  pageKeys.sort();
@@ -8822,6 +8862,25 @@ async function appHasStModule(appCwdPath) {
8822
8862
  }
8823
8863
  var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
8824
8864
  var IMPLICIT_DICT_DIR = path13.join(".akan", "generated", "dict");
8865
+ var IMPLICIT_OVERRIDES_DIR = path13.join(".akan", "generated", "overrides");
8866
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
8867
+ async function writeGeneratedOverridesLayoutFile(opts) {
8868
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
8869
+ const absPath = path13.join(path13.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
8870
+ const userRel = path13.relative(path13.dirname(absPath), opts.userAbsPath).split(path13.sep).join("/");
8871
+ const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
8872
+ const source2 = `"use client";
8873
+ import { UiOverrideProvider } from "akanjs/ui";
8874
+ import { createElement, type ReactNode } from "react";
8875
+ import value from ${JSON.stringify(userSpecifier)};
8876
+
8877
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
8878
+ return createElement(UiOverrideProvider, { value }, children);
8879
+ }
8880
+ `;
8881
+ await Bun.write(absPath, source2);
8882
+ return absPath;
8883
+ }
8825
8884
  function getRootBoundarySegments(key) {
8826
8885
  const match = LAYOUT_KEY_RE.exec(key);
8827
8886
  if (!match)
@@ -9014,9 +9073,17 @@ async function resolveSsrPageEntries(opts) {
9014
9073
  const segments = getRootBoundarySegments(key);
9015
9074
  return segments !== null && isRootBoundarySegments(segments, basePaths);
9016
9075
  }));
9017
- const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) => ({
9018
- key,
9019
- moduleAbsPath: path13.resolve(absPageDir, key)
9076
+ const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
9077
+ const userAbsPath = path13.resolve(absPageDir, key);
9078
+ if (OVERRIDES_KEY_RE.test(key)) {
9079
+ const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
9080
+ appCwdPath: opts.appCwdPath,
9081
+ key,
9082
+ userAbsPath
9083
+ });
9084
+ return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
9085
+ }
9086
+ return { key, moduleAbsPath: userAbsPath };
9020
9087
  }));
9021
9088
  const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
9022
9089
  key: implicitRootLayoutKey(boundary.segments),
@@ -9048,7 +9115,7 @@ function computeRouteSeedIndex(pageEntries) {
9048
9115
  for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
9049
9116
  const parsed = parseRouteModuleKey2(key);
9050
9117
  const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
9051
- if (parsed.kind === "layout") {
9118
+ if (parsed.kind === "layout" || parsed.kind === "overrides") {
9052
9119
  const prefix = parsed.routeSegments.join("/");
9053
9120
  const prev = layoutsByPrefix.get(prefix) ?? [];
9054
9121
  layoutsByPrefix.set(prefix, [...prev, ...files]);
@@ -11434,6 +11501,8 @@ import path28 from "path";
11434
11501
  var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
11435
11502
  var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
11436
11503
  var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
11504
+ var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
11505
+ var FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
11437
11506
  var MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"];
11438
11507
  var SERVICE_UI_TYPES = ["Util", "Zone"];
11439
11508
  var SCALAR_UI_TYPES = ["Template", "Unit"];
@@ -11530,18 +11599,20 @@ class DevGeneratedIndexSync {
11530
11599
  return this.#moduleContent(path28.dirname(indexPath));
11531
11600
  }
11532
11601
  async#facetContent(dir) {
11602
+ const nameCasePattern = path28.basename(dir) === "ui" ? FACET_PASCAL_CASE_RE : FACET_CAMEL_CASE_RE;
11533
11603
  const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
11534
11604
  const exportNames = entries.flatMap((entry) => {
11535
11605
  const name = entry.name;
11536
11606
  if (name.startsWith("."))
11537
11607
  return [];
11538
11608
  if (entry.isDirectory())
11539
- return [name];
11609
+ return nameCasePattern.test(name) ? [name] : [];
11540
11610
  if (!entry.isFile())
11541
11611
  return [];
11542
11612
  if (!FACET_SOURCE_FILE_RE.test(name) || FACET_EXCLUDED_FILE_RE.test(name))
11543
11613
  return [];
11544
- return [name.replace(FACET_SOURCE_FILE_RE, "")];
11614
+ const exportName = name.replace(FACET_SOURCE_FILE_RE, "");
11615
+ return nameCasePattern.test(exportName) ? [exportName] : [];
11545
11616
  }).sort();
11546
11617
  if (exportNames.length === 0)
11547
11618
  return null;
@@ -15698,6 +15769,42 @@ var CONVENTION_SUFFIXES = [
15698
15769
  ".signal.ts",
15699
15770
  ".store.ts"
15700
15771
  ];
15772
+ var PAGE_RESERVED_EXPORTS = new Set([
15773
+ "pageConfig",
15774
+ "head",
15775
+ "metadata",
15776
+ "generateHead",
15777
+ "generateMetadata",
15778
+ "fonts",
15779
+ "manifest",
15780
+ "theme",
15781
+ "reconnect",
15782
+ "wsConnect",
15783
+ "layoutStyle",
15784
+ "gaTrackingId"
15785
+ ]);
15786
+ var RULE_FIXES = {
15787
+ "akan.global.duplicate-exported-function-name": "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
15788
+ "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
15789
+ "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
15790
+ "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
15791
+ "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
15792
+ "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
15793
+ "akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
15794
+ "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
15795
+ "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
15796
+ "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
15797
+ "akan.file.component-internal-declaration": "Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
15798
+ "akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
15799
+ "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
15800
+ "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
15801
+ "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component."
15802
+ };
15803
+ function getRuleFix(rule) {
15804
+ if (rule.startsWith("akan.convention"))
15805
+ return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
15806
+ return RULE_FIXES[rule];
15807
+ }
15701
15808
 
15702
15809
  class AkanQualityScanner {
15703
15810
  async scan(workspaceRoot) {
@@ -15713,7 +15820,7 @@ class AkanQualityScanner {
15713
15820
  return {
15714
15821
  workspaceRoot,
15715
15822
  scannedFiles: sourceFiles.length,
15716
- warnings: warnings.sort(compareWarnings),
15823
+ warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
15717
15824
  suggestedRules: SUGGESTED_RULES
15718
15825
  };
15719
15826
  }
@@ -15838,22 +15945,43 @@ class AkanQualityScanner {
15838
15945
  #scanComponentQuality(sourceFile2) {
15839
15946
  if (!isComponentDeclarationFile(sourceFile2.file))
15840
15947
  return [];
15841
- const exportedNames = getExportedDeclarationNames(sourceFile2.sourceFile);
15842
- const allowedInternalInterfaces = new Set([...exportedNames].map((name) => `${name}Props`));
15948
+ const isPage = isPageRouteFile(sourceFile2.file);
15949
+ const declarations = getComponentFileDeclarations(sourceFile2.sourceFile);
15950
+ const compoundComponentNames = getCompoundComponentNames(sourceFile2.sourceFile);
15951
+ const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
15952
+ const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
15953
+ const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
15843
15954
  const warnings = [];
15844
- for (const declaration of getInternalTypeOrFunctionDeclarations(sourceFile2.sourceFile)) {
15845
- if (exportedNames.has(declaration.name))
15955
+ for (const declaration of declarations) {
15956
+ if (declaration.isDefaultExport)
15846
15957
  continue;
15847
- if (declaration.kind === "interface" && allowedInternalInterfaces.has(declaration.name))
15958
+ if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
15848
15959
  continue;
15849
- warnings.push({
15850
- rule: "akan.file.component-internal-declaration",
15851
- scope: "file",
15852
- severity: "warning",
15853
- file: sourceFile2.file,
15854
- line: declaration.line,
15855
- message: `Component file should not declare non-exported ${declaration.kind} "${declaration.name}". Only a "<Component>Props" interface may stay internal; move other types or helpers to a separate file in ui/, webkit/, or common/ by purpose.`
15856
- });
15960
+ if (declaration.exported) {
15961
+ if (isAllowedComponentExport(declaration, isPage))
15962
+ continue;
15963
+ warnings.push({
15964
+ rule: "akan.file.component-export",
15965
+ scope: "file",
15966
+ severity: "warning",
15967
+ file: sourceFile2.file,
15968
+ line: declaration.line,
15969
+ message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
15970
+ });
15971
+ continue;
15972
+ }
15973
+ if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
15974
+ continue;
15975
+ if (isRestrictedInternalKind(declaration.kind)) {
15976
+ warnings.push({
15977
+ rule: "akan.file.component-internal-declaration",
15978
+ scope: "file",
15979
+ severity: "warning",
15980
+ file: sourceFile2.file,
15981
+ line: declaration.line,
15982
+ message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
15983
+ });
15984
+ }
15857
15985
  }
15858
15986
  return warnings;
15859
15987
  }
@@ -15932,6 +16060,8 @@ function formatQualityWarnings(warnings) {
15932
16060
  if (warning.locations?.length) {
15933
16061
  lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15934
16062
  }
16063
+ if (warning.fix)
16064
+ lines.push(` fix: ${warning.fix}`);
15935
16065
  return lines;
15936
16066
  });
15937
16067
  }
@@ -16023,61 +16153,75 @@ function isComponentDeclarationFile(file) {
16023
16153
  return true;
16024
16154
  return root === "apps" && area === "page";
16025
16155
  }
16026
- function getExportedDeclarationNames(sourceFile2) {
16027
- const names = new Set;
16156
+ function getComponentFileDeclarations(sourceFile2) {
16157
+ const reExportedNames = new Set;
16028
16158
  for (const statement of sourceFile2.statements) {
16029
16159
  if (ts11.isExportDeclaration(statement) && statement.exportClause && ts11.isNamedExports(statement.exportClause)) {
16030
- for (const element of statement.exportClause.elements) {
16031
- names.add(element.name.text);
16032
- if (element.propertyName)
16033
- names.add(element.propertyName.text);
16034
- }
16035
- continue;
16160
+ for (const element of statement.exportClause.elements)
16161
+ reExportedNames.add((element.propertyName ?? element.name).text);
16036
16162
  }
16037
- if (!isExported(statement))
16038
- continue;
16039
- for (const name of getStatementDeclarationNames(statement))
16040
- names.add(name);
16041
16163
  }
16042
- return names;
16043
- }
16044
- function getStatementDeclarationNames(statement) {
16045
- if (ts11.isFunctionDeclaration(statement) && statement.name)
16046
- return [statement.name.text];
16047
- if (ts11.isClassDeclaration(statement) && statement.name)
16048
- return [statement.name.text];
16049
- if (ts11.isInterfaceDeclaration(statement))
16050
- return [statement.name.text];
16051
- if (ts11.isTypeAliasDeclaration(statement))
16052
- return [statement.name.text];
16053
- if (ts11.isEnumDeclaration(statement))
16054
- return [statement.name.text];
16055
- if (ts11.isVariableStatement(statement)) {
16056
- return statement.declarationList.declarations.filter((declaration) => ts11.isIdentifier(declaration.name)).map((declaration) => declaration.name.text);
16057
- }
16058
- return [];
16059
- }
16060
- function getInternalTypeOrFunctionDeclarations(sourceFile2) {
16061
16164
  const declarations = [];
16062
16165
  for (const statement of sourceFile2.statements) {
16063
- if (isExported(statement))
16064
- continue;
16065
- if (ts11.isInterfaceDeclaration(statement)) {
16066
- declarations.push({ name: statement.name.text, kind: "interface", line: getLine(sourceFile2, statement) });
16067
- } else if (ts11.isTypeAliasDeclaration(statement)) {
16068
- declarations.push({ name: statement.name.text, kind: "type", line: getLine(sourceFile2, statement) });
16069
- } else if (ts11.isFunctionDeclaration(statement) && statement.name) {
16070
- declarations.push({ name: statement.name.text, kind: "function", line: getLine(sourceFile2, statement) });
16071
- } else if (ts11.isVariableStatement(statement)) {
16166
+ const isDefaultExport = isDefaultExportStatement(statement);
16167
+ const inlineExported = isExported(statement);
16168
+ const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
16169
+ if (ts11.isInterfaceDeclaration(statement))
16170
+ add(statement.name.text, "interface", getLine(sourceFile2, statement));
16171
+ else if (ts11.isTypeAliasDeclaration(statement))
16172
+ add(statement.name.text, "type", getLine(sourceFile2, statement));
16173
+ else if (ts11.isEnumDeclaration(statement))
16174
+ add(statement.name.text, "enum", getLine(sourceFile2, statement));
16175
+ else if (ts11.isFunctionDeclaration(statement) && statement.name)
16176
+ add(statement.name.text, "function", getLine(sourceFile2, statement));
16177
+ else if (ts11.isClassDeclaration(statement) && statement.name)
16178
+ add(statement.name.text, "class", getLine(sourceFile2, statement));
16179
+ else if (ts11.isVariableStatement(statement)) {
16072
16180
  for (const declaration of statement.declarationList.declarations) {
16073
- if (!ts11.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
16181
+ if (!ts11.isIdentifier(declaration.name))
16074
16182
  continue;
16075
- declarations.push({ name: declaration.name.text, kind: "function", line: getLine(sourceFile2, declaration) });
16183
+ const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
16184
+ add(declaration.name.text, kind, getLine(sourceFile2, declaration));
16076
16185
  }
16077
16186
  }
16078
16187
  }
16079
16188
  return declarations;
16080
16189
  }
16190
+ function getCompoundComponentNames(sourceFile2) {
16191
+ const names = new Set;
16192
+ for (const statement of sourceFile2.statements) {
16193
+ if (!ts11.isExpressionStatement(statement))
16194
+ continue;
16195
+ const { expression } = statement;
16196
+ if (!ts11.isBinaryExpression(expression) || expression.operatorToken.kind !== ts11.SyntaxKind.EqualsToken)
16197
+ continue;
16198
+ if (!ts11.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
16199
+ continue;
16200
+ names.add(expression.left.name.text);
16201
+ if (ts11.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
16202
+ names.add(expression.right.text);
16203
+ }
16204
+ return names;
16205
+ }
16206
+ function isComponentValueKind(kind) {
16207
+ return kind === "variable" || kind === "function" || kind === "class";
16208
+ }
16209
+ function isRestrictedInternalKind(kind) {
16210
+ return kind === "interface" || kind === "type" || kind === "function";
16211
+ }
16212
+ function isAllowedComponentExport(declaration, isPage) {
16213
+ if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
16214
+ return true;
16215
+ return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
16216
+ }
16217
+ function isPascalCaseName(name) {
16218
+ return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
16219
+ }
16220
+ function isDefaultExportStatement(statement) {
16221
+ if (ts11.isExportAssignment(statement))
16222
+ return !statement.isExportEquals;
16223
+ return !!(ts11.getCombinedModifierFlags(statement) & ts11.ModifierFlags.Default);
16224
+ }
16081
16225
  function getPlaceholderExportWarnings(sourceFile2) {
16082
16226
  if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
16083
16227
  return [];
package/index.js CHANGED
@@ -719,6 +719,7 @@ var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
719
719
  var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
720
720
  var NATIVE_RUNTIME_PACKAGES = ["sharp"];
721
721
  var DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"];
722
+ var MOBILE_RUNTIME_PACKAGES = ["firebase"];
722
723
  var DATABASE_MODE_RUNTIME_PACKAGES = {
723
724
  single: [],
724
725
  multiple: ["@libsql/client", "bullmq", "ioredis", "protobufjs"],
@@ -728,6 +729,7 @@ var AKAN_RUNTIME_PACKAGES = new Set([
728
729
  ...SSR_RUNTIME_PACKAGES,
729
730
  ...NATIVE_RUNTIME_PACKAGES,
730
731
  ...DEFAULT_BACKEND_RUNTIME_PACKAGES,
732
+ ...MOBILE_RUNTIME_PACKAGES,
731
733
  ...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat()
732
734
  ]);
733
735
  var DEFAULT_AKAN_IMAGE_CONFIG = {
@@ -1011,11 +1013,20 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
1011
1013
  return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
1012
1014
  }
1013
1015
  getMissingDatabaseModeDependencySpecs(databaseMode = this.defaultDatabaseMode) {
1016
+ return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
1017
+ }
1018
+ getMobileRuntimePackages() {
1019
+ return [...MOBILE_RUNTIME_PACKAGES];
1020
+ }
1021
+ getMissingMobileDependencySpecs() {
1022
+ return this.#getMissingDependencySpecs(this.getMobileRuntimePackages());
1023
+ }
1024
+ #getMissingDependencySpecs(libs) {
1014
1025
  const rootDependencies = {
1015
1026
  ...this.rootPackageJson.dependencies,
1016
1027
  ...this.rootPackageJson.devDependencies
1017
1028
  };
1018
- return this.getDatabaseModeRuntimePackages(databaseMode).filter((lib) => !rootDependencies[lib]).map((lib) => {
1029
+ return libs.filter((lib) => !rootDependencies[lib]).map((lib) => {
1019
1030
  const version = this.#resolveProductionDependencyVersion(lib);
1020
1031
  if (!version)
1021
1032
  throw new Error(`Dependency ${lib} not found in package.json`);
@@ -2689,6 +2700,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
2689
2700
  throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
2690
2701
  }
2691
2702
  }
2703
+ function validateOverridesSourceExports(source, filePath) {
2704
+ const sourceFile = ts3.createSourceFile(filePath, source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
2705
+ const fail = (message) => {
2706
+ throw new Error(`[route-convention] ${message}: ${filePath}`);
2707
+ };
2708
+ let defaultOverride = null;
2709
+ for (const statement of sourceFile.statements) {
2710
+ if (ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression))
2711
+ continue;
2712
+ if (ts3.isImportDeclaration(statement))
2713
+ continue;
2714
+ if (ts3.isInterfaceDeclaration(statement) || ts3.isTypeAliasDeclaration(statement))
2715
+ continue;
2716
+ if (ts3.isExportAssignment(statement) && !statement.isExportEquals) {
2717
+ defaultOverride = statement;
2718
+ continue;
2719
+ }
2720
+ fail(`_overrides.tsx may only contain imports and a single "export default override({ ... })"`);
2721
+ }
2722
+ if (!defaultOverride)
2723
+ fail(`_overrides.tsx must "export default override({ ... })"`);
2724
+ const expression = defaultOverride.expression;
2725
+ if (!ts3.isCallExpression(expression) || !ts3.isIdentifier(expression.expression) || expression.expression.text !== "override")
2726
+ fail(`_overrides.tsx default export must be a call to "override", e.g. "export default override({ Modal: BrandModal })"`);
2727
+ }
2692
2728
 
2693
2729
  class Executor {
2694
2730
  static verbose = false;
@@ -3712,7 +3748,11 @@ class AppExecutor extends SysExecutor {
3712
3748
  throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
3713
3749
  }
3714
3750
  const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
3715
- validateRouteSourceExports(await Bun.file(absPath).text(), absPath, parsed.kind, { rootLayout: isRootLayout });
3751
+ const routeSource = await Bun.file(absPath).text();
3752
+ if (parsed.kind === "overrides")
3753
+ validateOverridesSourceExports(routeSource, absPath);
3754
+ else
3755
+ validateRouteSourceExports(routeSource, absPath, parsed.kind, { rootLayout: isRootLayout });
3716
3756
  pageKeys.push(key);
3717
3757
  }
3718
3758
  pageKeys.sort();
@@ -8820,6 +8860,25 @@ async function appHasStModule(appCwdPath) {
8820
8860
  }
8821
8861
  var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
8822
8862
  var IMPLICIT_DICT_DIR = path13.join(".akan", "generated", "dict");
8863
+ var IMPLICIT_OVERRIDES_DIR = path13.join(".akan", "generated", "overrides");
8864
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
8865
+ async function writeGeneratedOverridesLayoutFile(opts) {
8866
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
8867
+ const absPath = path13.join(path13.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
8868
+ const userRel = path13.relative(path13.dirname(absPath), opts.userAbsPath).split(path13.sep).join("/");
8869
+ const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
8870
+ const source2 = `"use client";
8871
+ import { UiOverrideProvider } from "akanjs/ui";
8872
+ import { createElement, type ReactNode } from "react";
8873
+ import value from ${JSON.stringify(userSpecifier)};
8874
+
8875
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
8876
+ return createElement(UiOverrideProvider, { value }, children);
8877
+ }
8878
+ `;
8879
+ await Bun.write(absPath, source2);
8880
+ return absPath;
8881
+ }
8823
8882
  function getRootBoundarySegments(key) {
8824
8883
  const match = LAYOUT_KEY_RE.exec(key);
8825
8884
  if (!match)
@@ -9012,9 +9071,17 @@ async function resolveSsrPageEntries(opts) {
9012
9071
  const segments = getRootBoundarySegments(key);
9013
9072
  return segments !== null && isRootBoundarySegments(segments, basePaths);
9014
9073
  }));
9015
- const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) => ({
9016
- key,
9017
- moduleAbsPath: path13.resolve(absPageDir, key)
9074
+ const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
9075
+ const userAbsPath = path13.resolve(absPageDir, key);
9076
+ if (OVERRIDES_KEY_RE.test(key)) {
9077
+ const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
9078
+ appCwdPath: opts.appCwdPath,
9079
+ key,
9080
+ userAbsPath
9081
+ });
9082
+ return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
9083
+ }
9084
+ return { key, moduleAbsPath: userAbsPath };
9018
9085
  }));
9019
9086
  const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
9020
9087
  key: implicitRootLayoutKey(boundary.segments),
@@ -9046,7 +9113,7 @@ function computeRouteSeedIndex(pageEntries) {
9046
9113
  for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
9047
9114
  const parsed = parseRouteModuleKey2(key);
9048
9115
  const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
9049
- if (parsed.kind === "layout") {
9116
+ if (parsed.kind === "layout" || parsed.kind === "overrides") {
9050
9117
  const prefix = parsed.routeSegments.join("/");
9051
9118
  const prev = layoutsByPrefix.get(prefix) ?? [];
9052
9119
  layoutsByPrefix.set(prefix, [...prev, ...files]);
@@ -11432,6 +11499,8 @@ import path28 from "path";
11432
11499
  var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
11433
11500
  var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
11434
11501
  var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
11502
+ var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
11503
+ var FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
11435
11504
  var MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"];
11436
11505
  var SERVICE_UI_TYPES = ["Util", "Zone"];
11437
11506
  var SCALAR_UI_TYPES = ["Template", "Unit"];
@@ -11528,18 +11597,20 @@ class DevGeneratedIndexSync {
11528
11597
  return this.#moduleContent(path28.dirname(indexPath));
11529
11598
  }
11530
11599
  async#facetContent(dir) {
11600
+ const nameCasePattern = path28.basename(dir) === "ui" ? FACET_PASCAL_CASE_RE : FACET_CAMEL_CASE_RE;
11531
11601
  const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
11532
11602
  const exportNames = entries.flatMap((entry) => {
11533
11603
  const name = entry.name;
11534
11604
  if (name.startsWith("."))
11535
11605
  return [];
11536
11606
  if (entry.isDirectory())
11537
- return [name];
11607
+ return nameCasePattern.test(name) ? [name] : [];
11538
11608
  if (!entry.isFile())
11539
11609
  return [];
11540
11610
  if (!FACET_SOURCE_FILE_RE.test(name) || FACET_EXCLUDED_FILE_RE.test(name))
11541
11611
  return [];
11542
- return [name.replace(FACET_SOURCE_FILE_RE, "")];
11612
+ const exportName = name.replace(FACET_SOURCE_FILE_RE, "");
11613
+ return nameCasePattern.test(exportName) ? [exportName] : [];
11543
11614
  }).sort();
11544
11615
  if (exportNames.length === 0)
11545
11616
  return null;
@@ -15696,6 +15767,42 @@ var CONVENTION_SUFFIXES = [
15696
15767
  ".signal.ts",
15697
15768
  ".store.ts"
15698
15769
  ];
15770
+ var PAGE_RESERVED_EXPORTS = new Set([
15771
+ "pageConfig",
15772
+ "head",
15773
+ "metadata",
15774
+ "generateHead",
15775
+ "generateMetadata",
15776
+ "fonts",
15777
+ "manifest",
15778
+ "theme",
15779
+ "reconnect",
15780
+ "wsConnect",
15781
+ "layoutStyle",
15782
+ "gaTrackingId"
15783
+ ]);
15784
+ var RULE_FIXES = {
15785
+ "akan.global.duplicate-exported-function-name": "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
15786
+ "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
15787
+ "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
15788
+ "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
15789
+ "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
15790
+ "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
15791
+ "akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
15792
+ "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
15793
+ "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
15794
+ "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
15795
+ "akan.file.component-internal-declaration": "Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
15796
+ "akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
15797
+ "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
15798
+ "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
15799
+ "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component."
15800
+ };
15801
+ function getRuleFix(rule) {
15802
+ if (rule.startsWith("akan.convention"))
15803
+ return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
15804
+ return RULE_FIXES[rule];
15805
+ }
15699
15806
 
15700
15807
  class AkanQualityScanner {
15701
15808
  async scan(workspaceRoot) {
@@ -15711,7 +15818,7 @@ class AkanQualityScanner {
15711
15818
  return {
15712
15819
  workspaceRoot,
15713
15820
  scannedFiles: sourceFiles.length,
15714
- warnings: warnings.sort(compareWarnings),
15821
+ warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
15715
15822
  suggestedRules: SUGGESTED_RULES
15716
15823
  };
15717
15824
  }
@@ -15836,22 +15943,43 @@ class AkanQualityScanner {
15836
15943
  #scanComponentQuality(sourceFile2) {
15837
15944
  if (!isComponentDeclarationFile(sourceFile2.file))
15838
15945
  return [];
15839
- const exportedNames = getExportedDeclarationNames(sourceFile2.sourceFile);
15840
- const allowedInternalInterfaces = new Set([...exportedNames].map((name) => `${name}Props`));
15946
+ const isPage = isPageRouteFile(sourceFile2.file);
15947
+ const declarations = getComponentFileDeclarations(sourceFile2.sourceFile);
15948
+ const compoundComponentNames = getCompoundComponentNames(sourceFile2.sourceFile);
15949
+ const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
15950
+ const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
15951
+ const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
15841
15952
  const warnings = [];
15842
- for (const declaration of getInternalTypeOrFunctionDeclarations(sourceFile2.sourceFile)) {
15843
- if (exportedNames.has(declaration.name))
15953
+ for (const declaration of declarations) {
15954
+ if (declaration.isDefaultExport)
15844
15955
  continue;
15845
- if (declaration.kind === "interface" && allowedInternalInterfaces.has(declaration.name))
15956
+ if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
15846
15957
  continue;
15847
- warnings.push({
15848
- rule: "akan.file.component-internal-declaration",
15849
- scope: "file",
15850
- severity: "warning",
15851
- file: sourceFile2.file,
15852
- line: declaration.line,
15853
- message: `Component file should not declare non-exported ${declaration.kind} "${declaration.name}". Only a "<Component>Props" interface may stay internal; move other types or helpers to a separate file in ui/, webkit/, or common/ by purpose.`
15854
- });
15958
+ if (declaration.exported) {
15959
+ if (isAllowedComponentExport(declaration, isPage))
15960
+ continue;
15961
+ warnings.push({
15962
+ rule: "akan.file.component-export",
15963
+ scope: "file",
15964
+ severity: "warning",
15965
+ file: sourceFile2.file,
15966
+ line: declaration.line,
15967
+ message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
15968
+ });
15969
+ continue;
15970
+ }
15971
+ if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
15972
+ continue;
15973
+ if (isRestrictedInternalKind(declaration.kind)) {
15974
+ warnings.push({
15975
+ rule: "akan.file.component-internal-declaration",
15976
+ scope: "file",
15977
+ severity: "warning",
15978
+ file: sourceFile2.file,
15979
+ line: declaration.line,
15980
+ message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
15981
+ });
15982
+ }
15855
15983
  }
15856
15984
  return warnings;
15857
15985
  }
@@ -15930,6 +16058,8 @@ function formatQualityWarnings(warnings) {
15930
16058
  if (warning.locations?.length) {
15931
16059
  lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15932
16060
  }
16061
+ if (warning.fix)
16062
+ lines.push(` fix: ${warning.fix}`);
15933
16063
  return lines;
15934
16064
  });
15935
16065
  }
@@ -16021,61 +16151,75 @@ function isComponentDeclarationFile(file) {
16021
16151
  return true;
16022
16152
  return root === "apps" && area === "page";
16023
16153
  }
16024
- function getExportedDeclarationNames(sourceFile2) {
16025
- const names = new Set;
16154
+ function getComponentFileDeclarations(sourceFile2) {
16155
+ const reExportedNames = new Set;
16026
16156
  for (const statement of sourceFile2.statements) {
16027
16157
  if (ts11.isExportDeclaration(statement) && statement.exportClause && ts11.isNamedExports(statement.exportClause)) {
16028
- for (const element of statement.exportClause.elements) {
16029
- names.add(element.name.text);
16030
- if (element.propertyName)
16031
- names.add(element.propertyName.text);
16032
- }
16033
- continue;
16158
+ for (const element of statement.exportClause.elements)
16159
+ reExportedNames.add((element.propertyName ?? element.name).text);
16034
16160
  }
16035
- if (!isExported(statement))
16036
- continue;
16037
- for (const name of getStatementDeclarationNames(statement))
16038
- names.add(name);
16039
- }
16040
- return names;
16041
- }
16042
- function getStatementDeclarationNames(statement) {
16043
- if (ts11.isFunctionDeclaration(statement) && statement.name)
16044
- return [statement.name.text];
16045
- if (ts11.isClassDeclaration(statement) && statement.name)
16046
- return [statement.name.text];
16047
- if (ts11.isInterfaceDeclaration(statement))
16048
- return [statement.name.text];
16049
- if (ts11.isTypeAliasDeclaration(statement))
16050
- return [statement.name.text];
16051
- if (ts11.isEnumDeclaration(statement))
16052
- return [statement.name.text];
16053
- if (ts11.isVariableStatement(statement)) {
16054
- return statement.declarationList.declarations.filter((declaration) => ts11.isIdentifier(declaration.name)).map((declaration) => declaration.name.text);
16055
16161
  }
16056
- return [];
16057
- }
16058
- function getInternalTypeOrFunctionDeclarations(sourceFile2) {
16059
16162
  const declarations = [];
16060
16163
  for (const statement of sourceFile2.statements) {
16061
- if (isExported(statement))
16062
- continue;
16063
- if (ts11.isInterfaceDeclaration(statement)) {
16064
- declarations.push({ name: statement.name.text, kind: "interface", line: getLine(sourceFile2, statement) });
16065
- } else if (ts11.isTypeAliasDeclaration(statement)) {
16066
- declarations.push({ name: statement.name.text, kind: "type", line: getLine(sourceFile2, statement) });
16067
- } else if (ts11.isFunctionDeclaration(statement) && statement.name) {
16068
- declarations.push({ name: statement.name.text, kind: "function", line: getLine(sourceFile2, statement) });
16069
- } else if (ts11.isVariableStatement(statement)) {
16164
+ const isDefaultExport = isDefaultExportStatement(statement);
16165
+ const inlineExported = isExported(statement);
16166
+ const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
16167
+ if (ts11.isInterfaceDeclaration(statement))
16168
+ add(statement.name.text, "interface", getLine(sourceFile2, statement));
16169
+ else if (ts11.isTypeAliasDeclaration(statement))
16170
+ add(statement.name.text, "type", getLine(sourceFile2, statement));
16171
+ else if (ts11.isEnumDeclaration(statement))
16172
+ add(statement.name.text, "enum", getLine(sourceFile2, statement));
16173
+ else if (ts11.isFunctionDeclaration(statement) && statement.name)
16174
+ add(statement.name.text, "function", getLine(sourceFile2, statement));
16175
+ else if (ts11.isClassDeclaration(statement) && statement.name)
16176
+ add(statement.name.text, "class", getLine(sourceFile2, statement));
16177
+ else if (ts11.isVariableStatement(statement)) {
16070
16178
  for (const declaration of statement.declarationList.declarations) {
16071
- if (!ts11.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
16179
+ if (!ts11.isIdentifier(declaration.name))
16072
16180
  continue;
16073
- declarations.push({ name: declaration.name.text, kind: "function", line: getLine(sourceFile2, declaration) });
16181
+ const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
16182
+ add(declaration.name.text, kind, getLine(sourceFile2, declaration));
16074
16183
  }
16075
16184
  }
16076
16185
  }
16077
16186
  return declarations;
16078
16187
  }
16188
+ function getCompoundComponentNames(sourceFile2) {
16189
+ const names = new Set;
16190
+ for (const statement of sourceFile2.statements) {
16191
+ if (!ts11.isExpressionStatement(statement))
16192
+ continue;
16193
+ const { expression } = statement;
16194
+ if (!ts11.isBinaryExpression(expression) || expression.operatorToken.kind !== ts11.SyntaxKind.EqualsToken)
16195
+ continue;
16196
+ if (!ts11.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
16197
+ continue;
16198
+ names.add(expression.left.name.text);
16199
+ if (ts11.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
16200
+ names.add(expression.right.text);
16201
+ }
16202
+ return names;
16203
+ }
16204
+ function isComponentValueKind(kind) {
16205
+ return kind === "variable" || kind === "function" || kind === "class";
16206
+ }
16207
+ function isRestrictedInternalKind(kind) {
16208
+ return kind === "interface" || kind === "type" || kind === "function";
16209
+ }
16210
+ function isAllowedComponentExport(declaration, isPage) {
16211
+ if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
16212
+ return true;
16213
+ return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
16214
+ }
16215
+ function isPascalCaseName(name) {
16216
+ return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
16217
+ }
16218
+ function isDefaultExportStatement(statement) {
16219
+ if (ts11.isExportAssignment(statement))
16220
+ return !statement.isExportEquals;
16221
+ return !!(ts11.getCombinedModifierFlags(statement) & ts11.ModifierFlags.Default);
16222
+ }
16079
16223
  function getPlaceholderExportWarnings(sourceFile2) {
16080
16224
  if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
16081
16225
  return [];
@@ -17032,6 +17176,31 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
17032
17176
  throw error;
17033
17177
  }
17034
17178
  }
17179
+ async confirmMobileDependencyInstall(installSpecs) {
17180
+ return await confirm3({
17181
+ message: [`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`, "Install them now?"].join(" "),
17182
+ default: true
17183
+ });
17184
+ }
17185
+ async syncMobileDependencies(app, akanConfig3) {
17186
+ const installSpecs = akanConfig3.getMissingMobileDependencySpecs();
17187
+ if (installSpecs.length === 0)
17188
+ return;
17189
+ const shouldInstall = await this.confirmMobileDependencyInstall(installSpecs);
17190
+ if (!shouldInstall)
17191
+ throw new Error(`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`);
17192
+ const spinner2 = app.workspace.spinning("Installing mobile dependencies...");
17193
+ try {
17194
+ await app.workspace.spawn("bun", ["add", ...installSpecs], {
17195
+ stdio: "inherit"
17196
+ });
17197
+ await app.workspace.getPackageJson({ refresh: true });
17198
+ spinner2.succeed("Installed mobile dependencies");
17199
+ } catch (error) {
17200
+ spinner2.fail("Failed to install mobile dependencies");
17201
+ throw error;
17202
+ }
17203
+ }
17035
17204
  async createApplication(appName, workspace, { start = false, libs = [] } = {}) {
17036
17205
  const spinner2 = workspace.spinning("Creating application...");
17037
17206
  const app = await this.applicationRunner.createApplication(appName, workspace, libs);
@@ -17153,6 +17322,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
17153
17322
  noAllowProvisioningUpdates = false
17154
17323
  } = {}) {
17155
17324
  await app.scanSync({ write });
17325
+ await this.syncMobileDependencies(app, await app.getConfig());
17156
17326
  await this.applicationRunner.startIos(app, {
17157
17327
  open,
17158
17328
  operation,
@@ -17181,6 +17351,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
17181
17351
  regenerate = false
17182
17352
  } = {}) {
17183
17353
  await app.scanSync({ write });
17354
+ await this.syncMobileDependencies(app, await app.getConfig());
17184
17355
  await this.applicationRunner.startAndroid(app, {
17185
17356
  open,
17186
17357
  operation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.3.11-rc.9",
3
+ "version": "2.3.12-rc.0",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "2.3.11-rc.9",
37
+ "akanjs": "2.3.12-rc.0",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -12,18 +12,24 @@ interface Options {
12
12
 
13
13
  const sourceFilePattern = /\.(ts|tsx)$/;
14
14
  const excludedFilePattern = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
15
+ // `ui` exports PascalCase names only; `common`/`srvkit`/`webkit` export camelCase names only. Names with
16
+ // dots, underscores, or hyphens (e.g. `foo.helper`, `Globe_Dynamic`, `kebab-case`) match neither and are skipped.
17
+ const pascalCasePattern = /^[A-Z][A-Za-z0-9]*$/;
18
+ const camelCasePattern = /^[a-z][A-Za-z0-9]*$/;
19
+ const nameCasePatternForFacet = (facet: string) => (facet === "ui" ? pascalCasePattern : camelCasePattern);
15
20
 
16
21
  export default async function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict = {}, options: Options = {}) {
17
22
  const { exec, facet } = options;
18
23
  if (!exec || !facet || !(await exec.exists(facet))) return null;
19
24
 
25
+ const nameCasePattern = nameCasePatternForFacet(facet);
20
26
  const { files, dirs } = await exec.getFilesAndDirs(facet);
21
27
  const exportNames = [
22
28
  ...files
23
29
  .filter((filename) => sourceFilePattern.test(filename) && !excludedFilePattern.test(filename))
24
30
  .map((filename) => filename.replace(sourceFilePattern, "")),
25
31
  ...dirs.filter((dirname) => !dirname.startsWith(".")),
26
- ].sort();
32
+ ].filter((name) => nameCasePattern.test(name)).sort();
27
33
 
28
34
  if (exportNames.length === 0) return null;
29
35
  return {
@@ -10,7 +10,7 @@ alwaysApply: true
10
10
  - To test a built artifact locally, run it from the generated app directory with the required Akan runtime environment variables.
11
11
 
12
12
  ```bash
13
- cd dist/apps/<appName> && USE_AKANJS_PKGS=true AKAN_PUBLIC_REPO_NAME=akansoft AKAN_PUBLIC_SERVE_DOMAIN="akamir.com" AKAN_PUBLIC_APP_NAME=seon AKAN_PUBLIC_ENV=local AKAN_PUBLIC_OPERATION_MODE=local SERVER_MODE=federation AKAN_PUBLIC_BASE_PATHS=neul,apptest,seon bun main.js
13
+ cd dist/apps/<appName> && USE_AKANJS_PKGS=true AKAN_PUBLIC_REPO_NAME=akansoft AKAN_PUBLIC_SERVE_DOMAIN="akanjs.com" AKAN_PUBLIC_APP_NAME=seon AKAN_PUBLIC_ENV=local AKAN_PUBLIC_OPERATION_MODE=local SERVER_MODE=federation AKAN_PUBLIC_BASE_PATHS=operator,office,homepage bun main.js
14
14
  ```
15
15
 
16
16
  - Adjust `<appName>`, `AKAN_PUBLIC_APP_NAME`, and `AKAN_PUBLIC_BASE_PATHS` to match the app being tested.
@@ -9,5 +9,5 @@ alwaysApply: false
9
9
  - Use Bun and ESM assumptions from the root `tsconfig.json`.
10
10
  - Prefer path aliases over deep relative imports when crossing package boundaries.
11
11
  - Use `akanjs/*` for framework facets, `@apps/*` for apps, `@libs/*` for shared libs, and `@contract/*` for contract code.
12
- - Respect existing client/server entrypoints such as `@libs/shared/client`, `@libs/shared/server`, `@apps/puffinplace/client`, and `@apps/puffinplace/server`.
12
+ - Respect existing client/server entrypoints such as `@libs/shared/client`, `@libs/shared/server`, `@apps/myapp/client`, and `@apps/myapp/server`.
13
13
  - Let Biome organize imports instead of manually reshuffling unrelated imports.