@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.
Files changed (44) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +2 -2
  3. package/akanConfig/akanConfig.test.ts +58 -38
  4. package/akanConfig/akanConfig.ts +88 -48
  5. package/akanConfig/types.ts +3 -3
  6. package/akanContext.ts +5 -2
  7. package/artifact/implicitRootLayout.test.ts +23 -0
  8. package/artifact/implicitRootLayout.ts +24 -0
  9. package/biome.base.json +26 -0
  10. package/commandDecorators/command.ts +1 -1
  11. package/executors.ts +1 -12
  12. package/incrementalBuilder/buildBatch.proc.ts +7 -1
  13. package/index.ts +0 -2
  14. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  15. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  16. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  17. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  18. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  19. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  20. package/lint/no-document-cookie.grit +29 -0
  21. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  22. package/lint/no-web-storage.grit +30 -0
  23. package/mobile/index.ts +6 -0
  24. package/mobile/mobileTarget.ts +2 -0
  25. package/mobile/nativeApi.test.ts +30 -0
  26. package/mobile/nativeApi.ts +40 -0
  27. package/mobile/nativeApp.test.ts +85 -0
  28. package/mobile/nativeApp.ts +210 -0
  29. package/mobile/nativeConfig.test.ts +199 -0
  30. package/mobile/nativeConfig.ts +184 -0
  31. package/mobile/nativeWebDir.test.ts +80 -0
  32. package/mobile/nativeWebDir.ts +62 -0
  33. package/mobile/placeholderAppId.test.ts +13 -0
  34. package/mobile/placeholderAppId.ts +16 -0
  35. package/mobile/usageDescriptions.test.ts +27 -0
  36. package/mobile/usageDescriptions.ts +24 -0
  37. package/package.json +2 -7
  38. package/qualityScanner.ts +4 -2
  39. package/scanInfo.ts +5 -10
  40. package/workspaceLayout.test.ts +13 -3
  41. package/workspaceLayout.ts +16 -4
  42. package/applicationReleasePackager.ts +0 -189
  43. package/capacitorApp.test.ts +0 -388
  44. 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)) addViolation(filename, `unsupported ${exec.type} root file`);
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)) addViolation(dirname, `unsupported ${exec.type} root folder`);
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")
@@ -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 ["mobile", "plugin", "secrets", "srvkit", "webkit"]) {
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", "capacitor.config.ts", "akan.app.json"]) {
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", "android", "ios", "mobile", "script", "secrets"]) {
64
+ for (const dirname of [".akan", "script", "secrets"]) {
55
65
  expect(libRootAllowedDirs.has(dirname)).toBe(false);
56
66
  }
57
67
  });
@@ -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
- }