@octane-xplat/gif 0.14.0 → 0.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/gif",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Animated images (GIF/animated webp) for Octane xplat — web <img> + @nativescript-community/ui-image",
5
5
  "keywords": [
6
6
  "android",
@@ -23,9 +23,10 @@
23
23
  "directory": "packages/gif"
24
24
  },
25
25
  "files": [
26
- "dist",
27
26
  "types",
28
- "src"
27
+ "src",
28
+ "!src/**/*.test.*",
29
+ "!src/**/*.spec.*"
29
30
  ],
30
31
  "type": "module",
31
32
  "exports": {
@@ -36,14 +37,14 @@
36
37
  },
37
38
  "web": {
38
39
  "types": "./types/generated/index.web.d.ts",
39
- "default": "./dist/web/index.js"
40
+ "default": "./src/index.web.ts"
40
41
  },
41
42
  "native": {
42
43
  "types": "./types/generated/index.d.ts",
43
- "default": "./dist/native/index.js"
44
+ "default": "./src/index.ts"
44
45
  },
45
46
  "types": "./types/generated/index.d.ts",
46
- "default": "./dist/native/index.js"
47
+ "default": "./src/index.ts"
47
48
  }
48
49
  },
49
50
  "publishConfig": {
@@ -53,12 +54,13 @@
53
54
  "@nativescript-community/ui-image": "4.6.20"
54
55
  },
55
56
  "devDependencies": {
56
- "@nativescript-community/octane": "0.2.4",
57
+ "@nativescript-community/octane": "0.2.5",
57
58
  "@nativescript/core": "9.1.3",
59
+ "@octane-xplat/macos-renderer": "0.15.0",
58
60
  "@octanejs/vite-plugin": "0.1.61",
59
61
  "@tsrx/typescript-plugin": "0.4.11",
60
62
  "octane": "0.11.0",
61
- "tsrx-typegen": "0.2.2",
63
+ "tsrx-typegen": "0.3.0",
62
64
  "typescript": "5.9.3",
63
65
  "vite": "8.3.0"
64
66
  },
@@ -76,7 +78,6 @@
76
78
  }
77
79
  },
78
80
  "scripts": {
79
- "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
80
81
  "test:packed": "node tests/packed-consumer.mjs",
81
82
  "typegen": "tsrx-typegen --target octane",
82
83
  "pack:check": "tsrx-typegen --pack-check"
@@ -0,0 +1,8 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import type { AnimatedImageProps } from './props'
3
+
4
+ /** Unsupported placeholder — the Windows host has no animated-image engine
5
+ * (ui-image ships iOS/Android only). */
6
+ export function AnimatedImage(_props: AnimatedImageProps) @{
7
+ <label className="vx-unsupported" text="Animated images are unsupported on the Windows host" />
8
+ }
@@ -0,0 +1,5 @@
1
+ // Windows barrel — explicit leaf imports keep @nativescript-community/
2
+ // ui-image (iOS/Android only) out of the windows graph at the file boundary.
3
+ export { AnimatedImage } from './AnimatedImage.windows.tsrx'
4
+ export { prefetch } from './prefetch.windows'
5
+ export type { AnimatedImageProps, PrefetchOptions } from './props'
@@ -1,4 +1,4 @@
1
- /** VENDORED — identical copy in packages/gif/src/prefetch-batch.ts; keep in
1
+ /** VENDORED — identical copy in packages/image/src/prefetch-batch.ts; keep in
2
2
  * sync.
3
3
  *
4
4
  * Shared batch driver for the image-engine leaves' `prefetch` contract:
@@ -26,16 +26,21 @@ export function prefetchBatch(
26
26
  return new Promise<boolean>((resolve) => {
27
27
  let next = 0
28
28
  let active = Math.min(limit, urls.length)
29
+ // Surviving workers stop dequeuing once a failure decides the result —
30
+ // only already-running warm() calls settle on their own.
31
+ let failed = false
29
32
  for (let i = 0; i < active; i++) {
30
33
  void (async () => {
31
- while (next < urls.length) {
34
+ while (!failed && next < urls.length) {
32
35
  const url = urls[next++]
33
36
  try {
34
37
  if (!(await warm(url))) {
38
+ failed = true
35
39
  resolve(false)
36
40
  return
37
41
  }
38
42
  } catch {
43
+ failed = true
39
44
  resolve(false)
40
45
  return
41
46
  }
@@ -0,0 +1,8 @@
1
+ import type { PrefetchOptions } from './props'
2
+
3
+ /** prefetch — no-op on Windows: there is no image pipeline to warm
4
+ * (AnimatedImage renders a placeholder there). Resolves false to report
5
+ * that honestly. */
6
+ export function prefetch(_srcs: string | string[], _options?: PrefetchOptions): Promise<boolean> {
7
+ return Promise.resolve(false)
8
+ }
@@ -7,6 +7,7 @@
7
7
  "escape-props.web.d.ts",
8
8
  "index.d.ts",
9
9
  "index.web.d.ts",
10
+ "native-elements.d.ts",
10
11
  "prefetch-batch.d.ts",
11
12
  "prefetch.d.ts",
12
13
  "prefetch.web.d.ts",
@@ -0,0 +1,8 @@
1
+ import { Img } from '@nativescript-community/ui-image'
2
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
3
+
4
+ declare module '@nativescript-community/octane/intrinsics' {
5
+ interface NativeScriptElements {
6
+ animatedimage: Attributes<typeof Img>
7
+ }
8
+ }
@@ -1,4 +1,4 @@
1
- /** VENDORED — identical copy in packages/gif/src/prefetch-batch.ts; keep in
1
+ /** VENDORED — identical copy in packages/image/src/prefetch-batch.ts; keep in
2
2
  * sync.
3
3
  *
4
4
  * Shared batch driver for the image-engine leaves' `prefetch` contract:
@@ -2,4 +2,7 @@ import type { AnimatedImageProps, PrefetchOptions } from './generated/props.js'
2
2
 
3
3
  export type { AnimatedImageProps, PrefetchOptions } from './generated/props.js'
4
4
  export declare function AnimatedImage(props: AnimatedImageProps): unknown
5
- export declare function prefetch(srcs: string | string[], options?: PrefetchOptions): Promise<boolean>
5
+ export declare function prefetch(
6
+ srcs: string | string[],
7
+ options?: PrefetchOptions,
8
+ ): Promise<boolean>
@@ -1,39 +0,0 @@
1
- import { applyEscapeProps } from "./escape-props.js";
2
- import { defineUniversalComponent, registerElement, universalPlan, universalValue } from "@nativescript-community/octane";
3
- import { Img } from "@nativescript-community/ui-image";
4
- //#region src/AnimatedImage.tsrx
5
- var __octaneUniversalPlan0 = universalPlan("nativescript", {
6
- "kind": "host",
7
- "type": "animatedimage",
8
- "props": {
9
- "animatedImageView": true,
10
- "autoPlayAnimations": true
11
- },
12
- "bindings": [
13
- ["ref", 0],
14
- ["id", 1],
15
- ["className", 2],
16
- ["style", 3],
17
- ["src", 4],
18
- ["stretch", 5],
19
- ["width", 6],
20
- ["height", 7],
21
- ["accessibilityLabel", 8]
22
- ]
23
- });
24
- registerElement("animatedimage", Img);
25
- var AnimatedImage = defineUniversalComponent("nativescript", function AnimatedImage(props) {
26
- return universalValue(__octaneUniversalPlan0, [
27
- (v) => applyEscapeProps(v, props),
28
- props.id,
29
- props.className,
30
- props.style,
31
- props.src,
32
- props.stretch ?? "aspectFit",
33
- props.width,
34
- props.height,
35
- props.alt
36
- ]);
37
- }, { "module": "@nativescript-community/octane" });
38
- //#endregion
39
- export { AnimatedImage };
@@ -1,12 +0,0 @@
1
- import { isAndroid, isIOS } from "@nativescript/core";
2
- //#region src/escape-props.ts
3
- /** Apply only the active device's escape bag after the shared props — the
4
- * same convention @octane-xplat/ui uses, kept local so this leaf does not
5
- * depend on ui. */
6
- function applyEscapeProps(view, props) {
7
- if (!view) return;
8
- const bag = isIOS ? props.ios : isAndroid ? props.android : void 0;
9
- if (bag) Object.assign(view, bag);
10
- }
11
- //#endregion
12
- export { applyEscapeProps };
@@ -1,3 +0,0 @@
1
- import { AnimatedImage } from "./AnimatedImage.tsrx.js";
2
- import { prefetch } from "./prefetch.js";
3
- export { AnimatedImage, prefetch };
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "buildId": "9f62df29-bdd1-4368-8975-9d9c629a99bd",
4
- "mode": "production",
5
- "capabilities": {
6
- "independentHydration": false
7
- }
8
- }
@@ -1,27 +0,0 @@
1
- function prefetchBatch(srcs, concurrency, warm) {
2
- const urls = Array.isArray(srcs) ? srcs : [srcs];
3
- if (urls.length === 0) return Promise.resolve(true);
4
- const requested = Math.floor(concurrency ?? 5);
5
- const limit = Number.isFinite(requested) && requested > 0 ? requested : 5;
6
- return new Promise((resolve) => {
7
- let next = 0;
8
- let active = Math.min(limit, urls.length);
9
- for (let i = 0; i < active; i++) (async () => {
10
- while (next < urls.length) {
11
- const url = urls[next++];
12
- try {
13
- if (!await warm(url)) {
14
- resolve(false);
15
- return;
16
- }
17
- } catch {
18
- resolve(false);
19
- return;
20
- }
21
- }
22
- if (--active === 0) resolve(true);
23
- })();
24
- });
25
- }
26
- //#endregion
27
- export { prefetchBatch };
@@ -1,26 +0,0 @@
1
- import { prefetchBatch } from "./prefetch-batch.js";
2
- import { getImagePipeline, initialize } from "@nativescript-community/ui-image";
3
- import { isAndroid } from "@nativescript/core";
4
- //#region src/prefetch.ts
5
- /** prefetch — warm the image engine's disk cache for `srcs` without displaying
6
- * anything. Android (Fresco): `prefetchToDiskCache` stores encoded bytes on
7
- * disk and defers bitmap decode to display. iOS (SDWebImage): the prefetcher
8
- * stores the downloaded image to the disk cache only. Either way a later
9
- * `AnimatedImage` with the same src hits disk instead of the network.
10
- * Resolves false when any URL fails; resolves false when the pipeline is not
11
- * available yet (e.g. called before app launch).
12
- * `options.concurrency` caps parallel fetches (default 5); `options.headers`
13
- * is honored on Android and silently dropped on iOS (upstream gap — see
14
- * PrefetchOptions). */
15
- function prefetch(srcs, options) {
16
- try {
17
- if (isAndroid) initialize();
18
- const pipeline = getImagePipeline();
19
- const { concurrency, ...engineOptions } = options ?? {};
20
- return prefetchBatch(srcs, concurrency, (url) => pipeline.prefetchToDiskCache(url, engineOptions).then(() => true, () => false));
21
- } catch {
22
- return Promise.resolve(false);
23
- }
24
- }
25
- //#endregion
26
- export { prefetch };
@@ -1,48 +0,0 @@
1
- import { applyEscapeProps } from "./escape-props.web.js";
2
- import { __s, bag7, clone, queueRefAttach, queueRefDetach, setStyle, template } from "octane";
3
- import { replaceRef, setClassNameIfChanged, setPlainAttributeIfChanged, setURLAttributeIfChanged } from "octane/internal/client";
4
- //#region src/AnimatedImage.web.tsrx
5
- var _t$0 = /* @__PURE__ */ template("<img/>");
6
- var _rm$0 = [
7
- "r",
8
- "b",
9
- "a"
10
- ];
11
- var OBJECT_FIT = {
12
- none: "none",
13
- fill: "fill",
14
- aspectFit: "contain",
15
- aspectFill: "cover"
16
- };
17
- var AnimatedImage = /* @__PURE__ */ __s(function AnimatedImage(props, __s, __extra) {
18
- let _b = __s.slots[0];
19
- if (_b === void 0) {
20
- let _m1;
21
- const _root = clone(_t$0);
22
- queueRefAttach(__s, _m1 = (e) => applyEscapeProps(e, props), _root);
23
- (__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.b, _b.a));
24
- _b = bag7(__s, _root, _root, _m1);
25
- __s.refFields = _rm$0;
26
- } else {
27
- const _r = (e) => applyEscapeProps(e, props);
28
- if (_r !== _b.b) _b.b = replaceRef(__s, _b.b, _r, _b.a);
29
- }
30
- _b.c = setPlainAttributeIfChanged(props.id, _b.c, _b.a, "id");
31
- _b.d = setClassNameIfChanged(props.className, _b.d, _b.a);
32
- {
33
- const _v = {
34
- objectFit: OBJECT_FIT[props.stretch ?? "aspectFit"],
35
- width: props.width,
36
- height: props.height,
37
- ...props.style
38
- };
39
- if (_b.e !== _v) {
40
- setStyle(_b.a, _v, _b.e);
41
- _b.e = _v;
42
- }
43
- }
44
- _b.f = setURLAttributeIfChanged(props.src, _b.f, _b.a, "src");
45
- _b.g = setPlainAttributeIfChanged(props.alt, _b.g, _b.a, "alt");
46
- });
47
- //#endregion
48
- export { AnimatedImage };
@@ -1,18 +0,0 @@
1
- //#region src/escape-props.web.ts
2
- /** Apply DOM escape props after the primitive's own props. Dash-named keys
3
- * are attributes; other keys use the DOM property where available. Mirrors
4
- * @octane-xplat/ui's web escape-props so this leaf does not depend on ui. */
5
- function applyEscapeProps(element, props) {
6
- if (!element || !props.web) return;
7
- for (const [key, value] of Object.entries(props.web)) if (key.includes("-")) {
8
- if (value == null || value === false) element.removeAttribute(key);
9
- else element.setAttribute(key, value === true ? "" : String(value));
10
- } else if (key in element) try {
11
- element[key] = value;
12
- } catch {
13
- element.setAttribute(key, String(value));
14
- }
15
- else if (value != null) element.setAttribute(key, String(value));
16
- }
17
- //#endregion
18
- export { applyEscapeProps };
package/dist/web/index.js DELETED
@@ -1,3 +0,0 @@
1
- import { AnimatedImage } from "./AnimatedImage.web.tsrx.js";
2
- import { prefetch } from "./prefetch.web.js";
3
- export { AnimatedImage, prefetch };
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "buildId": "9a9ed12d-3da2-4093-be31-7902d623df05",
4
- "mode": "production",
5
- "capabilities": {
6
- "independentHydration": false
7
- }
8
- }
@@ -1,27 +0,0 @@
1
- function prefetchBatch(srcs, concurrency, warm) {
2
- const urls = Array.isArray(srcs) ? srcs : [srcs];
3
- if (urls.length === 0) return Promise.resolve(true);
4
- const requested = Math.floor(concurrency ?? 5);
5
- const limit = Number.isFinite(requested) && requested > 0 ? requested : 5;
6
- return new Promise((resolve) => {
7
- let next = 0;
8
- let active = Math.min(limit, urls.length);
9
- for (let i = 0; i < active; i++) (async () => {
10
- while (next < urls.length) {
11
- const url = urls[next++];
12
- try {
13
- if (!await warm(url)) {
14
- resolve(false);
15
- return;
16
- }
17
- } catch {
18
- resolve(false);
19
- return;
20
- }
21
- }
22
- if (--active === 0) resolve(true);
23
- })();
24
- });
25
- }
26
- //#endregion
27
- export { prefetchBatch };
@@ -1,17 +0,0 @@
1
- import { prefetchBatch } from "./prefetch-batch.js";
2
- //#region src/prefetch.web.ts
3
- /** prefetch — load each `src` through a throwaway <img> so the response lands
4
- * in the browser HTTP cache; decode and render stay with the displaying
5
- * element. `options.headers` is ignored — an <img> request cannot carry
6
- * custom headers. `options.concurrency` caps parallel loads (default 5).
7
- * Resolves false when any URL fails. */
8
- function prefetch(srcs, options) {
9
- return prefetchBatch(srcs, options?.concurrency, (url) => new Promise((resolve) => {
10
- const image = new Image();
11
- image.onload = () => resolve(true);
12
- image.onerror = () => resolve(false);
13
- image.src = url;
14
- }));
15
- }
16
- //#endregion
17
- export { prefetch };
@@ -1,104 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest'
2
- import { prefetch } from './prefetch.web'
3
- import { prefetchBatch } from './prefetch-batch'
4
-
5
- // Same web prefetch contract as @octane-xplat/image: throwaway <img> per URL,
6
- // awaited until load/error, false when any URL fails, JS-side concurrency cap.
7
-
8
- class MockImage {
9
- static instances: MockImage[] = []
10
- onload: (() => void) | null = null
11
- onerror: (() => void) | null = null
12
- set src(_url: string) {
13
- MockImage.instances.push(this)
14
- }
15
- load() {
16
- this.onload?.()
17
- }
18
- fail() {
19
- this.onerror?.()
20
- }
21
- }
22
-
23
- const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
24
-
25
- async function drainPending(promise: Promise<boolean>) {
26
- let result: boolean | undefined
27
- void promise.then((value) => {
28
- result = value
29
- })
30
-
31
- while (result === undefined) {
32
- for (const image of MockImage.instances.splice(0)) {
33
- image.load()
34
- }
35
-
36
- await flush()
37
- }
38
-
39
- return result
40
- }
41
-
42
- describe('prefetch (web)', () => {
43
- afterEach(() => {
44
- MockImage.instances = []
45
- vi.unstubAllGlobals()
46
- })
47
-
48
- it('resolves true only after every <img> fires load', async () => {
49
- vi.stubGlobal('Image', MockImage)
50
- const promise = prefetch(['https://x/a.gif', 'https://x/b.gif'])
51
- let settled = false
52
- void promise.then(() => {
53
- settled = true
54
- })
55
-
56
- await flush()
57
- expect(settled).toBe(false)
58
- expect(await drainPending(promise)).toBe(true)
59
- })
60
-
61
- it('resolves false when any URL fails', async () => {
62
- vi.stubGlobal('Image', MockImage)
63
- const promise = prefetch(['https://x/a.gif', 'https://x/b.gif'])
64
- const [failed] = MockImage.instances.splice(0)
65
- failed.fail()
66
- expect(await promise).toBe(false)
67
- })
68
-
69
- it('accepts a single URL and resolves true for an empty batch', async () => {
70
- vi.stubGlobal('Image', MockImage)
71
- expect(await prefetch([])).toBe(true)
72
- const promise = prefetch('https://x/a.gif')
73
- expect(MockImage.instances).toHaveLength(1)
74
- expect(await drainPending(promise)).toBe(true)
75
- })
76
-
77
- it('caps in-flight loads at options.concurrency', async () => {
78
- vi.stubGlobal('Image', MockImage)
79
- const promise = prefetch(
80
- Array.from({ length: 7 }, (_, i) => `https://x/${i}.gif`),
81
- { concurrency: 2, headers: { Authorization: 'Bearer x' } },
82
- )
83
-
84
- expect(MockImage.instances).toHaveLength(2)
85
- MockImage.instances.splice(0).forEach((image) => image.load())
86
- await flush()
87
- await flush()
88
- expect(MockImage.instances.length).toBeLessThanOrEqual(2)
89
- expect(await drainPending(promise)).toBe(true)
90
- })
91
-
92
- it('defaults to five parallel loads', async () => {
93
- vi.stubGlobal('Image', MockImage)
94
- const promise = prefetch(Array.from({ length: 9 }, (_, i) => `https://x/${i}.gif`))
95
- expect(MockImage.instances).toHaveLength(5)
96
- expect(await drainPending(promise)).toBe(true)
97
- })
98
- })
99
-
100
- describe('prefetchBatch', () => {
101
- it('reports false if the warm callback throws', async () => {
102
- expect(await prefetchBatch(['a', 'b'], 5, () => Promise.reject(new Error('boom')))).toBe(false)
103
- })
104
- })