@akanjs/cli 2.3.9-rc.9 → 2.3.10-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 +849 -99
- package/index.js +993 -147
- package/package.json +2 -2
- package/templates/workspaceRoot/CLAUDE.md.template +3 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
4
|
// pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
|
|
5
|
-
import
|
|
5
|
+
import path44 from "path";
|
|
6
6
|
|
|
7
7
|
// pkgs/@akanjs/devkit/aiEditor.ts
|
|
8
8
|
import { input, select } from "@inquirer/prompts";
|
|
@@ -744,6 +744,44 @@ var DEFAULT_AKAN_IMAGE_CONFIG = {
|
|
|
744
744
|
fetchTimeoutMs: 7000,
|
|
745
745
|
maxRemoteBytes: 25 * 1024 * 1024
|
|
746
746
|
};
|
|
747
|
+
var normalizeIndexPath = (indexPath) => {
|
|
748
|
+
const normalized = indexPath?.trim();
|
|
749
|
+
if (!normalized)
|
|
750
|
+
return;
|
|
751
|
+
const path2 = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
752
|
+
return path2 === "/" ? "/" : path2;
|
|
753
|
+
};
|
|
754
|
+
var normalizeStringList = (values) => {
|
|
755
|
+
const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
|
|
756
|
+
return normalized.length > 0 ? [...new Set(normalized)] : undefined;
|
|
757
|
+
};
|
|
758
|
+
var normalizeDeepLinkDomain = (domain) => {
|
|
759
|
+
const normalized = domain.trim();
|
|
760
|
+
if (!normalized)
|
|
761
|
+
return "";
|
|
762
|
+
try {
|
|
763
|
+
const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
|
|
764
|
+
return url.host.toLowerCase();
|
|
765
|
+
} catch {
|
|
766
|
+
return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
var normalizeDeepLinks = (deepLinks) => {
|
|
770
|
+
if (!deepLinks)
|
|
771
|
+
return;
|
|
772
|
+
const schemes = normalizeStringList(deepLinks.schemes);
|
|
773
|
+
const domains = normalizeStringList(deepLinks.domains?.map(normalizeDeepLinkDomain));
|
|
774
|
+
const teamId = deepLinks.ios?.teamId?.trim();
|
|
775
|
+
const sha256CertFingerprints = normalizeStringList(deepLinks.android?.sha256CertFingerprints);
|
|
776
|
+
if (!schemes && !domains && !teamId && !sha256CertFingerprints)
|
|
777
|
+
return;
|
|
778
|
+
return {
|
|
779
|
+
...schemes ? { schemes } : {},
|
|
780
|
+
...domains ? { domains } : {},
|
|
781
|
+
...teamId ? { ios: { teamId } } : {},
|
|
782
|
+
...sha256CertFingerprints ? { android: { sha256CertFingerprints } } : {}
|
|
783
|
+
};
|
|
784
|
+
};
|
|
747
785
|
|
|
748
786
|
class AkanAppConfig {
|
|
749
787
|
app;
|
|
@@ -757,6 +795,7 @@ class AkanAppConfig {
|
|
|
757
795
|
i18n;
|
|
758
796
|
publicEnv;
|
|
759
797
|
mobile;
|
|
798
|
+
secrets;
|
|
760
799
|
baseDevEnv;
|
|
761
800
|
libs;
|
|
762
801
|
domains = new Set;
|
|
@@ -783,11 +822,16 @@ class AkanAppConfig {
|
|
|
783
822
|
process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
|
|
784
823
|
process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
|
|
785
824
|
this.publicEnv = config?.publicEnv ?? [];
|
|
825
|
+
this.secrets = config?.secrets ?? [];
|
|
786
826
|
this.mobile = this.#resolveMobileConfig(config.mobile);
|
|
787
827
|
this.docker = this.#makeDockerContent(config?.docker ?? {});
|
|
788
828
|
}
|
|
789
829
|
#resolveMobileConfig(mobile) {
|
|
790
|
-
const {
|
|
830
|
+
const {
|
|
831
|
+
targets: rawTargets,
|
|
832
|
+
indexPath: _indexPath,
|
|
833
|
+
...rawMobile
|
|
834
|
+
} = mobile ?? {};
|
|
791
835
|
const appName = rawMobile.appName ?? this.app.name;
|
|
792
836
|
const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
|
|
793
837
|
const version = rawMobile.version ?? "0.0.1";
|
|
@@ -800,6 +844,8 @@ class AkanAppConfig {
|
|
|
800
844
|
const target = rawTarget;
|
|
801
845
|
const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
|
|
802
846
|
const basePath2 = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
|
|
847
|
+
const indexPath = normalizeIndexPath(target.indexPath);
|
|
848
|
+
const deepLinks = normalizeDeepLinks(target.deepLinks);
|
|
803
849
|
if (basePath2 && !this.basePaths.has(basePath2)) {
|
|
804
850
|
throw new Error(`Mobile target '${name}' uses unknown basePath '${basePath2}' in apps/${this.app.name}/akan.config.ts`);
|
|
805
851
|
}
|
|
@@ -808,6 +854,8 @@ class AkanAppConfig {
|
|
|
808
854
|
...target,
|
|
809
855
|
name,
|
|
810
856
|
basePath: basePath2,
|
|
857
|
+
indexPath,
|
|
858
|
+
deepLinks,
|
|
811
859
|
appName: target.appName ?? appName,
|
|
812
860
|
appId: target.appId ?? appId,
|
|
813
861
|
version: target.version ?? version,
|
|
@@ -12116,7 +12164,14 @@ class SsrBaseArtifactBuilder {
|
|
|
12116
12164
|
basePaths: [...akanConfig2.basePaths],
|
|
12117
12165
|
branches: [...akanConfig2.branches],
|
|
12118
12166
|
i18n: akanConfig2.i18n,
|
|
12119
|
-
imageConfig: akanConfig2.images
|
|
12167
|
+
imageConfig: akanConfig2.images,
|
|
12168
|
+
deepLinkAssociations: Object.values(akanConfig2.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
|
|
12169
|
+
targetName: target.name,
|
|
12170
|
+
appId: target.appId,
|
|
12171
|
+
domains: target.deepLinks?.domains ?? [],
|
|
12172
|
+
iosTeamId: target.deepLinks?.ios?.teamId,
|
|
12173
|
+
androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
|
|
12174
|
+
}))
|
|
12120
12175
|
};
|
|
12121
12176
|
await Bun.write(path35.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
|
|
12122
12177
|
`);
|
|
@@ -12504,6 +12559,7 @@ void run().catch((error) => {
|
|
|
12504
12559
|
}
|
|
12505
12560
|
// pkgs/@akanjs/devkit/applicationReleasePackager.ts
|
|
12506
12561
|
import { cp, mkdir as mkdir9, rm as rm4 } from "fs/promises";
|
|
12562
|
+
import path38 from "path";
|
|
12507
12563
|
|
|
12508
12564
|
// pkgs/@akanjs/devkit/uploadRelease.ts
|
|
12509
12565
|
import { HttpClient as HttpClient2, Logger as Logger9 } from "akanjs/common";
|
|
@@ -12619,13 +12675,9 @@ class ApplicationReleasePackager {
|
|
|
12619
12675
|
await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
|
|
12620
12676
|
await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
|
|
12621
12677
|
await rm4(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
"-C",
|
|
12626
|
-
buildRoot,
|
|
12627
|
-
"./"
|
|
12628
|
-
]);
|
|
12678
|
+
const releaseRoot = this.#app.workspace.workspaceRoot;
|
|
12679
|
+
const releaseArchivePath = path38.relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`).split(path38.sep).join("/");
|
|
12680
|
+
await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
|
|
12629
12681
|
await this.#writeCsrZipIfPresent();
|
|
12630
12682
|
return { platformVersion };
|
|
12631
12683
|
}
|
|
@@ -12646,22 +12698,18 @@ class ApplicationReleasePackager {
|
|
|
12646
12698
|
await cp(this.#app.dist.cwdPath, `${sourceRoot}/apps/${this.#app.name}`, { recursive: true });
|
|
12647
12699
|
const libDeps = ["social", "shared", "platform", "util"];
|
|
12648
12700
|
await Promise.all(libDeps.map((lib) => cp(`${this.#app.workspace.cwdPath}/libs/${lib}`, `${sourceRoot}/libs/${lib}`, { recursive: true })));
|
|
12649
|
-
await Promise.all([".next", "ios", "android", "public/libs"].map(async (
|
|
12650
|
-
const targetPath = `${sourceRoot}/apps/${this.#app.name}/${
|
|
12701
|
+
await Promise.all([".next", "ios", "android", "public/libs"].map(async (path39) => {
|
|
12702
|
+
const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path39}`;
|
|
12651
12703
|
if (await FileSys.dirExists(targetPath))
|
|
12652
12704
|
await rm4(targetPath, { recursive: true, force: true });
|
|
12653
12705
|
}));
|
|
12654
12706
|
const syncPaths = [".husky", ".gitignore", "package.json"];
|
|
12655
|
-
await Promise.all(syncPaths.map((
|
|
12707
|
+
await Promise.all(syncPaths.map((path39) => cp(`${this.#app.workspace.cwdPath}/${path39}`, `${sourceRoot}/${path39}`, { recursive: true })));
|
|
12656
12708
|
await this.#writeSourceTsconfig(sourceRoot, libDeps);
|
|
12657
12709
|
await Bun.write(`${sourceRoot}/README.md`, readme);
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
"-C",
|
|
12662
|
-
sourceRoot,
|
|
12663
|
-
"./"
|
|
12664
|
-
]);
|
|
12710
|
+
const sourceCwd = this.#app.workspace.cwdPath;
|
|
12711
|
+
const sourceArchivePath = path38.relative(sourceCwd, `${sourceCwd}/releases/sources/${this.#app.name}-source.tar.gz`).split(path38.sep).join("/");
|
|
12712
|
+
await this.#app.workspace.spawn("tar", ["-zcf", sourceArchivePath, "-C", sourceRoot, "./"], { cwd: sourceCwd });
|
|
12665
12713
|
}
|
|
12666
12714
|
async#resetSourceRoot(sourceRoot) {
|
|
12667
12715
|
if (await FileSys.dirExists(sourceRoot)) {
|
|
@@ -12743,20 +12791,20 @@ class ApplicationReleasePackager {
|
|
|
12743
12791
|
}
|
|
12744
12792
|
}
|
|
12745
12793
|
// pkgs/@akanjs/devkit/applicationTestPreload.ts
|
|
12746
|
-
import
|
|
12794
|
+
import path39 from "path";
|
|
12747
12795
|
var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
|
|
12748
12796
|
async function resolveSignalTestPreloadPath(target) {
|
|
12749
12797
|
const candidates = [];
|
|
12750
12798
|
const addResolvedPackageCandidate = (basePath2) => {
|
|
12751
12799
|
try {
|
|
12752
|
-
candidates.push(
|
|
12800
|
+
candidates.push(path39.join(path39.dirname(Bun.resolveSync("akanjs/package.json", basePath2)), SIGNAL_TEST_PRELOAD_PATH));
|
|
12753
12801
|
} catch {}
|
|
12754
12802
|
};
|
|
12755
12803
|
addResolvedPackageCandidate(target.cwdPath);
|
|
12756
12804
|
addResolvedPackageCandidate(process.cwd());
|
|
12757
|
-
addResolvedPackageCandidate(
|
|
12805
|
+
addResolvedPackageCandidate(path39.dirname(Bun.main));
|
|
12758
12806
|
addResolvedPackageCandidate(import.meta.dir);
|
|
12759
|
-
candidates.push(
|
|
12807
|
+
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));
|
|
12760
12808
|
for (const candidate of [...new Set(candidates)]) {
|
|
12761
12809
|
if (await Bun.file(candidate).exists())
|
|
12762
12810
|
return candidate;
|
|
@@ -12766,7 +12814,7 @@ async function resolveSignalTestPreloadPath(target) {
|
|
|
12766
12814
|
// pkgs/@akanjs/devkit/builder.ts
|
|
12767
12815
|
import { existsSync as existsSync2 } from "fs";
|
|
12768
12816
|
import { mkdir as mkdir10 } from "fs/promises";
|
|
12769
|
-
import
|
|
12817
|
+
import path40 from "path";
|
|
12770
12818
|
var SKIP_ENTRY_DIR_SET = new Set(["node_modules", "dist", "build", ".git", ".next"]);
|
|
12771
12819
|
var assetExtensions = [".css", ".md", ".js", ".png", ".ico", ".svg", ".json", ".template"];
|
|
12772
12820
|
var assetLoader = Object.fromEntries(assetExtensions.map((ext) => [ext, "file"]));
|
|
@@ -12783,14 +12831,14 @@ class Builder {
|
|
|
12783
12831
|
#globEntrypoints(cwd, pattern) {
|
|
12784
12832
|
const glob = new Bun.Glob(pattern);
|
|
12785
12833
|
return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
|
|
12786
|
-
const segments = relativePath.split(
|
|
12834
|
+
const segments = relativePath.split(path40.sep);
|
|
12787
12835
|
return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
|
|
12788
|
-
}).map((rel) =>
|
|
12836
|
+
}).map((rel) => path40.join(cwd, rel));
|
|
12789
12837
|
}
|
|
12790
12838
|
#globFiles(cwd, pattern = "**/*.*") {
|
|
12791
12839
|
const glob = new Bun.Glob(pattern);
|
|
12792
12840
|
return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
|
|
12793
|
-
const segments = relativePath.split(
|
|
12841
|
+
const segments = relativePath.split(path40.sep);
|
|
12794
12842
|
return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
|
|
12795
12843
|
});
|
|
12796
12844
|
}
|
|
@@ -12798,17 +12846,17 @@ class Builder {
|
|
|
12798
12846
|
const out = [];
|
|
12799
12847
|
for (const p of additionalEntryPoints) {
|
|
12800
12848
|
if (p.includes("*")) {
|
|
12801
|
-
const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${
|
|
12849
|
+
const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path40.sep}`) ? p.slice(cwd.length + 1) : p;
|
|
12802
12850
|
out.push(...this.#globEntrypoints(cwd, rel));
|
|
12803
12851
|
} else
|
|
12804
|
-
out.push(
|
|
12852
|
+
out.push(path40.isAbsolute(p) ? p : path40.join(cwd, p));
|
|
12805
12853
|
}
|
|
12806
12854
|
return out;
|
|
12807
12855
|
}
|
|
12808
12856
|
#getBuildOptions({ bundle = false, additionalEntryPoints = [] } = {}) {
|
|
12809
12857
|
const cwd = this.#executor.cwdPath;
|
|
12810
12858
|
const entrypoints = [
|
|
12811
|
-
...bundle ? [
|
|
12859
|
+
...bundle ? [path40.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}"),
|
|
12812
12860
|
...this.#resolveAdditionalEntrypoints(cwd, additionalEntryPoints)
|
|
12813
12861
|
];
|
|
12814
12862
|
return {
|
|
@@ -12829,9 +12877,9 @@ class Builder {
|
|
|
12829
12877
|
for (const relativePath of this.#globFiles(cwd)) {
|
|
12830
12878
|
if (relativePath === "package.json")
|
|
12831
12879
|
continue;
|
|
12832
|
-
const sourcePath =
|
|
12833
|
-
const targetPath =
|
|
12834
|
-
await mkdir10(
|
|
12880
|
+
const sourcePath = path40.join(cwd, relativePath);
|
|
12881
|
+
const targetPath = path40.join(this.#distExecutor.cwdPath, relativePath);
|
|
12882
|
+
await mkdir10(path40.dirname(targetPath), { recursive: true });
|
|
12835
12883
|
await Bun.write(targetPath, Bun.file(sourcePath));
|
|
12836
12884
|
}
|
|
12837
12885
|
}
|
|
@@ -12842,13 +12890,13 @@ class Builder {
|
|
|
12842
12890
|
return withoutFormatDir;
|
|
12843
12891
|
if (!hasDotSlash && withoutFormatDir === publishedPath)
|
|
12844
12892
|
return publishedPath;
|
|
12845
|
-
const parsed =
|
|
12893
|
+
const parsed = path40.posix.parse(withoutFormatDir);
|
|
12846
12894
|
if (![".js", ".mjs", ".cjs"].includes(parsed.ext))
|
|
12847
12895
|
return withoutFormatDir;
|
|
12848
|
-
const withoutExt =
|
|
12896
|
+
const withoutExt = path40.posix.join(parsed.dir, parsed.name);
|
|
12849
12897
|
const sourcePath = withoutExt.startsWith("./") ? withoutExt.slice(2) : withoutExt;
|
|
12850
12898
|
const sourceCandidates = [`${sourcePath}.ts`, `${sourcePath}.tsx`];
|
|
12851
|
-
const matchedSource = sourceCandidates.find((candidate) => existsSync2(
|
|
12899
|
+
const matchedSource = sourceCandidates.find((candidate) => existsSync2(path40.join(this.#executor.cwdPath, candidate)));
|
|
12852
12900
|
if (!matchedSource)
|
|
12853
12901
|
return withoutFormatDir;
|
|
12854
12902
|
return hasDotSlash ? `./${matchedSource}` : matchedSource;
|
|
@@ -12897,9 +12945,9 @@ class Builder {
|
|
|
12897
12945
|
}
|
|
12898
12946
|
}
|
|
12899
12947
|
// pkgs/@akanjs/devkit/capacitorApp.ts
|
|
12900
|
-
import { cp as cp2, mkdir as mkdir11, rm as rm5 } from "fs/promises";
|
|
12948
|
+
import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm5, writeFile as writeFile2 } from "fs/promises";
|
|
12901
12949
|
import os from "os";
|
|
12902
|
-
import
|
|
12950
|
+
import path41 from "path";
|
|
12903
12951
|
import { select as select2 } from "@inquirer/prompts";
|
|
12904
12952
|
import { MobileProject } from "@trapezedev/project";
|
|
12905
12953
|
import { capitalize as capitalize5 } from "akanjs/common";
|
|
@@ -13106,13 +13154,13 @@ var rootCapacitorConfigFilenames = [
|
|
|
13106
13154
|
"capacitor.config.js",
|
|
13107
13155
|
"capacitor.config.json"
|
|
13108
13156
|
];
|
|
13109
|
-
var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) =>
|
|
13157
|
+
var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path41.join(appRoot, file));
|
|
13110
13158
|
async function clearRootCapacitorConfigs(appRoot) {
|
|
13111
13159
|
await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm5(file, { force: true })));
|
|
13112
13160
|
}
|
|
13113
13161
|
async function writeRootCapacitorConfig(appRoot, content) {
|
|
13114
13162
|
await clearRootCapacitorConfigs(appRoot);
|
|
13115
|
-
await Bun.write(
|
|
13163
|
+
await Bun.write(path41.join(appRoot, "capacitor.config.json"), content);
|
|
13116
13164
|
}
|
|
13117
13165
|
var getLocalIP = () => {
|
|
13118
13166
|
const interfaces = os.networkInterfaces();
|
|
@@ -13221,13 +13269,13 @@ function buildIosNativeRunCommand({
|
|
|
13221
13269
|
scheme = "App",
|
|
13222
13270
|
configuration = "Debug"
|
|
13223
13271
|
}) {
|
|
13224
|
-
const derivedDataPath =
|
|
13272
|
+
const derivedDataPath = path41.join(appRoot, "ios/DerivedData", device.id);
|
|
13225
13273
|
const productPlatform = device.kind === "device" ? "iphoneos" : "iphonesimulator";
|
|
13226
13274
|
const destination = device.kind === "device" ? `id=${device.xcodebuildId ?? device.id}` : `platform=iOS Simulator,id=${device.id}`;
|
|
13227
13275
|
return {
|
|
13228
13276
|
configuration,
|
|
13229
13277
|
derivedDataPath,
|
|
13230
|
-
appPath:
|
|
13278
|
+
appPath: path41.join(derivedDataPath, "Build/Products", `${configuration}-${productPlatform}`, "App.app"),
|
|
13231
13279
|
xcodebuildArgs: [
|
|
13232
13280
|
"-project",
|
|
13233
13281
|
"App.xcodeproj",
|
|
@@ -13411,11 +13459,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
|
|
|
13411
13459
|
const {
|
|
13412
13460
|
name,
|
|
13413
13461
|
basePath: _basePath,
|
|
13462
|
+
indexPath: _indexPath,
|
|
13414
13463
|
version: _version,
|
|
13415
13464
|
buildNum: _buildNum,
|
|
13416
13465
|
assets: _assets,
|
|
13417
13466
|
permissions: _permissions,
|
|
13418
|
-
|
|
13467
|
+
deepLinks: _deepLinks,
|
|
13419
13468
|
files: _files,
|
|
13420
13469
|
appId,
|
|
13421
13470
|
appName,
|
|
@@ -13437,7 +13486,7 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
|
|
|
13437
13486
|
...capacitorConfig,
|
|
13438
13487
|
appId,
|
|
13439
13488
|
appName,
|
|
13440
|
-
webDir:
|
|
13489
|
+
webDir: path41.posix.join(".akan", "mobile", name, "www"),
|
|
13441
13490
|
plugins: {
|
|
13442
13491
|
CapacitorCookies: { enabled: true },
|
|
13443
13492
|
...pluginsConfig,
|
|
@@ -13494,10 +13543,10 @@ class CapacitorApp {
|
|
|
13494
13543
|
constructor(app, target) {
|
|
13495
13544
|
this.app = app;
|
|
13496
13545
|
this.target = target;
|
|
13497
|
-
this.targetRootPath =
|
|
13498
|
-
this.targetRoot =
|
|
13499
|
-
this.targetWebRoot =
|
|
13500
|
-
this.targetAssetRoot =
|
|
13546
|
+
this.targetRootPath = path41.posix.join(".akan", "mobile", this.target.name);
|
|
13547
|
+
this.targetRoot = path41.join(this.app.cwdPath, this.targetRootPath);
|
|
13548
|
+
this.targetWebRoot = path41.join(this.targetRoot, "www");
|
|
13549
|
+
this.targetAssetRoot = path41.join(this.targetRoot, "assets");
|
|
13501
13550
|
this.project = new MobileProject(this.app.cwdPath, {
|
|
13502
13551
|
android: { path: this.androidRootPath },
|
|
13503
13552
|
ios: { path: this.iosProjectPath }
|
|
@@ -13512,9 +13561,9 @@ class CapacitorApp {
|
|
|
13512
13561
|
await mkdir11(this.targetRoot, { recursive: true });
|
|
13513
13562
|
if (regenerate) {
|
|
13514
13563
|
if (!platform || platform === "ios")
|
|
13515
|
-
await rm5(
|
|
13564
|
+
await rm5(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
|
|
13516
13565
|
if (!platform || platform === "android")
|
|
13517
|
-
await rm5(
|
|
13566
|
+
await rm5(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
|
|
13518
13567
|
}
|
|
13519
13568
|
const project = this.project;
|
|
13520
13569
|
await this.project.load();
|
|
@@ -13537,7 +13586,7 @@ class CapacitorApp {
|
|
|
13537
13586
|
await this.#prepareExternalFiles("ios");
|
|
13538
13587
|
await this.#applyIosMetadata();
|
|
13539
13588
|
await this.#applyPermissions();
|
|
13540
|
-
await this.#
|
|
13589
|
+
await this.#applyDeepLinks("ios", { operation, env });
|
|
13541
13590
|
await this.project.commit();
|
|
13542
13591
|
await this.#generateAssets({ operation, env });
|
|
13543
13592
|
this.app.verbose(`syncing iOS`);
|
|
@@ -13637,7 +13686,7 @@ ${textError instanceof Error ? textError.message : ""}`);
|
|
|
13637
13686
|
const xcodebuildArgs = noAllowProvisioningUpdates ? command.xcodebuildArgs : [...command.xcodebuildArgs.slice(0, -1), "-allowProvisioningUpdates", ...command.xcodebuildArgs.slice(-1)];
|
|
13638
13687
|
try {
|
|
13639
13688
|
await this.#spawn("xcodebuild", xcodebuildArgs, {
|
|
13640
|
-
cwd:
|
|
13689
|
+
cwd: path41.join(this.app.cwdPath, this.iosProjectPath),
|
|
13641
13690
|
env: mobileEnv
|
|
13642
13691
|
});
|
|
13643
13692
|
const devicectlId = runTarget.devicectlId ?? runTarget.id;
|
|
@@ -13662,7 +13711,7 @@ ${textError instanceof Error ? textError.message : ""}`);
|
|
|
13662
13711
|
return isRecord2(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App";
|
|
13663
13712
|
}
|
|
13664
13713
|
async#getIosDevelopmentTeam() {
|
|
13665
|
-
const pbxprojPath =
|
|
13714
|
+
const pbxprojPath = path41.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
|
|
13666
13715
|
if (!await Bun.file(pbxprojPath).exists())
|
|
13667
13716
|
return;
|
|
13668
13717
|
return (await Bun.file(pbxprojPath).text()).match(/DEVELOPMENT_TEAM = ([^;]+);/)?.[1]?.trim();
|
|
@@ -13680,15 +13729,16 @@ ${error.message}`;
|
|
|
13680
13729
|
await this.#prepareExternalFiles("android");
|
|
13681
13730
|
await this.#applyAndroidMetadata();
|
|
13682
13731
|
await this.#applyPermissions();
|
|
13683
|
-
await this.#
|
|
13732
|
+
await this.#applyDeepLinks("android", { operation, env });
|
|
13684
13733
|
await this.project.commit();
|
|
13685
13734
|
await this.#generateAssets({ operation, env });
|
|
13686
13735
|
await this.#ensureAndroidAssetsDir();
|
|
13687
13736
|
await this.#ensureAndroidDebugKeystore();
|
|
13688
13737
|
await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
|
|
13738
|
+
await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
|
|
13689
13739
|
}
|
|
13690
13740
|
async#updateAndroidBuildTypes() {
|
|
13691
|
-
const appGradle = await FileEditor.create(
|
|
13741
|
+
const appGradle = await FileEditor.create(path41.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
|
|
13692
13742
|
const buildTypesBlock = `
|
|
13693
13743
|
debug {
|
|
13694
13744
|
applicationIdSuffix ".debug"
|
|
@@ -13732,7 +13782,7 @@ ${error.message}`;
|
|
|
13732
13782
|
const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
|
|
13733
13783
|
await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
|
|
13734
13784
|
stdio: "inherit",
|
|
13735
|
-
cwd:
|
|
13785
|
+
cwd: path41.join(this.app.cwdPath, this.androidRootPath),
|
|
13736
13786
|
env: await this.#commandEnv("release", env)
|
|
13737
13787
|
});
|
|
13738
13788
|
}
|
|
@@ -13740,10 +13790,10 @@ ${error.message}`;
|
|
|
13740
13790
|
await this.#spawnMobile("npx", ["cap", "open", "android"], { operation: "local", env: "local" });
|
|
13741
13791
|
}
|
|
13742
13792
|
async#ensureAndroidAssetsDir() {
|
|
13743
|
-
await mkdir11(
|
|
13793
|
+
await mkdir11(path41.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
|
|
13744
13794
|
}
|
|
13745
13795
|
async#ensureAndroidDebugKeystore() {
|
|
13746
|
-
const keystorePath =
|
|
13796
|
+
const keystorePath = path41.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
|
|
13747
13797
|
if (await Bun.file(keystorePath).exists())
|
|
13748
13798
|
return;
|
|
13749
13799
|
await this.#spawn("keytool", [
|
|
@@ -13782,7 +13832,7 @@ ${error.message}`;
|
|
|
13782
13832
|
await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
|
|
13783
13833
|
}
|
|
13784
13834
|
async#assertAndroidReleaseSigningConfig() {
|
|
13785
|
-
const gradlePropertiesPath =
|
|
13835
|
+
const gradlePropertiesPath = path41.join(this.app.cwdPath, this.androidRootPath, "gradle.properties");
|
|
13786
13836
|
const gradleProperties = await Bun.file(gradlePropertiesPath).exists() ? await Bun.file(gradlePropertiesPath).text() : "";
|
|
13787
13837
|
const missingKeys = getMissingAndroidReleaseSigningKeys({ gradleProperties });
|
|
13788
13838
|
if (missingKeys.length > 0)
|
|
@@ -13809,16 +13859,20 @@ ${error.message}`;
|
|
|
13809
13859
|
await this.#prepareAndroid({ operation: "release", env: "main" });
|
|
13810
13860
|
}
|
|
13811
13861
|
async prepareWww() {
|
|
13812
|
-
const htmlSource =
|
|
13862
|
+
const htmlSource = path41.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
|
|
13813
13863
|
if (!await Bun.file(htmlSource).exists())
|
|
13814
13864
|
throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
|
|
13815
13865
|
await rm5(this.targetWebRoot, { recursive: true, force: true });
|
|
13816
13866
|
await mkdir11(this.targetWebRoot, { recursive: true });
|
|
13817
|
-
await Bun.write(
|
|
13867
|
+
await Bun.write(path41.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
|
|
13818
13868
|
}
|
|
13819
13869
|
#injectMobileTargetMeta(html) {
|
|
13820
13870
|
const basePath2 = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
13821
|
-
const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({
|
|
13871
|
+
const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({
|
|
13872
|
+
name: this.target.name,
|
|
13873
|
+
basePath: basePath2,
|
|
13874
|
+
indexPath: this.target.indexPath
|
|
13875
|
+
})};</script>`;
|
|
13822
13876
|
if (html.includes("window.__AKAN_MOBILE_TARGET__"))
|
|
13823
13877
|
return html;
|
|
13824
13878
|
return html.replace(/<\/head\s*>/i, `${script}
|
|
@@ -13834,7 +13888,7 @@ ${error.message}`;
|
|
|
13834
13888
|
});
|
|
13835
13889
|
const content = `${JSON.stringify(config, null, 2)}
|
|
13836
13890
|
`;
|
|
13837
|
-
await Bun.write(
|
|
13891
|
+
await Bun.write(path41.join(this.targetRoot, "capacitor.config.json"), content);
|
|
13838
13892
|
return content;
|
|
13839
13893
|
}
|
|
13840
13894
|
async#prepareTargetAssets() {
|
|
@@ -13842,11 +13896,11 @@ ${error.message}`;
|
|
|
13842
13896
|
return;
|
|
13843
13897
|
await mkdir11(this.targetAssetRoot, { recursive: true });
|
|
13844
13898
|
if (this.target.assets.icon)
|
|
13845
|
-
await cp2(
|
|
13899
|
+
await cp2(path41.join(this.app.cwdPath, this.target.assets.icon), path41.join(this.targetAssetRoot, "icon.png"), {
|
|
13846
13900
|
force: true
|
|
13847
13901
|
});
|
|
13848
13902
|
if (this.target.assets.splash)
|
|
13849
|
-
await cp2(
|
|
13903
|
+
await cp2(path41.join(this.app.cwdPath, this.target.assets.splash), path41.join(this.targetAssetRoot, "splash.png"), {
|
|
13850
13904
|
force: true
|
|
13851
13905
|
});
|
|
13852
13906
|
}
|
|
@@ -13854,11 +13908,11 @@ ${error.message}`;
|
|
|
13854
13908
|
const files = this.target.files?.[platform];
|
|
13855
13909
|
if (!files)
|
|
13856
13910
|
return;
|
|
13857
|
-
const platformRoot =
|
|
13911
|
+
const platformRoot = path41.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
|
|
13858
13912
|
await Promise.all(Object.entries(files).map(async ([to, from]) => {
|
|
13859
|
-
const targetPath =
|
|
13860
|
-
await mkdir11(
|
|
13861
|
-
await cp2(
|
|
13913
|
+
const targetPath = path41.join(platformRoot, to);
|
|
13914
|
+
await mkdir11(path41.dirname(targetPath), { recursive: true });
|
|
13915
|
+
await cp2(path41.join(this.app.cwdPath, from), targetPath, { force: true });
|
|
13862
13916
|
}));
|
|
13863
13917
|
}
|
|
13864
13918
|
async#generateAssets({ operation, env }) {
|
|
@@ -13868,7 +13922,7 @@ ${error.message}`;
|
|
|
13868
13922
|
"@capacitor/assets",
|
|
13869
13923
|
"generate",
|
|
13870
13924
|
"--assetPath",
|
|
13871
|
-
|
|
13925
|
+
path41.posix.join(this.targetRootPath, "assets"),
|
|
13872
13926
|
"--iosProject",
|
|
13873
13927
|
this.iosProjectPath,
|
|
13874
13928
|
"--androidProject",
|
|
@@ -13901,25 +13955,48 @@ ${error.message}`;
|
|
|
13901
13955
|
await this.addPush();
|
|
13902
13956
|
}
|
|
13903
13957
|
}
|
|
13904
|
-
async#
|
|
13905
|
-
const
|
|
13906
|
-
if (!
|
|
13958
|
+
async#applyDeepLinks(platform, { operation, env }) {
|
|
13959
|
+
const deepLinks = this.target.deepLinks;
|
|
13960
|
+
if (!deepLinks)
|
|
13907
13961
|
return;
|
|
13908
|
-
const schemes =
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
13962
|
+
const schemes = deepLinks.schemes ?? [];
|
|
13963
|
+
const domains = deepLinks.domains ?? [];
|
|
13964
|
+
if (domains.length > 0)
|
|
13965
|
+
this.#assertDeepLinkVerificationConfig(platform, { operation, env });
|
|
13966
|
+
if (platform === "ios") {
|
|
13967
|
+
if (schemes.length > 0) {
|
|
13968
|
+
await this.#setPermissionInIos({
|
|
13969
|
+
appTransportSecurity: ""
|
|
13970
|
+
});
|
|
13971
|
+
await this.#setUrlSchemesInIos(schemes);
|
|
13972
|
+
}
|
|
13973
|
+
if (domains.length > 0)
|
|
13974
|
+
await this.#setAssociatedDomainsInIos(domains);
|
|
13975
|
+
return;
|
|
13976
|
+
}
|
|
13977
|
+
if (platform === "android") {
|
|
13913
13978
|
for (const scheme of schemes) {
|
|
13914
13979
|
this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="${scheme}" /></intent-filter>`);
|
|
13915
13980
|
}
|
|
13981
|
+
for (const domain of domains) {
|
|
13982
|
+
const pathPrefix = resolveMobilePath(this.target, "/");
|
|
13983
|
+
this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" /></intent-filter>`);
|
|
13984
|
+
}
|
|
13985
|
+
await this.#setDeepLinksInAndroid(schemes, domains);
|
|
13916
13986
|
}
|
|
13917
|
-
|
|
13918
|
-
|
|
13987
|
+
}
|
|
13988
|
+
#assertDeepLinkVerificationConfig(platform, { operation, env }) {
|
|
13989
|
+
const deepLinks = this.target.deepLinks;
|
|
13990
|
+
if (!deepLinks?.domains?.length)
|
|
13991
|
+
return;
|
|
13992
|
+
if (platform === "ios" && !deepLinks.ios?.teamId) {
|
|
13993
|
+
throw new Error(`Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.ios.teamId in apps/${this.app.name}/akan.config.ts`);
|
|
13919
13994
|
}
|
|
13920
|
-
|
|
13921
|
-
const
|
|
13922
|
-
|
|
13995
|
+
if (platform === "android" && !deepLinks.android?.sha256CertFingerprints?.length) {
|
|
13996
|
+
const message = `Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.android.sha256CertFingerprints in apps/${this.app.name}/akan.config.ts`;
|
|
13997
|
+
if (operation === "release" || env === "main")
|
|
13998
|
+
throw new Error(message);
|
|
13999
|
+
this.app.logger.warn(message);
|
|
13923
14000
|
}
|
|
13924
14001
|
}
|
|
13925
14002
|
async#commandEnv(operation, env) {
|
|
@@ -13940,6 +14017,8 @@ ${error.message}`;
|
|
|
13940
14017
|
const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
|
|
13941
14018
|
if (basePath2)
|
|
13942
14019
|
params.set("akanMobileBasePath", basePath2);
|
|
14020
|
+
if (this.target.indexPath)
|
|
14021
|
+
params.set("akanMobileIndexPath", this.target.indexPath);
|
|
13943
14022
|
return `http://${ip}:${port}/${pathname}?${params}`;
|
|
13944
14023
|
}
|
|
13945
14024
|
async#clearRootCapacitorConfigs() {
|
|
@@ -13999,6 +14078,112 @@ ${error.message}`;
|
|
|
13999
14078
|
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs)
|
|
14000
14079
|
]);
|
|
14001
14080
|
}
|
|
14081
|
+
async#setUrlSchemesInIos(schemes) {
|
|
14082
|
+
const urlTypes = schemes.map((scheme) => ({
|
|
14083
|
+
CFBundleURLName: this.target.appId,
|
|
14084
|
+
CFBundleURLSchemes: [scheme]
|
|
14085
|
+
}));
|
|
14086
|
+
await Promise.all([
|
|
14087
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { CFBundleURLTypes: urlTypes }),
|
|
14088
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
|
|
14089
|
+
]);
|
|
14090
|
+
}
|
|
14091
|
+
async#setAssociatedDomainsInIos(domains) {
|
|
14092
|
+
const entitlementsRelPath = "App/App.entitlements";
|
|
14093
|
+
const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
|
|
14094
|
+
const values = domains.map((domain) => `applinks:${domain}`);
|
|
14095
|
+
const body = [
|
|
14096
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
14097
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
14098
|
+
'<plist version="1.0">',
|
|
14099
|
+
"<dict>",
|
|
14100
|
+
" <key>com.apple.developer.associated-domains</key>",
|
|
14101
|
+
" <array>",
|
|
14102
|
+
...values.map((value) => ` <string>${value}</string>`),
|
|
14103
|
+
" </array>",
|
|
14104
|
+
"</dict>",
|
|
14105
|
+
"</plist>",
|
|
14106
|
+
""
|
|
14107
|
+
].join(`
|
|
14108
|
+
`);
|
|
14109
|
+
await writeFile2(entitlementsPath, body);
|
|
14110
|
+
await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
|
|
14111
|
+
}
|
|
14112
|
+
async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
|
|
14113
|
+
const pbxprojPath = path41.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
|
|
14114
|
+
const lines = (await readFile2(pbxprojPath, "utf8")).split(`
|
|
14115
|
+
`);
|
|
14116
|
+
let changed = false;
|
|
14117
|
+
for (let index = 0;index < lines.length; index++) {
|
|
14118
|
+
const line = lines[index] ?? "";
|
|
14119
|
+
if (!line.includes(`PRODUCT_BUNDLE_IDENTIFIER = ${this.target.appId};`))
|
|
14120
|
+
continue;
|
|
14121
|
+
let start = index;
|
|
14122
|
+
while (start >= 0 && !lines[start]?.includes("buildSettings = {"))
|
|
14123
|
+
start--;
|
|
14124
|
+
let end = index;
|
|
14125
|
+
while (end < lines.length && !/^\s*\};\s*$/.test(lines[end] ?? ""))
|
|
14126
|
+
end++;
|
|
14127
|
+
const settings = lines.slice(start, end + 1);
|
|
14128
|
+
if (settings.some((setting) => setting.includes("CODE_SIGN_ENTITLEMENTS")))
|
|
14129
|
+
continue;
|
|
14130
|
+
const indent = line.match(/^\s*/)?.[0] ?? "";
|
|
14131
|
+
lines.splice(index, 0, `${indent}CODE_SIGN_ENTITLEMENTS = ${entitlementsRelPath};`);
|
|
14132
|
+
index++;
|
|
14133
|
+
changed = true;
|
|
14134
|
+
}
|
|
14135
|
+
if (changed)
|
|
14136
|
+
await writeFile2(pbxprojPath, lines.join(`
|
|
14137
|
+
`));
|
|
14138
|
+
}
|
|
14139
|
+
async#setUrlSchemesInAndroid(schemes) {
|
|
14140
|
+
const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
|
|
14141
|
+
let manifest = await readFile2(manifestPath, "utf8");
|
|
14142
|
+
let changed = false;
|
|
14143
|
+
for (const scheme of schemes) {
|
|
14144
|
+
if (manifest.includes(`android:scheme="${scheme}"`))
|
|
14145
|
+
continue;
|
|
14146
|
+
const filter = [
|
|
14147
|
+
" <intent-filter>",
|
|
14148
|
+
' <action android:name="android.intent.action.VIEW" />',
|
|
14149
|
+
' <category android:name="android.intent.category.DEFAULT" />',
|
|
14150
|
+
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
14151
|
+
` <data android:scheme="${scheme}" />`,
|
|
14152
|
+
" </intent-filter>"
|
|
14153
|
+
].join(`
|
|
14154
|
+
`);
|
|
14155
|
+
manifest = manifest.replace(/(\s*<\/activity>)/, `
|
|
14156
|
+
${filter}$1`);
|
|
14157
|
+
changed = true;
|
|
14158
|
+
}
|
|
14159
|
+
if (changed)
|
|
14160
|
+
await writeFile2(manifestPath, manifest);
|
|
14161
|
+
}
|
|
14162
|
+
async#setDeepLinksInAndroid(schemes, domains) {
|
|
14163
|
+
await this.#setUrlSchemesInAndroid(schemes);
|
|
14164
|
+
const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
|
|
14165
|
+
let manifest = await readFile2(manifestPath, "utf8");
|
|
14166
|
+
let changed = false;
|
|
14167
|
+
const pathPrefix = resolveMobilePath(this.target, "/");
|
|
14168
|
+
for (const domain of domains) {
|
|
14169
|
+
if (manifest.includes(`android:host="${domain}"`) && manifest.includes('android:scheme="https"'))
|
|
14170
|
+
continue;
|
|
14171
|
+
const filter = [
|
|
14172
|
+
' <intent-filter android:autoVerify="true">',
|
|
14173
|
+
' <action android:name="android.intent.action.VIEW" />',
|
|
14174
|
+
' <category android:name="android.intent.category.DEFAULT" />',
|
|
14175
|
+
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
14176
|
+
` <data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
|
|
14177
|
+
" </intent-filter>"
|
|
14178
|
+
].join(`
|
|
14179
|
+
`);
|
|
14180
|
+
manifest = manifest.replace(/(\s*<\/activity>)/, `
|
|
14181
|
+
${filter}$1`);
|
|
14182
|
+
changed = true;
|
|
14183
|
+
}
|
|
14184
|
+
if (changed)
|
|
14185
|
+
await writeFile2(manifestPath, manifest);
|
|
14186
|
+
}
|
|
14002
14187
|
#setFeaturesInAndroid(features) {
|
|
14003
14188
|
for (const feature of features) {
|
|
14004
14189
|
if (this.#hasFeatureInAndroid(feature)) {
|
|
@@ -14078,7 +14263,7 @@ var Pkg = createInternalArgToken("Pkg");
|
|
|
14078
14263
|
var Module = createInternalArgToken("Module");
|
|
14079
14264
|
var Workspace = createInternalArgToken("Workspace");
|
|
14080
14265
|
// pkgs/@akanjs/devkit/commandDecorators/command.ts
|
|
14081
|
-
import
|
|
14266
|
+
import path42 from "path";
|
|
14082
14267
|
import { confirm, input as input2, select as select3 } from "@inquirer/prompts";
|
|
14083
14268
|
import { Logger as Logger10 } from "akanjs/common";
|
|
14084
14269
|
import chalk6 from "chalk";
|
|
@@ -14582,7 +14767,7 @@ var runCommands = async (...commands) => {
|
|
|
14582
14767
|
process.exit(1);
|
|
14583
14768
|
});
|
|
14584
14769
|
const __dirname2 = getDirname(import.meta.url);
|
|
14585
|
-
const packageJsonCandidates = [`${
|
|
14770
|
+
const packageJsonCandidates = [`${path42.dirname(Bun.main)}/package.json`, `${__dirname2}/../package.json`];
|
|
14586
14771
|
let cliPackageJson = null;
|
|
14587
14772
|
for (const packageJsonPath of packageJsonCandidates) {
|
|
14588
14773
|
if (!await FileSys.fileExists(packageJsonPath))
|
|
@@ -14862,8 +15047,8 @@ var scanModuleSpecifiers = (source2, filePath, includeExports) => {
|
|
|
14862
15047
|
return importSpecifiers;
|
|
14863
15048
|
};
|
|
14864
15049
|
var parseTsConfig = (tsConfigPath = "./tsconfig.json") => {
|
|
14865
|
-
const configFile = ts10.readConfigFile(tsConfigPath, (
|
|
14866
|
-
return ts10.sys.readFile(
|
|
15050
|
+
const configFile = ts10.readConfigFile(tsConfigPath, (path43) => {
|
|
15051
|
+
return ts10.sys.readFile(path43);
|
|
14867
15052
|
});
|
|
14868
15053
|
return ts10.parseJsonConfigFileContent(configFile.config, ts10.sys, realpathSync(tsConfigPath).replace(/[^/\\]+$/, ""));
|
|
14869
15054
|
};
|
|
@@ -15079,6 +15264,571 @@ ${document}
|
|
|
15079
15264
|
`;
|
|
15080
15265
|
}
|
|
15081
15266
|
}
|
|
15267
|
+
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
15268
|
+
import { createHash } from "crypto";
|
|
15269
|
+
import { readdir as readdir3, readFile as readFile3, stat as stat4 } from "fs/promises";
|
|
15270
|
+
import path43 from "path";
|
|
15271
|
+
import ignore2 from "ignore";
|
|
15272
|
+
import ts11 from "typescript";
|
|
15273
|
+
var MAX_FILE_LINES = 2000;
|
|
15274
|
+
var PLACEHOLDER_EXPORT_NAMES = new Set([
|
|
15275
|
+
"aa",
|
|
15276
|
+
"dumb",
|
|
15277
|
+
"dumb2",
|
|
15278
|
+
"someBaseLogic",
|
|
15279
|
+
"someCommonLogic",
|
|
15280
|
+
"someFrontendLogic",
|
|
15281
|
+
"someBackendLogic"
|
|
15282
|
+
]);
|
|
15283
|
+
var SUGGESTED_RULES = [
|
|
15284
|
+
"Keep generated scanSync index files out of hand-written changes; generated indexes should only contain one-depth export statements.",
|
|
15285
|
+
"Generated Akan index files should not contain placeholder exports such as aa, dumb, dumb2, or some*Logic.",
|
|
15286
|
+
"Dictionary text should not contain scaffold wording, misspellings, or stale copied domain nouns.",
|
|
15287
|
+
"Warn earlier on very long Akan files: 500 lines for services, 800 lines for Template/Zone files, and 1000 lines for Util files.",
|
|
15288
|
+
"Global declarations, Window augmentation, and prototype mutation should stay in explicitly approved low-level integration files.",
|
|
15289
|
+
"Keep app root folders small and conventional: application code belongs under common, env, lib, page, private, public, script, srvkit, ui, or webkit.",
|
|
15290
|
+
"Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
|
|
15291
|
+
"Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
|
|
15292
|
+
"Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
|
|
15293
|
+
"Move shared app utilities to apps/*/common instead of creating apps/*/base.",
|
|
15294
|
+
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
|
|
15295
|
+
];
|
|
15296
|
+
var APP_ROOT_FILES = new Set([
|
|
15297
|
+
"akan.app.json",
|
|
15298
|
+
"akan.config.ts",
|
|
15299
|
+
"capacitor.config.ts",
|
|
15300
|
+
"client.ts",
|
|
15301
|
+
"main.ts",
|
|
15302
|
+
"package.json",
|
|
15303
|
+
"server.ts",
|
|
15304
|
+
"tsconfig.json"
|
|
15305
|
+
]);
|
|
15306
|
+
var LIB_ROOT_FILES = new Set([
|
|
15307
|
+
"cnst.ts",
|
|
15308
|
+
"db.ts",
|
|
15309
|
+
"dict.ts",
|
|
15310
|
+
"option.ts",
|
|
15311
|
+
"sig.ts",
|
|
15312
|
+
"srv.ts",
|
|
15313
|
+
"st.ts",
|
|
15314
|
+
"useClient.ts",
|
|
15315
|
+
"useServer.ts"
|
|
15316
|
+
]);
|
|
15317
|
+
var CONVENTION_SUFFIXES = [
|
|
15318
|
+
".constant.ts",
|
|
15319
|
+
".dictionary.ts",
|
|
15320
|
+
".document.ts",
|
|
15321
|
+
".service.ts",
|
|
15322
|
+
".signal.ts",
|
|
15323
|
+
".store.ts"
|
|
15324
|
+
];
|
|
15325
|
+
|
|
15326
|
+
class AkanQualityScanner {
|
|
15327
|
+
async scan(workspaceRoot) {
|
|
15328
|
+
const targetFiles = await this.#collectTargetFiles(workspaceRoot);
|
|
15329
|
+
const sourceFiles = await Promise.all(targetFiles.map((file) => this.#readSourceFile(workspaceRoot, file)));
|
|
15330
|
+
const warnings = [
|
|
15331
|
+
...this.#scanGlobalQuality(sourceFiles),
|
|
15332
|
+
...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
|
|
15333
|
+
...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
|
|
15334
|
+
...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
|
|
15335
|
+
];
|
|
15336
|
+
return {
|
|
15337
|
+
workspaceRoot,
|
|
15338
|
+
scannedFiles: sourceFiles.length,
|
|
15339
|
+
warnings: warnings.sort(compareWarnings),
|
|
15340
|
+
suggestedRules: SUGGESTED_RULES
|
|
15341
|
+
};
|
|
15342
|
+
}
|
|
15343
|
+
async#collectTargetFiles(workspaceRoot) {
|
|
15344
|
+
const ignoreFilter = ignore2().add(await this.#readGitIgnore(workspaceRoot));
|
|
15345
|
+
const files = [];
|
|
15346
|
+
for (const targetRoot of ["apps", "libs"]) {
|
|
15347
|
+
const absoluteTargetRoot = path43.join(workspaceRoot, targetRoot);
|
|
15348
|
+
if (!await isDirectory(absoluteTargetRoot))
|
|
15349
|
+
continue;
|
|
15350
|
+
await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
|
|
15351
|
+
}
|
|
15352
|
+
return files.sort();
|
|
15353
|
+
}
|
|
15354
|
+
async#readGitIgnore(workspaceRoot) {
|
|
15355
|
+
const gitIgnorePath = path43.join(workspaceRoot, ".gitignore");
|
|
15356
|
+
if (!await Bun.file(gitIgnorePath).exists())
|
|
15357
|
+
return [];
|
|
15358
|
+
return (await readFile3(gitIgnorePath, "utf8")).split(/\r?\n/);
|
|
15359
|
+
}
|
|
15360
|
+
async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
|
|
15361
|
+
const entries = await readdir3(currentPath, { withFileTypes: true });
|
|
15362
|
+
for (const entry of entries) {
|
|
15363
|
+
const absolutePath = path43.join(currentPath, entry.name);
|
|
15364
|
+
const relativePath = toPosix(path43.relative(workspaceRoot, absolutePath));
|
|
15365
|
+
if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
|
|
15366
|
+
continue;
|
|
15367
|
+
if (entry.isDirectory()) {
|
|
15368
|
+
await this.#walkTargetFiles(workspaceRoot, absolutePath, ignoreFilter, files);
|
|
15369
|
+
continue;
|
|
15370
|
+
}
|
|
15371
|
+
if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
|
|
15372
|
+
files.push(relativePath);
|
|
15373
|
+
}
|
|
15374
|
+
}
|
|
15375
|
+
}
|
|
15376
|
+
async#readSourceFile(workspaceRoot, file) {
|
|
15377
|
+
const absolutePath = path43.join(workspaceRoot, file);
|
|
15378
|
+
const content = await readFile3(absolutePath, "utf8");
|
|
15379
|
+
return {
|
|
15380
|
+
file,
|
|
15381
|
+
absolutePath,
|
|
15382
|
+
content,
|
|
15383
|
+
sourceFile: ts11.createSourceFile(file, content, ts11.ScriptTarget.Latest, true, getScriptKind(file))
|
|
15384
|
+
};
|
|
15385
|
+
}
|
|
15386
|
+
#scanGlobalQuality(sourceFiles) {
|
|
15387
|
+
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15388
|
+
const warnings = [];
|
|
15389
|
+
for (const [name, declarations] of groupBy(exportedFunctionLikes, (declaration) => declaration.name)) {
|
|
15390
|
+
if (declarations.length < 2)
|
|
15391
|
+
continue;
|
|
15392
|
+
warnings.push({
|
|
15393
|
+
rule: "akan.global.duplicate-exported-function-name",
|
|
15394
|
+
scope: "global",
|
|
15395
|
+
severity: "warning",
|
|
15396
|
+
message: `Exported function or class name "${name}" is declared in ${declarations.length} files.`,
|
|
15397
|
+
locations: declarations.map(({ file, line }) => ({ file, line }))
|
|
15398
|
+
});
|
|
15399
|
+
}
|
|
15400
|
+
const declarationsWithBody = exportedFunctionLikes.filter((declaration) => declaration.bodyFingerprint);
|
|
15401
|
+
for (const [fingerprint, declarations] of groupBy(declarationsWithBody, (declaration) => declaration.bodyFingerprint ?? "")) {
|
|
15402
|
+
const uniqueNames = new Set(declarations.map((declaration) => declaration.name));
|
|
15403
|
+
if (declarations.length < 2 || uniqueNames.size < 2 || fingerprint === "")
|
|
15404
|
+
continue;
|
|
15405
|
+
warnings.push({
|
|
15406
|
+
rule: "akan.global.duplicate-exported-function-body",
|
|
15407
|
+
scope: "global",
|
|
15408
|
+
severity: "warning",
|
|
15409
|
+
message: `Exported functions/classes share the same implementation body: ${declarations.map((declaration) => declaration.name).join(", ")}.`,
|
|
15410
|
+
locations: declarations.map(({ file, line }) => ({ file, line }))
|
|
15411
|
+
});
|
|
15412
|
+
}
|
|
15413
|
+
return warnings;
|
|
15414
|
+
}
|
|
15415
|
+
#scanSingleFileQuality(sourceFile2) {
|
|
15416
|
+
const warnings = [];
|
|
15417
|
+
const lineCount = sourceFile2.content.split(/\r?\n/).length;
|
|
15418
|
+
const recommendedLineLimit = getRecommendedLineLimit(sourceFile2.file);
|
|
15419
|
+
if (recommendedLineLimit && lineCount > recommendedLineLimit) {
|
|
15420
|
+
warnings.push({
|
|
15421
|
+
rule: "akan.file.recommended-max-lines",
|
|
15422
|
+
scope: "file",
|
|
15423
|
+
severity: "warning",
|
|
15424
|
+
file: sourceFile2.file,
|
|
15425
|
+
message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`
|
|
15426
|
+
});
|
|
15427
|
+
}
|
|
15428
|
+
if (lineCount > MAX_FILE_LINES) {
|
|
15429
|
+
warnings.push({
|
|
15430
|
+
rule: "akan.file.max-lines",
|
|
15431
|
+
scope: "file",
|
|
15432
|
+
severity: "warning",
|
|
15433
|
+
file: sourceFile2.file,
|
|
15434
|
+
message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
|
|
15435
|
+
});
|
|
15436
|
+
}
|
|
15437
|
+
warnings.push(...getPlaceholderExportWarnings(sourceFile2));
|
|
15438
|
+
warnings.push(...getDictionaryTextWarnings(sourceFile2));
|
|
15439
|
+
warnings.push(...getGlobalMutationWarnings(sourceFile2));
|
|
15440
|
+
const exportedClassNames = getExportedClassNames(sourceFile2.sourceFile);
|
|
15441
|
+
if (exportedClassNames.length === 0)
|
|
15442
|
+
return warnings;
|
|
15443
|
+
const allowedInterfaceNames = new Set(exportedClassNames.map((name) => `${name}Options`));
|
|
15444
|
+
for (const declaration of getTopLevelDeclarations(sourceFile2)) {
|
|
15445
|
+
if (declaration.kind === "class" && exportedClassNames.includes(declaration.name))
|
|
15446
|
+
continue;
|
|
15447
|
+
if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
|
|
15448
|
+
continue;
|
|
15449
|
+
warnings.push({
|
|
15450
|
+
rule: "akan.file.class-export-global-declaration",
|
|
15451
|
+
scope: "file",
|
|
15452
|
+
severity: "warning",
|
|
15453
|
+
file: sourceFile2.file,
|
|
15454
|
+
line: declaration.line,
|
|
15455
|
+
message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`
|
|
15456
|
+
});
|
|
15457
|
+
}
|
|
15458
|
+
return warnings;
|
|
15459
|
+
}
|
|
15460
|
+
#scanConventionQuality(sourceFile2) {
|
|
15461
|
+
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
|
|
15462
|
+
if (!suffix)
|
|
15463
|
+
return [];
|
|
15464
|
+
const modelName = toPascalCase(path43.basename(sourceFile2.file, suffix));
|
|
15465
|
+
const warnings = [];
|
|
15466
|
+
for (const declaration of getTopLevelDeclarations(sourceFile2)) {
|
|
15467
|
+
if (isAllowedConventionDeclaration(suffix, modelName, declaration))
|
|
15468
|
+
continue;
|
|
15469
|
+
warnings.push({
|
|
15470
|
+
rule: `akan.convention${suffix.replace(".ts", "")}`,
|
|
15471
|
+
scope: "convention",
|
|
15472
|
+
severity: "warning",
|
|
15473
|
+
file: sourceFile2.file,
|
|
15474
|
+
line: declaration.line,
|
|
15475
|
+
message: `${path43.basename(sourceFile2.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
|
|
15476
|
+
});
|
|
15477
|
+
}
|
|
15478
|
+
return warnings;
|
|
15479
|
+
}
|
|
15480
|
+
#scanLayoutQuality(sourceFile2) {
|
|
15481
|
+
const segments = sourceFile2.file.split("/");
|
|
15482
|
+
const warnings = [];
|
|
15483
|
+
if (segments[0] === "apps" && segments.length === 3 && !APP_ROOT_FILES.has(segments[2])) {
|
|
15484
|
+
warnings.push({
|
|
15485
|
+
rule: "akan.layout.app-root-file",
|
|
15486
|
+
scope: "layout",
|
|
15487
|
+
severity: "warning",
|
|
15488
|
+
file: sourceFile2.file,
|
|
15489
|
+
message: `Unexpected app root file "${segments[2]}". Keep application code in conventional app folders.`
|
|
15490
|
+
});
|
|
15491
|
+
}
|
|
15492
|
+
const libRootFile = getLibRootFile(sourceFile2.file);
|
|
15493
|
+
if (libRootFile && !LIB_ROOT_FILES.has(libRootFile)) {
|
|
15494
|
+
warnings.push({
|
|
15495
|
+
rule: "akan.layout.lib-root-file",
|
|
15496
|
+
scope: "layout",
|
|
15497
|
+
severity: "warning",
|
|
15498
|
+
file: sourceFile2.file,
|
|
15499
|
+
message: `Unexpected lib root file "${libRootFile}". Keep direct lib root files limited to generated support facets.`
|
|
15500
|
+
});
|
|
15501
|
+
}
|
|
15502
|
+
const moduleUiWarning = getModuleUiWarning(sourceFile2.file);
|
|
15503
|
+
if (moduleUiWarning)
|
|
15504
|
+
warnings.push(moduleUiWarning);
|
|
15505
|
+
return warnings;
|
|
15506
|
+
}
|
|
15507
|
+
}
|
|
15508
|
+
function formatQualityScanResult(result) {
|
|
15509
|
+
const sections = [
|
|
15510
|
+
"Akan Code Quality Scan",
|
|
15511
|
+
`workspace: ${result.workspaceRoot}`,
|
|
15512
|
+
`scanned files: ${result.scannedFiles}`,
|
|
15513
|
+
`warnings: ${result.warnings.length}`,
|
|
15514
|
+
"",
|
|
15515
|
+
"Warnings:",
|
|
15516
|
+
"",
|
|
15517
|
+
...formatQualityWarnings(result.warnings),
|
|
15518
|
+
"",
|
|
15519
|
+
"Suggested quality rules:",
|
|
15520
|
+
"",
|
|
15521
|
+
...result.suggestedRules.map((rule) => ` - ${rule}`)
|
|
15522
|
+
];
|
|
15523
|
+
return sections.join(`
|
|
15524
|
+
`);
|
|
15525
|
+
}
|
|
15526
|
+
function formatQualityWarnings(warnings) {
|
|
15527
|
+
if (warnings.length === 0)
|
|
15528
|
+
return ["No warnings found."];
|
|
15529
|
+
return warnings.flatMap((warning) => {
|
|
15530
|
+
const location = formatQualityLocation(warning.file, warning.line);
|
|
15531
|
+
const lines = [`${location} - warning ${warning.rule}: ${warning.message}`];
|
|
15532
|
+
if (warning.locations?.length) {
|
|
15533
|
+
lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
|
|
15534
|
+
}
|
|
15535
|
+
return lines;
|
|
15536
|
+
});
|
|
15537
|
+
}
|
|
15538
|
+
function formatQualityLocation(file, line) {
|
|
15539
|
+
return `${file ?? "<global>"}:${line ?? 1}:1`;
|
|
15540
|
+
}
|
|
15541
|
+
function getExportedFunctionLikes(sourceFile2) {
|
|
15542
|
+
const declarations = [];
|
|
15543
|
+
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15544
|
+
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15545
|
+
declarations.push({
|
|
15546
|
+
name: statement.name.text,
|
|
15547
|
+
kind: "function",
|
|
15548
|
+
file: sourceFile2.file,
|
|
15549
|
+
line: getLine(sourceFile2.sourceFile, statement),
|
|
15550
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
15551
|
+
});
|
|
15552
|
+
}
|
|
15553
|
+
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15554
|
+
declarations.push({
|
|
15555
|
+
name: statement.name.text,
|
|
15556
|
+
kind: "class",
|
|
15557
|
+
file: sourceFile2.file,
|
|
15558
|
+
line: getLine(sourceFile2.sourceFile, statement),
|
|
15559
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
15560
|
+
});
|
|
15561
|
+
}
|
|
15562
|
+
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
15563
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
15564
|
+
if (!ts11.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
15565
|
+
continue;
|
|
15566
|
+
declarations.push({
|
|
15567
|
+
name: declaration.name.text,
|
|
15568
|
+
kind: "function-variable",
|
|
15569
|
+
file: sourceFile2.file,
|
|
15570
|
+
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15571
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
15572
|
+
});
|
|
15573
|
+
}
|
|
15574
|
+
}
|
|
15575
|
+
}
|
|
15576
|
+
return declarations;
|
|
15577
|
+
}
|
|
15578
|
+
function getExportedClassNames(sourceFile2) {
|
|
15579
|
+
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15580
|
+
}
|
|
15581
|
+
function getPlaceholderExportWarnings(sourceFile2) {
|
|
15582
|
+
if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
|
|
15583
|
+
return [];
|
|
15584
|
+
return getTopLevelDeclarations(sourceFile2).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => ({
|
|
15585
|
+
rule: "akan.file.placeholder-export",
|
|
15586
|
+
scope: "file",
|
|
15587
|
+
severity: "warning",
|
|
15588
|
+
file: sourceFile2.file,
|
|
15589
|
+
line: declaration.line,
|
|
15590
|
+
message: `Generated or barrel index file should not export placeholder "${declaration.name}".`
|
|
15591
|
+
}));
|
|
15592
|
+
}
|
|
15593
|
+
function getDictionaryTextWarnings(sourceFile2) {
|
|
15594
|
+
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15595
|
+
return [];
|
|
15596
|
+
const stalePatterns = [
|
|
15597
|
+
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
15598
|
+
{ pattern: /settting/, label: "misspelling: settting" },
|
|
15599
|
+
{ pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
|
|
15600
|
+
];
|
|
15601
|
+
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15602
|
+
rule: "akan.file.dictionary-stale-text",
|
|
15603
|
+
scope: "file",
|
|
15604
|
+
severity: "warning",
|
|
15605
|
+
file: sourceFile2.file,
|
|
15606
|
+
line,
|
|
15607
|
+
message: `Dictionary text appears to contain ${label}.`
|
|
15608
|
+
})));
|
|
15609
|
+
}
|
|
15610
|
+
function getGlobalMutationWarnings(sourceFile2) {
|
|
15611
|
+
const warnings = [];
|
|
15612
|
+
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15613
|
+
if (ts11.isModuleDeclaration(statement) && statement.name.getText(sourceFile2.sourceFile) === "global") {
|
|
15614
|
+
warnings.push({
|
|
15615
|
+
rule: "akan.file.global-declaration",
|
|
15616
|
+
scope: "file",
|
|
15617
|
+
severity: "warning",
|
|
15618
|
+
file: sourceFile2.file,
|
|
15619
|
+
line: getLine(sourceFile2.sourceFile, statement),
|
|
15620
|
+
message: "Global declarations require an explicit low-level integration allowlist."
|
|
15621
|
+
});
|
|
15622
|
+
}
|
|
15623
|
+
if (ts11.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
15624
|
+
warnings.push({
|
|
15625
|
+
rule: "akan.file.window-augmentation",
|
|
15626
|
+
scope: "file",
|
|
15627
|
+
severity: "warning",
|
|
15628
|
+
file: sourceFile2.file,
|
|
15629
|
+
line: getLine(sourceFile2.sourceFile, statement),
|
|
15630
|
+
message: "Window augmentation should be isolated to approved browser integration files."
|
|
15631
|
+
});
|
|
15632
|
+
}
|
|
15633
|
+
if (ts11.isExpressionStatement(statement) && statement.expression.getText(sourceFile2.sourceFile).includes(".prototype.")) {
|
|
15634
|
+
warnings.push({
|
|
15635
|
+
rule: "akan.file.prototype-mutation",
|
|
15636
|
+
scope: "file",
|
|
15637
|
+
severity: "warning",
|
|
15638
|
+
file: sourceFile2.file,
|
|
15639
|
+
line: getLine(sourceFile2.sourceFile, statement),
|
|
15640
|
+
message: "Prototype mutation should be avoided or isolated to approved low-level integration files."
|
|
15641
|
+
});
|
|
15642
|
+
}
|
|
15643
|
+
}
|
|
15644
|
+
return warnings;
|
|
15645
|
+
}
|
|
15646
|
+
function getTopLevelDeclarations(sourceFile2) {
|
|
15647
|
+
return sourceFile2.sourceFile.statements.flatMap((statement) => getTopLevelDeclaration(sourceFile2.sourceFile, statement));
|
|
15648
|
+
}
|
|
15649
|
+
function getTopLevelDeclaration(sourceFile2, statement) {
|
|
15650
|
+
const line = getLine(sourceFile2, statement);
|
|
15651
|
+
if (ts11.isClassDeclaration(statement) && statement.name) {
|
|
15652
|
+
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
15653
|
+
}
|
|
15654
|
+
if (ts11.isFunctionDeclaration(statement) && statement.name) {
|
|
15655
|
+
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
15656
|
+
}
|
|
15657
|
+
if (ts11.isInterfaceDeclaration(statement)) {
|
|
15658
|
+
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
15659
|
+
}
|
|
15660
|
+
if (ts11.isTypeAliasDeclaration(statement)) {
|
|
15661
|
+
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
15662
|
+
}
|
|
15663
|
+
if (ts11.isEnumDeclaration(statement)) {
|
|
15664
|
+
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
15665
|
+
}
|
|
15666
|
+
if (ts11.isVariableStatement(statement)) {
|
|
15667
|
+
return statement.declarationList.declarations.filter((declaration) => ts11.isIdentifier(declaration.name)).map((declaration) => ({
|
|
15668
|
+
name: declaration.name.text,
|
|
15669
|
+
kind: "variable",
|
|
15670
|
+
line: getLine(sourceFile2, declaration),
|
|
15671
|
+
exported: isExported(statement),
|
|
15672
|
+
node: statement
|
|
15673
|
+
}));
|
|
15674
|
+
}
|
|
15675
|
+
if (ts11.isExportDeclaration(statement)) {
|
|
15676
|
+
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
15677
|
+
}
|
|
15678
|
+
return [];
|
|
15679
|
+
}
|
|
15680
|
+
function isAllowedConventionDeclaration(suffix, modelName, declaration) {
|
|
15681
|
+
if (suffix === ".dictionary.ts")
|
|
15682
|
+
return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
15683
|
+
if (suffix === ".constant.ts")
|
|
15684
|
+
return isAllowedConstantDeclaration(modelName, declaration);
|
|
15685
|
+
if (suffix === ".document.ts")
|
|
15686
|
+
return declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name);
|
|
15687
|
+
if (suffix === ".service.ts")
|
|
15688
|
+
return declaration.kind === "class" && declaration.name === `${modelName}Service`;
|
|
15689
|
+
if (suffix === ".signal.ts")
|
|
15690
|
+
return declaration.kind === "class" && [`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name);
|
|
15691
|
+
if (suffix === ".store.ts")
|
|
15692
|
+
return declaration.kind === "class" && declaration.name === `${modelName}Store`;
|
|
15693
|
+
return false;
|
|
15694
|
+
}
|
|
15695
|
+
function isAllowedConstantDeclaration(modelName, declaration) {
|
|
15696
|
+
if (declaration.kind !== "class")
|
|
15697
|
+
return false;
|
|
15698
|
+
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
15699
|
+
return true;
|
|
15700
|
+
if (!ts11.isClassDeclaration(declaration.node))
|
|
15701
|
+
return false;
|
|
15702
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
|
|
15703
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
15704
|
+
return !!expression && expression.getText().startsWith("enumOf(");
|
|
15705
|
+
}
|
|
15706
|
+
function getConventionDescription(suffix, modelName) {
|
|
15707
|
+
if (suffix === ".dictionary.ts")
|
|
15708
|
+
return "export const dictionary";
|
|
15709
|
+
if (suffix === ".constant.ts")
|
|
15710
|
+
return `${modelName}Input, ${modelName}Object, ${modelName}, Light${modelName}, ${modelName}Insight, or enumOf classes`;
|
|
15711
|
+
if (suffix === ".document.ts")
|
|
15712
|
+
return `${modelName}Filter, ${modelName}, or ${modelName}Model`;
|
|
15713
|
+
if (suffix === ".service.ts")
|
|
15714
|
+
return `${modelName}Service`;
|
|
15715
|
+
if (suffix === ".signal.ts")
|
|
15716
|
+
return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
|
|
15717
|
+
return `${modelName}Store`;
|
|
15718
|
+
}
|
|
15719
|
+
function getLibRootFile(file) {
|
|
15720
|
+
const segments = file.split("/");
|
|
15721
|
+
if (segments[0] === "libs" && segments.length === 4 && segments[2] === "lib")
|
|
15722
|
+
return segments[3];
|
|
15723
|
+
if (segments[0] === "apps" && segments.length === 5 && segments[2] === "lib")
|
|
15724
|
+
return segments[4];
|
|
15725
|
+
return null;
|
|
15726
|
+
}
|
|
15727
|
+
function getModuleUiWarning(file) {
|
|
15728
|
+
if (!file.endsWith(".tsx"))
|
|
15729
|
+
return null;
|
|
15730
|
+
const moduleInfo = getModuleInfo(file);
|
|
15731
|
+
if (!moduleInfo)
|
|
15732
|
+
return null;
|
|
15733
|
+
const { moduleName, fileName, kind } = moduleInfo;
|
|
15734
|
+
const pascalName = toPascalCase(moduleName.replace(/^_+/, ""));
|
|
15735
|
+
const allowedSuffixes = kind === "database" ? ["Template", "Unit", "Util", "View", "Zone"] : kind === "service" ? ["Util", "Zone"] : ["Template", "Unit"];
|
|
15736
|
+
const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
|
|
15737
|
+
if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
|
|
15738
|
+
return null;
|
|
15739
|
+
return {
|
|
15740
|
+
rule: "akan.layout.module-ui-file",
|
|
15741
|
+
scope: "layout",
|
|
15742
|
+
severity: "warning",
|
|
15743
|
+
file,
|
|
15744
|
+
message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`
|
|
15745
|
+
};
|
|
15746
|
+
}
|
|
15747
|
+
function getModuleInfo(file) {
|
|
15748
|
+
const segments = file.split("/");
|
|
15749
|
+
const libIndex = segments.indexOf("lib");
|
|
15750
|
+
if (libIndex < 0)
|
|
15751
|
+
return null;
|
|
15752
|
+
const moduleName = segments[libIndex + 1];
|
|
15753
|
+
if (moduleName === "__scalar") {
|
|
15754
|
+
if (segments.length !== libIndex + 4)
|
|
15755
|
+
return null;
|
|
15756
|
+
return { moduleName: segments[libIndex + 2], fileName: segments[libIndex + 3], kind: "scalar" };
|
|
15757
|
+
}
|
|
15758
|
+
if (segments.length !== libIndex + 3)
|
|
15759
|
+
return null;
|
|
15760
|
+
const fileName = segments[libIndex + 2];
|
|
15761
|
+
if (moduleName.startsWith("__")) {
|
|
15762
|
+
const scalarName = moduleName === "__scalar" ? segments[libIndex + 2] : moduleName;
|
|
15763
|
+
return { moduleName: scalarName, fileName, kind: "scalar" };
|
|
15764
|
+
}
|
|
15765
|
+
if (moduleName.startsWith("_"))
|
|
15766
|
+
return { moduleName, fileName, kind: "service" };
|
|
15767
|
+
return { moduleName, fileName, kind: "database" };
|
|
15768
|
+
}
|
|
15769
|
+
function isExportedConst(declaration) {
|
|
15770
|
+
return declaration.exported && ts11.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts11.NodeFlags.Const) !== 0;
|
|
15771
|
+
}
|
|
15772
|
+
function isExported(node) {
|
|
15773
|
+
return !!ts11.getCombinedModifierFlags(node) && !!(ts11.getCombinedModifierFlags(node) & ts11.ModifierFlags.Export);
|
|
15774
|
+
}
|
|
15775
|
+
function isFunctionLikeInitializer(node) {
|
|
15776
|
+
return !!node && (ts11.isArrowFunction(node) || ts11.isFunctionExpression(node));
|
|
15777
|
+
}
|
|
15778
|
+
function getBodyFingerprint(sourceFile2, node) {
|
|
15779
|
+
if (!node)
|
|
15780
|
+
return;
|
|
15781
|
+
const normalizedBody = node.getText(sourceFile2).replace(/\s+/g, " ").trim();
|
|
15782
|
+
if (normalizedBody.length < 80)
|
|
15783
|
+
return;
|
|
15784
|
+
return createHash("sha256").update(normalizedBody).digest("hex");
|
|
15785
|
+
}
|
|
15786
|
+
function shouldSkipPath(relativePath, isDirectory, ignoreFilter) {
|
|
15787
|
+
const ignorePath = isDirectory ? `${relativePath}/` : relativePath;
|
|
15788
|
+
return relativePath === ".git" || relativePath.includes("/.git/") || relativePath.includes("/node_modules/") || ignoreFilter.ignores(relativePath) || ignoreFilter.ignores(ignorePath);
|
|
15789
|
+
}
|
|
15790
|
+
async function isDirectory(absolutePath) {
|
|
15791
|
+
try {
|
|
15792
|
+
return (await stat4(absolutePath)).isDirectory();
|
|
15793
|
+
} catch {
|
|
15794
|
+
return false;
|
|
15795
|
+
}
|
|
15796
|
+
}
|
|
15797
|
+
function getScriptKind(file) {
|
|
15798
|
+
return file.endsWith(".tsx") ? ts11.ScriptKind.TSX : ts11.ScriptKind.TS;
|
|
15799
|
+
}
|
|
15800
|
+
function getLine(sourceFile2, node) {
|
|
15801
|
+
return sourceFile2.getLineAndCharacterOfPosition(node.getStart(sourceFile2)).line + 1;
|
|
15802
|
+
}
|
|
15803
|
+
function getRecommendedLineLimit(file) {
|
|
15804
|
+
if (file.endsWith(".service.ts"))
|
|
15805
|
+
return 500;
|
|
15806
|
+
if (file.endsWith(".Template.tsx") || file.endsWith(".Zone.tsx"))
|
|
15807
|
+
return 800;
|
|
15808
|
+
if (file.endsWith(".Util.tsx"))
|
|
15809
|
+
return 1000;
|
|
15810
|
+
return null;
|
|
15811
|
+
}
|
|
15812
|
+
function findPatternLines(content, pattern) {
|
|
15813
|
+
return content.split(/\r?\n/).flatMap((line, index) => pattern.test(line) ? [index + 1] : []);
|
|
15814
|
+
}
|
|
15815
|
+
function toPascalCase(value) {
|
|
15816
|
+
return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
|
|
15817
|
+
}
|
|
15818
|
+
function toPosix(value) {
|
|
15819
|
+
return value.split(path43.sep).join("/");
|
|
15820
|
+
}
|
|
15821
|
+
function groupBy(items, getKey) {
|
|
15822
|
+
const grouped = new Map;
|
|
15823
|
+
for (const item of items) {
|
|
15824
|
+
const key = getKey(item);
|
|
15825
|
+
grouped.set(key, [...grouped.get(key) ?? [], item]);
|
|
15826
|
+
}
|
|
15827
|
+
return grouped;
|
|
15828
|
+
}
|
|
15829
|
+
function compareWarnings(a, b) {
|
|
15830
|
+
return a.scope.localeCompare(b.scope) || (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule);
|
|
15831
|
+
}
|
|
15082
15832
|
// pkgs/@akanjs/devkit/selectModel.ts
|
|
15083
15833
|
import { select as select5 } from "@inquirer/prompts";
|
|
15084
15834
|
// pkgs/@akanjs/devkit/streamAi.ts
|
|
@@ -15196,10 +15946,10 @@ class IncrementalBuilder {
|
|
|
15196
15946
|
}
|
|
15197
15947
|
}
|
|
15198
15948
|
batchTouchesPagesTree(appDir, batch) {
|
|
15199
|
-
const absAppDir =
|
|
15949
|
+
const absAppDir = path44.resolve(appDir);
|
|
15200
15950
|
for (const f of batch.files) {
|
|
15201
|
-
const abs =
|
|
15202
|
-
if (!abs.startsWith(`${absAppDir}${
|
|
15951
|
+
const abs = path44.resolve(f);
|
|
15952
|
+
if (!abs.startsWith(`${absAppDir}${path44.sep}`) && abs !== absAppDir)
|
|
15203
15953
|
continue;
|
|
15204
15954
|
if (/\.(tsx|ts|jsx|js)$/.test(abs))
|
|
15205
15955
|
return true;
|
|
@@ -15207,15 +15957,15 @@ class IncrementalBuilder {
|
|
|
15207
15957
|
return false;
|
|
15208
15958
|
}
|
|
15209
15959
|
async batchMayChangePageKeys(appDir, batch) {
|
|
15210
|
-
const absAppDir =
|
|
15211
|
-
const pageKeys = new Set((await this.#app.getPageKeys()).map((key) =>
|
|
15960
|
+
const absAppDir = path44.resolve(appDir);
|
|
15961
|
+
const pageKeys = new Set((await this.#app.getPageKeys()).map((key) => path44.normalize(key)));
|
|
15212
15962
|
for (const f of batch.files) {
|
|
15213
|
-
const abs =
|
|
15214
|
-
if (!abs.startsWith(`${absAppDir}${
|
|
15963
|
+
const abs = path44.resolve(f);
|
|
15964
|
+
if (!abs.startsWith(`${absAppDir}${path44.sep}`) && abs !== absAppDir)
|
|
15215
15965
|
continue;
|
|
15216
15966
|
if (!/\.(tsx|ts|jsx|js)$/.test(abs))
|
|
15217
15967
|
continue;
|
|
15218
|
-
const rel =
|
|
15968
|
+
const rel = path44.normalize(path44.relative(absAppDir, abs));
|
|
15219
15969
|
if (!await Bun.file(abs).exists() || !pageKeys.has(rel))
|
|
15220
15970
|
return true;
|
|
15221
15971
|
}
|
|
@@ -15243,7 +15993,7 @@ class IncrementalBuilder {
|
|
|
15243
15993
|
${cssText}`).toString(36);
|
|
15244
15994
|
const cssRelPath = `styles/${cssAssetName}-${cssHash}.css`;
|
|
15245
15995
|
const cssUrl = `/_akan/styles/${cssAssetName}-${cssHash}.css`;
|
|
15246
|
-
await Bun.write(
|
|
15996
|
+
await Bun.write(path44.join(artifactDir, cssRelPath), cssText);
|
|
15247
15997
|
cssAssetEntries.push([basePath2, { cssUrl, cssRelPath }]);
|
|
15248
15998
|
cssBase64ByUrl[cssUrl] = Buffer.from(new TextEncoder().encode(cssText)).toString("base64");
|
|
15249
15999
|
})()
|
|
@@ -15311,10 +16061,10 @@ ${cssText}`).toString(36);
|
|
|
15311
16061
|
if (changedFiles.length === 0)
|
|
15312
16062
|
return false;
|
|
15313
16063
|
return changedFiles.some((file) => {
|
|
15314
|
-
const normalized =
|
|
16064
|
+
const normalized = path44.resolve(file);
|
|
15315
16065
|
if (/\.(woff2?|ttf|otf)$/i.test(normalized))
|
|
15316
16066
|
return true;
|
|
15317
|
-
return this.#optimizedFonts.files.some((fontFile) =>
|
|
16067
|
+
return this.#optimizedFonts.files.some((fontFile) => path44.resolve(fontFile) === normalized);
|
|
15318
16068
|
});
|
|
15319
16069
|
}
|
|
15320
16070
|
async installWatcher() {
|