@livx.cc/appwrap 0.42.1 → 0.42.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.42.1",
3
+ "version": "0.42.2",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -8,13 +8,16 @@
8
8
  - FileTimestamp (C617.1) — NativeScript reads bundle/app file metadata at runtime
9
9
  - SystemBootTime (35F9.1) — NativeScript timers/uptime
10
10
  - DiskSpace (E174.1) — NativeScript file I/O
11
- NSPrivacyTracking is false (no IDFA, no cross-app/third-party linkage). The shell surfaces install
12
- context for FIRST-PARTY analytics via kit.context():
13
- - DeviceID (CRC0) — identifierForVendor (IDFV), app-scoped, reset on uninstall; NOT IDFA.
14
- - ProductInteraction (385F.1) — usage/app-environment props (platform, version, install source).
15
- Both are declared NOT linked to identity and NOT used for tracking. If your PWA collects MORE
16
- (account, location, contacts) or DOES track (ads/3rd-party linkage), add the matching
17
- NSPrivacyCollectedDataTypes / set NSPrivacyTracking=true here — see tracking-analytics + store-readiness.
11
+ NSPrivacyTracking is false (no IDFA, no cross-app/third-party linkage) and NSPrivacyCollectedDataTypes
12
+ is EMPTY by default: the stock runtime does NOT collect or transmit any data. kit.context() merely
13
+ RETURNS install context (IDFV, platform, version) to the app on request — it never sends it anywhere —
14
+ so the default binary matches a "Data Not Collected" App Store privacy label.
15
+
16
+ If YOUR app wires analytics that actually transmits data (DeviceID/IDFV, ProductInteraction, account,
17
+ location, …) you MUST declare it here — replace this whole file via your app's overrides dir
18
+ (appwrap.overrides/App_Resources/iOS/PrivacyInfo.xcprivacy). `applyOverrides` copies overrides over
19
+ the generated native/ last, so your file wins. Add the matching NSPrivacyCollectedDataTypes entries
20
+ (and set NSPrivacyTracking=true if you track) — see tracking-analytics + store-readiness.
18
21
  -->
19
22
  <plist version="1.0">
20
23
  <dict>
@@ -23,32 +26,7 @@
23
26
  <key>NSPrivacyTrackingDomains</key>
24
27
  <array/>
25
28
  <key>NSPrivacyCollectedDataTypes</key>
26
- <array>
27
- <dict>
28
- <key>NSPrivacyCollectedDataType</key>
29
- <string>NSPrivacyCollectedDataTypeDeviceID</string>
30
- <key>NSPrivacyCollectedDataTypeLinked</key>
31
- <false/>
32
- <key>NSPrivacyCollectedDataTypeTracking</key>
33
- <false/>
34
- <key>NSPrivacyCollectedDataTypePurposes</key>
35
- <array>
36
- <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
37
- </array>
38
- </dict>
39
- <dict>
40
- <key>NSPrivacyCollectedDataType</key>
41
- <string>NSPrivacyCollectedDataTypeProductInteraction</string>
42
- <key>NSPrivacyCollectedDataTypeLinked</key>
43
- <false/>
44
- <key>NSPrivacyCollectedDataTypeTracking</key>
45
- <false/>
46
- <key>NSPrivacyCollectedDataTypePurposes</key>
47
- <array>
48
- <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
49
- </array>
50
- </dict>
51
- </array>
29
+ <array/>
52
30
  <key>NSPrivacyAccessedAPITypes</key>
53
31
  <array>
54
32
  <dict>
package/src/cli.ts CHANGED
@@ -1275,10 +1275,18 @@ function stampVersionManifest(outDir: string, cfg: AppwrapConfig): void {
1275
1275
 
1276
1276
  /** Pure-native escape hatch: copy the consumer's overrides dir OVER the generated wrapper, last,
1277
1277
  * so it wins. For legacy/custom native code the declarative config can't express. */
1278
- function applyOverrides(cwd: string, outDir: string, cfg: AppwrapConfig): void {
1278
+ export function applyOverrides(cwd: string, outDir: string, cfg: AppwrapConfig): void {
1279
1279
  const dir = resolve(cwd, cfg.overrides ?? 'appwrap.overrides');
1280
- if (!existsSync(dir)) return;
1280
+ // Files the template still provides must survive an override removal (regenerateCore ran first and
1281
+ // re-copied them) → protect them from the override prune.
1282
+ const templateFiles = existsSync(TEMPLATE_DIR) ? collectRelFiles(TEMPLATE_DIR, templateCopyFilter) : new Set<string>();
1283
+ if (!existsSync(dir)) {
1284
+ // No overrides now: prune anything a PRIOR overrides run left behind (renamed/removed override files).
1285
+ pruneStale(outDir, OVERRIDES_MANIFEST, new Set(), templateFiles);
1286
+ return;
1287
+ }
1281
1288
  cpSync(dir, outDir, { recursive: true, force: true });
1289
+ pruneStale(outDir, OVERRIDES_MANIFEST, collectRelFiles(dir), templateFiles);
1282
1290
  console.log(` over ← ${dir} (native overrides applied)`);
1283
1291
  }
1284
1292
 
@@ -1422,6 +1430,54 @@ function copyCiTemplates(cwd: string, outDir: string, cfg: AppwrapConfig, force
1422
1430
  console.log(ci);
1423
1431
  }
1424
1432
 
1433
+ // Manifests of what the two sync-owned sources (runtime template, consumer overrides) generated LAST run.
1434
+ // Used to prune files that were REMOVED/renamed in the source — cpSync(force) overwrites but never deletes,
1435
+ // so without this a relocated source lingers in native/ and gets re-bundled.
1436
+ const TEMPLATE_MANIFEST = '.appwrap-template-manifest.json';
1437
+ const OVERRIDES_MANIFEST = '.appwrap-overrides-manifest.json';
1438
+
1439
+ /** Files under TEMPLATE_DIR to skip when copying/walking it (deps, build output, PWA staging, per-module
1440
+ * native — those are handled selectively elsewhere). Shared by the cpSync filter and the prune walk so
1441
+ * the copied set and the pruned set are defined identically. */
1442
+ const templateCopyFilter = (src: string): boolean =>
1443
+ !/(?:^|\/)(node_modules|platforms|hooks|app\/www|modules-native)(\/|$)/.test(src.slice(TEMPLATE_DIR.length));
1444
+
1445
+ /** File (not dir) paths under `root`, relative to it, skipping entries `accept` rejects. */
1446
+ function collectRelFiles(root: string, accept: (abs: string) => boolean = () => true): Set<string> {
1447
+ const out = new Set<string>();
1448
+ const rec = (dir: string, rel: string): void => {
1449
+ let entries;
1450
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
1451
+ for (const e of entries) {
1452
+ const abs = join(dir, e.name);
1453
+ if (!accept(abs)) continue;
1454
+ const r = rel ? `${rel}/${e.name}` : e.name;
1455
+ if (e.isDirectory()) rec(abs, r);
1456
+ else out.add(r);
1457
+ }
1458
+ };
1459
+ rec(root, '');
1460
+ return out;
1461
+ }
1462
+
1463
+ /** Prune generated files sync OWNS that vanished from their source. Deletes only paths recorded in the
1464
+ * PRIOR manifest that are absent from the `current` source set — so it never touches user data or any
1465
+ * region absent from a previous run's manifest. `protect` keeps paths another owned source still ships
1466
+ * (e.g. an override was removed but the template still provides that file). */
1467
+ function pruneStale(outDir: string, manifest: string, current: Set<string>, protect?: Set<string>): void {
1468
+ const mf = join(outDir, manifest);
1469
+ let prev: string[] = [];
1470
+ try { prev = JSON.parse(readFileSync(mf, 'utf8')) as string[]; } catch { /* first run — nothing to prune */ }
1471
+ for (const rel of prev) {
1472
+ if (current.has(rel) || protect?.has(rel)) continue;
1473
+ const abs = join(outDir, rel);
1474
+ try {
1475
+ if (existsSync(abs)) { rmSync(abs, { force: true }); console.log(` prune ✕ ${rel} (removed from source)`); }
1476
+ } catch { /* non-fatal */ }
1477
+ }
1478
+ try { writeFileSync(mf, JSON.stringify([...current])); } catch { /* non-fatal */ }
1479
+ }
1480
+
1425
1481
  /**
1426
1482
  * Reproduce native/ from source — the shared core of `init` and `sync`. Copies the runtime shell
1427
1483
  * template, re-stamps EVERY config artifact, and re-copies the built PWA. `native/` is disposable, so a
@@ -1441,8 +1497,10 @@ function regenerateCore(cwd: string, outDir: string, cfg: AppwrapConfig, opts: {
1441
1497
  // modules-native/ is copied selectively per active module (copyModuleNativeSrc), not wholesale.
1442
1498
  // Match RELATIVE to TEMPLATE_DIR — when installed from npm, TEMPLATE_DIR itself sits under
1443
1499
  // node_modules/, so testing the absolute path would wrongly exclude the entire template.
1444
- filter: (src) => !/(?:^|\/)(node_modules|platforms|hooks|app\/www|modules-native)(\/|$)/.test(src.slice(TEMPLATE_DIR.length)),
1500
+ filter: templateCopyFilter,
1445
1501
  });
1502
+ // Delete template files removed/renamed since the last regenerate (cpSync only overwrites, never deletes).
1503
+ pruneStale(outDir, TEMPLATE_MANIFEST, collectRelFiles(TEMPLATE_DIR, templateCopyFilter));
1446
1504
  stampShellConfig(outDir, cfg);
1447
1505
  stampNativeScriptConfig(outDir, cfg);
1448
1506
  stampIOSDisplayName(outDir, cfg, req);
@@ -2176,7 +2234,7 @@ function pinTeamIdToConfig(configPath: string, teamId: string): void {
2176
2234
  * App_Resources/ is intentionally excluded — sync() rewrites it every run, so its mtime always
2177
2235
  * changes and would make the fingerprint permanently stale.
2178
2236
  * Collision risk is acceptable — a false "match" just skips a redundant build, not a correctness bug. */
2179
- function buildFingerprint(cwd: string, cfg: { pwaDist?: string }, _outDir: string): string {
2237
+ export function buildFingerprint(cwd: string, cfg: { pwaDist?: string; overrides?: string }, flags: Record<string, string> = {}): string {
2180
2238
  const mtime = (p: string): number => {
2181
2239
  if (!existsSync(p)) return 0;
2182
2240
  try {
@@ -2190,8 +2248,12 @@ function buildFingerprint(cwd: string, cfg: { pwaDist?: string }, _outDir: strin
2190
2248
  return s.mtimeMs;
2191
2249
  } catch { return 0; }
2192
2250
  };
2251
+ // Fingerprint EVERY app source the build actually consumes — not just dist. Missing the overrides dir
2252
+ // (native escape hatch) or a `.js`/`.json`/`--config` config file made edits there produce a stale
2253
+ // "inputs unchanged" skip. Resolve the real config path (ts→js→json / --config) instead of hardcoding.
2193
2254
  const distDir = cfg.pwaDist ? resolve(cwd, cfg.pwaDist) : join(cwd, 'dist');
2194
- const parts = [mtime(distDir), mtime(join(cwd, 'appwrap.config.ts'))];
2255
+ const overridesDir = resolve(cwd, cfg.overrides ?? 'appwrap.overrides');
2256
+ const parts = [mtime(distDir), mtime(overridesDir), mtime(resolveConfigPath(cwd, flags))];
2195
2257
  // Simple djb2-style hash — good enough for a build-skip check (not cryptographic).
2196
2258
  let h = 5381;
2197
2259
  for (const n of parts) h = (((h << 5) + h) ^ (n | 0)) >>> 0;
@@ -2499,7 +2561,7 @@ async function deployAndroid(cwd: string, flags: Record<string, string>, cfgOver
2499
2561
  // unchanged since the last build. --force/-f always rebuilds. (Rebuilding the web above usually
2500
2562
  // bumps the fingerprint; pair with --no-web-build to actually hit this fast-path.)
2501
2563
  const force = 'force' in flags || 'f' in flags;
2502
- const fp = buildFingerprint(cwd, cfg, outDir);
2564
+ const fp = buildFingerprint(cwd, cfg, flags);
2503
2565
  const cache = readBuildCache(outDir, 'android');
2504
2566
  if (!force && existsSync(apk) && cache?.fingerprint === fp && cache?.artifactPath === apk) {
2505
2567
  console.log(`⚡ Skipping build — inputs unchanged since last build (${apk.split('/').pop()})`);
@@ -2577,7 +2639,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
2577
2639
  // Auto: always checks fingerprint; never skips if sources/deps changed.
2578
2640
  const resume = 'resume' in flags || 'r' in flags;
2579
2641
  const force = 'force' in flags || 'f' in flags;
2580
- const fp = buildFingerprint(cwd, cfg, outDir);
2642
+ const fp = buildFingerprint(cwd, cfg, flags);
2581
2643
  const cache = readBuildCache(outDir, 'ios');
2582
2644
  const existingIpa = existsSync(ipaDir)
2583
2645
  ? readdirSync(ipaDir).find((f) => f.endsWith('.ipa'))