@akanjs/devkit 3.0.0-beta.20 → 3.0.0-beta.21
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/README.ko.md +2 -2
- package/README.md +2 -2
- package/akanConfig/akanConfig.test.ts +58 -38
- package/akanConfig/akanConfig.ts +88 -48
- package/akanConfig/types.ts +3 -3
- package/akanContext.ts +5 -2
- package/artifact/implicitRootLayout.test.ts +23 -0
- package/artifact/implicitRootLayout.ts +24 -0
- package/biome.base.json +26 -0
- package/commandDecorators/command.ts +1 -1
- package/executors.ts +1 -12
- package/incrementalBuilder/buildBatch.proc.ts +7 -1
- package/index.ts +0 -2
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/mobile/index.ts +6 -0
- package/mobile/mobileTarget.ts +2 -0
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +85 -0
- package/mobile/nativeApp.ts +210 -0
- package/mobile/nativeConfig.test.ts +199 -0
- package/mobile/nativeConfig.ts +184 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +62 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +2 -7
- package/qualityScanner.ts +4 -2
- package/scanInfo.ts +5 -10
- package/workspaceLayout.test.ts +13 -3
- package/workspaceLayout.ts +16 -4
- package/applicationReleasePackager.ts +0 -189
- package/capacitorApp.test.ts +0 -388
- package/capacitorApp.ts +0 -1520
package/scanInfo.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import type {
|
|
4
3
|
AppConfigResult,
|
|
@@ -11,7 +10,7 @@ import type {
|
|
|
11
10
|
} from "./akanConfig";
|
|
12
11
|
import { AkanAppConfig } from "./akanConfig";
|
|
13
12
|
import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
14
|
-
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
13
|
+
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles, rootEntryHintOf } from "./workspaceLayout";
|
|
15
14
|
|
|
16
15
|
const scalarFileTypes = ["constant", "dictionary", "document", "template", "unit", "util", "view", "zone"] as const;
|
|
17
16
|
type ScalarFileType = (typeof scalarFileTypes)[number];
|
|
@@ -37,7 +36,6 @@ const fileTypeOf = (filename: string) =>
|
|
|
37
36
|
|
|
38
37
|
type ModuleKind = "database" | "service" | "scalar";
|
|
39
38
|
|
|
40
|
-
const generatedRootCapacitorConfigFiles = ["capacitor.config.js", "capacitor.config.json"] as const;
|
|
41
39
|
const internalLibDirs = new Set(["__lib", "__scalar"]);
|
|
42
40
|
const moduleNonUiFileTypes = {
|
|
43
41
|
database: new Set(["constant", "dictionary", "document", "service", "signal", "store"]),
|
|
@@ -56,17 +54,12 @@ const createDependencyScanner = async (exec: AppExecutor | LibExecutor | PkgExec
|
|
|
56
54
|
|
|
57
55
|
const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
|
|
58
56
|
path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
|
|
59
|
-
async function clearGeneratedRootCapacitorConfigs(exec: AppExecutor | LibExecutor) {
|
|
60
|
-
if (exec.type !== "app") return;
|
|
61
|
-
await Promise.all(generatedRootCapacitorConfigFiles.map((filename) => rm(exec.getPath(filename), { force: true })));
|
|
62
|
-
}
|
|
63
57
|
const getModuleNameFromPath = (kind: ModuleKind, modulePath: string) => {
|
|
64
58
|
const dirname = path.basename(modulePath);
|
|
65
59
|
return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
|
|
66
60
|
};
|
|
67
61
|
|
|
68
62
|
async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: { files: string[]; dirs: string[] }) {
|
|
69
|
-
await clearGeneratedRootCapacitorConfigs(exec);
|
|
70
63
|
const violations: string[] = [];
|
|
71
64
|
const addViolation = (relativePath: string, reason: string) => {
|
|
72
65
|
violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
|
|
@@ -76,9 +69,11 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
|
|
|
76
69
|
const allowedRootDirs: ReadonlySet<string> = rootAllowedDirs[exec.type];
|
|
77
70
|
const { files, dirs } = await exec.getFilesAndDirs(".");
|
|
78
71
|
for (const filename of files)
|
|
79
|
-
if (!allowedRootFiles.has(filename))
|
|
72
|
+
if (!allowedRootFiles.has(filename))
|
|
73
|
+
addViolation(filename, rootEntryHintOf(exec.type, filename) ?? `unsupported ${exec.type} root file`);
|
|
80
74
|
for (const dirname of dirs)
|
|
81
|
-
if (!allowedRootDirs.has(dirname))
|
|
75
|
+
if (!allowedRootDirs.has(dirname))
|
|
76
|
+
addViolation(dirname, rootEntryHintOf(exec.type, dirname) ?? `unsupported ${exec.type} root folder`);
|
|
82
77
|
|
|
83
78
|
//* A lib has no `getPageKeys`, so its own route files are validated here.
|
|
84
79
|
if (exec.type === "lib")
|
package/workspaceLayout.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isScannedRootEntry,
|
|
7
7
|
libRootAllowedDirs,
|
|
8
8
|
libRootAllowedFiles,
|
|
9
|
+
rootEntryHintOf,
|
|
9
10
|
} from "./workspaceLayout";
|
|
10
11
|
|
|
11
12
|
describe("app root layout allowlist", () => {
|
|
@@ -15,7 +16,7 @@ describe("app root layout allowlist", () => {
|
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
test("admits every documented app root folder", () => {
|
|
18
|
-
for (const dirname of ["
|
|
19
|
+
for (const dirname of [".akan", "plugin", "script", "secrets", "srvkit", "webkit"]) {
|
|
19
20
|
expect(appRootAllowedDirs.has(dirname)).toBe(true);
|
|
20
21
|
}
|
|
21
22
|
});
|
|
@@ -25,6 +26,15 @@ describe("app root layout allowlist", () => {
|
|
|
25
26
|
expect(appRootAllowedDirs.has("base")).toBe(false);
|
|
26
27
|
});
|
|
27
28
|
|
|
29
|
+
test("names what a Capacitor app kept in its root as a leftover, not an unknown entry", () => {
|
|
30
|
+
for (const name of ["android", "ios", "mobile", "capacitor.config.ts", "capacitor.config.json"]) {
|
|
31
|
+
expect(appRootAllowedFiles.has(name) || appRootAllowedDirs.has(name)).toBe(false);
|
|
32
|
+
expect(rootEntryHintOf("app", name)).toContain(".akan/mobile/<target>");
|
|
33
|
+
}
|
|
34
|
+
expect(rootEntryHintOf("app", "base")).toBeNull();
|
|
35
|
+
expect(rootEntryHintOf("lib", "ios")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
28
38
|
test("skips dotfile artifacts the sync glob never sees, but keeps .akan", () => {
|
|
29
39
|
expect(isScannedRootEntry("app", ".DS_Store")).toBe(false);
|
|
30
40
|
expect(isScannedRootEntry("app", ".akan")).toBe(true);
|
|
@@ -48,10 +58,10 @@ describe("lib root layout allowlist", () => {
|
|
|
48
58
|
});
|
|
49
59
|
|
|
50
60
|
test("rejects the app-only run and mobile entries", () => {
|
|
51
|
-
for (const filename of ["main.ts", "
|
|
61
|
+
for (const filename of ["main.ts", "akan.app.json"]) {
|
|
52
62
|
expect(libRootAllowedFiles.has(filename)).toBe(false);
|
|
53
63
|
}
|
|
54
|
-
for (const dirname of [".akan", "
|
|
64
|
+
for (const dirname of [".akan", "script", "secrets"]) {
|
|
55
65
|
expect(libRootAllowedDirs.has(dirname)).toBe(false);
|
|
56
66
|
}
|
|
57
67
|
});
|
package/workspaceLayout.ts
CHANGED
|
@@ -8,7 +8,6 @@ export const appRootAllowedFiles = new Set([
|
|
|
8
8
|
"CLAUDE.md",
|
|
9
9
|
"akan.app.json",
|
|
10
10
|
"akan.config.ts",
|
|
11
|
-
"capacitor.config.ts",
|
|
12
11
|
"client.ts",
|
|
13
12
|
"main.ts",
|
|
14
13
|
"package.json",
|
|
@@ -19,12 +18,9 @@ export const appRootAllowedFiles = new Set([
|
|
|
19
18
|
|
|
20
19
|
export const appRootAllowedDirs = new Set([
|
|
21
20
|
".akan",
|
|
22
|
-
"android",
|
|
23
21
|
"common",
|
|
24
22
|
"env",
|
|
25
|
-
"ios",
|
|
26
23
|
"lib",
|
|
27
|
-
"mobile",
|
|
28
24
|
"page",
|
|
29
25
|
"plugin",
|
|
30
26
|
"private",
|
|
@@ -86,6 +82,22 @@ const libFacetRootTestPattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(
|
|
|
86
82
|
export const isAllowedLibFacetRootFile = (filename: string) =>
|
|
87
83
|
libFacetRootAllowedFiles.has(filename) || libFacetRootTestPattern.test(filename);
|
|
88
84
|
|
|
85
|
+
//* What a Capacitor app kept in its root. The native runtime generates its projects under `.akan/mobile/<target>`, so
|
|
86
|
+
//* these are named as leftovers instead of as unknown entries.
|
|
87
|
+
const retiredAppRootEntries = new Set([
|
|
88
|
+
"android",
|
|
89
|
+
"ios",
|
|
90
|
+
"mobile",
|
|
91
|
+
"capacitor.config.ts",
|
|
92
|
+
"capacitor.config.js",
|
|
93
|
+
"capacitor.config.json",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
export const rootEntryHintOf = (type: SysType, name: string) =>
|
|
97
|
+
type === "app" && retiredAppRootEntries.has(name)
|
|
98
|
+
? "a Capacitor-era entry; the native runtime generates its projects under .akan/mobile/<target>, so delete it"
|
|
99
|
+
: null;
|
|
100
|
+
|
|
89
101
|
// scanSync reads roots through `Bun.Glob("*")`, which skips dotfiles, so doctor skips them the same way.
|
|
90
102
|
export const isScannedRootEntry = (type: SysType, name: string) =>
|
|
91
103
|
!name.startsWith(".") || rootAllowedDirs[type].has(name);
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { cp, mkdir, rm } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { App } from "./commandDecorators";
|
|
4
|
-
import { FileSys } from "./fileSys";
|
|
5
|
-
import { uploadRelease } from "./uploadRelease";
|
|
6
|
-
|
|
7
|
-
export interface ReleaseSourceOptions {
|
|
8
|
-
rebuild?: boolean;
|
|
9
|
-
buildNum?: number;
|
|
10
|
-
environment?: string;
|
|
11
|
-
local?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface ApplicationReleasePackagerOptions {
|
|
15
|
-
build: () => Promise<void>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class ApplicationReleasePackager {
|
|
19
|
-
#app: App;
|
|
20
|
-
#build: () => Promise<void>;
|
|
21
|
-
|
|
22
|
-
constructor(app: App, options: ApplicationReleasePackagerOptions) {
|
|
23
|
-
this.#app = app;
|
|
24
|
-
this.#build = options.build;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async releaseSource({
|
|
28
|
-
rebuild,
|
|
29
|
-
buildNum = 0,
|
|
30
|
-
environment = "debug",
|
|
31
|
-
local = true,
|
|
32
|
-
}: ReleaseSourceOptions = {}): Promise<void> {
|
|
33
|
-
const { platformVersion } = await this.#prepareReleaseBuild({ rebuild, buildNum });
|
|
34
|
-
await this.#writeSourceArchive({ readme: this.#confidentialReadme() });
|
|
35
|
-
await uploadRelease(this.#app.name, {
|
|
36
|
-
local,
|
|
37
|
-
buildNum,
|
|
38
|
-
environment,
|
|
39
|
-
platformVersion,
|
|
40
|
-
workspaceRoot: this.#app.workspace.cwdPath,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async compressProjectFiles({ rebuild, buildNum = 0 }: ReleaseSourceOptions = {}): Promise<void> {
|
|
45
|
-
await this.#prepareReleaseBuild({ rebuild, buildNum });
|
|
46
|
-
await this.#writeSourceArchive({ readme: this.#publicReadme() });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async #prepareReleaseBuild({ rebuild, buildNum }: { rebuild?: boolean; buildNum: number }) {
|
|
50
|
-
const akanConfig = await this.#app.getConfig();
|
|
51
|
-
const platformVersion = akanConfig.mobile.version;
|
|
52
|
-
const buildRoot = `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}`;
|
|
53
|
-
if (await FileSys.dirExists(buildRoot)) await rm(buildRoot, { recursive: true, force: true });
|
|
54
|
-
await mkdir(buildRoot, { recursive: true });
|
|
55
|
-
if (rebuild || !(await FileSys.dirExists(`${this.#app.dist.cwdPath}/backend`))) await this.#build();
|
|
56
|
-
|
|
57
|
-
const buildVersion = `${platformVersion}-${buildNum}`;
|
|
58
|
-
const buildPath = `${buildRoot}/${buildVersion}`;
|
|
59
|
-
await mkdir(buildPath, { recursive: true });
|
|
60
|
-
await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
|
|
61
|
-
|
|
62
|
-
await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
|
|
63
|
-
await rm(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
|
|
64
|
-
|
|
65
|
-
const releaseRoot = this.#app.workspace.workspaceRoot;
|
|
66
|
-
await this.#tar(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`, buildRoot);
|
|
67
|
-
await this.#writeCsrZipIfPresent();
|
|
68
|
-
return { platformVersion };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async #writeCsrZipIfPresent(): Promise<void> {
|
|
72
|
-
if (!(await FileSys.dirExists(`${this.#app.dist.cwdPath}/csr`))) return;
|
|
73
|
-
await cp(`${this.#app.dist.cwdPath}/csr`, "./csr", { recursive: true });
|
|
74
|
-
await this.#app.workspace.spawn("zip", [
|
|
75
|
-
"-r",
|
|
76
|
-
`${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-appBuild.zip`,
|
|
77
|
-
"./csr",
|
|
78
|
-
]);
|
|
79
|
-
await rm("./csr", { recursive: true, force: true });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async #writeSourceArchive({ readme }: { readme: string }): Promise<void> {
|
|
83
|
-
const sourceRoot = `${this.#app.workspace.workspaceRoot}/releases/sources/${this.#app.name}`;
|
|
84
|
-
await this.#resetSourceRoot(sourceRoot);
|
|
85
|
-
await cp(this.#app.dist.cwdPath, `${sourceRoot}/apps/${this.#app.name}`, { recursive: true });
|
|
86
|
-
|
|
87
|
-
const libDeps = ["social", "shared", "platform", "util"];
|
|
88
|
-
await Promise.all(
|
|
89
|
-
libDeps.map((lib) =>
|
|
90
|
-
cp(`${this.#app.workspace.cwdPath}/libs/${lib}`, `${sourceRoot}/libs/${lib}`, { recursive: true }),
|
|
91
|
-
),
|
|
92
|
-
);
|
|
93
|
-
await Promise.all(
|
|
94
|
-
[".next", "ios", "android", "public/libs"].map(async (path) => {
|
|
95
|
-
const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path}`;
|
|
96
|
-
if (await FileSys.dirExists(targetPath)) await rm(targetPath, { recursive: true, force: true });
|
|
97
|
-
}),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const syncPaths = [".husky", ".gitignore", "package.json"];
|
|
101
|
-
await Promise.all(
|
|
102
|
-
syncPaths.map((path) =>
|
|
103
|
-
cp(`${this.#app.workspace.cwdPath}/${path}`, `${sourceRoot}/${path}`, { recursive: true }),
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
await this.#writeSourceTsconfig(sourceRoot, libDeps);
|
|
107
|
-
await Bun.write(`${sourceRoot}/README.md`, readme);
|
|
108
|
-
const sourceCwd = this.#app.workspace.cwdPath;
|
|
109
|
-
await this.#tar(sourceCwd, `${sourceCwd}/releases/sources/${this.#app.name}-source.tar.gz`, sourceRoot);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async #tar(cwd: string, archivePath: string, sourceDir: string) {
|
|
113
|
-
// Relative to cwd so tar never sees a Windows drive letter ("C:\..."), which GNU tar reads as a remote "host:file".
|
|
114
|
-
const relativeArchivePath = path.relative(cwd, archivePath).split(path.sep).join("/");
|
|
115
|
-
await this.#app.workspace.spawn("tar", ["-zcf", relativeArchivePath, "-C", sourceDir, "./"], { cwd });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async #resetSourceRoot(sourceRoot: string): Promise<void> {
|
|
119
|
-
if (await FileSys.dirExists(sourceRoot))
|
|
120
|
-
for (let attempt = 0; attempt < 3; attempt++)
|
|
121
|
-
await rm(sourceRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
122
|
-
await mkdir(sourceRoot, { recursive: true });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async #writeSourceTsconfig(sourceRoot: string, libDeps: string[]): Promise<void> {
|
|
126
|
-
const tsconfig = (await this.#app.workspace.readJson("tsconfig.json")) as {
|
|
127
|
-
compilerOptions: { paths: Record<string, string[]> };
|
|
128
|
-
};
|
|
129
|
-
const pathEntries: [string, string[]][] = [[`@${this.#app.name}/*`, [`apps/${this.#app.name}/*`]]];
|
|
130
|
-
for (const lib of libDeps) {
|
|
131
|
-
pathEntries.push([`@${lib}`, [`libs/${lib}/index.ts`]], [`@${lib}/*`, [`libs/${lib}/*`]]);
|
|
132
|
-
}
|
|
133
|
-
tsconfig.compilerOptions.paths = Object.fromEntries(pathEntries) as Record<string, string[]>;
|
|
134
|
-
await Bun.write(`${sourceRoot}/tsconfig.json`, JSON.stringify(tsconfig, null, 2));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
static #envSetup = ` cat <<EOF >> .env
|
|
138
|
-
# ENV For Server => debug | debug.local | develop | develop.local | main | main.local
|
|
139
|
-
SERVER_ENV=debug.local
|
|
140
|
-
# Run Mode For Server => federation | batch | all
|
|
141
|
-
SERVER_MODE=federation
|
|
142
|
-
# ENV For Client => debug | debug.local | develop | develop.local | main | main.local
|
|
143
|
-
AKAN_PUBLIC_CLIENT_ENV=debug.local
|
|
144
|
-
ANALYZE=false
|
|
145
|
-
EOF
|
|
146
|
-
`;
|
|
147
|
-
|
|
148
|
-
#confidentialReadme(): string {
|
|
149
|
-
return `# ${this.#app.name}
|
|
150
|
-
본 프로젝트의 소스코드 및 관련자료는 모두 비밀정보로 관리됩니다.
|
|
151
|
-
|
|
152
|
-
## Get Started
|
|
153
|
-
Run the code below.
|
|
154
|
-
\`\`\`
|
|
155
|
-
bun install -g bun
|
|
156
|
-
bun i
|
|
157
|
-
|
|
158
|
-
${ApplicationReleasePackager.#envSetup}
|
|
159
|
-
akn start-backend ${this.#app.name}
|
|
160
|
-
# or akn start-frontend ${this.#app.name}, etc
|
|
161
|
-
\`\`\`
|
|
162
|
-
|
|
163
|
-
## Build
|
|
164
|
-
Run the code below.
|
|
165
|
-
\`\`\`
|
|
166
|
-
akn build-backend ${this.#app.name}
|
|
167
|
-
# or akn build-frontend ${this.#app.name}, etc
|
|
168
|
-
\`\`\`
|
|
169
|
-
`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
#publicReadme(): string {
|
|
173
|
-
return `# ${this.#app.name}
|
|
174
|
-
## Get Started
|
|
175
|
-
Run the code below.
|
|
176
|
-
\`\`\`
|
|
177
|
-
${ApplicationReleasePackager.#envSetup}
|
|
178
|
-
akan start ${this.#app.name}
|
|
179
|
-
\`\`\`
|
|
180
|
-
|
|
181
|
-
## Build
|
|
182
|
-
Run the code below.
|
|
183
|
-
\`\`\`
|
|
184
|
-
akan build ${this.#app.name}
|
|
185
|
-
# or akn build-frontend ${this.#app.name}, etc
|
|
186
|
-
\`\`\`
|
|
187
|
-
`;
|
|
188
|
-
}
|
|
189
|
-
}
|