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