@akanjs/cli 2.3.12 → 2.3.13-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -801,15 +801,17 @@ class AkanAppConfig {
801
801
  secrets;
802
802
  baseDevEnv;
803
803
  libs;
804
+ plugins;
804
805
  domains = new Set;
805
806
  subRoutes = new Map;
806
807
  basePaths = new Set;
807
808
  branches = new Set(["debug", "develop", "main"]);
808
- constructor(app, libs, rootPackageJson, config, baseDevEnv) {
809
+ constructor(app, libs, rootPackageJson, config, baseDevEnv, plugins = []) {
809
810
  this.app = app;
810
811
  this.rootPackageJson = rootPackageJson;
811
812
  this.libs = libs;
812
813
  this.baseDevEnv = baseDevEnv;
814
+ this.plugins = plugins;
813
815
  this.#applyRoutes(config?.routes);
814
816
  this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
815
817
  this.externalLibs = config?.externalLibs ?? [];
@@ -991,16 +993,16 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
991
993
  app.workspace.getLibs(),
992
994
  app.workspace.getPackageJson()
993
995
  ]);
994
- const config = typeof configImp === "function" ? configImp(app) : configImp;
995
- return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv);
996
+ const resolved = typeof configImp === "function" ? configImp(app) : configImp;
997
+ const { plugins, ...config } = resolved ?? {};
998
+ return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? []);
996
999
  }
997
1000
  #resolveProductionDependencyVersion(lib) {
998
1001
  const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
999
1002
  if (rootVersion)
1000
1003
  return rootVersion;
1001
1004
  const akanPackageJson = getAkanPackageJson();
1002
- if (AKAN_RUNTIME_PACKAGES.has(lib))
1003
- return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
1005
+ return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
1004
1006
  }
1005
1007
  #getProductionRuntimePackages() {
1006
1008
  return [
@@ -1094,14 +1096,17 @@ function mergeImageConfig(config = {}) {
1094
1096
  class AkanLibConfig {
1095
1097
  lib;
1096
1098
  externalLibs;
1097
- constructor(lib, config) {
1099
+ plugins;
1100
+ constructor(lib, config, plugins = []) {
1098
1101
  this.lib = lib;
1099
1102
  this.externalLibs = config?.externalLibs ?? [];
1103
+ this.plugins = plugins;
1100
1104
  }
1101
1105
  static async from(lib) {
1102
1106
  const [configImp] = await Promise.all([import(`${lib.cwdPath}/akan.config.ts`).then((mod) => mod.default)]);
1103
- const config = typeof configImp === "function" ? configImp(lib) : configImp;
1104
- return new AkanLibConfig(lib, config);
1107
+ const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
1108
+ const { plugins, ...config } = resolved ?? {};
1109
+ return new AkanLibConfig(lib, config, plugins ?? []);
1105
1110
  }
1106
1111
  }
1107
1112
  //! need to refactor
@@ -1119,63 +1124,6 @@ var decreaseBuildNum = async (app) => {
1119
1124
  const akanConfigContent = akanConfig.replace(`buildNum: ${appConfig.mobile.buildNum}`, `buildNum: ${appConfig.mobile.buildNum - 1}`);
1120
1125
  fs.writeFileSync(akanConfigPath, akanConfigContent);
1121
1126
  };
1122
- // pkgs/@akanjs/devkit/firebaseMessagingSw.ts
1123
- var FIREBASE_WEB_SDK_VERSION = "12.13.0";
1124
- function normalizeFirebaseClientConfig(config) {
1125
- if (!config || typeof config !== "object")
1126
- return null;
1127
- const value = config;
1128
- if (typeof value.apiKey !== "string" || typeof value.projectId !== "string" || typeof value.messagingSenderId !== "string" || typeof value.appId !== "string") {
1129
- return null;
1130
- }
1131
- return {
1132
- apiKey: value.apiKey,
1133
- ...typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {},
1134
- projectId: value.projectId,
1135
- ...typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {},
1136
- messagingSenderId: value.messagingSenderId,
1137
- appId: value.appId
1138
- };
1139
- }
1140
- function createFirebaseMessagingServiceWorker(config) {
1141
- const configJson = JSON.stringify(config);
1142
- return `/* Generated by Akan.js. Do not edit. */
1143
- const firebaseConfig = ${configJson};
1144
-
1145
- if (firebaseConfig) {
1146
- importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
1147
- importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
1148
-
1149
- firebase.initializeApp(firebaseConfig);
1150
- const messaging = firebase.messaging();
1151
-
1152
- const notificationUrl = (payload) =>
1153
- payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
1154
-
1155
- messaging.onBackgroundMessage((payload) => {
1156
- const title = payload?.notification?.title || "";
1157
- const options = {
1158
- body: payload?.notification?.body,
1159
- icon: payload?.notification?.icon,
1160
- image: payload?.notification?.image,
1161
- data: {
1162
- url: notificationUrl(payload),
1163
- FCM_MSG: payload,
1164
- },
1165
- };
1166
- self.registration.showNotification(title, options);
1167
- });
1168
- }
1169
-
1170
- self.addEventListener("notificationclick", (event) => {
1171
- const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
1172
- event.notification?.close();
1173
- if (!url) return;
1174
- event.waitUntil(clients.openWindow(url));
1175
- });
1176
- `;
1177
- }
1178
-
1179
1127
  // pkgs/@akanjs/devkit/getDirname.ts
1180
1128
  import path2 from "path";
1181
1129
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -3413,7 +3361,7 @@ class WorkspaceExecutor extends Executor {
3413
3361
  return viewExampleFiles;
3414
3362
  }
3415
3363
  }
3416
- var scanFacetDirs = ["ui", "webkit", "srvkit", "common"];
3364
+ var scanFacetDirs = ["ui", "webkit", "srvkit", "common", "plugin"];
3417
3365
 
3418
3366
  class SysExecutor extends Executor {
3419
3367
  workspace;
@@ -3796,28 +3744,69 @@ class AppExecutor extends SysExecutor {
3796
3744
  if (write)
3797
3745
  await this.syncAssets(scanInfo.getScanResult().libDeps);
3798
3746
  if (write)
3799
- await this.#syncFirebaseMessagingSw();
3747
+ await this.#runPluginSyncAssets();
3800
3748
  return scanInfo;
3801
3749
  }
3802
- async#syncFirebaseMessagingSw() {
3803
- const swRelPath = "public/firebase-messaging-sw.js";
3804
- if (await FileSys.fileExists(this.getPath(swRelPath)))
3805
- return;
3806
- const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
3807
- if (!await FileSys.fileExists(envClientPath))
3808
- return;
3809
- let firebaseConfig = null;
3810
- try {
3811
- const envUrl = pathToFileURL(envClientPath);
3812
- envUrl.searchParams.set("t", String(Date.now()));
3813
- const envModule = await import(envUrl.href);
3814
- firebaseConfig = normalizeFirebaseClientConfig(envModule.env?.firebase);
3815
- } catch {
3750
+ async#runPluginSyncAssets() {
3751
+ const plugins = await this.collectPlugins();
3752
+ if (!plugins.some((plugin) => plugin.syncAssets))
3816
3753
  return;
3754
+ const ctx = this.#makeSyncContext();
3755
+ for (const plugin of plugins)
3756
+ await plugin.syncAssets?.(ctx);
3757
+ }
3758
+ #makeSyncContext() {
3759
+ return {
3760
+ appName: this.name,
3761
+ appPath: this.cwdPath,
3762
+ executor: this,
3763
+ getPath: (rel) => this.getPath(rel),
3764
+ fileExists: (rel) => FileSys.fileExists(this.getPath(rel)),
3765
+ writeFile: async (rel, content, opts) => {
3766
+ await this.writeFile(rel, content, opts);
3767
+ },
3768
+ readEnvClient: async () => {
3769
+ const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
3770
+ if (!await FileSys.fileExists(envClientPath))
3771
+ return null;
3772
+ try {
3773
+ const envUrl = pathToFileURL(envClientPath);
3774
+ envUrl.searchParams.set("t", String(Date.now()));
3775
+ const envModule = await import(envUrl.href);
3776
+ return envModule.env ?? null;
3777
+ } catch {
3778
+ return null;
3779
+ }
3780
+ }
3781
+ };
3782
+ }
3783
+ async collectPlugins() {
3784
+ const scanInfo = await this.scan({ write: false });
3785
+ const libDeps = scanInfo.getLibs();
3786
+ const appConfig = await this.getConfig();
3787
+ const collected = [...appConfig.plugins];
3788
+ for (const libName of libDeps) {
3789
+ const libConfig = await LibExecutor.from(this, libName).getConfig().catch(() => null);
3790
+ if (libConfig)
3791
+ collected.push(...libConfig.plugins);
3817
3792
  }
3818
- if (!firebaseConfig)
3819
- return;
3820
- await this.writeFile(swRelPath, createFirebaseMessagingServiceWorker(firebaseConfig), { overwrite: false });
3793
+ const seen = new Set;
3794
+ return collected.filter((plugin) => {
3795
+ if (seen.has(plugin.name))
3796
+ return false;
3797
+ seen.add(plugin.name);
3798
+ return true;
3799
+ });
3800
+ }
3801
+ async getPluginRuntimePackages() {
3802
+ const plugins = await this.collectPlugins();
3803
+ const appConfig = await this.getConfig();
3804
+ const ctx = {
3805
+ appName: this.name,
3806
+ mobile: appConfig.mobile,
3807
+ hasMobilePermission: (permission) => Object.values(appConfig.mobile.targets).some((target) => target.permissions?.includes(permission) ?? false)
3808
+ };
3809
+ return [...new Set(plugins.flatMap((plugin) => plugin.runtimePackages?.(ctx) ?? []))];
3821
3810
  }
3822
3811
  async increaseBuildNum() {
3823
3812
  await increaseBuildNum(this);
@@ -11352,7 +11341,7 @@ function getPageKeyBasePath(pageKey, basePaths) {
11352
11341
  import path27 from "path";
11353
11342
  var SOURCE_EXTS4 = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
11354
11343
  var CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
11355
- var BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit"]);
11344
+ var BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
11356
11345
  var CLIENT_SUFFIXES = [".Template.tsx", ".Unit.tsx", ".Util.tsx", ".View.tsx", ".Zone.tsx", ".store.ts"];
11357
11346
  var SHARED_SUFFIXES2 = [".constant.ts", ".dictionary.ts", ".signal.ts"];
11358
11347
  var SERVER_SUFFIXES2 = [".service.ts", ".document.ts"];
@@ -11498,7 +11487,7 @@ var uniqueResolved = (files) => [...new Set(files.map((file) => path27.resolve(f
11498
11487
  // pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
11499
11488
  import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm3, stat as stat3, writeFile } from "fs/promises";
11500
11489
  import path28 from "path";
11501
- var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
11490
+ var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
11502
11491
  var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
11503
11492
  var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
11504
11493
  var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
@@ -13553,7 +13542,6 @@ var getLocalIP = () => {
13553
13542
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13554
13543
  var asString = (value) => typeof value === "string" ? value : undefined;
13555
13544
  var firstString = (...values) => values.find((value) => typeof value === "string");
13556
- var resolveIosApnsEnvironment = ({ operation }) => operation === "release" ? "production" : "development";
13557
13545
  var scoreIosDeviceTarget = (target) => {
13558
13546
  const state = target.state?.toLowerCase() ?? "";
13559
13547
  return (state.includes("available") ? 4 : 0) + (state.includes("wired") ? 2 : 0) + (state.includes("paired") ? 1 : 0);
@@ -13925,6 +13913,7 @@ class CapacitorApp {
13925
13913
  iosProjectPath = "ios/App";
13926
13914
  androidRootPath = "android";
13927
13915
  androidAssetsPath = "android/app/src/main/assets";
13916
+ #iosEntitlements = {};
13928
13917
  constructor(app, target) {
13929
13918
  this.app = app;
13930
13919
  this.target = target;
@@ -13972,6 +13961,7 @@ class CapacitorApp {
13972
13961
  await this.#applyIosMetadata();
13973
13962
  await this.#applyPermissions({ operation, env });
13974
13963
  await this.#applyDeepLinks("ios", { operation, env });
13964
+ await this.#flushIosEntitlements();
13975
13965
  await this.project.commit();
13976
13966
  await this.#generateAssets({ operation, env });
13977
13967
  this.app.verbose(`syncing iOS`);
@@ -14329,17 +14319,51 @@ ${error.message}`;
14329
14319
  await this.project.android.setAppName(this.target.appName);
14330
14320
  }
14331
14321
  async#applyPermissions({ operation, env }) {
14322
+ const plugins = await this.app.collectPlugins();
14323
+ const nativePlugins = new Map;
14324
+ for (const plugin of plugins) {
14325
+ const permission = plugin.capacitor?.permission;
14326
+ if (!permission || !plugin.capacitor?.configureNative)
14327
+ continue;
14328
+ const claimants = nativePlugins.get(permission) ?? [];
14329
+ claimants.push(plugin);
14330
+ nativePlugins.set(permission, claimants);
14331
+ }
14332
14332
  for (const permission of this.target.permissions ?? []) {
14333
- if (permission === "camera")
14334
- await this.addCamera();
14335
- else if (permission === "contacts")
14336
- await this.addContact();
14337
- else if (permission === "location")
14338
- await this.addLocation();
14339
- else if (permission === "push")
14340
- await this.addPush({ operation, env });
14333
+ const claimants = nativePlugins.get(permission);
14334
+ if (!claimants?.length) {
14335
+ this.app.logger.warn(`Mobile target '${this.target.name}' declares permission '${permission}' but no plugin configures it. ` + `Depend on a lib (e.g. @libs/util) whose akan.config declares a plugin with capacitor.permission '${permission}'.`);
14336
+ continue;
14337
+ }
14338
+ const ctx = this.#makeNativeContext({ operation, env });
14339
+ for (const plugin of claimants)
14340
+ await plugin.capacitor?.configureNative?.(ctx);
14341
14341
  }
14342
14342
  }
14343
+ #makeNativeContext({ operation, env }) {
14344
+ return {
14345
+ appPath: this.app.cwdPath,
14346
+ executor: this.app,
14347
+ target: this.target,
14348
+ operation,
14349
+ env,
14350
+ setIosUsageDescriptions: (descriptions) => this.#setPermissionInIos(descriptions),
14351
+ updateIosInfoPlist: async (values) => {
14352
+ await Promise.all([
14353
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", values),
14354
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", values)
14355
+ ]);
14356
+ },
14357
+ addIosEntitlements: (entitlements) => this.#addIosEntitlements(entitlements),
14358
+ editIosAppDelegate: (transform) => this.#editIosAppDelegate(transform),
14359
+ addAndroidPermissions: (permissions) => {
14360
+ this.#setPermissionsInAndroid(permissions);
14361
+ },
14362
+ addAndroidFeatures: (features) => {
14363
+ this.#setFeaturesInAndroid(features);
14364
+ }
14365
+ };
14366
+ }
14343
14367
  async#applyDeepLinks(platform, { operation, env }) {
14344
14368
  const deepLinks = this.target.deepLinks;
14345
14369
  if (!deepLinks)
@@ -14356,7 +14380,9 @@ ${error.message}`;
14356
14380
  await this.#setUrlSchemesInIos(schemes);
14357
14381
  }
14358
14382
  if (domains.length > 0)
14359
- await this.#setAssociatedDomainsInIos(domains, { operation, env });
14383
+ this.#addIosEntitlements({
14384
+ "com.apple.developer.associated-domains": domains.map((domain) => `applinks:${domain}`)
14385
+ });
14360
14386
  return;
14361
14387
  }
14362
14388
  if (platform === "android") {
@@ -14428,85 +14454,18 @@ ${error.message}`;
14428
14454
  await this.#clearRootCapacitorConfigs();
14429
14455
  }
14430
14456
  }
14431
- async addCamera() {
14432
- await this.#setPermissionInIos({
14433
- cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
14434
- photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
14435
- photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos."
14436
- });
14437
- this.#setPermissionsInAndroid(["READ_MEDIA_IMAGES", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]);
14438
- }
14439
- async addContact() {
14440
- await this.#setPermissionInIos({
14441
- contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts."
14442
- });
14443
- this.#setPermissionsInAndroid(["READ_CONTACTS", "WRITE_CONTACTS"]);
14444
- }
14445
- async addLocation() {
14446
- await this.#setPermissionInIos({
14447
- locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
14448
- locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location."
14449
- });
14450
- this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
14451
- this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
14452
- }
14453
- async addPush({ operation, env }) {
14454
- await this.#setPermissionInIos({
14455
- userNotificationsUsageDescription: "$(PRODUCT_NAME) uses notifications to keep you updated."
14456
- });
14457
- await Promise.all([
14458
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { UIBackgroundModes: ["remote-notification"] }),
14459
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { UIBackgroundModes: ["remote-notification"] }),
14460
- this.#writeEntitlementsInIos(this.target.deepLinks?.domains ?? [], { operation, env }),
14461
- this.#ensurePushAppDelegateInIos()
14462
- ]);
14463
- this.#setPermissionsInAndroid(["POST_NOTIFICATIONS"]);
14457
+ #addIosEntitlements(entitlements) {
14458
+ Object.assign(this.#iosEntitlements, entitlements);
14464
14459
  }
14465
- async#ensurePushAppDelegateInIos() {
14460
+ async#editIosAppDelegate(transform) {
14466
14461
  const appDelegatePath = path41.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
14467
14462
  if (!await Bun.file(appDelegatePath).exists())
14468
14463
  return;
14469
14464
  const editor = await FileEditor.create(appDelegatePath);
14470
- let content = editor.getContent();
14471
- if (!content.includes("import FirebaseCore")) {
14472
- content = content.replace("import Capacitor", `import Capacitor
14473
- import FirebaseCore`);
14474
- }
14475
- if (!content.includes("import FirebaseMessaging")) {
14476
- content = content.replace("import FirebaseCore", `import FirebaseCore
14477
- import FirebaseMessaging`);
14478
- }
14479
- if (!content.includes("FirebaseApp.configure()")) {
14480
- content = content.replace(/\n(\s*)return true/, `
14481
- $1FirebaseApp.configure()
14482
-
14483
- $1return true`);
14484
- }
14485
- if (!content.includes("didRegisterForRemoteNotificationsWithDeviceToken")) {
14486
- const delegateMethods = `
14487
- func application(_ application: UIApplication,
14488
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
14489
- Messaging.messaging().apnsToken = deviceToken
14490
- NotificationCenter.default.post(
14491
- name: .capacitorDidRegisterForRemoteNotifications,
14492
- object: deviceToken
14493
- )
14494
- }
14495
-
14496
- func application(_ application: UIApplication,
14497
- didFailToRegisterForRemoteNotificationsWithError error: Error) {
14498
- NotificationCenter.default.post(
14499
- name: .capacitorDidFailToRegisterForRemoteNotifications,
14500
- object: error
14501
- )
14502
- }
14503
- `;
14504
- content = content.replace(`
14505
- func applicationWillResignActive`, `${delegateMethods}
14506
- func applicationWillResignActive`);
14507
- }
14508
- if (content !== editor.getContent())
14509
- await editor.setContent(content).save();
14465
+ const content = editor.getContent();
14466
+ const next = transform(content);
14467
+ if (next !== content)
14468
+ await editor.setContent(next).save();
14510
14469
  }
14511
14470
  async#setPermissionInIos(permissions) {
14512
14471
  const updateNs = Object.fromEntries(Object.entries(permissions).map(([key, value]) => [`NS${capitalize5(key)}`, value]));
@@ -14525,27 +14484,29 @@ $1return true`);
14525
14484
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
14526
14485
  ]);
14527
14486
  }
14528
- async#setAssociatedDomainsInIos(domains, { operation, env }) {
14529
- await this.#writeEntitlementsInIos(domains, { operation, env });
14487
+ #serializeIosEntitlements(entitlements) {
14488
+ const lines = [];
14489
+ for (const [key, value] of Object.entries(entitlements)) {
14490
+ lines.push(` <key>${key}</key>`);
14491
+ if (Array.isArray(value)) {
14492
+ lines.push(" <array>", ...value.map((item) => ` <string>${item}</string>`), " </array>");
14493
+ } else {
14494
+ lines.push(` <string>${value}</string>`);
14495
+ }
14496
+ }
14497
+ return lines;
14530
14498
  }
14531
- async#writeEntitlementsInIos(domains, runConfig) {
14499
+ async#flushIosEntitlements() {
14500
+ if (Object.keys(this.#iosEntitlements).length === 0)
14501
+ return;
14532
14502
  const entitlementsRelPath = "App/App.entitlements";
14533
14503
  const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
14534
- const values = domains.map((domain) => `applinks:${domain}`);
14535
- const usesPush = this.target.permissions?.includes("push") ?? false;
14536
- const apsEnvironment = resolveIosApnsEnvironment(runConfig);
14537
14504
  const body = [
14538
14505
  '<?xml version="1.0" encoding="UTF-8"?>',
14539
14506
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14540
14507
  '<plist version="1.0">',
14541
14508
  "<dict>",
14542
- ...usesPush ? [" <key>aps-environment</key>", ` <string>${apsEnvironment}</string>`] : [],
14543
- ...values.length > 0 ? [
14544
- " <key>com.apple.developer.associated-domains</key>",
14545
- " <array>",
14546
- ...values.map((value) => ` <string>${value}</string>`),
14547
- " </array>"
14548
- ] : [],
14509
+ ...this.#serializeIosEntitlements(this.#iosEntitlements),
14549
14510
  "</dict>",
14550
14511
  "</plist>",
14551
14512
  ""
@@ -15733,7 +15694,7 @@ var SUGGESTED_RULES = [
15733
15694
  "Dictionary text should not contain scaffold wording, misspellings, or stale copied domain nouns.",
15734
15695
  "Warn earlier on very long Akan files: 500 lines for services, 800 lines for Template/Zone files, and 1000 lines for Util files.",
15735
15696
  "Global declarations, Window augmentation, and prototype mutation should stay in explicitly approved low-level integration files.",
15736
- "Keep app root folders small and conventional: application code belongs under common, env, lib, page, private, public, script, srvkit, ui, or webkit.",
15697
+ "Keep app root folders small and conventional: application code belongs under common, env, lib, page, plugin, private, public, script, srvkit, ui, or webkit.",
15737
15698
  "Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
15738
15699
  "Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
15739
15700
  "Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
package/index.js CHANGED
@@ -799,15 +799,17 @@ class AkanAppConfig {
799
799
  secrets;
800
800
  baseDevEnv;
801
801
  libs;
802
+ plugins;
802
803
  domains = new Set;
803
804
  subRoutes = new Map;
804
805
  basePaths = new Set;
805
806
  branches = new Set(["debug", "develop", "main"]);
806
- constructor(app, libs, rootPackageJson, config, baseDevEnv) {
807
+ constructor(app, libs, rootPackageJson, config, baseDevEnv, plugins = []) {
807
808
  this.app = app;
808
809
  this.rootPackageJson = rootPackageJson;
809
810
  this.libs = libs;
810
811
  this.baseDevEnv = baseDevEnv;
812
+ this.plugins = plugins;
811
813
  this.#applyRoutes(config?.routes);
812
814
  this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
813
815
  this.externalLibs = config?.externalLibs ?? [];
@@ -989,16 +991,16 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
989
991
  app.workspace.getLibs(),
990
992
  app.workspace.getPackageJson()
991
993
  ]);
992
- const config = typeof configImp === "function" ? configImp(app) : configImp;
993
- return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv);
994
+ const resolved = typeof configImp === "function" ? configImp(app) : configImp;
995
+ const { plugins, ...config } = resolved ?? {};
996
+ return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? []);
994
997
  }
995
998
  #resolveProductionDependencyVersion(lib) {
996
999
  const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
997
1000
  if (rootVersion)
998
1001
  return rootVersion;
999
1002
  const akanPackageJson = getAkanPackageJson();
1000
- if (AKAN_RUNTIME_PACKAGES.has(lib))
1001
- return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
1003
+ return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
1002
1004
  }
1003
1005
  #getProductionRuntimePackages() {
1004
1006
  return [
@@ -1092,14 +1094,17 @@ function mergeImageConfig(config = {}) {
1092
1094
  class AkanLibConfig {
1093
1095
  lib;
1094
1096
  externalLibs;
1095
- constructor(lib, config) {
1097
+ plugins;
1098
+ constructor(lib, config, plugins = []) {
1096
1099
  this.lib = lib;
1097
1100
  this.externalLibs = config?.externalLibs ?? [];
1101
+ this.plugins = plugins;
1098
1102
  }
1099
1103
  static async from(lib) {
1100
1104
  const [configImp] = await Promise.all([import(`${lib.cwdPath}/akan.config.ts`).then((mod) => mod.default)]);
1101
- const config = typeof configImp === "function" ? configImp(lib) : configImp;
1102
- return new AkanLibConfig(lib, config);
1105
+ const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
1106
+ const { plugins, ...config } = resolved ?? {};
1107
+ return new AkanLibConfig(lib, config, plugins ?? []);
1103
1108
  }
1104
1109
  }
1105
1110
  //! need to refactor
@@ -1117,63 +1122,6 @@ var decreaseBuildNum = async (app) => {
1117
1122
  const akanConfigContent = akanConfig.replace(`buildNum: ${appConfig.mobile.buildNum}`, `buildNum: ${appConfig.mobile.buildNum - 1}`);
1118
1123
  fs.writeFileSync(akanConfigPath, akanConfigContent);
1119
1124
  };
1120
- // pkgs/@akanjs/devkit/firebaseMessagingSw.ts
1121
- var FIREBASE_WEB_SDK_VERSION = "12.13.0";
1122
- function normalizeFirebaseClientConfig(config) {
1123
- if (!config || typeof config !== "object")
1124
- return null;
1125
- const value = config;
1126
- if (typeof value.apiKey !== "string" || typeof value.projectId !== "string" || typeof value.messagingSenderId !== "string" || typeof value.appId !== "string") {
1127
- return null;
1128
- }
1129
- return {
1130
- apiKey: value.apiKey,
1131
- ...typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {},
1132
- projectId: value.projectId,
1133
- ...typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {},
1134
- messagingSenderId: value.messagingSenderId,
1135
- appId: value.appId
1136
- };
1137
- }
1138
- function createFirebaseMessagingServiceWorker(config) {
1139
- const configJson = JSON.stringify(config);
1140
- return `/* Generated by Akan.js. Do not edit. */
1141
- const firebaseConfig = ${configJson};
1142
-
1143
- if (firebaseConfig) {
1144
- importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
1145
- importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
1146
-
1147
- firebase.initializeApp(firebaseConfig);
1148
- const messaging = firebase.messaging();
1149
-
1150
- const notificationUrl = (payload) =>
1151
- payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
1152
-
1153
- messaging.onBackgroundMessage((payload) => {
1154
- const title = payload?.notification?.title || "";
1155
- const options = {
1156
- body: payload?.notification?.body,
1157
- icon: payload?.notification?.icon,
1158
- image: payload?.notification?.image,
1159
- data: {
1160
- url: notificationUrl(payload),
1161
- FCM_MSG: payload,
1162
- },
1163
- };
1164
- self.registration.showNotification(title, options);
1165
- });
1166
- }
1167
-
1168
- self.addEventListener("notificationclick", (event) => {
1169
- const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
1170
- event.notification?.close();
1171
- if (!url) return;
1172
- event.waitUntil(clients.openWindow(url));
1173
- });
1174
- `;
1175
- }
1176
-
1177
1125
  // pkgs/@akanjs/devkit/getDirname.ts
1178
1126
  import path2 from "path";
1179
1127
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -3411,7 +3359,7 @@ class WorkspaceExecutor extends Executor {
3411
3359
  return viewExampleFiles;
3412
3360
  }
3413
3361
  }
3414
- var scanFacetDirs = ["ui", "webkit", "srvkit", "common"];
3362
+ var scanFacetDirs = ["ui", "webkit", "srvkit", "common", "plugin"];
3415
3363
 
3416
3364
  class SysExecutor extends Executor {
3417
3365
  workspace;
@@ -3794,28 +3742,69 @@ class AppExecutor extends SysExecutor {
3794
3742
  if (write)
3795
3743
  await this.syncAssets(scanInfo.getScanResult().libDeps);
3796
3744
  if (write)
3797
- await this.#syncFirebaseMessagingSw();
3745
+ await this.#runPluginSyncAssets();
3798
3746
  return scanInfo;
3799
3747
  }
3800
- async#syncFirebaseMessagingSw() {
3801
- const swRelPath = "public/firebase-messaging-sw.js";
3802
- if (await FileSys.fileExists(this.getPath(swRelPath)))
3803
- return;
3804
- const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
3805
- if (!await FileSys.fileExists(envClientPath))
3806
- return;
3807
- let firebaseConfig = null;
3808
- try {
3809
- const envUrl = pathToFileURL(envClientPath);
3810
- envUrl.searchParams.set("t", String(Date.now()));
3811
- const envModule = await import(envUrl.href);
3812
- firebaseConfig = normalizeFirebaseClientConfig(envModule.env?.firebase);
3813
- } catch {
3748
+ async#runPluginSyncAssets() {
3749
+ const plugins = await this.collectPlugins();
3750
+ if (!plugins.some((plugin) => plugin.syncAssets))
3814
3751
  return;
3752
+ const ctx = this.#makeSyncContext();
3753
+ for (const plugin of plugins)
3754
+ await plugin.syncAssets?.(ctx);
3755
+ }
3756
+ #makeSyncContext() {
3757
+ return {
3758
+ appName: this.name,
3759
+ appPath: this.cwdPath,
3760
+ executor: this,
3761
+ getPath: (rel) => this.getPath(rel),
3762
+ fileExists: (rel) => FileSys.fileExists(this.getPath(rel)),
3763
+ writeFile: async (rel, content, opts) => {
3764
+ await this.writeFile(rel, content, opts);
3765
+ },
3766
+ readEnvClient: async () => {
3767
+ const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
3768
+ if (!await FileSys.fileExists(envClientPath))
3769
+ return null;
3770
+ try {
3771
+ const envUrl = pathToFileURL(envClientPath);
3772
+ envUrl.searchParams.set("t", String(Date.now()));
3773
+ const envModule = await import(envUrl.href);
3774
+ return envModule.env ?? null;
3775
+ } catch {
3776
+ return null;
3777
+ }
3778
+ }
3779
+ };
3780
+ }
3781
+ async collectPlugins() {
3782
+ const scanInfo = await this.scan({ write: false });
3783
+ const libDeps = scanInfo.getLibs();
3784
+ const appConfig = await this.getConfig();
3785
+ const collected = [...appConfig.plugins];
3786
+ for (const libName of libDeps) {
3787
+ const libConfig = await LibExecutor.from(this, libName).getConfig().catch(() => null);
3788
+ if (libConfig)
3789
+ collected.push(...libConfig.plugins);
3815
3790
  }
3816
- if (!firebaseConfig)
3817
- return;
3818
- await this.writeFile(swRelPath, createFirebaseMessagingServiceWorker(firebaseConfig), { overwrite: false });
3791
+ const seen = new Set;
3792
+ return collected.filter((plugin) => {
3793
+ if (seen.has(plugin.name))
3794
+ return false;
3795
+ seen.add(plugin.name);
3796
+ return true;
3797
+ });
3798
+ }
3799
+ async getPluginRuntimePackages() {
3800
+ const plugins = await this.collectPlugins();
3801
+ const appConfig = await this.getConfig();
3802
+ const ctx = {
3803
+ appName: this.name,
3804
+ mobile: appConfig.mobile,
3805
+ hasMobilePermission: (permission) => Object.values(appConfig.mobile.targets).some((target) => target.permissions?.includes(permission) ?? false)
3806
+ };
3807
+ return [...new Set(plugins.flatMap((plugin) => plugin.runtimePackages?.(ctx) ?? []))];
3819
3808
  }
3820
3809
  async increaseBuildNum() {
3821
3810
  await increaseBuildNum(this);
@@ -11350,7 +11339,7 @@ function getPageKeyBasePath(pageKey, basePaths) {
11350
11339
  import path27 from "path";
11351
11340
  var SOURCE_EXTS4 = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
11352
11341
  var CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
11353
- var BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit"]);
11342
+ var BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
11354
11343
  var CLIENT_SUFFIXES = [".Template.tsx", ".Unit.tsx", ".Util.tsx", ".View.tsx", ".Zone.tsx", ".store.ts"];
11355
11344
  var SHARED_SUFFIXES2 = [".constant.ts", ".dictionary.ts", ".signal.ts"];
11356
11345
  var SERVER_SUFFIXES2 = [".service.ts", ".document.ts"];
@@ -11496,7 +11485,7 @@ var uniqueResolved = (files) => [...new Set(files.map((file) => path27.resolve(f
11496
11485
  // pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
11497
11486
  import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm3, stat as stat3, writeFile } from "fs/promises";
11498
11487
  import path28 from "path";
11499
- var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
11488
+ var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
11500
11489
  var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
11501
11490
  var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
11502
11491
  var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
@@ -13551,7 +13540,6 @@ var getLocalIP = () => {
13551
13540
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13552
13541
  var asString = (value) => typeof value === "string" ? value : undefined;
13553
13542
  var firstString = (...values) => values.find((value) => typeof value === "string");
13554
- var resolveIosApnsEnvironment = ({ operation }) => operation === "release" ? "production" : "development";
13555
13543
  var scoreIosDeviceTarget = (target) => {
13556
13544
  const state = target.state?.toLowerCase() ?? "";
13557
13545
  return (state.includes("available") ? 4 : 0) + (state.includes("wired") ? 2 : 0) + (state.includes("paired") ? 1 : 0);
@@ -13923,6 +13911,7 @@ class CapacitorApp {
13923
13911
  iosProjectPath = "ios/App";
13924
13912
  androidRootPath = "android";
13925
13913
  androidAssetsPath = "android/app/src/main/assets";
13914
+ #iosEntitlements = {};
13926
13915
  constructor(app, target) {
13927
13916
  this.app = app;
13928
13917
  this.target = target;
@@ -13970,6 +13959,7 @@ class CapacitorApp {
13970
13959
  await this.#applyIosMetadata();
13971
13960
  await this.#applyPermissions({ operation, env });
13972
13961
  await this.#applyDeepLinks("ios", { operation, env });
13962
+ await this.#flushIosEntitlements();
13973
13963
  await this.project.commit();
13974
13964
  await this.#generateAssets({ operation, env });
13975
13965
  this.app.verbose(`syncing iOS`);
@@ -14327,17 +14317,51 @@ ${error.message}`;
14327
14317
  await this.project.android.setAppName(this.target.appName);
14328
14318
  }
14329
14319
  async#applyPermissions({ operation, env }) {
14320
+ const plugins = await this.app.collectPlugins();
14321
+ const nativePlugins = new Map;
14322
+ for (const plugin of plugins) {
14323
+ const permission = plugin.capacitor?.permission;
14324
+ if (!permission || !plugin.capacitor?.configureNative)
14325
+ continue;
14326
+ const claimants = nativePlugins.get(permission) ?? [];
14327
+ claimants.push(plugin);
14328
+ nativePlugins.set(permission, claimants);
14329
+ }
14330
14330
  for (const permission of this.target.permissions ?? []) {
14331
- if (permission === "camera")
14332
- await this.addCamera();
14333
- else if (permission === "contacts")
14334
- await this.addContact();
14335
- else if (permission === "location")
14336
- await this.addLocation();
14337
- else if (permission === "push")
14338
- await this.addPush({ operation, env });
14331
+ const claimants = nativePlugins.get(permission);
14332
+ if (!claimants?.length) {
14333
+ this.app.logger.warn(`Mobile target '${this.target.name}' declares permission '${permission}' but no plugin configures it. ` + `Depend on a lib (e.g. @libs/util) whose akan.config declares a plugin with capacitor.permission '${permission}'.`);
14334
+ continue;
14335
+ }
14336
+ const ctx = this.#makeNativeContext({ operation, env });
14337
+ for (const plugin of claimants)
14338
+ await plugin.capacitor?.configureNative?.(ctx);
14339
14339
  }
14340
14340
  }
14341
+ #makeNativeContext({ operation, env }) {
14342
+ return {
14343
+ appPath: this.app.cwdPath,
14344
+ executor: this.app,
14345
+ target: this.target,
14346
+ operation,
14347
+ env,
14348
+ setIosUsageDescriptions: (descriptions) => this.#setPermissionInIos(descriptions),
14349
+ updateIosInfoPlist: async (values) => {
14350
+ await Promise.all([
14351
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", values),
14352
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", values)
14353
+ ]);
14354
+ },
14355
+ addIosEntitlements: (entitlements) => this.#addIosEntitlements(entitlements),
14356
+ editIosAppDelegate: (transform) => this.#editIosAppDelegate(transform),
14357
+ addAndroidPermissions: (permissions) => {
14358
+ this.#setPermissionsInAndroid(permissions);
14359
+ },
14360
+ addAndroidFeatures: (features) => {
14361
+ this.#setFeaturesInAndroid(features);
14362
+ }
14363
+ };
14364
+ }
14341
14365
  async#applyDeepLinks(platform, { operation, env }) {
14342
14366
  const deepLinks = this.target.deepLinks;
14343
14367
  if (!deepLinks)
@@ -14354,7 +14378,9 @@ ${error.message}`;
14354
14378
  await this.#setUrlSchemesInIos(schemes);
14355
14379
  }
14356
14380
  if (domains.length > 0)
14357
- await this.#setAssociatedDomainsInIos(domains, { operation, env });
14381
+ this.#addIosEntitlements({
14382
+ "com.apple.developer.associated-domains": domains.map((domain) => `applinks:${domain}`)
14383
+ });
14358
14384
  return;
14359
14385
  }
14360
14386
  if (platform === "android") {
@@ -14426,85 +14452,18 @@ ${error.message}`;
14426
14452
  await this.#clearRootCapacitorConfigs();
14427
14453
  }
14428
14454
  }
14429
- async addCamera() {
14430
- await this.#setPermissionInIos({
14431
- cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
14432
- photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
14433
- photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos."
14434
- });
14435
- this.#setPermissionsInAndroid(["READ_MEDIA_IMAGES", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]);
14436
- }
14437
- async addContact() {
14438
- await this.#setPermissionInIos({
14439
- contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts."
14440
- });
14441
- this.#setPermissionsInAndroid(["READ_CONTACTS", "WRITE_CONTACTS"]);
14442
- }
14443
- async addLocation() {
14444
- await this.#setPermissionInIos({
14445
- locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
14446
- locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location."
14447
- });
14448
- this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
14449
- this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
14450
- }
14451
- async addPush({ operation, env }) {
14452
- await this.#setPermissionInIos({
14453
- userNotificationsUsageDescription: "$(PRODUCT_NAME) uses notifications to keep you updated."
14454
- });
14455
- await Promise.all([
14456
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { UIBackgroundModes: ["remote-notification"] }),
14457
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { UIBackgroundModes: ["remote-notification"] }),
14458
- this.#writeEntitlementsInIos(this.target.deepLinks?.domains ?? [], { operation, env }),
14459
- this.#ensurePushAppDelegateInIos()
14460
- ]);
14461
- this.#setPermissionsInAndroid(["POST_NOTIFICATIONS"]);
14455
+ #addIosEntitlements(entitlements) {
14456
+ Object.assign(this.#iosEntitlements, entitlements);
14462
14457
  }
14463
- async#ensurePushAppDelegateInIos() {
14458
+ async#editIosAppDelegate(transform) {
14464
14459
  const appDelegatePath = path41.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
14465
14460
  if (!await Bun.file(appDelegatePath).exists())
14466
14461
  return;
14467
14462
  const editor = await FileEditor.create(appDelegatePath);
14468
- let content = editor.getContent();
14469
- if (!content.includes("import FirebaseCore")) {
14470
- content = content.replace("import Capacitor", `import Capacitor
14471
- import FirebaseCore`);
14472
- }
14473
- if (!content.includes("import FirebaseMessaging")) {
14474
- content = content.replace("import FirebaseCore", `import FirebaseCore
14475
- import FirebaseMessaging`);
14476
- }
14477
- if (!content.includes("FirebaseApp.configure()")) {
14478
- content = content.replace(/\n(\s*)return true/, `
14479
- $1FirebaseApp.configure()
14480
-
14481
- $1return true`);
14482
- }
14483
- if (!content.includes("didRegisterForRemoteNotificationsWithDeviceToken")) {
14484
- const delegateMethods = `
14485
- func application(_ application: UIApplication,
14486
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
14487
- Messaging.messaging().apnsToken = deviceToken
14488
- NotificationCenter.default.post(
14489
- name: .capacitorDidRegisterForRemoteNotifications,
14490
- object: deviceToken
14491
- )
14492
- }
14493
-
14494
- func application(_ application: UIApplication,
14495
- didFailToRegisterForRemoteNotificationsWithError error: Error) {
14496
- NotificationCenter.default.post(
14497
- name: .capacitorDidFailToRegisterForRemoteNotifications,
14498
- object: error
14499
- )
14500
- }
14501
- `;
14502
- content = content.replace(`
14503
- func applicationWillResignActive`, `${delegateMethods}
14504
- func applicationWillResignActive`);
14505
- }
14506
- if (content !== editor.getContent())
14507
- await editor.setContent(content).save();
14463
+ const content = editor.getContent();
14464
+ const next = transform(content);
14465
+ if (next !== content)
14466
+ await editor.setContent(next).save();
14508
14467
  }
14509
14468
  async#setPermissionInIos(permissions) {
14510
14469
  const updateNs = Object.fromEntries(Object.entries(permissions).map(([key, value]) => [`NS${capitalize5(key)}`, value]));
@@ -14523,27 +14482,29 @@ $1return true`);
14523
14482
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
14524
14483
  ]);
14525
14484
  }
14526
- async#setAssociatedDomainsInIos(domains, { operation, env }) {
14527
- await this.#writeEntitlementsInIos(domains, { operation, env });
14485
+ #serializeIosEntitlements(entitlements) {
14486
+ const lines = [];
14487
+ for (const [key, value] of Object.entries(entitlements)) {
14488
+ lines.push(` <key>${key}</key>`);
14489
+ if (Array.isArray(value)) {
14490
+ lines.push(" <array>", ...value.map((item) => ` <string>${item}</string>`), " </array>");
14491
+ } else {
14492
+ lines.push(` <string>${value}</string>`);
14493
+ }
14494
+ }
14495
+ return lines;
14528
14496
  }
14529
- async#writeEntitlementsInIos(domains, runConfig) {
14497
+ async#flushIosEntitlements() {
14498
+ if (Object.keys(this.#iosEntitlements).length === 0)
14499
+ return;
14530
14500
  const entitlementsRelPath = "App/App.entitlements";
14531
14501
  const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
14532
- const values = domains.map((domain) => `applinks:${domain}`);
14533
- const usesPush = this.target.permissions?.includes("push") ?? false;
14534
- const apsEnvironment = resolveIosApnsEnvironment(runConfig);
14535
14502
  const body = [
14536
14503
  '<?xml version="1.0" encoding="UTF-8"?>',
14537
14504
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14538
14505
  '<plist version="1.0">',
14539
14506
  "<dict>",
14540
- ...usesPush ? [" <key>aps-environment</key>", ` <string>${apsEnvironment}</string>`] : [],
14541
- ...values.length > 0 ? [
14542
- " <key>com.apple.developer.associated-domains</key>",
14543
- " <array>",
14544
- ...values.map((value) => ` <string>${value}</string>`),
14545
- " </array>"
14546
- ] : [],
14507
+ ...this.#serializeIosEntitlements(this.#iosEntitlements),
14547
14508
  "</dict>",
14548
14509
  "</plist>",
14549
14510
  ""
@@ -15731,7 +15692,7 @@ var SUGGESTED_RULES = [
15731
15692
  "Dictionary text should not contain scaffold wording, misspellings, or stale copied domain nouns.",
15732
15693
  "Warn earlier on very long Akan files: 500 lines for services, 800 lines for Template/Zone files, and 1000 lines for Util files.",
15733
15694
  "Global declarations, Window augmentation, and prototype mutation should stay in explicitly approved low-level integration files.",
15734
- "Keep app root folders small and conventional: application code belongs under common, env, lib, page, private, public, script, srvkit, ui, or webkit.",
15695
+ "Keep app root folders small and conventional: application code belongs under common, env, lib, page, plugin, private, public, script, srvkit, ui, or webkit.",
15735
15696
  "Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
15736
15697
  "Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
15737
15698
  "Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.3.12",
3
+ "version": "2.3.13-rc.0",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "2.3.12",
37
+ "akanjs": "2.3.13-rc.0",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -14,22 +14,30 @@ const sourceFilePattern = /\.(ts|tsx)$/;
14
14
  const excludedFilePattern = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
15
15
  // `ui` exports PascalCase names only; `common`/`srvkit`/`webkit` export camelCase names only. Names with
16
16
  // dots, underscores, or hyphens (e.g. `foo.helper`, `Globe_Dynamic`, `kebab-case`) match neither and are skipped.
17
+ // `plugin` additionally exports `<camelCase>.plugin` files (the plugin-facet file convention, e.g.
18
+ // `pushNotification.plugin.ts`) alongside plain camelCase helpers.
17
19
  const pascalCasePattern = /^[A-Z][A-Za-z0-9]*$/;
18
20
  const camelCasePattern = /^[a-z][A-Za-z0-9]*$/;
19
- const nameCasePatternForFacet = (facet: string) => (facet === "ui" ? pascalCasePattern : camelCasePattern);
21
+ const pluginNamePattern = /^[a-z][A-Za-z0-9]*\.plugin$/;
22
+ const matchesFacetNameConvention = (facet: string, name: string) => {
23
+ if (facet === "ui") return pascalCasePattern.test(name);
24
+ if (facet === "plugin") return camelCasePattern.test(name) || pluginNamePattern.test(name);
25
+ return camelCasePattern.test(name);
26
+ };
20
27
 
21
28
  export default async function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict = {}, options: Options = {}) {
22
29
  const { exec, facet } = options;
23
30
  if (!exec || !facet || !(await exec.exists(facet))) return null;
24
31
 
25
- const nameCasePattern = nameCasePatternForFacet(facet);
26
32
  const { files, dirs } = await exec.getFilesAndDirs(facet);
27
33
  const exportNames = [
28
34
  ...files
29
35
  .filter((filename) => sourceFilePattern.test(filename) && !excludedFilePattern.test(filename))
30
36
  .map((filename) => filename.replace(sourceFilePattern, "")),
31
37
  ...dirs.filter((dirname) => !dirname.startsWith(".")),
32
- ].filter((name) => nameCasePattern.test(name)).sort();
38
+ ]
39
+ .filter((name) => matchesFacetNameConvention(facet, name))
40
+ .sort();
33
41
 
34
42
  if (exportNames.length === 0) return null;
35
43
  return {
@@ -99,6 +99,7 @@ apps/*/ui/index.ts
99
99
  apps/*/webkit/index.ts
100
100
  apps/*/srvkit/index.ts
101
101
  apps/*/common/index.ts
102
+ apps/*/plugin/index.ts
102
103
  apps/*/client.ts
103
104
  apps/*/server.ts
104
105
  libs/*/lib/cnst.ts
@@ -116,6 +117,7 @@ libs/*/ui/index.ts
116
117
  libs/*/webkit/index.ts
117
118
  libs/*/srvkit/index.ts
118
119
  libs/*/common/index.ts
120
+ libs/*/plugin/index.ts
119
121
  libs/*/client.ts
120
122
  libs/*/server.ts
121
123
  libs/*/index.ts