@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
|
-
|
|
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
|
-
|
|
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
|
|
15842
|
-
const
|
|
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
|
|
15845
|
-
if (
|
|
15940
|
+
for (const declaration of declarations) {
|
|
15941
|
+
if (declaration.isDefaultExport)
|
|
15846
15942
|
continue;
|
|
15847
|
-
if (declaration.kind === "interface" &&
|
|
15943
|
+
if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
|
|
15848
15944
|
continue;
|
|
15849
|
-
|
|
15850
|
-
|
|
15851
|
-
|
|
15852
|
-
|
|
15853
|
-
|
|
15854
|
-
|
|
15855
|
-
|
|
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
|
|
16027
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
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)
|
|
16166
|
+
if (!ts11.isIdentifier(declaration.name))
|
|
16074
16167
|
continue;
|
|
16075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
15840
|
-
const
|
|
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
|
|
15843
|
-
if (
|
|
15938
|
+
for (const declaration of declarations) {
|
|
15939
|
+
if (declaration.isDefaultExport)
|
|
15844
15940
|
continue;
|
|
15845
|
-
if (declaration.kind === "interface" &&
|
|
15941
|
+
if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
|
|
15846
15942
|
continue;
|
|
15847
|
-
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
15851
|
-
|
|
15852
|
-
|
|
15853
|
-
|
|
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
|
|
16025
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
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)
|
|
16164
|
+
if (!ts11.isIdentifier(declaration.name))
|
|
16072
16165
|
continue;
|
|
16073
|
-
|
|
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
|
|
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
|
|
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="
|
|
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
|
|
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.
|