@akanjs/cli 2.3.11-rc.9 → 2.3.11

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.
@@ -2691,6 +2691,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
2691
2691
  throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
2692
2692
  }
2693
2693
  }
2694
+ function validateOverridesSourceExports(source, filePath) {
2695
+ const sourceFile = ts3.createSourceFile(filePath, source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
2696
+ const fail = (message) => {
2697
+ throw new Error(`[route-convention] ${message}: ${filePath}`);
2698
+ };
2699
+ let defaultOverride = null;
2700
+ for (const statement of sourceFile.statements) {
2701
+ if (ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression))
2702
+ continue;
2703
+ if (ts3.isImportDeclaration(statement))
2704
+ continue;
2705
+ if (ts3.isInterfaceDeclaration(statement) || ts3.isTypeAliasDeclaration(statement))
2706
+ continue;
2707
+ if (ts3.isExportAssignment(statement) && !statement.isExportEquals) {
2708
+ defaultOverride = statement;
2709
+ continue;
2710
+ }
2711
+ fail(`_overrides.tsx may only contain imports and a single "export default override({ ... })"`);
2712
+ }
2713
+ if (!defaultOverride)
2714
+ fail(`_overrides.tsx must "export default override({ ... })"`);
2715
+ const expression = defaultOverride.expression;
2716
+ if (!ts3.isCallExpression(expression) || !ts3.isIdentifier(expression.expression) || expression.expression.text !== "override")
2717
+ fail(`_overrides.tsx default export must be a call to "override", e.g. "export default override({ Modal: BrandModal })"`);
2718
+ }
2694
2719
 
2695
2720
  class Executor {
2696
2721
  static verbose = false;
@@ -3714,7 +3739,11 @@ class AppExecutor extends SysExecutor {
3714
3739
  throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
3715
3740
  }
3716
3741
  const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
3717
- validateRouteSourceExports(await Bun.file(absPath).text(), absPath, parsed.kind, { rootLayout: isRootLayout });
3742
+ const routeSource = await Bun.file(absPath).text();
3743
+ if (parsed.kind === "overrides")
3744
+ validateOverridesSourceExports(routeSource, absPath);
3745
+ else
3746
+ validateRouteSourceExports(routeSource, absPath, parsed.kind, { rootLayout: isRootLayout });
3718
3747
  pageKeys.push(key);
3719
3748
  }
3720
3749
  pageKeys.sort();
@@ -8822,6 +8851,25 @@ async function appHasStModule(appCwdPath) {
8822
8851
  }
8823
8852
  var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
8824
8853
  var IMPLICIT_DICT_DIR = path13.join(".akan", "generated", "dict");
8854
+ var IMPLICIT_OVERRIDES_DIR = path13.join(".akan", "generated", "overrides");
8855
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
8856
+ async function writeGeneratedOverridesLayoutFile(opts) {
8857
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
8858
+ const absPath = path13.join(path13.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
8859
+ const userRel = path13.relative(path13.dirname(absPath), opts.userAbsPath).split(path13.sep).join("/");
8860
+ const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
8861
+ const source2 = `"use client";
8862
+ import { UiOverrideProvider } from "akanjs/ui";
8863
+ import { createElement, type ReactNode } from "react";
8864
+ import value from ${JSON.stringify(userSpecifier)};
8865
+
8866
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
8867
+ return createElement(UiOverrideProvider, { value }, children);
8868
+ }
8869
+ `;
8870
+ await Bun.write(absPath, source2);
8871
+ return absPath;
8872
+ }
8825
8873
  function getRootBoundarySegments(key) {
8826
8874
  const match = LAYOUT_KEY_RE.exec(key);
8827
8875
  if (!match)
@@ -9014,9 +9062,17 @@ async function resolveSsrPageEntries(opts) {
9014
9062
  const segments = getRootBoundarySegments(key);
9015
9063
  return segments !== null && isRootBoundarySegments(segments, basePaths);
9016
9064
  }));
9017
- const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) => ({
9018
- key,
9019
- moduleAbsPath: path13.resolve(absPageDir, key)
9065
+ const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
9066
+ const userAbsPath = path13.resolve(absPageDir, key);
9067
+ if (OVERRIDES_KEY_RE.test(key)) {
9068
+ const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
9069
+ appCwdPath: opts.appCwdPath,
9070
+ key,
9071
+ userAbsPath
9072
+ });
9073
+ return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
9074
+ }
9075
+ return { key, moduleAbsPath: userAbsPath };
9020
9076
  }));
9021
9077
  const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
9022
9078
  key: implicitRootLayoutKey(boundary.segments),
@@ -9048,7 +9104,7 @@ function computeRouteSeedIndex(pageEntries) {
9048
9104
  for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
9049
9105
  const parsed = parseRouteModuleKey2(key);
9050
9106
  const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
9051
- if (parsed.kind === "layout") {
9107
+ if (parsed.kind === "layout" || parsed.kind === "overrides") {
9052
9108
  const prefix = parsed.routeSegments.join("/");
9053
9109
  const prev = layoutsByPrefix.get(prefix) ?? [];
9054
9110
  layoutsByPrefix.set(prefix, [...prev, ...files]);
@@ -15698,6 +15754,42 @@ var CONVENTION_SUFFIXES = [
15698
15754
  ".signal.ts",
15699
15755
  ".store.ts"
15700
15756
  ];
15757
+ var PAGE_RESERVED_EXPORTS = new Set([
15758
+ "pageConfig",
15759
+ "head",
15760
+ "metadata",
15761
+ "generateHead",
15762
+ "generateMetadata",
15763
+ "fonts",
15764
+ "manifest",
15765
+ "theme",
15766
+ "reconnect",
15767
+ "wsConnect",
15768
+ "layoutStyle",
15769
+ "gaTrackingId"
15770
+ ]);
15771
+ var RULE_FIXES = {
15772
+ "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.",
15773
+ "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
15774
+ "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
15775
+ "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
15776
+ "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
15777
+ "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
15778
+ "akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
15779
+ "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
15780
+ "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
15781
+ "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
15782
+ "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`.",
15783
+ "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.",
15784
+ "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
15785
+ "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
15786
+ "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."
15787
+ };
15788
+ function getRuleFix(rule) {
15789
+ if (rule.startsWith("akan.convention"))
15790
+ return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
15791
+ return RULE_FIXES[rule];
15792
+ }
15701
15793
 
15702
15794
  class AkanQualityScanner {
15703
15795
  async scan(workspaceRoot) {
@@ -15713,7 +15805,7 @@ class AkanQualityScanner {
15713
15805
  return {
15714
15806
  workspaceRoot,
15715
15807
  scannedFiles: sourceFiles.length,
15716
- warnings: warnings.sort(compareWarnings),
15808
+ warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
15717
15809
  suggestedRules: SUGGESTED_RULES
15718
15810
  };
15719
15811
  }
@@ -15838,22 +15930,43 @@ class AkanQualityScanner {
15838
15930
  #scanComponentQuality(sourceFile2) {
15839
15931
  if (!isComponentDeclarationFile(sourceFile2.file))
15840
15932
  return [];
15841
- const exportedNames = getExportedDeclarationNames(sourceFile2.sourceFile);
15842
- const allowedInternalInterfaces = new Set([...exportedNames].map((name) => `${name}Props`));
15933
+ const isPage = isPageRouteFile(sourceFile2.file);
15934
+ const declarations = getComponentFileDeclarations(sourceFile2.sourceFile);
15935
+ const compoundComponentNames = getCompoundComponentNames(sourceFile2.sourceFile);
15936
+ const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
15937
+ const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
15938
+ const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
15843
15939
  const warnings = [];
15844
- for (const declaration of getInternalTypeOrFunctionDeclarations(sourceFile2.sourceFile)) {
15845
- if (exportedNames.has(declaration.name))
15940
+ for (const declaration of declarations) {
15941
+ if (declaration.isDefaultExport)
15846
15942
  continue;
15847
- if (declaration.kind === "interface" && allowedInternalInterfaces.has(declaration.name))
15943
+ if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
15848
15944
  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
- });
15945
+ if (declaration.exported) {
15946
+ if (isAllowedComponentExport(declaration, isPage))
15947
+ continue;
15948
+ warnings.push({
15949
+ rule: "akan.file.component-export",
15950
+ scope: "file",
15951
+ severity: "warning",
15952
+ file: sourceFile2.file,
15953
+ line: declaration.line,
15954
+ message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
15955
+ });
15956
+ continue;
15957
+ }
15958
+ if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
15959
+ continue;
15960
+ if (isRestrictedInternalKind(declaration.kind)) {
15961
+ warnings.push({
15962
+ rule: "akan.file.component-internal-declaration",
15963
+ scope: "file",
15964
+ severity: "warning",
15965
+ file: sourceFile2.file,
15966
+ line: declaration.line,
15967
+ message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
15968
+ });
15969
+ }
15857
15970
  }
15858
15971
  return warnings;
15859
15972
  }
@@ -15932,6 +16045,8 @@ function formatQualityWarnings(warnings) {
15932
16045
  if (warning.locations?.length) {
15933
16046
  lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15934
16047
  }
16048
+ if (warning.fix)
16049
+ lines.push(` fix: ${warning.fix}`);
15935
16050
  return lines;
15936
16051
  });
15937
16052
  }
@@ -16023,61 +16138,75 @@ function isComponentDeclarationFile(file) {
16023
16138
  return true;
16024
16139
  return root === "apps" && area === "page";
16025
16140
  }
16026
- function getExportedDeclarationNames(sourceFile2) {
16027
- const names = new Set;
16141
+ function getComponentFileDeclarations(sourceFile2) {
16142
+ const reExportedNames = new Set;
16028
16143
  for (const statement of sourceFile2.statements) {
16029
16144
  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;
16145
+ for (const element of statement.exportClause.elements)
16146
+ reExportedNames.add((element.propertyName ?? element.name).text);
16036
16147
  }
16037
- if (!isExported(statement))
16038
- continue;
16039
- for (const name of getStatementDeclarationNames(statement))
16040
- names.add(name);
16041
- }
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
16148
  }
16058
- return [];
16059
- }
16060
- function getInternalTypeOrFunctionDeclarations(sourceFile2) {
16061
16149
  const declarations = [];
16062
16150
  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)) {
16151
+ const isDefaultExport = isDefaultExportStatement(statement);
16152
+ const inlineExported = isExported(statement);
16153
+ const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
16154
+ if (ts11.isInterfaceDeclaration(statement))
16155
+ add(statement.name.text, "interface", getLine(sourceFile2, statement));
16156
+ else if (ts11.isTypeAliasDeclaration(statement))
16157
+ add(statement.name.text, "type", getLine(sourceFile2, statement));
16158
+ else if (ts11.isEnumDeclaration(statement))
16159
+ add(statement.name.text, "enum", getLine(sourceFile2, statement));
16160
+ else if (ts11.isFunctionDeclaration(statement) && statement.name)
16161
+ add(statement.name.text, "function", getLine(sourceFile2, statement));
16162
+ else if (ts11.isClassDeclaration(statement) && statement.name)
16163
+ add(statement.name.text, "class", getLine(sourceFile2, statement));
16164
+ else if (ts11.isVariableStatement(statement)) {
16072
16165
  for (const declaration of statement.declarationList.declarations) {
16073
- if (!ts11.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
16166
+ if (!ts11.isIdentifier(declaration.name))
16074
16167
  continue;
16075
- declarations.push({ name: declaration.name.text, kind: "function", line: getLine(sourceFile2, declaration) });
16168
+ const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
16169
+ add(declaration.name.text, kind, getLine(sourceFile2, declaration));
16076
16170
  }
16077
16171
  }
16078
16172
  }
16079
16173
  return declarations;
16080
16174
  }
16175
+ function getCompoundComponentNames(sourceFile2) {
16176
+ const names = new Set;
16177
+ for (const statement of sourceFile2.statements) {
16178
+ if (!ts11.isExpressionStatement(statement))
16179
+ continue;
16180
+ const { expression } = statement;
16181
+ if (!ts11.isBinaryExpression(expression) || expression.operatorToken.kind !== ts11.SyntaxKind.EqualsToken)
16182
+ continue;
16183
+ if (!ts11.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
16184
+ continue;
16185
+ names.add(expression.left.name.text);
16186
+ if (ts11.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
16187
+ names.add(expression.right.text);
16188
+ }
16189
+ return names;
16190
+ }
16191
+ function isComponentValueKind(kind) {
16192
+ return kind === "variable" || kind === "function" || kind === "class";
16193
+ }
16194
+ function isRestrictedInternalKind(kind) {
16195
+ return kind === "interface" || kind === "type" || kind === "function";
16196
+ }
16197
+ function isAllowedComponentExport(declaration, isPage) {
16198
+ if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
16199
+ return true;
16200
+ return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
16201
+ }
16202
+ function isPascalCaseName(name) {
16203
+ return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
16204
+ }
16205
+ function isDefaultExportStatement(statement) {
16206
+ if (ts11.isExportAssignment(statement))
16207
+ return !statement.isExportEquals;
16208
+ return !!(ts11.getCombinedModifierFlags(statement) & ts11.ModifierFlags.Default);
16209
+ }
16081
16210
  function getPlaceholderExportWarnings(sourceFile2) {
16082
16211
  if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
16083
16212
  return [];
package/index.js CHANGED
@@ -2689,6 +2689,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
2689
2689
  throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
2690
2690
  }
2691
2691
  }
2692
+ function validateOverridesSourceExports(source, filePath) {
2693
+ const sourceFile = ts3.createSourceFile(filePath, source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
2694
+ const fail = (message) => {
2695
+ throw new Error(`[route-convention] ${message}: ${filePath}`);
2696
+ };
2697
+ let defaultOverride = null;
2698
+ for (const statement of sourceFile.statements) {
2699
+ if (ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression))
2700
+ continue;
2701
+ if (ts3.isImportDeclaration(statement))
2702
+ continue;
2703
+ if (ts3.isInterfaceDeclaration(statement) || ts3.isTypeAliasDeclaration(statement))
2704
+ continue;
2705
+ if (ts3.isExportAssignment(statement) && !statement.isExportEquals) {
2706
+ defaultOverride = statement;
2707
+ continue;
2708
+ }
2709
+ fail(`_overrides.tsx may only contain imports and a single "export default override({ ... })"`);
2710
+ }
2711
+ if (!defaultOverride)
2712
+ fail(`_overrides.tsx must "export default override({ ... })"`);
2713
+ const expression = defaultOverride.expression;
2714
+ if (!ts3.isCallExpression(expression) || !ts3.isIdentifier(expression.expression) || expression.expression.text !== "override")
2715
+ fail(`_overrides.tsx default export must be a call to "override", e.g. "export default override({ Modal: BrandModal })"`);
2716
+ }
2692
2717
 
2693
2718
  class Executor {
2694
2719
  static verbose = false;
@@ -3712,7 +3737,11 @@ class AppExecutor extends SysExecutor {
3712
3737
  throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
3713
3738
  }
3714
3739
  const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
3715
- validateRouteSourceExports(await Bun.file(absPath).text(), absPath, parsed.kind, { rootLayout: isRootLayout });
3740
+ const routeSource = await Bun.file(absPath).text();
3741
+ if (parsed.kind === "overrides")
3742
+ validateOverridesSourceExports(routeSource, absPath);
3743
+ else
3744
+ validateRouteSourceExports(routeSource, absPath, parsed.kind, { rootLayout: isRootLayout });
3716
3745
  pageKeys.push(key);
3717
3746
  }
3718
3747
  pageKeys.sort();
@@ -8820,6 +8849,25 @@ async function appHasStModule(appCwdPath) {
8820
8849
  }
8821
8850
  var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
8822
8851
  var IMPLICIT_DICT_DIR = path13.join(".akan", "generated", "dict");
8852
+ var IMPLICIT_OVERRIDES_DIR = path13.join(".akan", "generated", "overrides");
8853
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
8854
+ async function writeGeneratedOverridesLayoutFile(opts) {
8855
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
8856
+ const absPath = path13.join(path13.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
8857
+ const userRel = path13.relative(path13.dirname(absPath), opts.userAbsPath).split(path13.sep).join("/");
8858
+ const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
8859
+ const source2 = `"use client";
8860
+ import { UiOverrideProvider } from "akanjs/ui";
8861
+ import { createElement, type ReactNode } from "react";
8862
+ import value from ${JSON.stringify(userSpecifier)};
8863
+
8864
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
8865
+ return createElement(UiOverrideProvider, { value }, children);
8866
+ }
8867
+ `;
8868
+ await Bun.write(absPath, source2);
8869
+ return absPath;
8870
+ }
8823
8871
  function getRootBoundarySegments(key) {
8824
8872
  const match = LAYOUT_KEY_RE.exec(key);
8825
8873
  if (!match)
@@ -9012,9 +9060,17 @@ async function resolveSsrPageEntries(opts) {
9012
9060
  const segments = getRootBoundarySegments(key);
9013
9061
  return segments !== null && isRootBoundarySegments(segments, basePaths);
9014
9062
  }));
9015
- const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) => ({
9016
- key,
9017
- moduleAbsPath: path13.resolve(absPageDir, key)
9063
+ const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
9064
+ const userAbsPath = path13.resolve(absPageDir, key);
9065
+ if (OVERRIDES_KEY_RE.test(key)) {
9066
+ const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
9067
+ appCwdPath: opts.appCwdPath,
9068
+ key,
9069
+ userAbsPath
9070
+ });
9071
+ return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
9072
+ }
9073
+ return { key, moduleAbsPath: userAbsPath };
9018
9074
  }));
9019
9075
  const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
9020
9076
  key: implicitRootLayoutKey(boundary.segments),
@@ -9046,7 +9102,7 @@ function computeRouteSeedIndex(pageEntries) {
9046
9102
  for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
9047
9103
  const parsed = parseRouteModuleKey2(key);
9048
9104
  const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
9049
- if (parsed.kind === "layout") {
9105
+ if (parsed.kind === "layout" || parsed.kind === "overrides") {
9050
9106
  const prefix = parsed.routeSegments.join("/");
9051
9107
  const prev = layoutsByPrefix.get(prefix) ?? [];
9052
9108
  layoutsByPrefix.set(prefix, [...prev, ...files]);
@@ -15696,6 +15752,42 @@ var CONVENTION_SUFFIXES = [
15696
15752
  ".signal.ts",
15697
15753
  ".store.ts"
15698
15754
  ];
15755
+ var PAGE_RESERVED_EXPORTS = new Set([
15756
+ "pageConfig",
15757
+ "head",
15758
+ "metadata",
15759
+ "generateHead",
15760
+ "generateMetadata",
15761
+ "fonts",
15762
+ "manifest",
15763
+ "theme",
15764
+ "reconnect",
15765
+ "wsConnect",
15766
+ "layoutStyle",
15767
+ "gaTrackingId"
15768
+ ]);
15769
+ var RULE_FIXES = {
15770
+ "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.",
15771
+ "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
15772
+ "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
15773
+ "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
15774
+ "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
15775
+ "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
15776
+ "akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
15777
+ "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
15778
+ "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
15779
+ "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
15780
+ "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`.",
15781
+ "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.",
15782
+ "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
15783
+ "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
15784
+ "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."
15785
+ };
15786
+ function getRuleFix(rule) {
15787
+ if (rule.startsWith("akan.convention"))
15788
+ return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
15789
+ return RULE_FIXES[rule];
15790
+ }
15699
15791
 
15700
15792
  class AkanQualityScanner {
15701
15793
  async scan(workspaceRoot) {
@@ -15711,7 +15803,7 @@ class AkanQualityScanner {
15711
15803
  return {
15712
15804
  workspaceRoot,
15713
15805
  scannedFiles: sourceFiles.length,
15714
- warnings: warnings.sort(compareWarnings),
15806
+ warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
15715
15807
  suggestedRules: SUGGESTED_RULES
15716
15808
  };
15717
15809
  }
@@ -15836,22 +15928,43 @@ class AkanQualityScanner {
15836
15928
  #scanComponentQuality(sourceFile2) {
15837
15929
  if (!isComponentDeclarationFile(sourceFile2.file))
15838
15930
  return [];
15839
- const exportedNames = getExportedDeclarationNames(sourceFile2.sourceFile);
15840
- const allowedInternalInterfaces = new Set([...exportedNames].map((name) => `${name}Props`));
15931
+ const isPage = isPageRouteFile(sourceFile2.file);
15932
+ const declarations = getComponentFileDeclarations(sourceFile2.sourceFile);
15933
+ const compoundComponentNames = getCompoundComponentNames(sourceFile2.sourceFile);
15934
+ const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
15935
+ const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
15936
+ const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
15841
15937
  const warnings = [];
15842
- for (const declaration of getInternalTypeOrFunctionDeclarations(sourceFile2.sourceFile)) {
15843
- if (exportedNames.has(declaration.name))
15938
+ for (const declaration of declarations) {
15939
+ if (declaration.isDefaultExport)
15844
15940
  continue;
15845
- if (declaration.kind === "interface" && allowedInternalInterfaces.has(declaration.name))
15941
+ if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
15846
15942
  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
- });
15943
+ if (declaration.exported) {
15944
+ if (isAllowedComponentExport(declaration, isPage))
15945
+ continue;
15946
+ warnings.push({
15947
+ rule: "akan.file.component-export",
15948
+ scope: "file",
15949
+ severity: "warning",
15950
+ file: sourceFile2.file,
15951
+ line: declaration.line,
15952
+ message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
15953
+ });
15954
+ continue;
15955
+ }
15956
+ if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
15957
+ continue;
15958
+ if (isRestrictedInternalKind(declaration.kind)) {
15959
+ warnings.push({
15960
+ rule: "akan.file.component-internal-declaration",
15961
+ scope: "file",
15962
+ severity: "warning",
15963
+ file: sourceFile2.file,
15964
+ line: declaration.line,
15965
+ message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
15966
+ });
15967
+ }
15855
15968
  }
15856
15969
  return warnings;
15857
15970
  }
@@ -15930,6 +16043,8 @@ function formatQualityWarnings(warnings) {
15930
16043
  if (warning.locations?.length) {
15931
16044
  lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15932
16045
  }
16046
+ if (warning.fix)
16047
+ lines.push(` fix: ${warning.fix}`);
15933
16048
  return lines;
15934
16049
  });
15935
16050
  }
@@ -16021,61 +16136,75 @@ function isComponentDeclarationFile(file) {
16021
16136
  return true;
16022
16137
  return root === "apps" && area === "page";
16023
16138
  }
16024
- function getExportedDeclarationNames(sourceFile2) {
16025
- const names = new Set;
16139
+ function getComponentFileDeclarations(sourceFile2) {
16140
+ const reExportedNames = new Set;
16026
16141
  for (const statement of sourceFile2.statements) {
16027
16142
  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;
16143
+ for (const element of statement.exportClause.elements)
16144
+ reExportedNames.add((element.propertyName ?? element.name).text);
16034
16145
  }
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
16146
  }
16056
- return [];
16057
- }
16058
- function getInternalTypeOrFunctionDeclarations(sourceFile2) {
16059
16147
  const declarations = [];
16060
16148
  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)) {
16149
+ const isDefaultExport = isDefaultExportStatement(statement);
16150
+ const inlineExported = isExported(statement);
16151
+ const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
16152
+ if (ts11.isInterfaceDeclaration(statement))
16153
+ add(statement.name.text, "interface", getLine(sourceFile2, statement));
16154
+ else if (ts11.isTypeAliasDeclaration(statement))
16155
+ add(statement.name.text, "type", getLine(sourceFile2, statement));
16156
+ else if (ts11.isEnumDeclaration(statement))
16157
+ add(statement.name.text, "enum", getLine(sourceFile2, statement));
16158
+ else if (ts11.isFunctionDeclaration(statement) && statement.name)
16159
+ add(statement.name.text, "function", getLine(sourceFile2, statement));
16160
+ else if (ts11.isClassDeclaration(statement) && statement.name)
16161
+ add(statement.name.text, "class", getLine(sourceFile2, statement));
16162
+ else if (ts11.isVariableStatement(statement)) {
16070
16163
  for (const declaration of statement.declarationList.declarations) {
16071
- if (!ts11.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
16164
+ if (!ts11.isIdentifier(declaration.name))
16072
16165
  continue;
16073
- declarations.push({ name: declaration.name.text, kind: "function", line: getLine(sourceFile2, declaration) });
16166
+ const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
16167
+ add(declaration.name.text, kind, getLine(sourceFile2, declaration));
16074
16168
  }
16075
16169
  }
16076
16170
  }
16077
16171
  return declarations;
16078
16172
  }
16173
+ function getCompoundComponentNames(sourceFile2) {
16174
+ const names = new Set;
16175
+ for (const statement of sourceFile2.statements) {
16176
+ if (!ts11.isExpressionStatement(statement))
16177
+ continue;
16178
+ const { expression } = statement;
16179
+ if (!ts11.isBinaryExpression(expression) || expression.operatorToken.kind !== ts11.SyntaxKind.EqualsToken)
16180
+ continue;
16181
+ if (!ts11.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
16182
+ continue;
16183
+ names.add(expression.left.name.text);
16184
+ if (ts11.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
16185
+ names.add(expression.right.text);
16186
+ }
16187
+ return names;
16188
+ }
16189
+ function isComponentValueKind(kind) {
16190
+ return kind === "variable" || kind === "function" || kind === "class";
16191
+ }
16192
+ function isRestrictedInternalKind(kind) {
16193
+ return kind === "interface" || kind === "type" || kind === "function";
16194
+ }
16195
+ function isAllowedComponentExport(declaration, isPage) {
16196
+ if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
16197
+ return true;
16198
+ return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
16199
+ }
16200
+ function isPascalCaseName(name) {
16201
+ return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
16202
+ }
16203
+ function isDefaultExportStatement(statement) {
16204
+ if (ts11.isExportAssignment(statement))
16205
+ return !statement.isExportEquals;
16206
+ return !!(ts11.getCombinedModifierFlags(statement) & ts11.ModifierFlags.Default);
16207
+ }
16079
16208
  function getPlaceholderExportWarnings(sourceFile2) {
16080
16209
  if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
16081
16210
  return [];
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.11",
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.11",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -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/<% appName %>/client`, and `@apps/<% appName %>/server`.
13
13
  - Let Biome organize imports instead of manually reshuffling unrelated imports.