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.
- package/cli/application/application.command.ts +11 -3
- package/cli/index.js +117 -24
- package/cli/package/package.runner.ts +7 -3
- package/cli/templates/app/page/_layout.tsx +0 -1
- package/cli/templates/workspaceRoot/.gitignore.template +1 -11
- package/client/csrTypes.ts +1 -1
- package/devkit/capacitor.base.config.ts +1 -1
- package/devkit/capacitorApp.ts +5 -1
- package/devkit/commandDecorators/argMeta.ts +28 -14
- package/devkit/commandDecorators/command.ts +41 -15
- package/devkit/commandDecorators/commandBuilder.ts +78 -42
- package/devkit/commandDecorators/helpFormatter.ts +7 -4
- package/devkit/frontendBuild/cssCompiler.ts +9 -3
- package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +2 -1
- package/devkit/mobile/mobileTarget.ts +48 -8
- package/devkit/src/capacitorApp.ts +277 -0
- package/devkit/transforms/barrelImportsPlugin.ts +6 -0
- package/package.json +2 -1
- package/server/hmr/clientScript.ts +8 -5
- package/ui/Portal.tsx +2 -0
- package/ui/System/CSR.tsx +6 -5
- package/ui/System/SSR.tsx +2 -2
- package/webkit/bootCsr.tsx +8 -5
- package/cli/templates/app/common/commonLogic.ts +0 -12
- package/cli/templates/app/common/index.ts +0 -10
- package/cli/templates/app/srvkit/backendLogic.ts +0 -12
- package/cli/templates/app/srvkit/index.ts +0 -10
- package/cli/templates/app/ui/UiComponent.ts +0 -16
- package/cli/templates/app/ui/index.ts +0 -10
- package/cli/templates/app/webkit/frontendLogic.ts +0 -12
- package/cli/templates/app/webkit/index.ts +0 -10
- package/cli/templates/module/index.tsx +0 -44
- /package/cli/templates/app/public/{favicon.ico → favicon.ico.template} +0 -0
- /package/cli/templates/app/public/{logo.png → logo.png.template} +0 -0
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
13
|
-
type
|
|
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
|
|
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
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("/");
|
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 (
|
|
34
|
-
|
|
35
|
-
return [
|
|
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.
|
|
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": {
|