@akanjs/cli 2.3.11-rc.8 → 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.
- package/incrementalBuilder.proc.js +232 -10
- package/index.js +232 -10
- package/package.json +2 -2
- package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +1 -1
- package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +1 -1
- package/templates/workspaceRoot/biome.json.template +14 -0
|
@@ -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) {
|
|
@@ -15706,13 +15798,14 @@ class AkanQualityScanner {
|
|
|
15706
15798
|
const warnings = [
|
|
15707
15799
|
...this.#scanGlobalQuality(sourceFiles),
|
|
15708
15800
|
...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
|
|
15801
|
+
...sourceFiles.flatMap((sourceFile2) => this.#scanComponentQuality(sourceFile2)),
|
|
15709
15802
|
...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
|
|
15710
15803
|
...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
|
|
15711
15804
|
];
|
|
15712
15805
|
return {
|
|
15713
15806
|
workspaceRoot,
|
|
15714
15807
|
scannedFiles: sourceFiles.length,
|
|
15715
|
-
warnings: warnings.sort(compareWarnings),
|
|
15808
|
+
warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
|
|
15716
15809
|
suggestedRules: SUGGESTED_RULES
|
|
15717
15810
|
};
|
|
15718
15811
|
}
|
|
@@ -15834,6 +15927,49 @@ class AkanQualityScanner {
|
|
|
15834
15927
|
}
|
|
15835
15928
|
return warnings;
|
|
15836
15929
|
}
|
|
15930
|
+
#scanComponentQuality(sourceFile2) {
|
|
15931
|
+
if (!isComponentDeclarationFile(sourceFile2.file))
|
|
15932
|
+
return [];
|
|
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`));
|
|
15939
|
+
const warnings = [];
|
|
15940
|
+
for (const declaration of declarations) {
|
|
15941
|
+
if (declaration.isDefaultExport)
|
|
15942
|
+
continue;
|
|
15943
|
+
if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
|
|
15944
|
+
continue;
|
|
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
|
+
}
|
|
15970
|
+
}
|
|
15971
|
+
return warnings;
|
|
15972
|
+
}
|
|
15837
15973
|
#scanConventionQuality(sourceFile2) {
|
|
15838
15974
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
|
|
15839
15975
|
if (!suffix)
|
|
@@ -15909,6 +16045,8 @@ function formatQualityWarnings(warnings) {
|
|
|
15909
16045
|
if (warning.locations?.length) {
|
|
15910
16046
|
lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
|
|
15911
16047
|
}
|
|
16048
|
+
if (warning.fix)
|
|
16049
|
+
lines.push(` fix: ${warning.fix}`);
|
|
15912
16050
|
return lines;
|
|
15913
16051
|
});
|
|
15914
16052
|
}
|
|
@@ -15917,7 +16055,7 @@ function formatQualityLocation(file, line) {
|
|
|
15917
16055
|
}
|
|
15918
16056
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15919
16057
|
const declarations = [];
|
|
15920
|
-
const
|
|
16058
|
+
const nameExempt = isPageRouteFile(sourceFile2.file) || isUiComponentFile(sourceFile2.file);
|
|
15921
16059
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15922
16060
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15923
16061
|
declarations.push({
|
|
@@ -15926,7 +16064,7 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15926
16064
|
file: sourceFile2.file,
|
|
15927
16065
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15928
16066
|
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15929
|
-
duplicateNameExempt:
|
|
16067
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15930
16068
|
});
|
|
15931
16069
|
}
|
|
15932
16070
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15936,7 +16074,7 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15936
16074
|
file: sourceFile2.file,
|
|
15937
16075
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15938
16076
|
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15939
|
-
duplicateNameExempt:
|
|
16077
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15940
16078
|
});
|
|
15941
16079
|
}
|
|
15942
16080
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15949,7 +16087,7 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15949
16087
|
file: sourceFile2.file,
|
|
15950
16088
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15951
16089
|
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15952
|
-
duplicateNameExempt:
|
|
16090
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15953
16091
|
});
|
|
15954
16092
|
}
|
|
15955
16093
|
}
|
|
@@ -15960,6 +16098,10 @@ function isPageRouteFile(file) {
|
|
|
15960
16098
|
const segments = file.split("/");
|
|
15961
16099
|
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
15962
16100
|
}
|
|
16101
|
+
function isUiComponentFile(file) {
|
|
16102
|
+
const segments = file.split("/");
|
|
16103
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
16104
|
+
}
|
|
15963
16105
|
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
15964
16106
|
if (!isInLibModule(file))
|
|
15965
16107
|
return false;
|
|
@@ -15985,6 +16127,86 @@ function isEnumClassStatement(sourceFile2, statement) {
|
|
|
15985
16127
|
function getExportedClassNames(sourceFile2) {
|
|
15986
16128
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15987
16129
|
}
|
|
16130
|
+
function isComponentDeclarationFile(file) {
|
|
16131
|
+
if (!file.endsWith(".tsx"))
|
|
16132
|
+
return false;
|
|
16133
|
+
const segments = file.split("/");
|
|
16134
|
+
const [root, , area] = segments;
|
|
16135
|
+
if (root !== "apps" && root !== "libs")
|
|
16136
|
+
return false;
|
|
16137
|
+
if (area === "lib" || area === "ui")
|
|
16138
|
+
return true;
|
|
16139
|
+
return root === "apps" && area === "page";
|
|
16140
|
+
}
|
|
16141
|
+
function getComponentFileDeclarations(sourceFile2) {
|
|
16142
|
+
const reExportedNames = new Set;
|
|
16143
|
+
for (const statement of sourceFile2.statements) {
|
|
16144
|
+
if (ts11.isExportDeclaration(statement) && statement.exportClause && ts11.isNamedExports(statement.exportClause)) {
|
|
16145
|
+
for (const element of statement.exportClause.elements)
|
|
16146
|
+
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
16147
|
+
}
|
|
16148
|
+
}
|
|
16149
|
+
const declarations = [];
|
|
16150
|
+
for (const statement of sourceFile2.statements) {
|
|
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)) {
|
|
16165
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
16166
|
+
if (!ts11.isIdentifier(declaration.name))
|
|
16167
|
+
continue;
|
|
16168
|
+
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
16169
|
+
add(declaration.name.text, kind, getLine(sourceFile2, declaration));
|
|
16170
|
+
}
|
|
16171
|
+
}
|
|
16172
|
+
}
|
|
16173
|
+
return declarations;
|
|
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
|
+
}
|
|
15988
16210
|
function getPlaceholderExportWarnings(sourceFile2) {
|
|
15989
16211
|
if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
|
|
15990
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) {
|
|
@@ -15704,13 +15796,14 @@ class AkanQualityScanner {
|
|
|
15704
15796
|
const warnings = [
|
|
15705
15797
|
...this.#scanGlobalQuality(sourceFiles),
|
|
15706
15798
|
...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
|
|
15799
|
+
...sourceFiles.flatMap((sourceFile2) => this.#scanComponentQuality(sourceFile2)),
|
|
15707
15800
|
...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
|
|
15708
15801
|
...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
|
|
15709
15802
|
];
|
|
15710
15803
|
return {
|
|
15711
15804
|
workspaceRoot,
|
|
15712
15805
|
scannedFiles: sourceFiles.length,
|
|
15713
|
-
warnings: warnings.sort(compareWarnings),
|
|
15806
|
+
warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
|
|
15714
15807
|
suggestedRules: SUGGESTED_RULES
|
|
15715
15808
|
};
|
|
15716
15809
|
}
|
|
@@ -15832,6 +15925,49 @@ class AkanQualityScanner {
|
|
|
15832
15925
|
}
|
|
15833
15926
|
return warnings;
|
|
15834
15927
|
}
|
|
15928
|
+
#scanComponentQuality(sourceFile2) {
|
|
15929
|
+
if (!isComponentDeclarationFile(sourceFile2.file))
|
|
15930
|
+
return [];
|
|
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`));
|
|
15937
|
+
const warnings = [];
|
|
15938
|
+
for (const declaration of declarations) {
|
|
15939
|
+
if (declaration.isDefaultExport)
|
|
15940
|
+
continue;
|
|
15941
|
+
if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
|
|
15942
|
+
continue;
|
|
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
|
+
}
|
|
15968
|
+
}
|
|
15969
|
+
return warnings;
|
|
15970
|
+
}
|
|
15835
15971
|
#scanConventionQuality(sourceFile2) {
|
|
15836
15972
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
|
|
15837
15973
|
if (!suffix)
|
|
@@ -15907,6 +16043,8 @@ function formatQualityWarnings(warnings) {
|
|
|
15907
16043
|
if (warning.locations?.length) {
|
|
15908
16044
|
lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
|
|
15909
16045
|
}
|
|
16046
|
+
if (warning.fix)
|
|
16047
|
+
lines.push(` fix: ${warning.fix}`);
|
|
15910
16048
|
return lines;
|
|
15911
16049
|
});
|
|
15912
16050
|
}
|
|
@@ -15915,7 +16053,7 @@ function formatQualityLocation(file, line) {
|
|
|
15915
16053
|
}
|
|
15916
16054
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15917
16055
|
const declarations = [];
|
|
15918
|
-
const
|
|
16056
|
+
const nameExempt = isPageRouteFile(sourceFile2.file) || isUiComponentFile(sourceFile2.file);
|
|
15919
16057
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15920
16058
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15921
16059
|
declarations.push({
|
|
@@ -15924,7 +16062,7 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15924
16062
|
file: sourceFile2.file,
|
|
15925
16063
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15926
16064
|
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15927
|
-
duplicateNameExempt:
|
|
16065
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15928
16066
|
});
|
|
15929
16067
|
}
|
|
15930
16068
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15934,7 +16072,7 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15934
16072
|
file: sourceFile2.file,
|
|
15935
16073
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15936
16074
|
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15937
|
-
duplicateNameExempt:
|
|
16075
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15938
16076
|
});
|
|
15939
16077
|
}
|
|
15940
16078
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15947,7 +16085,7 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15947
16085
|
file: sourceFile2.file,
|
|
15948
16086
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15949
16087
|
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15950
|
-
duplicateNameExempt:
|
|
16088
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15951
16089
|
});
|
|
15952
16090
|
}
|
|
15953
16091
|
}
|
|
@@ -15958,6 +16096,10 @@ function isPageRouteFile(file) {
|
|
|
15958
16096
|
const segments = file.split("/");
|
|
15959
16097
|
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
15960
16098
|
}
|
|
16099
|
+
function isUiComponentFile(file) {
|
|
16100
|
+
const segments = file.split("/");
|
|
16101
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
16102
|
+
}
|
|
15961
16103
|
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
15962
16104
|
if (!isInLibModule(file))
|
|
15963
16105
|
return false;
|
|
@@ -15983,6 +16125,86 @@ function isEnumClassStatement(sourceFile2, statement) {
|
|
|
15983
16125
|
function getExportedClassNames(sourceFile2) {
|
|
15984
16126
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15985
16127
|
}
|
|
16128
|
+
function isComponentDeclarationFile(file) {
|
|
16129
|
+
if (!file.endsWith(".tsx"))
|
|
16130
|
+
return false;
|
|
16131
|
+
const segments = file.split("/");
|
|
16132
|
+
const [root, , area] = segments;
|
|
16133
|
+
if (root !== "apps" && root !== "libs")
|
|
16134
|
+
return false;
|
|
16135
|
+
if (area === "lib" || area === "ui")
|
|
16136
|
+
return true;
|
|
16137
|
+
return root === "apps" && area === "page";
|
|
16138
|
+
}
|
|
16139
|
+
function getComponentFileDeclarations(sourceFile2) {
|
|
16140
|
+
const reExportedNames = new Set;
|
|
16141
|
+
for (const statement of sourceFile2.statements) {
|
|
16142
|
+
if (ts11.isExportDeclaration(statement) && statement.exportClause && ts11.isNamedExports(statement.exportClause)) {
|
|
16143
|
+
for (const element of statement.exportClause.elements)
|
|
16144
|
+
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
16147
|
+
const declarations = [];
|
|
16148
|
+
for (const statement of sourceFile2.statements) {
|
|
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)) {
|
|
16163
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
16164
|
+
if (!ts11.isIdentifier(declaration.name))
|
|
16165
|
+
continue;
|
|
16166
|
+
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
16167
|
+
add(declaration.name.text, kind, getLine(sourceFile2, declaration));
|
|
16168
|
+
}
|
|
16169
|
+
}
|
|
16170
|
+
}
|
|
16171
|
+
return declarations;
|
|
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
|
+
}
|
|
15986
16208
|
function getPlaceholderExportWarnings(sourceFile2) {
|
|
15987
16209
|
if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
|
|
15988
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.
|
|
@@ -132,6 +132,20 @@
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
"overrides": [
|
|
135
|
+
{
|
|
136
|
+
"includes": [
|
|
137
|
+
"apps/**/*.ts",
|
|
138
|
+
"apps/**/*.tsx",
|
|
139
|
+
"libs/**/*.ts",
|
|
140
|
+
"libs/**/*.tsx",
|
|
141
|
+
"!**/*.test.ts",
|
|
142
|
+
"!**/*.test.tsx",
|
|
143
|
+
"!**/*.spec.ts",
|
|
144
|
+
"!**/*.spec.tsx",
|
|
145
|
+
"!**/common/**"
|
|
146
|
+
],
|
|
147
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-throw-raw-error.grit"]
|
|
148
|
+
},
|
|
135
149
|
{
|
|
136
150
|
"includes": ["**/page/**/*.ts", "**/page/**/*.tsx", "**/*.Unit.tsx", "**/*.View.tsx"],
|
|
137
151
|
"plugins": [
|