@akanjs/cli 2.3.11-rc.1 → 2.3.11-rc.10
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 +678 -57
- package/index.js +1073 -100
- package/package.json +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -3
- package/templates/appSample/lib/task/task.service.ts +1 -1
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -2
- package/templates/crudPages/[__model__Id]/page.tsx +3 -3
- package/templates/crudPages/page.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +3 -3
- package/templates/module/__Model__.Template.tsx +4 -5
- package/templates/module/__Model__.Unit.tsx +1 -1
- package/templates/module/__Model__.View.tsx +1 -1
- package/templates/module/__Model__.Zone.tsx +5 -3
- package/templates/module/__model__.constant.ts +2 -1
- package/templates/module/__model__.dictionary.ts +3 -1
- package/templates/workspaceRoot/.cursor/rules/akan.mdc.template +4 -33
- 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/.gitignore.template +4 -1
- package/templates/workspaceRoot/AGENTS.md.template +143 -8
- package/templates/workspaceRoot/biome.json.template +20 -1
- package/templates/workspaceRoot/docs/GENERATED.md.template +0 -1
- package/templates/workspaceRoot/package.json.template +3 -3
|
@@ -666,6 +666,7 @@ import {
|
|
|
666
666
|
import { readFileSync as readFileSync3 } from "fs";
|
|
667
667
|
import { copyFile, mkdir as mkdir2, readdir as readDirEntries, stat as stat2 } from "fs/promises";
|
|
668
668
|
import path7 from "path";
|
|
669
|
+
import { pathToFileURL } from "url";
|
|
669
670
|
import {
|
|
670
671
|
capitalize,
|
|
671
672
|
isRouteSourceFile,
|
|
@@ -1107,6 +1108,63 @@ var decreaseBuildNum = async (app) => {
|
|
|
1107
1108
|
const akanConfigContent = akanConfig.replace(`buildNum: ${appConfig.mobile.buildNum}`, `buildNum: ${appConfig.mobile.buildNum - 1}`);
|
|
1108
1109
|
fs.writeFileSync(akanConfigPath, akanConfigContent);
|
|
1109
1110
|
};
|
|
1111
|
+
// pkgs/@akanjs/devkit/firebaseMessagingSw.ts
|
|
1112
|
+
var FIREBASE_WEB_SDK_VERSION = "12.13.0";
|
|
1113
|
+
function normalizeFirebaseClientConfig(config) {
|
|
1114
|
+
if (!config || typeof config !== "object")
|
|
1115
|
+
return null;
|
|
1116
|
+
const value = config;
|
|
1117
|
+
if (typeof value.apiKey !== "string" || typeof value.projectId !== "string" || typeof value.messagingSenderId !== "string" || typeof value.appId !== "string") {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
apiKey: value.apiKey,
|
|
1122
|
+
...typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {},
|
|
1123
|
+
projectId: value.projectId,
|
|
1124
|
+
...typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {},
|
|
1125
|
+
messagingSenderId: value.messagingSenderId,
|
|
1126
|
+
appId: value.appId
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function createFirebaseMessagingServiceWorker(config) {
|
|
1130
|
+
const configJson = JSON.stringify(config);
|
|
1131
|
+
return `/* Generated by Akan.js. Do not edit. */
|
|
1132
|
+
const firebaseConfig = ${configJson};
|
|
1133
|
+
|
|
1134
|
+
if (firebaseConfig) {
|
|
1135
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
|
|
1136
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
|
|
1137
|
+
|
|
1138
|
+
firebase.initializeApp(firebaseConfig);
|
|
1139
|
+
const messaging = firebase.messaging();
|
|
1140
|
+
|
|
1141
|
+
const notificationUrl = (payload) =>
|
|
1142
|
+
payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
|
|
1143
|
+
|
|
1144
|
+
messaging.onBackgroundMessage((payload) => {
|
|
1145
|
+
const title = payload?.notification?.title || "";
|
|
1146
|
+
const options = {
|
|
1147
|
+
body: payload?.notification?.body,
|
|
1148
|
+
icon: payload?.notification?.icon,
|
|
1149
|
+
image: payload?.notification?.image,
|
|
1150
|
+
data: {
|
|
1151
|
+
url: notificationUrl(payload),
|
|
1152
|
+
FCM_MSG: payload,
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1155
|
+
self.registration.showNotification(title, options);
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
self.addEventListener("notificationclick", (event) => {
|
|
1160
|
+
const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
|
|
1161
|
+
event.notification?.close();
|
|
1162
|
+
if (!url) return;
|
|
1163
|
+
event.waitUntil(clients.openWindow(url));
|
|
1164
|
+
});
|
|
1165
|
+
`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1110
1168
|
// pkgs/@akanjs/devkit/getDirname.ts
|
|
1111
1169
|
import path2 from "path";
|
|
1112
1170
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -1392,6 +1450,7 @@ ${errorText}, ${warningText} found`];
|
|
|
1392
1450
|
|
|
1393
1451
|
// pkgs/@akanjs/devkit/scanInfo.ts
|
|
1394
1452
|
import path5 from "path";
|
|
1453
|
+
import { rm } from "fs/promises";
|
|
1395
1454
|
|
|
1396
1455
|
// pkgs/@akanjs/devkit/dependencyScanner.ts
|
|
1397
1456
|
import { builtinModules } from "module";
|
|
@@ -1710,8 +1769,10 @@ var appRootAllowedFiles = new Set([
|
|
|
1710
1769
|
"main.ts",
|
|
1711
1770
|
"package.json",
|
|
1712
1771
|
"server.ts",
|
|
1713
|
-
"tsconfig.json"
|
|
1772
|
+
"tsconfig.json",
|
|
1773
|
+
"tsconfig.tsbuildinfo"
|
|
1714
1774
|
]);
|
|
1775
|
+
var generatedRootCapacitorConfigFiles = ["capacitor.config.js", "capacitor.config.json"];
|
|
1715
1776
|
var appRootAllowedDirs = new Set([
|
|
1716
1777
|
".akan",
|
|
1717
1778
|
"android",
|
|
@@ -1755,11 +1816,17 @@ var rootSignalTestFilePattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(
|
|
|
1755
1816
|
var isAllowedTestFile = (filename) => testFilePattern.test(filename);
|
|
1756
1817
|
var isAllowedLibRootFile = (filename) => libRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
|
|
1757
1818
|
var getScanPath = (exec, relativePath) => path5.posix.join(`${exec.type}s`, exec.name, relativePath.split(path5.sep).join("/"));
|
|
1819
|
+
async function clearGeneratedRootCapacitorConfigs(exec) {
|
|
1820
|
+
if (exec.type !== "app")
|
|
1821
|
+
return;
|
|
1822
|
+
await Promise.all(generatedRootCapacitorConfigFiles.map((filename) => rm(exec.getPath(filename), { force: true })));
|
|
1823
|
+
}
|
|
1758
1824
|
var getModuleNameFromPath = (kind, modulePath) => {
|
|
1759
1825
|
const dirname = path5.basename(modulePath);
|
|
1760
1826
|
return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
|
|
1761
1827
|
};
|
|
1762
1828
|
async function assertScanConvention(exec, libRoot) {
|
|
1829
|
+
await clearGeneratedRootCapacitorConfigs(exec);
|
|
1763
1830
|
const violations = [];
|
|
1764
1831
|
const addViolation = (relativePath, reason) => {
|
|
1765
1832
|
violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
|
|
@@ -2624,6 +2691,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
|
|
|
2624
2691
|
throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
|
|
2625
2692
|
}
|
|
2626
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
|
+
}
|
|
2627
2719
|
|
|
2628
2720
|
class Executor {
|
|
2629
2721
|
static verbose = false;
|
|
@@ -3576,6 +3668,8 @@ class AppExecutor extends SysExecutor {
|
|
|
3576
3668
|
...routeEnv,
|
|
3577
3669
|
...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
|
|
3578
3670
|
});
|
|
3671
|
+
if (type === "build")
|
|
3672
|
+
Object.assign(process.env, env);
|
|
3579
3673
|
return { env };
|
|
3580
3674
|
}
|
|
3581
3675
|
#publicEnv = null;
|
|
@@ -3645,7 +3739,11 @@ class AppExecutor extends SysExecutor {
|
|
|
3645
3739
|
throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
|
|
3646
3740
|
}
|
|
3647
3741
|
const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
|
|
3648
|
-
|
|
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 });
|
|
3649
3747
|
pageKeys.push(key);
|
|
3650
3748
|
}
|
|
3651
3749
|
pageKeys.sort();
|
|
@@ -3686,8 +3784,30 @@ class AppExecutor extends SysExecutor {
|
|
|
3686
3784
|
});
|
|
3687
3785
|
if (write)
|
|
3688
3786
|
await this.syncAssets(scanInfo.getScanResult().libDeps);
|
|
3787
|
+
if (write)
|
|
3788
|
+
await this.#syncFirebaseMessagingSw();
|
|
3689
3789
|
return scanInfo;
|
|
3690
3790
|
}
|
|
3791
|
+
async#syncFirebaseMessagingSw() {
|
|
3792
|
+
const swRelPath = "public/firebase-messaging-sw.js";
|
|
3793
|
+
if (await FileSys.fileExists(this.getPath(swRelPath)))
|
|
3794
|
+
return;
|
|
3795
|
+
const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
|
|
3796
|
+
if (!await FileSys.fileExists(envClientPath))
|
|
3797
|
+
return;
|
|
3798
|
+
let firebaseConfig = null;
|
|
3799
|
+
try {
|
|
3800
|
+
const envUrl = pathToFileURL(envClientPath);
|
|
3801
|
+
envUrl.searchParams.set("t", String(Date.now()));
|
|
3802
|
+
const envModule = await import(envUrl.href);
|
|
3803
|
+
firebaseConfig = normalizeFirebaseClientConfig(envModule.env?.firebase);
|
|
3804
|
+
} catch {
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
if (!firebaseConfig)
|
|
3808
|
+
return;
|
|
3809
|
+
await this.writeFile(swRelPath, createFirebaseMessagingServiceWorker(firebaseConfig), { overwrite: false });
|
|
3810
|
+
}
|
|
3691
3811
|
async increaseBuildNum() {
|
|
3692
3812
|
await increaseBuildNum(this);
|
|
3693
3813
|
}
|
|
@@ -4170,6 +4290,7 @@ var BACKEND_RESTART_DEBOUNCE_MS = 120;
|
|
|
4170
4290
|
var BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
|
|
4171
4291
|
var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
|
|
4172
4292
|
var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
|
|
4293
|
+
var BACKEND_STDERR_TAIL_LIMIT = 40;
|
|
4173
4294
|
var BUILDER_READY_TIMEOUT_MS = 150000;
|
|
4174
4295
|
var BUILDER_START_MAX_ATTEMPTS = 3;
|
|
4175
4296
|
var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
@@ -4372,6 +4493,7 @@ class AkanAppHost {
|
|
|
4372
4493
|
#pendingRestartReason = null;
|
|
4373
4494
|
#backendStartStatus = null;
|
|
4374
4495
|
#backendBuildStatusGeneration = 0;
|
|
4496
|
+
#backendStderrTail = [];
|
|
4375
4497
|
#lastGoodFrontend = {};
|
|
4376
4498
|
#buildStatusByPhase = new Map;
|
|
4377
4499
|
#pendingBuildStatusReplay = [];
|
|
@@ -4423,6 +4545,7 @@ class AkanAppHost {
|
|
|
4423
4545
|
this.#backendStartStatus = startStatus;
|
|
4424
4546
|
this.#setBackendLifecycleState("starting");
|
|
4425
4547
|
this.#backendReady = false;
|
|
4548
|
+
this.#backendStderrTail = [];
|
|
4426
4549
|
const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
|
|
4427
4550
|
cwd: this.app.workspace.workspaceRoot,
|
|
4428
4551
|
stdio: this.withInk ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
|
|
@@ -4456,6 +4579,37 @@ class AkanAppHost {
|
|
|
4456
4579
|
});
|
|
4457
4580
|
this.#backend = backend;
|
|
4458
4581
|
this.logger.verbose(`backend spawned pid=${backend.pid}`);
|
|
4582
|
+
if (this.withInk) {
|
|
4583
|
+
this.#forwardBackendStream(backend.stderr, "stderr");
|
|
4584
|
+
this.#forwardBackendStream(backend.stdout, "stdout");
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
#recordBackendStderr(chunk) {
|
|
4588
|
+
const lines = chunk.split(/\r?\n/).filter((line) => line.length > 0);
|
|
4589
|
+
if (lines.length === 0)
|
|
4590
|
+
return;
|
|
4591
|
+
this.#backendStderrTail.push(...lines);
|
|
4592
|
+
if (this.#backendStderrTail.length > BACKEND_STDERR_TAIL_LIMIT) {
|
|
4593
|
+
this.#backendStderrTail.splice(0, this.#backendStderrTail.length - BACKEND_STDERR_TAIL_LIMIT);
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
async#forwardBackendStream(stream, kind) {
|
|
4597
|
+
if (!stream)
|
|
4598
|
+
return;
|
|
4599
|
+
const decoder = new TextDecoder;
|
|
4600
|
+
try {
|
|
4601
|
+
for await (const chunk of stream) {
|
|
4602
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
4603
|
+
if (!text.trim())
|
|
4604
|
+
continue;
|
|
4605
|
+
if (kind === "stderr") {
|
|
4606
|
+
this.#recordBackendStderr(text);
|
|
4607
|
+
this.logger.warn(`[backend] ${text.trimEnd()}`);
|
|
4608
|
+
} else {
|
|
4609
|
+
this.logger.verbose(`[backend] ${text.trimEnd()}`);
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
} catch {}
|
|
4459
4613
|
}
|
|
4460
4614
|
#nextBackendBuildStatusGeneration(generation) {
|
|
4461
4615
|
if (typeof generation === "number") {
|
|
@@ -4580,6 +4734,11 @@ class AkanAppHost {
|
|
|
4580
4734
|
});
|
|
4581
4735
|
this.#sendOrQueueBuildStatus(failureStatus);
|
|
4582
4736
|
this.logger.warn(`[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`);
|
|
4737
|
+
if (this.#backendStderrTail.length > 0) {
|
|
4738
|
+
this.logger.warn(`[backend-recovery] recent backend stderr:
|
|
4739
|
+
${this.#backendStderrTail.join(`
|
|
4740
|
+
`)}`);
|
|
4741
|
+
}
|
|
4583
4742
|
this.#backendRecoveryTimer = setTimeout(() => {
|
|
4584
4743
|
this.#backendRecoveryTimer = null;
|
|
4585
4744
|
if (this.#backend)
|
|
@@ -4956,7 +5115,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
|
|
|
4956
5115
|
var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
|
|
4957
5116
|
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
4958
5117
|
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
4959
|
-
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
4960
5118
|
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
|
|
4961
5119
|
];
|
|
4962
5120
|
var writeGeneratedSyncState = async (workspace, state) => {
|
|
@@ -5945,6 +6103,117 @@ ${values.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${
|
|
|
5945
6103
|
return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
|
|
5946
6104
|
};
|
|
5947
6105
|
var parseValues = (value) => value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
|
|
6106
|
+
var hasSourceParseErrors = (content, fileName = "source.ts") => hasParseDiagnostics(sourceFileFor(fileName, content));
|
|
6107
|
+
var findClassDeclaration = (source, className) => {
|
|
6108
|
+
let found = null;
|
|
6109
|
+
const visit = (node) => {
|
|
6110
|
+
if (found)
|
|
6111
|
+
return;
|
|
6112
|
+
if (ts4.isClassDeclaration(node) && node.name?.text === className) {
|
|
6113
|
+
found = node;
|
|
6114
|
+
return;
|
|
6115
|
+
}
|
|
6116
|
+
ts4.forEachChild(node, visit);
|
|
6117
|
+
};
|
|
6118
|
+
ts4.forEachChild(source, visit);
|
|
6119
|
+
return found;
|
|
6120
|
+
};
|
|
6121
|
+
var firstHeritageCall = (node) => {
|
|
6122
|
+
const expression = (node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [])[0]?.expression;
|
|
6123
|
+
return expression && ts4.isCallExpression(expression) ? expression : null;
|
|
6124
|
+
};
|
|
6125
|
+
var factoryArrowOf = (call) => {
|
|
6126
|
+
const arg = call.arguments.find((argument) => ts4.isArrowFunction(argument) || ts4.isFunctionExpression(argument));
|
|
6127
|
+
return arg && (ts4.isArrowFunction(arg) || ts4.isFunctionExpression(arg)) ? arg : null;
|
|
6128
|
+
};
|
|
6129
|
+
var hasClassMethod = (content, className, methodName) => {
|
|
6130
|
+
const node = findClassDeclaration(sourceFileFor("service.ts", content), className);
|
|
6131
|
+
return Boolean(node?.members.some((member) => ts4.isMethodDeclaration(member) && nodeName(member.name) === methodName));
|
|
6132
|
+
};
|
|
6133
|
+
var insertClassMethod = (content, className, methodBlock) => {
|
|
6134
|
+
const source = sourceFileFor("service.ts", content);
|
|
6135
|
+
const node = findClassDeclaration(source, className);
|
|
6136
|
+
if (!node)
|
|
6137
|
+
return null;
|
|
6138
|
+
const closeBrace = node.getEnd() - 1;
|
|
6139
|
+
if (content[closeBrace] !== "}")
|
|
6140
|
+
return null;
|
|
6141
|
+
const lead = content.slice(0, closeBrace).endsWith(`
|
|
6142
|
+
`) ? "" : `
|
|
6143
|
+
`;
|
|
6144
|
+
return spliceText(content, closeBrace, closeBrace, `${lead}${methodBlock}
|
|
6145
|
+
`);
|
|
6146
|
+
};
|
|
6147
|
+
var arrowParamInnerRegion = (content, source, arrow) => {
|
|
6148
|
+
if (arrow.parameters.length > 0) {
|
|
6149
|
+
return {
|
|
6150
|
+
start: arrow.parameters[0].getStart(source),
|
|
6151
|
+
end: arrow.parameters[arrow.parameters.length - 1].getEnd()
|
|
6152
|
+
};
|
|
6153
|
+
}
|
|
6154
|
+
const open = content.indexOf("(", arrow.getStart(source));
|
|
6155
|
+
if (open < 0)
|
|
6156
|
+
return null;
|
|
6157
|
+
const close = content.indexOf(")", open);
|
|
6158
|
+
if (close < 0)
|
|
6159
|
+
return null;
|
|
6160
|
+
return { start: open + 1, end: close };
|
|
6161
|
+
};
|
|
6162
|
+
var factoryParamEdit = (content, source, arrow, plan) => {
|
|
6163
|
+
if (arrow.parameters.length > 1)
|
|
6164
|
+
return null;
|
|
6165
|
+
const region = arrowParamInnerRegion(content, source, arrow);
|
|
6166
|
+
if (!region)
|
|
6167
|
+
return null;
|
|
6168
|
+
if (arrow.parameters.length === 0) {
|
|
6169
|
+
return { ...region, text: plan.mode === "destructure" ? `{ ${plan.name} }` : plan.name };
|
|
6170
|
+
}
|
|
6171
|
+
const param = arrow.parameters[0];
|
|
6172
|
+
if (plan.mode === "positional")
|
|
6173
|
+
return nodeName(param.name) === plan.name ? "unchanged" : null;
|
|
6174
|
+
if (!ts4.isObjectBindingPattern(param.name))
|
|
6175
|
+
return null;
|
|
6176
|
+
const names = param.name.elements.map((element) => ts4.isIdentifier(element.name) ? element.name.text : null);
|
|
6177
|
+
if (names.some((name) => name === null))
|
|
6178
|
+
return null;
|
|
6179
|
+
if (names.includes(plan.name))
|
|
6180
|
+
return "unchanged";
|
|
6181
|
+
return {
|
|
6182
|
+
start: param.getStart(source),
|
|
6183
|
+
end: param.getEnd(),
|
|
6184
|
+
text: `{ ${[...names, plan.name].join(", ")} }`
|
|
6185
|
+
};
|
|
6186
|
+
};
|
|
6187
|
+
var factoryObjectOf = (source, className) => {
|
|
6188
|
+
const node = findClassDeclaration(source, className);
|
|
6189
|
+
const call = node ? firstHeritageCall(node) : null;
|
|
6190
|
+
const arrow = call ? factoryArrowOf(call) : null;
|
|
6191
|
+
const object = arrow ? firstObjectReturnedByArrow(arrow) : null;
|
|
6192
|
+
return arrow && object ? { arrow, object } : null;
|
|
6193
|
+
};
|
|
6194
|
+
var hasSignalFactoryEntry = (content, className, entryName) => {
|
|
6195
|
+
const located = factoryObjectOf(sourceFileFor("signal.ts", content), className);
|
|
6196
|
+
return Boolean(located?.object.properties.some((property) => propertyName(property) === entryName));
|
|
6197
|
+
};
|
|
6198
|
+
var insertSignalFactoryEntry = (content, className, entryName, entryLine, param) => {
|
|
6199
|
+
const source = sourceFileFor("signal.ts", content);
|
|
6200
|
+
const located = factoryObjectOf(source, className);
|
|
6201
|
+
if (!located)
|
|
6202
|
+
return null;
|
|
6203
|
+
const locator = locatedObject(source, located.object);
|
|
6204
|
+
if (locator.fields.some((field) => field.name === entryName))
|
|
6205
|
+
return content;
|
|
6206
|
+
const paramEdit = factoryParamEdit(content, source, located.arrow, param);
|
|
6207
|
+
if (paramEdit === null)
|
|
6208
|
+
return null;
|
|
6209
|
+
const withEntry = insertOrderedFieldLine(content, locator, entryName, entryLine, {
|
|
6210
|
+
fieldIndent: " ",
|
|
6211
|
+
closingIndent: ""
|
|
6212
|
+
});
|
|
6213
|
+
if (withEntry === content)
|
|
6214
|
+
return null;
|
|
6215
|
+
return paramEdit === "unchanged" ? withEntry : spliceText(withEntry, paramEdit.start, paramEdit.end, paramEdit.text);
|
|
6216
|
+
};
|
|
5948
6217
|
|
|
5949
6218
|
// pkgs/@akanjs/devkit/workflow/moduleIndex.ts
|
|
5950
6219
|
var indexFileKinds = [
|
|
@@ -6662,7 +6931,9 @@ var createWorkflowStepRegistry = ({
|
|
|
6662
6931
|
createScalar,
|
|
6663
6932
|
createUi,
|
|
6664
6933
|
addField,
|
|
6665
|
-
addEnumField
|
|
6934
|
+
addEnumField,
|
|
6935
|
+
addMutation,
|
|
6936
|
+
addSlice
|
|
6666
6937
|
}) => {
|
|
6667
6938
|
const inspect = async () => {
|
|
6668
6939
|
return;
|
|
@@ -6738,7 +7009,19 @@ var createWorkflowStepRegistry = ({
|
|
|
6738
7009
|
defaultValue: workflowStringInput(plan.inputs.default)
|
|
6739
7010
|
})),
|
|
6740
7011
|
[workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
|
|
6741
|
-
[workflowStepKey("add-enum-field", "update-option")]: inspect
|
|
7012
|
+
[workflowStepKey("add-enum-field", "update-option")]: inspect,
|
|
7013
|
+
[workflowStepKey("add-mutation", "update-service")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addMutation({
|
|
7014
|
+
app: workflowStringInput(plan.inputs.app),
|
|
7015
|
+
module: workflowStringInput(plan.inputs.module),
|
|
7016
|
+
mutation: workflowStringInput(plan.inputs.mutation)
|
|
7017
|
+
})),
|
|
7018
|
+
[workflowStepKey("add-mutation", "update-signal")]: inspect,
|
|
7019
|
+
[workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addSlice({
|
|
7020
|
+
app: workflowStringInput(plan.inputs.app),
|
|
7021
|
+
module: workflowStringInput(plan.inputs.module),
|
|
7022
|
+
slice: workflowStringInput(plan.inputs.slice)
|
|
7023
|
+
})),
|
|
7024
|
+
[workflowStepKey("add-slice", "update-signal-slice")]: inspect
|
|
6742
7025
|
};
|
|
6743
7026
|
};
|
|
6744
7027
|
class WorkflowExecutor {
|
|
@@ -7485,13 +7768,59 @@ var resourceList = [
|
|
|
7485
7768
|
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
|
|
7486
7769
|
];
|
|
7487
7770
|
var cursorMcpConfigPath = ".cursor/mcp.json";
|
|
7771
|
+
var claudeMcpConfigPath = ".mcp.json";
|
|
7772
|
+
var codexMcpConfigPath = ".codex/config.toml";
|
|
7773
|
+
var akanMcpInstallTargets = ["cursor", "claude", "codex"];
|
|
7774
|
+
var akanMcpInstallConfigPaths = {
|
|
7775
|
+
cursor: cursorMcpConfigPath,
|
|
7776
|
+
claude: claudeMcpConfigPath,
|
|
7777
|
+
codex: codexMcpConfigPath
|
|
7778
|
+
};
|
|
7488
7779
|
var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
7780
|
+
var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
|
|
7781
|
+
var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
|
|
7489
7782
|
var createAkanCursorMcpServer = (mode = "readonly") => ({
|
|
7490
7783
|
type: "stdio",
|
|
7491
7784
|
command: "bash",
|
|
7492
|
-
args: ["-lc",
|
|
7785
|
+
args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
|
|
7786
|
+
});
|
|
7787
|
+
var createAkanClaudeMcpServer = (mode = "readonly") => ({
|
|
7788
|
+
type: "stdio",
|
|
7789
|
+
command: "bash",
|
|
7790
|
+
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
|
|
7493
7791
|
});
|
|
7792
|
+
var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
|
|
7494
7793
|
var akanCursorMcpServer = createAkanCursorMcpServer();
|
|
7794
|
+
var codexMcpServerTableHeader = "[mcp_servers.akan]";
|
|
7795
|
+
var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
|
|
7796
|
+
command = "bash"
|
|
7797
|
+
args = ["-lc", "${akanMcpCommand(mode)}"]
|
|
7798
|
+
`;
|
|
7799
|
+
var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
|
|
7800
|
+
var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
|
|
7801
|
+
const nextBlock = block.endsWith(`
|
|
7802
|
+
`) ? block : `${block}
|
|
7803
|
+
`;
|
|
7804
|
+
const match = existing.match(codexAkanTablePattern);
|
|
7805
|
+
if (!match) {
|
|
7806
|
+
if (!existing.trim())
|
|
7807
|
+
return nextBlock;
|
|
7808
|
+
return `${existing.replace(/\s*$/, "")}
|
|
7809
|
+
|
|
7810
|
+
${nextBlock}`;
|
|
7811
|
+
}
|
|
7812
|
+
if (match[0].trim() === nextBlock.trim())
|
|
7813
|
+
return existing;
|
|
7814
|
+
if (!force)
|
|
7815
|
+
throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
7816
|
+
const start = match.index ?? 0;
|
|
7817
|
+
const before = existing.slice(0, start);
|
|
7818
|
+
const after = existing.slice(start + match[0].length);
|
|
7819
|
+
const separator = after && !after.startsWith(`
|
|
7820
|
+
`) ? `
|
|
7821
|
+
` : "";
|
|
7822
|
+
return `${before}${nextBlock}${separator}${after}`;
|
|
7823
|
+
};
|
|
7495
7824
|
var renderDoctorText = (result) => {
|
|
7496
7825
|
const lines = [`Akan doctor status: ${result.status}`];
|
|
7497
7826
|
if (result.diagnostics.length === 0) {
|
|
@@ -7514,7 +7843,6 @@ var generatedFiles = [
|
|
|
7514
7843
|
"*/lib/cnst.ts",
|
|
7515
7844
|
"*/lib/db.ts",
|
|
7516
7845
|
"*/lib/dict.ts",
|
|
7517
|
-
"*/lib/option.ts",
|
|
7518
7846
|
"*/lib/sig.ts",
|
|
7519
7847
|
"*/lib/srv.ts",
|
|
7520
7848
|
"*/lib/st.ts",
|
|
@@ -8508,7 +8836,7 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
|
|
|
8508
8836
|
}
|
|
8509
8837
|
}
|
|
8510
8838
|
// pkgs/@akanjs/devkit/applicationBuildRunner.ts
|
|
8511
|
-
import { mkdir as mkdir8, rm as
|
|
8839
|
+
import { mkdir as mkdir8, rm as rm4 } from "fs/promises";
|
|
8512
8840
|
import path37 from "path";
|
|
8513
8841
|
|
|
8514
8842
|
// pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
|
|
@@ -8523,6 +8851,25 @@ async function appHasStModule(appCwdPath) {
|
|
|
8523
8851
|
}
|
|
8524
8852
|
var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
|
|
8525
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
|
+
}
|
|
8526
8873
|
function getRootBoundarySegments(key) {
|
|
8527
8874
|
const match = LAYOUT_KEY_RE.exec(key);
|
|
8528
8875
|
if (!match)
|
|
@@ -8715,9 +9062,17 @@ async function resolveSsrPageEntries(opts) {
|
|
|
8715
9062
|
const segments = getRootBoundarySegments(key);
|
|
8716
9063
|
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
8717
9064
|
}));
|
|
8718
|
-
const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) =>
|
|
8719
|
-
key
|
|
8720
|
-
|
|
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 };
|
|
8721
9076
|
}));
|
|
8722
9077
|
const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
|
|
8723
9078
|
key: implicitRootLayoutKey(boundary.segments),
|
|
@@ -8749,7 +9104,7 @@ function computeRouteSeedIndex(pageEntries) {
|
|
|
8749
9104
|
for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
|
|
8750
9105
|
const parsed = parseRouteModuleKey2(key);
|
|
8751
9106
|
const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
|
|
8752
|
-
if (parsed.kind === "layout") {
|
|
9107
|
+
if (parsed.kind === "layout" || parsed.kind === "overrides") {
|
|
8753
9108
|
const prefix = parsed.routeSegments.join("/");
|
|
8754
9109
|
const prev = layoutsByPrefix.get(prefix) ?? [];
|
|
8755
9110
|
layoutsByPrefix.set(prefix, [...prev, ...files]);
|
|
@@ -10266,7 +10621,7 @@ class AllRoutesBuilder {
|
|
|
10266
10621
|
}
|
|
10267
10622
|
}
|
|
10268
10623
|
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
10269
|
-
import { mkdir as mkdir5, rm, unlink } from "fs/promises";
|
|
10624
|
+
import { mkdir as mkdir5, rm as rm2, unlink } from "fs/promises";
|
|
10270
10625
|
import path24 from "path";
|
|
10271
10626
|
|
|
10272
10627
|
// pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
|
|
@@ -10396,7 +10751,7 @@ class CsrArtifactBuilder {
|
|
|
10396
10751
|
const artifact = await this.#loadCsrArtifact();
|
|
10397
10752
|
const csrBasePaths = [...akanConfig2.basePaths];
|
|
10398
10753
|
const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
|
|
10399
|
-
await
|
|
10754
|
+
await rm2(this.#outputDir, { recursive: true, force: true });
|
|
10400
10755
|
await mkdir5(path24.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
|
|
10401
10756
|
const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath2) => this.#createHtmlFile(basePath2)));
|
|
10402
10757
|
const result = await Bun.build({
|
|
@@ -11130,7 +11485,7 @@ class DevChangePlanner {
|
|
|
11130
11485
|
}
|
|
11131
11486
|
var uniqueResolved = (files) => [...new Set(files.map((file) => path27.resolve(file)))].sort();
|
|
11132
11487
|
// pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
|
|
11133
|
-
import { mkdir as mkdir6, readdir as readdir2, readFile, rm as
|
|
11488
|
+
import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm3, stat as stat3, writeFile } from "fs/promises";
|
|
11134
11489
|
import path28 from "path";
|
|
11135
11490
|
var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
|
|
11136
11491
|
var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
|
|
@@ -11213,7 +11568,7 @@ class DevGeneratedIndexSync {
|
|
|
11213
11568
|
if (content === null) {
|
|
11214
11569
|
if (!await exists(indexPath))
|
|
11215
11570
|
return false;
|
|
11216
|
-
await
|
|
11571
|
+
await rm3(indexPath, { force: true });
|
|
11217
11572
|
return true;
|
|
11218
11573
|
}
|
|
11219
11574
|
const current = await readFile(indexPath, "utf8").catch(() => null);
|
|
@@ -12360,7 +12715,7 @@ class ApplicationBuildRunner {
|
|
|
12360
12715
|
await this.#app.getPageKeys({ refresh: true });
|
|
12361
12716
|
const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
|
|
12362
12717
|
if (clean)
|
|
12363
|
-
await
|
|
12718
|
+
await rm4(path37.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
|
|
12364
12719
|
await this.#checkProjectInChildProcess(tsconfigPath);
|
|
12365
12720
|
}
|
|
12366
12721
|
async#runPhase(id, label, task, summarize, options = {}) {
|
|
@@ -12560,7 +12915,7 @@ void run().catch((error) => {
|
|
|
12560
12915
|
}
|
|
12561
12916
|
}
|
|
12562
12917
|
// pkgs/@akanjs/devkit/applicationReleasePackager.ts
|
|
12563
|
-
import { cp, mkdir as mkdir9, rm as
|
|
12918
|
+
import { cp, mkdir as mkdir9, rm as rm5 } from "fs/promises";
|
|
12564
12919
|
import path38 from "path";
|
|
12565
12920
|
|
|
12566
12921
|
// pkgs/@akanjs/devkit/uploadRelease.ts
|
|
@@ -12667,7 +13022,7 @@ class ApplicationReleasePackager {
|
|
|
12667
13022
|
const platformVersion = akanConfig2.mobile.version;
|
|
12668
13023
|
const buildRoot = `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}`;
|
|
12669
13024
|
if (await FileSys.dirExists(buildRoot))
|
|
12670
|
-
await
|
|
13025
|
+
await rm5(buildRoot, { recursive: true, force: true });
|
|
12671
13026
|
await mkdir9(buildRoot, { recursive: true });
|
|
12672
13027
|
if (rebuild || !await FileSys.dirExists(`${this.#app.dist.cwdPath}/backend`))
|
|
12673
13028
|
await this.#build();
|
|
@@ -12676,7 +13031,7 @@ class ApplicationReleasePackager {
|
|
|
12676
13031
|
await mkdir9(buildPath, { recursive: true });
|
|
12677
13032
|
await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
|
|
12678
13033
|
await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
|
|
12679
|
-
await
|
|
13034
|
+
await rm5(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
|
|
12680
13035
|
const releaseRoot = this.#app.workspace.workspaceRoot;
|
|
12681
13036
|
const releaseArchivePath = path38.relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`).split(path38.sep).join("/");
|
|
12682
13037
|
await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
|
|
@@ -12692,7 +13047,7 @@ class ApplicationReleasePackager {
|
|
|
12692
13047
|
`${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-appBuild.zip`,
|
|
12693
13048
|
"./csr"
|
|
12694
13049
|
]);
|
|
12695
|
-
await
|
|
13050
|
+
await rm5("./csr", { recursive: true, force: true });
|
|
12696
13051
|
}
|
|
12697
13052
|
async#writeSourceArchive({ readme }) {
|
|
12698
13053
|
const sourceRoot = `${this.#app.workspace.workspaceRoot}/releases/sources/${this.#app.name}`;
|
|
@@ -12703,7 +13058,7 @@ class ApplicationReleasePackager {
|
|
|
12703
13058
|
await Promise.all([".next", "ios", "android", "public/libs"].map(async (path39) => {
|
|
12704
13059
|
const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path39}`;
|
|
12705
13060
|
if (await FileSys.dirExists(targetPath))
|
|
12706
|
-
await
|
|
13061
|
+
await rm5(targetPath, { recursive: true, force: true });
|
|
12707
13062
|
}));
|
|
12708
13063
|
const syncPaths = [".husky", ".gitignore", "package.json"];
|
|
12709
13064
|
await Promise.all(syncPaths.map((path39) => cp(`${this.#app.workspace.cwdPath}/${path39}`, `${sourceRoot}/${path39}`, { recursive: true })));
|
|
@@ -12718,7 +13073,7 @@ class ApplicationReleasePackager {
|
|
|
12718
13073
|
const maxRetry = 3;
|
|
12719
13074
|
for (let i = 0;i < maxRetry; i++) {
|
|
12720
13075
|
try {
|
|
12721
|
-
await
|
|
13076
|
+
await rm5(sourceRoot, { recursive: true, force: true });
|
|
12722
13077
|
} catch {}
|
|
12723
13078
|
}
|
|
12724
13079
|
}
|
|
@@ -12807,11 +13162,15 @@ async function resolveSignalTestPreloadPath(target) {
|
|
|
12807
13162
|
addResolvedPackageCandidate(path39.dirname(Bun.main));
|
|
12808
13163
|
addResolvedPackageCandidate(import.meta.dir);
|
|
12809
13164
|
candidates.push(path39.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(path39.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
|
|
12810
|
-
|
|
13165
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
13166
|
+
for (const candidate of uniqueCandidates) {
|
|
12811
13167
|
if (await Bun.file(candidate).exists())
|
|
12812
13168
|
return candidate;
|
|
12813
13169
|
}
|
|
12814
|
-
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}
|
|
13170
|
+
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.
|
|
13171
|
+
Probed paths:
|
|
13172
|
+
${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
13173
|
+
`)}`);
|
|
12815
13174
|
}
|
|
12816
13175
|
// pkgs/@akanjs/devkit/builder.ts
|
|
12817
13176
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -12947,7 +13306,7 @@ class Builder {
|
|
|
12947
13306
|
}
|
|
12948
13307
|
}
|
|
12949
13308
|
// pkgs/@akanjs/devkit/capacitorApp.ts
|
|
12950
|
-
import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as
|
|
13309
|
+
import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm6, writeFile as writeFile2 } from "fs/promises";
|
|
12951
13310
|
import os from "os";
|
|
12952
13311
|
import path41 from "path";
|
|
12953
13312
|
import { select as select2 } from "@inquirer/prompts";
|
|
@@ -13158,7 +13517,7 @@ var rootCapacitorConfigFilenames = [
|
|
|
13158
13517
|
];
|
|
13159
13518
|
var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path41.join(appRoot, file));
|
|
13160
13519
|
async function clearRootCapacitorConfigs(appRoot) {
|
|
13161
|
-
await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) =>
|
|
13520
|
+
await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm6(file, { force: true })));
|
|
13162
13521
|
}
|
|
13163
13522
|
async function writeRootCapacitorConfig(appRoot, content) {
|
|
13164
13523
|
await clearRootCapacitorConfigs(appRoot);
|
|
@@ -13179,6 +13538,7 @@ var getLocalIP = () => {
|
|
|
13179
13538
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13180
13539
|
var asString = (value) => typeof value === "string" ? value : undefined;
|
|
13181
13540
|
var firstString = (...values) => values.find((value) => typeof value === "string");
|
|
13541
|
+
var resolveIosApnsEnvironment = ({ operation }) => operation === "release" ? "production" : "development";
|
|
13182
13542
|
var scoreIosDeviceTarget = (target) => {
|
|
13183
13543
|
const state = target.state?.toLowerCase() ?? "";
|
|
13184
13544
|
return (state.includes("available") ? 4 : 0) + (state.includes("wired") ? 2 : 0) + (state.includes("paired") ? 1 : 0);
|
|
@@ -13484,6 +13844,8 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
|
|
|
13484
13844
|
const experimentalConfig = isRecord2(experimental) ? experimental : undefined;
|
|
13485
13845
|
const pluginsConfig = isRecord2(plugins) ? plugins : {};
|
|
13486
13846
|
const keyboardPluginConfig = isRecord2(pluginsConfig.Keyboard) ? pluginsConfig.Keyboard : {};
|
|
13847
|
+
const pushNotificationsPluginConfig = isRecord2(pluginsConfig.PushNotifications) ? pluginsConfig.PushNotifications : {};
|
|
13848
|
+
const usesPushNotifications = target.permissions?.includes("push") ?? false;
|
|
13487
13849
|
const config = {
|
|
13488
13850
|
...capacitorConfig,
|
|
13489
13851
|
appId,
|
|
@@ -13492,6 +13854,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
|
|
|
13492
13854
|
plugins: {
|
|
13493
13855
|
CapacitorCookies: { enabled: true },
|
|
13494
13856
|
...pluginsConfig,
|
|
13857
|
+
...usesPushNotifications || isRecord2(pluginsConfig.PushNotifications) ? {
|
|
13858
|
+
PushNotifications: {
|
|
13859
|
+
...usesPushNotifications ? { presentationOptions: ["badge", "sound", "alert"] } : {},
|
|
13860
|
+
...pushNotificationsPluginConfig
|
|
13861
|
+
}
|
|
13862
|
+
} : {},
|
|
13495
13863
|
Keyboard: {
|
|
13496
13864
|
resize: "none",
|
|
13497
13865
|
...keyboardPluginConfig
|
|
@@ -13563,9 +13931,9 @@ class CapacitorApp {
|
|
|
13563
13931
|
await mkdir11(this.targetRoot, { recursive: true });
|
|
13564
13932
|
if (regenerate) {
|
|
13565
13933
|
if (!platform || platform === "ios")
|
|
13566
|
-
await
|
|
13934
|
+
await rm6(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
|
|
13567
13935
|
if (!platform || platform === "android")
|
|
13568
|
-
await
|
|
13936
|
+
await rm6(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
|
|
13569
13937
|
}
|
|
13570
13938
|
const project = this.project;
|
|
13571
13939
|
await this.project.load();
|
|
@@ -13587,7 +13955,7 @@ class CapacitorApp {
|
|
|
13587
13955
|
await this.#prepareTargetAssets();
|
|
13588
13956
|
await this.#prepareExternalFiles("ios");
|
|
13589
13957
|
await this.#applyIosMetadata();
|
|
13590
|
-
await this.#applyPermissions();
|
|
13958
|
+
await this.#applyPermissions({ operation, env });
|
|
13591
13959
|
await this.#applyDeepLinks("ios", { operation, env });
|
|
13592
13960
|
await this.project.commit();
|
|
13593
13961
|
await this.#generateAssets({ operation, env });
|
|
@@ -13730,7 +14098,7 @@ ${error.message}`;
|
|
|
13730
14098
|
await this.#prepareTargetAssets();
|
|
13731
14099
|
await this.#prepareExternalFiles("android");
|
|
13732
14100
|
await this.#applyAndroidMetadata();
|
|
13733
|
-
await this.#applyPermissions();
|
|
14101
|
+
await this.#applyPermissions({ operation, env });
|
|
13734
14102
|
await this.#applyDeepLinks("android", { operation, env });
|
|
13735
14103
|
await this.project.commit();
|
|
13736
14104
|
await this.#generateAssets({ operation, env });
|
|
@@ -13864,7 +14232,7 @@ ${error.message}`;
|
|
|
13864
14232
|
const htmlSource = path41.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
|
|
13865
14233
|
if (!await Bun.file(htmlSource).exists())
|
|
13866
14234
|
throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
|
|
13867
|
-
await
|
|
14235
|
+
await rm6(this.targetWebRoot, { recursive: true, force: true });
|
|
13868
14236
|
await mkdir11(this.targetWebRoot, { recursive: true });
|
|
13869
14237
|
await Bun.write(path41.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
|
|
13870
14238
|
}
|
|
@@ -13945,7 +14313,7 @@ ${error.message}`;
|
|
|
13945
14313
|
await this.project.android.setVersionCode(this.target.buildNum);
|
|
13946
14314
|
await this.project.android.setAppName(this.target.appName);
|
|
13947
14315
|
}
|
|
13948
|
-
async#applyPermissions() {
|
|
14316
|
+
async#applyPermissions({ operation, env }) {
|
|
13949
14317
|
for (const permission of this.target.permissions ?? []) {
|
|
13950
14318
|
if (permission === "camera")
|
|
13951
14319
|
await this.addCamera();
|
|
@@ -13954,7 +14322,7 @@ ${error.message}`;
|
|
|
13954
14322
|
else if (permission === "location")
|
|
13955
14323
|
await this.addLocation();
|
|
13956
14324
|
else if (permission === "push")
|
|
13957
|
-
await this.addPush();
|
|
14325
|
+
await this.addPush({ operation, env });
|
|
13958
14326
|
}
|
|
13959
14327
|
}
|
|
13960
14328
|
async#applyDeepLinks(platform, { operation, env }) {
|
|
@@ -13973,7 +14341,7 @@ ${error.message}`;
|
|
|
13973
14341
|
await this.#setUrlSchemesInIos(schemes);
|
|
13974
14342
|
}
|
|
13975
14343
|
if (domains.length > 0)
|
|
13976
|
-
await this.#setAssociatedDomainsInIos(domains);
|
|
14344
|
+
await this.#setAssociatedDomainsInIos(domains, { operation, env });
|
|
13977
14345
|
return;
|
|
13978
14346
|
}
|
|
13979
14347
|
if (platform === "android") {
|
|
@@ -14067,12 +14435,64 @@ ${error.message}`;
|
|
|
14067
14435
|
this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
|
|
14068
14436
|
this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
|
|
14069
14437
|
}
|
|
14070
|
-
async addPush() {
|
|
14438
|
+
async addPush({ operation, env }) {
|
|
14071
14439
|
await this.#setPermissionInIos({
|
|
14072
14440
|
userNotificationsUsageDescription: "$(PRODUCT_NAME) uses notifications to keep you updated."
|
|
14073
14441
|
});
|
|
14442
|
+
await Promise.all([
|
|
14443
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { UIBackgroundModes: ["remote-notification"] }),
|
|
14444
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { UIBackgroundModes: ["remote-notification"] }),
|
|
14445
|
+
this.#writeEntitlementsInIos(this.target.deepLinks?.domains ?? [], { operation, env }),
|
|
14446
|
+
this.#ensurePushAppDelegateInIos()
|
|
14447
|
+
]);
|
|
14074
14448
|
this.#setPermissionsInAndroid(["POST_NOTIFICATIONS"]);
|
|
14075
14449
|
}
|
|
14450
|
+
async#ensurePushAppDelegateInIos() {
|
|
14451
|
+
const appDelegatePath = path41.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
|
|
14452
|
+
if (!await Bun.file(appDelegatePath).exists())
|
|
14453
|
+
return;
|
|
14454
|
+
const editor = await FileEditor.create(appDelegatePath);
|
|
14455
|
+
let content = editor.getContent();
|
|
14456
|
+
if (!content.includes("import FirebaseCore")) {
|
|
14457
|
+
content = content.replace("import Capacitor", `import Capacitor
|
|
14458
|
+
import FirebaseCore`);
|
|
14459
|
+
}
|
|
14460
|
+
if (!content.includes("import FirebaseMessaging")) {
|
|
14461
|
+
content = content.replace("import FirebaseCore", `import FirebaseCore
|
|
14462
|
+
import FirebaseMessaging`);
|
|
14463
|
+
}
|
|
14464
|
+
if (!content.includes("FirebaseApp.configure()")) {
|
|
14465
|
+
content = content.replace(/\n(\s*)return true/, `
|
|
14466
|
+
$1FirebaseApp.configure()
|
|
14467
|
+
|
|
14468
|
+
$1return true`);
|
|
14469
|
+
}
|
|
14470
|
+
if (!content.includes("didRegisterForRemoteNotificationsWithDeviceToken")) {
|
|
14471
|
+
const delegateMethods = `
|
|
14472
|
+
func application(_ application: UIApplication,
|
|
14473
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
14474
|
+
Messaging.messaging().apnsToken = deviceToken
|
|
14475
|
+
NotificationCenter.default.post(
|
|
14476
|
+
name: .capacitorDidRegisterForRemoteNotifications,
|
|
14477
|
+
object: deviceToken
|
|
14478
|
+
)
|
|
14479
|
+
}
|
|
14480
|
+
|
|
14481
|
+
func application(_ application: UIApplication,
|
|
14482
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
14483
|
+
NotificationCenter.default.post(
|
|
14484
|
+
name: .capacitorDidFailToRegisterForRemoteNotifications,
|
|
14485
|
+
object: error
|
|
14486
|
+
)
|
|
14487
|
+
}
|
|
14488
|
+
`;
|
|
14489
|
+
content = content.replace(`
|
|
14490
|
+
func applicationWillResignActive`, `${delegateMethods}
|
|
14491
|
+
func applicationWillResignActive`);
|
|
14492
|
+
}
|
|
14493
|
+
if (content !== editor.getContent())
|
|
14494
|
+
await editor.setContent(content).save();
|
|
14495
|
+
}
|
|
14076
14496
|
async#setPermissionInIos(permissions) {
|
|
14077
14497
|
const updateNs = Object.fromEntries(Object.entries(permissions).map(([key, value]) => [`NS${capitalize5(key)}`, value]));
|
|
14078
14498
|
await Promise.all([
|
|
@@ -14090,25 +14510,35 @@ ${error.message}`;
|
|
|
14090
14510
|
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
|
|
14091
14511
|
]);
|
|
14092
14512
|
}
|
|
14093
|
-
async#setAssociatedDomainsInIos(domains) {
|
|
14513
|
+
async#setAssociatedDomainsInIos(domains, { operation, env }) {
|
|
14514
|
+
await this.#writeEntitlementsInIos(domains, { operation, env });
|
|
14515
|
+
}
|
|
14516
|
+
async#writeEntitlementsInIos(domains, runConfig) {
|
|
14094
14517
|
const entitlementsRelPath = "App/App.entitlements";
|
|
14095
14518
|
const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
|
|
14096
14519
|
const values = domains.map((domain) => `applinks:${domain}`);
|
|
14520
|
+
const usesPush = this.target.permissions?.includes("push") ?? false;
|
|
14521
|
+
const apsEnvironment = resolveIosApnsEnvironment(runConfig);
|
|
14097
14522
|
const body = [
|
|
14098
14523
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
14099
14524
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
14100
14525
|
'<plist version="1.0">',
|
|
14101
14526
|
"<dict>",
|
|
14102
|
-
" <key>
|
|
14103
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14527
|
+
...usesPush ? [" <key>aps-environment</key>", ` <string>${apsEnvironment}</string>`] : [],
|
|
14528
|
+
...values.length > 0 ? [
|
|
14529
|
+
" <key>com.apple.developer.associated-domains</key>",
|
|
14530
|
+
" <array>",
|
|
14531
|
+
...values.map((value) => ` <string>${value}</string>`),
|
|
14532
|
+
" </array>"
|
|
14533
|
+
] : [],
|
|
14106
14534
|
"</dict>",
|
|
14107
14535
|
"</plist>",
|
|
14108
14536
|
""
|
|
14109
14537
|
].join(`
|
|
14110
14538
|
`);
|
|
14111
|
-
await
|
|
14539
|
+
const currentBody = await Bun.file(entitlementsPath).exists() ? await Bun.file(entitlementsPath).text() : undefined;
|
|
14540
|
+
if (currentBody !== body)
|
|
14541
|
+
await writeFile2(entitlementsPath, body);
|
|
14112
14542
|
await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
|
|
14113
14543
|
}
|
|
14114
14544
|
async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
|
|
@@ -14212,7 +14642,7 @@ ${filter}$1`);
|
|
|
14212
14642
|
for (const permission of permissions) {
|
|
14213
14643
|
if (this.#hasPermissionInAndroid(permission)) {
|
|
14214
14644
|
this.app.logger.info(`${permission} already exists in android`);
|
|
14215
|
-
|
|
14645
|
+
continue;
|
|
14216
14646
|
}
|
|
14217
14647
|
this.app.logger.info(`Adding ${permission} to android`);
|
|
14218
14648
|
this.project.android.getAndroidManifest().injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
|
|
@@ -14228,7 +14658,7 @@ ${filter}$1`);
|
|
|
14228
14658
|
return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
|
|
14229
14659
|
}
|
|
14230
14660
|
#hasPermissionInAndroid(permission) {
|
|
14231
|
-
return this.#getPermissionsInAndroid().includes(permission);
|
|
14661
|
+
return this.#getPermissionsInAndroid().includes(`android.permission.${permission}`);
|
|
14232
14662
|
}
|
|
14233
14663
|
}
|
|
14234
14664
|
// pkgs/@akanjs/devkit/commandDecorators/targetMeta.ts
|
|
@@ -15324,6 +15754,42 @@ var CONVENTION_SUFFIXES = [
|
|
|
15324
15754
|
".signal.ts",
|
|
15325
15755
|
".store.ts"
|
|
15326
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
|
+
}
|
|
15327
15793
|
|
|
15328
15794
|
class AkanQualityScanner {
|
|
15329
15795
|
async scan(workspaceRoot) {
|
|
@@ -15332,13 +15798,14 @@ class AkanQualityScanner {
|
|
|
15332
15798
|
const warnings = [
|
|
15333
15799
|
...this.#scanGlobalQuality(sourceFiles),
|
|
15334
15800
|
...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
|
|
15801
|
+
...sourceFiles.flatMap((sourceFile2) => this.#scanComponentQuality(sourceFile2)),
|
|
15335
15802
|
...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
|
|
15336
15803
|
...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
|
|
15337
15804
|
];
|
|
15338
15805
|
return {
|
|
15339
15806
|
workspaceRoot,
|
|
15340
15807
|
scannedFiles: sourceFiles.length,
|
|
15341
|
-
warnings: warnings.sort(compareWarnings),
|
|
15808
|
+
warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
|
|
15342
15809
|
suggestedRules: SUGGESTED_RULES
|
|
15343
15810
|
};
|
|
15344
15811
|
}
|
|
@@ -15388,7 +15855,8 @@ class AkanQualityScanner {
|
|
|
15388
15855
|
#scanGlobalQuality(sourceFiles) {
|
|
15389
15856
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15390
15857
|
const warnings = [];
|
|
15391
|
-
|
|
15858
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15859
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15392
15860
|
if (declarations.length < 2)
|
|
15393
15861
|
continue;
|
|
15394
15862
|
warnings.push({
|
|
@@ -15459,6 +15927,49 @@ class AkanQualityScanner {
|
|
|
15459
15927
|
}
|
|
15460
15928
|
return warnings;
|
|
15461
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
|
+
}
|
|
15462
15973
|
#scanConventionQuality(sourceFile2) {
|
|
15463
15974
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
|
|
15464
15975
|
if (!suffix)
|
|
@@ -15534,6 +16045,8 @@ function formatQualityWarnings(warnings) {
|
|
|
15534
16045
|
if (warning.locations?.length) {
|
|
15535
16046
|
lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
|
|
15536
16047
|
}
|
|
16048
|
+
if (warning.fix)
|
|
16049
|
+
lines.push(` fix: ${warning.fix}`);
|
|
15537
16050
|
return lines;
|
|
15538
16051
|
});
|
|
15539
16052
|
}
|
|
@@ -15542,6 +16055,7 @@ function formatQualityLocation(file, line) {
|
|
|
15542
16055
|
}
|
|
15543
16056
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15544
16057
|
const declarations = [];
|
|
16058
|
+
const nameExempt = isPageRouteFile(sourceFile2.file) || isUiComponentFile(sourceFile2.file);
|
|
15545
16059
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15546
16060
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15547
16061
|
declarations.push({
|
|
@@ -15549,7 +16063,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15549
16063
|
kind: "function",
|
|
15550
16064
|
file: sourceFile2.file,
|
|
15551
16065
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15552
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
16066
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
16067
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15553
16068
|
});
|
|
15554
16069
|
}
|
|
15555
16070
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15558,7 +16073,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15558
16073
|
kind: "class",
|
|
15559
16074
|
file: sourceFile2.file,
|
|
15560
16075
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15561
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
16076
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
16077
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15562
16078
|
});
|
|
15563
16079
|
}
|
|
15564
16080
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15570,16 +16086,127 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15570
16086
|
kind: "function-variable",
|
|
15571
16087
|
file: sourceFile2.file,
|
|
15572
16088
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15573
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
16089
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
16090
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15574
16091
|
});
|
|
15575
16092
|
}
|
|
15576
16093
|
}
|
|
15577
16094
|
}
|
|
15578
16095
|
return declarations;
|
|
15579
16096
|
}
|
|
16097
|
+
function isPageRouteFile(file) {
|
|
16098
|
+
const segments = file.split("/");
|
|
16099
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
16100
|
+
}
|
|
16101
|
+
function isUiComponentFile(file) {
|
|
16102
|
+
const segments = file.split("/");
|
|
16103
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
16104
|
+
}
|
|
16105
|
+
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
16106
|
+
if (!isInLibModule(file))
|
|
16107
|
+
return false;
|
|
16108
|
+
if (file.endsWith(".tsx"))
|
|
16109
|
+
return true;
|
|
16110
|
+
if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
|
|
16111
|
+
return true;
|
|
16112
|
+
if (file.endsWith(".constant.ts"))
|
|
16113
|
+
return !isEnumClass;
|
|
16114
|
+
return false;
|
|
16115
|
+
}
|
|
16116
|
+
function isInLibModule(file) {
|
|
16117
|
+
const segments = file.split("/");
|
|
16118
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
16119
|
+
}
|
|
16120
|
+
function isEnumClassStatement(sourceFile2, statement) {
|
|
16121
|
+
if (!ts11.isClassDeclaration(statement))
|
|
16122
|
+
return false;
|
|
16123
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
|
|
16124
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
16125
|
+
return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
|
|
16126
|
+
}
|
|
15580
16127
|
function getExportedClassNames(sourceFile2) {
|
|
15581
16128
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15582
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
|
+
}
|
|
15583
16210
|
function getPlaceholderExportWarnings(sourceFile2) {
|
|
15584
16211
|
if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
|
|
15585
16212
|
return [];
|
|
@@ -15595,11 +16222,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15595
16222
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15596
16223
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15597
16224
|
return [];
|
|
15598
|
-
const stalePatterns = [
|
|
15599
|
-
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
15600
|
-
{ pattern: /settting/, label: "misspelling: settting" },
|
|
15601
|
-
{ pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
|
|
15602
|
-
];
|
|
16225
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15603
16226
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15604
16227
|
rule: "akan.file.dictionary-stale-text",
|
|
15605
16228
|
scope: "file",
|
|
@@ -15720,10 +16343,8 @@ function getConventionDescription(suffix, modelName) {
|
|
|
15720
16343
|
}
|
|
15721
16344
|
function getLibRootFile(file) {
|
|
15722
16345
|
const segments = file.split("/");
|
|
15723
|
-
if (segments[0] === "libs" && segments.length === 4 && segments[2] === "lib")
|
|
16346
|
+
if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
|
|
15724
16347
|
return segments[3];
|
|
15725
|
-
if (segments[0] === "apps" && segments.length === 5 && segments[2] === "lib")
|
|
15726
|
-
return segments[4];
|
|
15727
16348
|
return null;
|
|
15728
16349
|
}
|
|
15729
16350
|
function getModuleUiWarning(file) {
|