akanjs 2.0.0-beta.7 → 2.0.0-beta.9

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 (34) hide show
  1. package/cli/application/application.command.ts +11 -3
  2. package/cli/index.js +117 -24
  3. package/cli/package/package.runner.ts +7 -3
  4. package/cli/templates/app/page/_layout.tsx +0 -1
  5. package/cli/templates/workspaceRoot/.gitignore.template +1 -11
  6. package/client/csrTypes.ts +1 -1
  7. package/devkit/capacitor.base.config.ts +1 -1
  8. package/devkit/capacitorApp.ts +5 -1
  9. package/devkit/commandDecorators/argMeta.ts +28 -14
  10. package/devkit/commandDecorators/command.ts +41 -15
  11. package/devkit/commandDecorators/commandBuilder.ts +78 -42
  12. package/devkit/commandDecorators/helpFormatter.ts +7 -4
  13. package/devkit/frontendBuild/cssCompiler.ts +9 -3
  14. package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +2 -1
  15. package/devkit/mobile/mobileTarget.ts +48 -8
  16. package/devkit/src/capacitorApp.ts +277 -0
  17. package/devkit/transforms/barrelImportsPlugin.ts +6 -0
  18. package/package.json +2 -1
  19. package/server/hmr/clientScript.ts +8 -5
  20. package/ui/Portal.tsx +2 -0
  21. package/ui/System/CSR.tsx +6 -5
  22. package/ui/System/SSR.tsx +2 -2
  23. package/webkit/bootCsr.tsx +8 -5
  24. package/cli/templates/app/common/commonLogic.ts +0 -12
  25. package/cli/templates/app/common/index.ts +0 -10
  26. package/cli/templates/app/srvkit/backendLogic.ts +0 -12
  27. package/cli/templates/app/srvkit/index.ts +0 -10
  28. package/cli/templates/app/ui/UiComponent.ts +0 -16
  29. package/cli/templates/app/ui/index.ts +0 -10
  30. package/cli/templates/app/webkit/frontendLogic.ts +0 -12
  31. package/cli/templates/app/webkit/index.ts +0 -10
  32. package/cli/templates/module/index.tsx +0 -44
  33. /package/cli/templates/app/public/{favicon.ico → favicon.ico.template} +0 -0
  34. /package/cli/templates/app/public/{logo.png → logo.png.template} +0 -0
@@ -1,4 +1,17 @@
1
- import type { ArgMeta, ArgsOption, InternalArgMeta, InternalArgToken, PrimitiveArgType } from "./argMeta";
1
+ import type {
2
+ App,
3
+ ArgMeta,
4
+ ArgsOption,
5
+ CommandContext,
6
+ Exec,
7
+ InternalArgMeta,
8
+ InternalArgToken,
9
+ Lib,
10
+ Module,
11
+ Pkg,
12
+ PrimitiveArgType,
13
+ Sys,
14
+ } from "./argMeta";
2
15
  import { normalizePrimitiveArgType } from "./argMeta";
3
16
  import { assertUniqueDependencies, type DependencyInstanceMap, injectDependencies } from "./dependencyBuilder";
4
17
  import { COMMAND_META, type CommandCls, type TargetMeta, type TargetOption } from "./targetMeta";
@@ -9,9 +22,8 @@ type PrimitiveValue<T extends PrimitiveArgType> = T extends StringConstructor
9
22
  : T extends NumberConstructor
10
23
  ? number
11
24
  : boolean;
12
- type ArgsOptionInput = Record<string, never> | ArgsOption;
13
- type MaybeNullable<Value, Option extends ArgsOptionInput> = Option extends { nullable: true } ? Value | null : Value;
14
- type AddArg<Params extends unknown[], Type extends PrimitiveArgType, Option extends ArgsOptionInput> = [
25
+ type MaybeNullable<Value, Option> = Option extends { nullable: true } ? Value | null : Value;
26
+ type AddArg<Params extends unknown[], Type extends PrimitiveArgType, Option> = [
15
27
  ...Params,
16
28
  MaybeNullable<PrimitiveValue<Type>, Option>,
17
29
  ];
@@ -22,12 +34,31 @@ type AddInternalArgs<Params extends unknown[], Tokens extends readonly InternalA
22
34
  ]
23
35
  ? AddInternalArgs<AddInternalArg<Params, Head>, Rest>
24
36
  : Params;
37
+ type ContextFromToken<Token extends InternalArgToken> = Token["_value"] extends App
38
+ ? { app: App }
39
+ : Token["_value"] extends Lib
40
+ ? { lib: Lib }
41
+ : Token["_value"] extends Sys
42
+ ? { sys: Sys }
43
+ : Token["_value"] extends Pkg
44
+ ? { pkg: Pkg }
45
+ : Token["_value"] extends Module
46
+ ? { module: Module }
47
+ : Token["_value"] extends Exec
48
+ ? { exec: Exec }
49
+ : object;
50
+ type AddInternalContext<Context, Tokens extends readonly InternalArgToken[]> = Tokens extends readonly [
51
+ infer Head extends InternalArgToken,
52
+ ...infer Rest extends InternalArgToken[],
53
+ ]
54
+ ? AddInternalContext<Context & ContextFromToken<Head>, Rest>
55
+ : Context;
25
56
  type CommandHandler<Deps extends readonly DependencyCls[], Params extends unknown[]> = (
26
57
  this: DependencyInstanceMap<Deps>,
27
58
  ...args: Params
28
59
  ) => unknown | Promise<unknown>;
29
60
 
30
- class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknown[] = []> {
61
+ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknown[] = [], Context = object> {
31
62
  readonly #args: (ArgMeta | InternalArgMeta)[];
32
63
 
33
64
  constructor(
@@ -37,12 +68,12 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
37
68
  this.#args = args;
38
69
  }
39
70
 
40
- arg<Type extends PrimitiveArgType, Option extends ArgsOptionInput = Record<string, never>>(
71
+ arg<Type extends PrimitiveArgType, Option extends ArgsOption<Context> = ArgsOption<Context>>(
41
72
  name: string,
42
73
  type: Type,
43
74
  argsOption: Option = {} as Option,
44
- ): TargetBuilder<Deps, AddArg<Params, Type, Option>> {
45
- return new TargetBuilder(this.targetOption, [
75
+ ): TargetBuilder<Deps, AddArg<Params, Type, Option>, Context> {
76
+ return new TargetBuilder<Deps, AddArg<Params, Type, Option>, Context>(this.targetOption, [
46
77
  ...this.#args,
47
78
  {
48
79
  name,
@@ -50,16 +81,16 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
50
81
  key: "",
51
82
  idx: this.#args.length,
52
83
  type: "Argument",
53
- },
84
+ } as ArgMeta<CommandContext>,
54
85
  ]);
55
86
  }
56
87
 
57
- option<Type extends PrimitiveArgType, Option extends ArgsOptionInput = Record<string, never>>(
88
+ option<Type extends PrimitiveArgType, Option extends ArgsOption<Context> = ArgsOption<Context>>(
58
89
  name: string,
59
90
  type: Type,
60
91
  argsOption: Option = {} as Option,
61
- ): TargetBuilder<Deps, AddArg<Params, Type, Option>> {
62
- return new TargetBuilder(this.targetOption, [
92
+ ): TargetBuilder<Deps, AddArg<Params, Type, Option>, Context> {
93
+ return new TargetBuilder<Deps, AddArg<Params, Type, Option>, Context>(this.targetOption, [
63
94
  ...this.#args,
64
95
  {
65
96
  name,
@@ -67,24 +98,27 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
67
98
  key: "",
68
99
  idx: this.#args.length,
69
100
  type: "Option",
70
- },
101
+ } as ArgMeta<CommandContext>,
71
102
  ]);
72
103
  }
73
104
 
74
- with<Tokens extends readonly InternalArgToken[]>(
105
+ with<const Tokens extends readonly InternalArgToken[]>(
75
106
  ...tokens: Tokens
76
- ): TargetBuilder<Deps, AddInternalArgs<Params, Tokens>> {
77
- return new TargetBuilder(this.targetOption, [
78
- ...this.#args,
79
- ...tokens.map(
80
- (token, offset) =>
81
- ({
82
- key: "",
83
- idx: this.#args.length + offset,
84
- type: token.type,
85
- }) satisfies InternalArgMeta,
86
- ),
87
- ]);
107
+ ): TargetBuilder<Deps, AddInternalArgs<Params, Tokens>, AddInternalContext<Context, Tokens>> {
108
+ return new TargetBuilder<Deps, AddInternalArgs<Params, Tokens>, AddInternalContext<Context, Tokens>>(
109
+ this.targetOption,
110
+ [
111
+ ...this.#args,
112
+ ...tokens.map(
113
+ (token, offset) =>
114
+ ({
115
+ key: "",
116
+ idx: this.#args.length + offset,
117
+ type: token.type,
118
+ }) satisfies InternalArgMeta,
119
+ ),
120
+ ],
121
+ );
88
122
  }
89
123
 
90
124
  exec(handler: CommandHandler<Deps, Params>) {
@@ -101,12 +135,12 @@ type CommandBuilderContext<Deps extends readonly DependencyCls[]> = {
101
135
  public: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
102
136
  cloud: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
103
137
  dev: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
104
- arg: <Type extends PrimitiveArgType, Option extends ArgsOptionInput = Record<string, never>>(
138
+ arg: <Type extends PrimitiveArgType, Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
105
139
  name: string,
106
140
  type: Type,
107
141
  argsOption?: Option,
108
142
  ) => ArgMeta;
109
- option: <Type extends PrimitiveArgType, Option extends ArgsOptionInput = Record<string, never>>(
143
+ option: <Type extends PrimitiveArgType, Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
110
144
  name: string,
111
145
  type: Type,
112
146
  argsOption?: Option,
@@ -125,20 +159,22 @@ const createContext = <Deps extends readonly DependencyCls[]>(): CommandBuilderC
125
159
  public: createTarget<Deps>("public"),
126
160
  cloud: createTarget<Deps>("cloud"),
127
161
  dev: createTarget<Deps>("dev"),
128
- arg: (name, type, argsOption) => ({
129
- name,
130
- argsOption: { ...(argsOption ?? {}), type: normalizePrimitiveArgType(type) },
131
- key: "",
132
- idx: -1,
133
- type: "Argument",
134
- }),
135
- option: (name, type, argsOption) => ({
136
- name,
137
- argsOption: { ...(argsOption ?? {}), type: normalizePrimitiveArgType(type) },
138
- key: "",
139
- idx: -1,
140
- type: "Option",
141
- }),
162
+ arg: (name, type, argsOption) =>
163
+ ({
164
+ name,
165
+ argsOption: { ...(argsOption ?? {}), type: normalizePrimitiveArgType(type) },
166
+ key: "",
167
+ idx: -1,
168
+ type: "Argument",
169
+ }) as ArgMeta<CommandContext>,
170
+ option: (name, type, argsOption) =>
171
+ ({
172
+ name,
173
+ argsOption: { ...(argsOption ?? {}), type: normalizePrimitiveArgType(type) },
174
+ key: "",
175
+ idx: -1,
176
+ type: "Option",
177
+ }) as ArgMeta<CommandContext>,
142
178
  });
143
179
 
144
180
  const buildCommandMeta = (definitions: Record<string, TargetDefinition>) => {
@@ -1,11 +1,10 @@
1
1
  import chalk from "chalk";
2
2
 
3
- import { getArgMetas } from "./argMeta";
3
+ import { type EnumChoice, getArgMetas } from "./argMeta";
4
4
  import { type CommandCls, getTargetMetas } from "./targetMeta";
5
5
 
6
6
  const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
7
- const formatChoice = (choice: string | number | { label: string; value: string | number | boolean }) =>
8
- typeof choice === "object" ? choice.label : choice.toString();
7
+ const formatChoice = (choice: EnumChoice) => (typeof choice === "object" ? choice.label : choice.toString());
9
8
 
10
9
  const groupCommands = (commands: CommandCls[]) => {
11
10
  const groups = new Map<string, { name: string; commands: { key: string; args: string[]; desc?: string }[] }>();
@@ -207,7 +206,11 @@ export const formatCommandHelp = (command: CommandCls, key: string) => {
207
206
  const optName = `${flag}--${kebabName}`;
208
207
  const optDesc = opt.desc ?? "";
209
208
  const defaultVal = opt.default !== undefined ? chalk.gray(` [default: ${String(opt.default)}]`) : "";
210
- const choices = opt.enum ? chalk.gray(` (${opt.enum.map(formatChoice).join(", ")})`) : "";
209
+ const choices = opt.enum
210
+ ? chalk.gray(
211
+ typeof opt.enum === "function" ? " ([dynamic choices])" : ` (${opt.enum.map(formatChoice).join(", ")})`,
212
+ )
213
+ : "";
211
214
  lines.push(` ${chalk.green(optName)} ${chalk.gray(optDesc)}${defaultVal}${choices}`);
212
215
  }
213
216
  lines.push("");
@@ -8,6 +8,8 @@ import { CssImportResolver } from "./cssImportResolver";
8
8
 
9
9
  const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"] as const;
10
10
  const NON_SOURCE_EXT_RE = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
11
+ const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
12
+ const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
11
13
 
12
14
  interface CssDiscovery {
13
15
  cssPaths: string[];
@@ -88,7 +90,7 @@ export class CssCompiler {
88
90
 
89
91
  while (queue.length > 0) {
90
92
  const filePath = queue.shift();
91
- if (!filePath || sourceFiles.has(filePath) || filePath.includes("node_modules")) continue;
93
+ if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath)) continue;
92
94
  sourceFiles.add(filePath);
93
95
 
94
96
  let content: string;
@@ -125,7 +127,7 @@ export class CssCompiler {
125
127
  }
126
128
  if (NON_SOURCE_EXT_RE.test(spec)) continue;
127
129
  const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
128
- if (!resolved || sourceFiles.has(resolved) || resolved.includes("node_modules")) continue;
130
+ if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved)) continue;
129
131
  queue.push(resolved);
130
132
  }
131
133
  }
@@ -221,7 +223,7 @@ export class CssCompiler {
221
223
  await Promise.all(
222
224
  dirs.map(async (dir) => {
223
225
  for await (const file of glob.scan({ cwd: dir, absolute: true })) {
224
- if (file.includes("node_modules")) continue;
226
+ if (isIgnoredNodeModuleSource(file)) continue;
225
227
  files.add(file);
226
228
  }
227
229
  }),
@@ -271,6 +273,10 @@ function isSourceFile(filePath: string) {
271
273
  return SOURCE_EXTS.includes(path.extname(filePath) as (typeof SOURCE_EXTS)[number]);
272
274
  }
273
275
 
276
+ export function isIgnoredNodeModuleSource(filePath: string): boolean {
277
+ return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
278
+ }
279
+
274
280
  function getPageKeyBasePath(pageKey: string, basePaths: string[]): string | null {
275
281
  const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
276
282
  const segments = normalized.split("/");
@@ -308,7 +308,8 @@ class IncrementalBuilder {
308
308
  }
309
309
 
310
310
  #shouldRebuildCsr() {
311
- return process.env.AKAN_DEV_CSR_REBUILD === "1";
311
+
312
+ return true;
312
313
  }
313
314
 
314
315
  static async create() {
@@ -16,23 +16,63 @@ export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]
16
16
  return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
17
17
  };
18
18
 
19
+ export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
20
+ const config = await app.getConfig();
21
+ const targetNames = Object.keys(config.mobile.targets);
22
+ if (targetNames.length > 1) return targetNames;
23
+ const basePaths = [...config.basePaths];
24
+ if (basePaths.length > 1) return basePaths;
25
+ if (targetNames.length > 0) return targetNames;
26
+ return basePaths;
27
+ };
28
+
29
+ const resolveMobileTargetByBasePath = (
30
+ targets: ResolvedMobileTarget[],
31
+ basePath: string,
32
+ ): ResolvedMobileTarget | undefined => {
33
+ const normalizedBasePath = basePath.replace(/^\/+|\/+$/g, "");
34
+ const byBasePath = targets.find((target) => target.config.basePath?.replace(/^\/+|\/+$/g, "") === normalizedBasePath);
35
+ if (byBasePath) return byBasePath;
36
+ const [template] = targets;
37
+ if (!template) return undefined;
38
+ return {
39
+ name: template.name,
40
+ config: {
41
+ ...template.config,
42
+ basePath: normalizedBasePath,
43
+ },
44
+ };
45
+ };
46
+
19
47
  export const resolveMobileTargets = async (
20
48
  app: App,
21
49
  selection: MobileTargetSelection,
22
50
  ): Promise<ResolvedMobileTarget[]> => {
51
+ const config = await app.getConfig();
23
52
  const targets = await getMobileTargets(app);
24
53
  if (targets.length === 0) throw new Error(`No mobile targets configured for ${app.name}`);
25
54
  if (!selection) {
26
- if (targets.length === 1) return targets;
27
- throw new Error(
28
- `Multiple mobile targets found for ${app.name}. Pass --target <${targets.map((t) => t.name).join("|")}|all>.`,
29
- );
55
+ const choices = await getMobileTargetChoices(app);
56
+ if (choices.length === 1) return resolveMobileTargets(app, choices[0]);
57
+ throw new Error(`Multiple mobile targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
58
+ }
59
+ if (selection === "all") {
60
+ if (Object.keys(config.mobile.targets).length > 1) return targets;
61
+ const basePaths = [...config.basePaths];
62
+ if (basePaths.length > 1) {
63
+ return basePaths.flatMap((basePath) => {
64
+ const resolved = resolveMobileTargetByBasePath(targets, basePath);
65
+ return resolved ? [resolved] : [];
66
+ });
67
+ }
68
+ return targets;
30
69
  }
31
- if (selection === "all") return targets;
32
70
  const target = targets.find((candidate) => candidate.name === selection);
33
- if (!target)
34
- throw new Error(`Mobile target '${selection}' was not found. Available: ${targets.map((t) => t.name).join(", ")}`);
35
- return [target];
71
+ if (target) return [target];
72
+ const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
73
+ if (basePathTarget && config.basePaths.has(selection.replace(/^\/+|\/+$/g, ""))) return [basePathTarget];
74
+ const choices = await getMobileTargetChoices(app);
75
+ throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
36
76
  };
37
77
 
38
78
  export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
@@ -0,0 +1,277 @@
1
+ import type { CapacitorConfig } from "@capacitor/cli";
2
+ import { MobileProject } from "@trapezedev/project";
3
+ import type { AndroidProject } from "@trapezedev/project/dist/android/project";
4
+ import type { IosProject } from "@trapezedev/project/dist/ios/project";
5
+ import { capitalize } from "akanjs/common";
6
+ import { type AppExecutor, FileSys } from "akanjs/devkit";
7
+
8
+ import { FileEditor } from "./fileEditor";
9
+
10
+ interface RunConfig extends CapacitorConfig {
11
+ operation: "local" | "release";
12
+ version: string;
13
+ buildNum: number;
14
+ appId?: string;
15
+ host?: "local" | "debug" | "develop" | "main";
16
+ }
17
+
18
+ export class CapacitorApp {
19
+ project: MobileProject & { ios: IosProject; android: AndroidProject };
20
+ iosTargetName = "App";
21
+ constructor(private readonly app: AppExecutor) {
22
+ this.project = new MobileProject(this.app.cwdPath, {
23
+ android: { path: "android" },
24
+ ios: { path: "ios/App" },
25
+ }) as MobileProject & { ios: IosProject; android: AndroidProject };
26
+ }
27
+ async init() {
28
+ const project = this.project as MobileProject;
29
+ await this.project.load();
30
+ const hasAndroid = await FileSys.fileExists(`${this.app.cwdPath}/android/app/build.gradle`);
31
+ const hasIos = await FileSys.fileExists(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`);
32
+ if (!project.android && !hasAndroid) {
33
+ await this.app.spawn("npx", ["cap", "add", "android"]);
34
+ await this.project.load();
35
+ }
36
+ if (!project.ios && !hasIos) {
37
+ await this.app.spawn("npx", ["cap", "add", "ios"]);
38
+ await this.project.load();
39
+ }
40
+ return this;
41
+ }
42
+ async save() {
43
+ await this.project.commit();
44
+ }
45
+ async #prepareIos() {
46
+ const isAdded = await FileSys.fileExists(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`);
47
+ if (!isAdded) {
48
+ await this.app.spawn("npx", ["cap", "add", "ios"]);
49
+ await this.app.spawn("npx", ["@capacitor/assets", "generate"]);
50
+ } else this.app.verbose(`iOS already added, skip adding process`);
51
+ this.app.verbose(`syncing iOS`);
52
+ await this.app.spawn("npx", ["cap", "sync", "ios"]);
53
+ this.app.verbose(`sync completed.`);
54
+ }
55
+ async buildIos() {
56
+ await this.#prepareIos();
57
+ this.app.verbose(`build completed iOS.`);
58
+ return;
59
+ }
60
+ async syncIos() {
61
+ await this.app.spawn("npx", ["cap", "sync", "ios"]);
62
+ }
63
+ async openIos() {
64
+ await this.app.spawn("npx", ["cap", "open", "ios"]);
65
+ }
66
+ async runIos({ operation, appId, version = "0.0.1", buildNum = 1, host = "local" }: RunConfig) {
67
+ const defaultAppId = `com.${this.app.name}.app`;
68
+ await this.#prepareIos();
69
+ this.project.ios.setBundleId("App", "Debug", appId ?? defaultAppId);
70
+ this.project.ios.setBundleId("App", "Release", appId ?? defaultAppId);
71
+ await this.project.ios.setVersion("App", "Debug", version);
72
+ await this.project.ios.setVersion("App", "Release", version);
73
+ await this.project.ios.setBuild("App", "Debug", buildNum);
74
+ await this.project.ios.setBuild("App", "Release", buildNum);
75
+ await this.project.commit();
76
+ await this.app.spawn(
77
+ "npx",
78
+ ["cross-env", `APP_OPERATION_MODE=${operation}`, `BUN_PUBLIC_ENV=${host}`, "npx", "cap", "run", "ios"],
79
+ {
80
+ stdio: "inherit",
81
+ },
82
+ );
83
+
84
+ }
85
+
86
+ async #prepareAndroid() {
87
+ const isAdded = await Bun.file(`${this.app.cwdPath}/android/app/build.gradle`).exists();
88
+ if (!isAdded) {
89
+ await this.app.spawn("npx", ["cap", "add", "android"]);
90
+ } else this.app.verbose(`Android already added, skip adding process`);
91
+ await this.app.spawn("npx", ["@capacitor/assets", "generate"]);
92
+ await this.app.spawn("npx", ["cap", "sync", "android"]);
93
+ }
94
+
95
+ async #updateAndroidBuildTypes() {
96
+
97
+ const appGradle = await FileEditor.create(`${this.app.cwdPath}/android/app/build.gradle`);
98
+ const buildTypesBlock = `
99
+ debug {
100
+ applicationIdSuffix ".debug"
101
+ versionNameSuffix "-DEBUG"
102
+ debuggable true
103
+ minifyEnabled false
104
+ }
105
+ `;
106
+ const singinConfigBlock = `
107
+ signingConfigs {
108
+ debug {
109
+ storeFile file('debug.keystore')
110
+ storePassword 'android'
111
+ keyAlias 'androiddebugkey'
112
+ keyPassword 'android'
113
+ }
114
+ release {
115
+ if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
116
+ storeFile file(MYAPP_RELEASE_STORE_FILE)
117
+ storePassword MYAPP_RELEASE_STORE_PASSWORD
118
+ keyAlias MYAPP_RELEASE_KEY_ALIAS
119
+ keyPassword MYAPP_RELEASE_KEY_PASSWORD
120
+ }
121
+ }
122
+ }
123
+ `;
124
+ if (appGradle.find("signingConfigs {") === -1) {
125
+ appGradle.insertBefore("buildTypes {", singinConfigBlock);
126
+ }
127
+ if (appGradle.find(`applicationIdSuffix ".debug"`) === -1) {
128
+ appGradle.insertAfter("buildTypes {", buildTypesBlock);
129
+ }
130
+ await appGradle.save();
131
+ }
132
+ async buildAndroid(assembleType: "apk" | "aab") {
133
+ await this.#prepareAndroid();
134
+ await this.#updateAndroidBuildTypes();
135
+
136
+ const isWindows = process.platform === "win32";
137
+ const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
138
+
139
+ await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
140
+ stdio: "inherit",
141
+ cwd: `${this.app.cwdPath}/android`,
142
+ });
143
+ }
144
+ async openAndroid() {
145
+ await this.app.spawn("npx", ["cap", "open", "android"]);
146
+ }
147
+ async syncAndroid() {
148
+ await this.#prepareAndroid();
149
+ this.app.log(`Sync Android Completed.`);
150
+ }
151
+ async runAndroid({ operation, appName, appId, version = "0.0.1", buildNum = 1, host = "local" }: RunConfig) {
152
+ const defaultAppId = `com.${this.app.name}.app`;
153
+ const defaultAppName = this.app.name;
154
+ await this.project.android.setVersionName(version);
155
+ await this.project.android.setPackageName(appId ?? defaultAppId);
156
+ await this.project.android.setVersionCode(buildNum);
157
+ const versionName = await this.project.android.getVersionName();
158
+ const versionCode = await this.project.android.getVersionCode();
159
+ await this.project.android.setAppName(appName ?? defaultAppName);
160
+ await this.project.commit();
161
+ await this.#prepareAndroid();
162
+
163
+ this.app.logger.info(`Running Android in ${operation} mode on ${host} host`);
164
+ await this.app.spawn(
165
+ "npx",
166
+ ["cross-env", `BUN_PUBLIC_ENV=${host}`, `APP_OPERATION_MODE=${operation}`, "npx", "cap", "run", "android"],
167
+ {
168
+ stdio: "inherit",
169
+ },
170
+ );
171
+ }
172
+
173
+ async updateAndroidVersion(version: string, buildNum: number) {
174
+
175
+ await this.project.android.setVersionName(version);
176
+ await this.project.android.setVersionCode(buildNum);
177
+ const versionName = await this.project.android.getVersionName();
178
+ const versionCode = await this.project.android.getVersionCode();
179
+ await this.project.commit();
180
+ }
181
+ async releaseIos() {
182
+
183
+ const isAdded = await Bun.file(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`).exists();
184
+ if (!isAdded) {
185
+ await this.app.spawn("npx cap add ios");
186
+ await this.app.spawn("npx @capacitor/assets generate");
187
+ } else this.app.log(`iOS already added, skip adding process`);
188
+ await this.app.spawn("cross-env", ["APP_OPERATION_MODE=release", "npx", "cap", "sync", "ios"]);
189
+ }
190
+ async releaseAndroid() {
191
+
192
+ const isAdded = await Bun.file(`${this.app.cwdPath}/android/app/build.gradle`).exists();
193
+ if (!isAdded) {
194
+ await this.app.spawn("npx cap add android");
195
+ await this.app.spawn("npx @capacitor/assets generate");
196
+ } else this.app.log(`android already added, skip adding process`);
197
+ await this.app.spawn("cross-env", ["APP_OPERATION_MODE=release", "npx", "cap", "sync", "android"]);
198
+ }
199
+ async addCamera() {
200
+ await this.#setPermissionInIos({
201
+ cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
202
+ photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
203
+ photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
204
+ });
205
+ this.#setPermissionsInAndroid(["READ_MEDIA_IMAGES", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]);
206
+ }
207
+ async addContact() {
208
+ await this.#setPermissionInIos({
209
+ contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts.",
210
+ });
211
+ this.#setPermissionsInAndroid(["READ_CONTACTS", "WRITE_CONTACTS"]);
212
+ }
213
+ async addLocation() {
214
+ await this.#setPermissionInIos({
215
+ locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
216
+ locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
217
+ });
218
+ this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
219
+ this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
220
+ }
221
+ async #setPermissionInIos(permissions: { [key: string]: string }) {
222
+ const updateNs = Object.fromEntries(
223
+ Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value]),
224
+ );
225
+ await Promise.all([
226
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
227
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
228
+ ]);
229
+ }
230
+ #setFeaturesInAndroid(features: string[]) {
231
+ for (const feature of features) {
232
+ if (this.#hasFeatureInAndroid(feature)) {
233
+ this.app.logger.info(`${feature} already exists in android`);
234
+ return this;
235
+ }
236
+ this.app.logger.info(`Adding ${feature} to android`);
237
+ this.project.android
238
+ .getAndroidManifest()
239
+ .injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
240
+ }
241
+ return this;
242
+ }
243
+ #getFeaturesInAndroid() {
244
+ const androidManifest = this.project.android.getAndroidManifest();
245
+ const element = androidManifest.getDocumentElement();
246
+ if (!element) throw new Error("manifest not found");
247
+ const usesFeature = element.getElementsByTagName("uses-feature");
248
+ return Array.from(usesFeature).map((feature) => feature.getAttribute("android:name"));
249
+ }
250
+ #hasFeatureInAndroid(feature: string) {
251
+ return this.#getFeaturesInAndroid().includes(feature);
252
+ }
253
+
254
+ #setPermissionsInAndroid(permissions: string[]) {
255
+ for (const permission of permissions) {
256
+ if (this.#hasPermissionInAndroid(permission)) {
257
+ this.app.logger.info(`${permission} already exists in android`);
258
+ return this;
259
+ }
260
+ this.app.logger.info(`Adding ${permission} to android`);
261
+ this.project.android
262
+ .getAndroidManifest()
263
+ .injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
264
+ }
265
+ return this;
266
+ }
267
+ #getPermissionsInAndroid() {
268
+ const androidManifest = this.project.android.getAndroidManifest();
269
+ const element = androidManifest.getDocumentElement();
270
+ if (!element) throw new Error("manifest not found");
271
+ const usesPermission = element.getElementsByTagName("uses-permission");
272
+ return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
273
+ }
274
+ #hasPermissionInAndroid(permission: string) {
275
+ return this.#getPermissionsInAndroid().includes(permission);
276
+ }
277
+ }
@@ -421,6 +421,10 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
421
421
 
422
422
  const tail = ";";
423
423
 
424
+ if (shouldPreserveBarrelSideEffects(stmt.specifier)) {
425
+ lines.push(`import "${stmt.specifier}"${tail}`);
426
+ }
427
+
424
428
  if (clause.defaultImport || remaining.length > 0) {
425
429
  const parts: string[] = [];
426
430
  if (clause.defaultImport) parts.push(clause.defaultImport);
@@ -437,6 +441,8 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
437
441
  return lines.join("\n");
438
442
  };
439
443
 
444
+ const shouldPreserveBarrelSideEffects = (specifier: string): boolean => /^@(apps|libs)\/[^/]+\/client$/.test(specifier);
445
+
440
446
  const serializeNamedItem = (item: NamedImportItem): string => {
441
447
  const prefix = item.isType ? "type " : "";
442
448
  if (item.imported === item.local) return `${prefix}${item.imported}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.0.0-beta.7",
3
+ "version": "2.0.0-beta.9",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "bin": {
@@ -196,6 +196,7 @@
196
196
  "uuid": "^13.0.2"
197
197
  },
198
198
  "devDependencies": {
199
+ "@biomejs/biome": "2.4.4",
199
200
  "@capacitor/cli": "^8.3.4"
200
201
  },
201
202
  "bun": {