@shotstack/shotstack-canvas 1.9.5 → 1.9.6

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/README.md CHANGED
@@ -1,13 +1,13 @@
1
- # @shotstack/shotstack-canvas
2
-
3
- One package → identical text shaping/wrapping/animation on Web & Node.
4
- - HarfBuzz WASM for shaping and glyph outlines (via `harfbuzzjs`).
5
- - Device-independent draw-ops.
6
- - Painters: Canvas2D (web) and node-canvas (node).
7
- - Deterministic time-driven animations.
8
-
9
- ## Install
10
-
11
- ```bash
12
- pnpm add @shotstack/shotstack-canvas
13
- # or npm i / yarn add
1
+ # @shotstack/shotstack-canvas
2
+
3
+ One package → identical text shaping/wrapping/animation on Web & Node.
4
+ - HarfBuzz WASM for shaping and glyph outlines (via `harfbuzzjs`).
5
+ - Device-independent draw-ops.
6
+ - Painters: Canvas2D (web) and node-canvas (node).
7
+ - Deterministic time-driven animations.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @shotstack/shotstack-canvas
13
+ # or npm i / yarn add
@@ -608,6 +608,9 @@ var FontRegistry = class _FontRegistry {
608
608
  const normalizedWeight = normalizeWeight(desc.weight);
609
609
  return `${desc.family}__${normalizedWeight}`;
610
610
  }
611
+ hasRegisteredFace(desc) {
612
+ return this.faces.has(this.key(desc));
613
+ }
611
614
  async registerFromBytes(bytes, desc) {
612
615
  try {
613
616
  if (!this.hb) await this.init();
@@ -3705,16 +3708,56 @@ async function createTextEngine(opts = {}) {
3705
3708
  }
3706
3709
  const layout = new LayoutEngine(fonts);
3707
3710
  const videoGenerator = new VideoGenerator();
3711
+ const customFontLoadCache = /* @__PURE__ */ new Map();
3712
+ const directRegistrationCache = /* @__PURE__ */ new Map();
3713
+ const normalizeWeight2 = (weight) => `${weight ?? "400"}`;
3714
+ const fontDescKey = (family, weight) => `${family}__${normalizeWeight2(weight)}`;
3715
+ const registerFontFromSource = async (source, desc) => {
3716
+ const normalizedDesc = {
3717
+ family: desc.family,
3718
+ weight: normalizeWeight2(desc.weight)
3719
+ };
3720
+ const cacheKey = `${source}|${fontDescKey(normalizedDesc.family, normalizedDesc.weight)}`;
3721
+ const cached = directRegistrationCache.get(cacheKey);
3722
+ if (cached) {
3723
+ return cached;
3724
+ }
3725
+ const registrationTask = (async () => {
3726
+ if (fonts.hasRegisteredFace(normalizedDesc)) {
3727
+ return;
3728
+ }
3729
+ const bytes = await loadFileOrHttpToArrayBuffer(source);
3730
+ await fonts.registerFromBytes(bytes, normalizedDesc);
3731
+ })();
3732
+ directRegistrationCache.set(cacheKey, registrationTask);
3733
+ return registrationTask;
3734
+ };
3735
+ const ensureCustomFontLoaded = async (customFont) => {
3736
+ const normalizedDesc = {
3737
+ family: customFont.family,
3738
+ weight: normalizeWeight2(customFont.weight)
3739
+ };
3740
+ const cacheKey = `${customFont.src}|${fontDescKey(normalizedDesc.family, normalizedDesc.weight)}`;
3741
+ const cached = customFontLoadCache.get(cacheKey);
3742
+ if (cached) {
3743
+ return cached;
3744
+ }
3745
+ const loadTask = (async () => {
3746
+ if (fonts.hasRegisteredFace(normalizedDesc)) {
3747
+ return;
3748
+ }
3749
+ const bytes = await loadFileOrHttpToArrayBuffer(customFont.src);
3750
+ await fonts.registerFromBytes(bytes, normalizedDesc);
3751
+ })();
3752
+ customFontLoadCache.set(cacheKey, loadTask);
3753
+ return loadTask;
3754
+ };
3708
3755
  async function ensureFonts(asset) {
3709
3756
  try {
3710
3757
  if (asset.customFonts) {
3711
3758
  for (const cf of asset.customFonts) {
3712
3759
  try {
3713
- const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
3714
- await fonts.registerFromBytes(bytes, {
3715
- family: cf.family,
3716
- weight: cf.weight ?? "400"
3717
- });
3760
+ await ensureCustomFontLoaded(cf);
3718
3761
  if (fonts.isColorEmojiFont(cf.family)) {
3719
3762
  const emojiBytes = fonts.getColorEmojiFontBytes(cf.family);
3720
3763
  if (emojiBytes) {
@@ -3754,8 +3797,7 @@ async function createTextEngine(opts = {}) {
3754
3797
  },
3755
3798
  async registerFontFromFile(path, desc) {
3756
3799
  try {
3757
- const bytes = await loadFileOrHttpToArrayBuffer(path);
3758
- await fonts.registerFromBytes(bytes, desc);
3800
+ await registerFontFromSource(path, desc);
3759
3801
  } catch (err) {
3760
3802
  throw new Error(
3761
3803
  `Failed to register font "${desc.family}" from file ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -3764,8 +3806,7 @@ async function createTextEngine(opts = {}) {
3764
3806
  },
3765
3807
  async registerFontFromUrl(url, desc) {
3766
3808
  try {
3767
- const bytes = await loadFileOrHttpToArrayBuffer(url);
3768
- await fonts.registerFromBytes(bytes, desc);
3809
+ await registerFontFromSource(url, desc);
3769
3810
  } catch (err) {
3770
3811
  throw new Error(
3771
3812
  `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
@@ -564,6 +564,9 @@ var FontRegistry = class _FontRegistry {
564
564
  const normalizedWeight = normalizeWeight(desc.weight);
565
565
  return `${desc.family}__${normalizedWeight}`;
566
566
  }
567
+ hasRegisteredFace(desc) {
568
+ return this.faces.has(this.key(desc));
569
+ }
567
570
  async registerFromBytes(bytes, desc) {
568
571
  try {
569
572
  if (!this.hb) await this.init();
@@ -3661,16 +3664,56 @@ async function createTextEngine(opts = {}) {
3661
3664
  }
3662
3665
  const layout = new LayoutEngine(fonts);
3663
3666
  const videoGenerator = new VideoGenerator();
3667
+ const customFontLoadCache = /* @__PURE__ */ new Map();
3668
+ const directRegistrationCache = /* @__PURE__ */ new Map();
3669
+ const normalizeWeight2 = (weight) => `${weight ?? "400"}`;
3670
+ const fontDescKey = (family, weight) => `${family}__${normalizeWeight2(weight)}`;
3671
+ const registerFontFromSource = async (source, desc) => {
3672
+ const normalizedDesc = {
3673
+ family: desc.family,
3674
+ weight: normalizeWeight2(desc.weight)
3675
+ };
3676
+ const cacheKey = `${source}|${fontDescKey(normalizedDesc.family, normalizedDesc.weight)}`;
3677
+ const cached = directRegistrationCache.get(cacheKey);
3678
+ if (cached) {
3679
+ return cached;
3680
+ }
3681
+ const registrationTask = (async () => {
3682
+ if (fonts.hasRegisteredFace(normalizedDesc)) {
3683
+ return;
3684
+ }
3685
+ const bytes = await loadFileOrHttpToArrayBuffer(source);
3686
+ await fonts.registerFromBytes(bytes, normalizedDesc);
3687
+ })();
3688
+ directRegistrationCache.set(cacheKey, registrationTask);
3689
+ return registrationTask;
3690
+ };
3691
+ const ensureCustomFontLoaded = async (customFont) => {
3692
+ const normalizedDesc = {
3693
+ family: customFont.family,
3694
+ weight: normalizeWeight2(customFont.weight)
3695
+ };
3696
+ const cacheKey = `${customFont.src}|${fontDescKey(normalizedDesc.family, normalizedDesc.weight)}`;
3697
+ const cached = customFontLoadCache.get(cacheKey);
3698
+ if (cached) {
3699
+ return cached;
3700
+ }
3701
+ const loadTask = (async () => {
3702
+ if (fonts.hasRegisteredFace(normalizedDesc)) {
3703
+ return;
3704
+ }
3705
+ const bytes = await loadFileOrHttpToArrayBuffer(customFont.src);
3706
+ await fonts.registerFromBytes(bytes, normalizedDesc);
3707
+ })();
3708
+ customFontLoadCache.set(cacheKey, loadTask);
3709
+ return loadTask;
3710
+ };
3664
3711
  async function ensureFonts(asset) {
3665
3712
  try {
3666
3713
  if (asset.customFonts) {
3667
3714
  for (const cf of asset.customFonts) {
3668
3715
  try {
3669
- const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
3670
- await fonts.registerFromBytes(bytes, {
3671
- family: cf.family,
3672
- weight: cf.weight ?? "400"
3673
- });
3716
+ await ensureCustomFontLoaded(cf);
3674
3717
  if (fonts.isColorEmojiFont(cf.family)) {
3675
3718
  const emojiBytes = fonts.getColorEmojiFontBytes(cf.family);
3676
3719
  if (emojiBytes) {
@@ -3710,8 +3753,7 @@ async function createTextEngine(opts = {}) {
3710
3753
  },
3711
3754
  async registerFontFromFile(path, desc) {
3712
3755
  try {
3713
- const bytes = await loadFileOrHttpToArrayBuffer(path);
3714
- await fonts.registerFromBytes(bytes, desc);
3756
+ await registerFontFromSource(path, desc);
3715
3757
  } catch (err) {
3716
3758
  throw new Error(
3717
3759
  `Failed to register font "${desc.family}" from file ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -3720,8 +3762,7 @@ async function createTextEngine(opts = {}) {
3720
3762
  },
3721
3763
  async registerFontFromUrl(url, desc) {
3722
3764
  try {
3723
- const bytes = await loadFileOrHttpToArrayBuffer(url);
3724
- await fonts.registerFromBytes(bytes, desc);
3765
+ await registerFontFromSource(url, desc);
3725
3766
  } catch (err) {
3726
3767
  throw new Error(
3727
3768
  `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
package/dist/entry.web.js CHANGED
@@ -564,6 +564,9 @@ var _FontRegistry = class _FontRegistry {
564
564
  const normalizedWeight = normalizeWeight(desc.weight);
565
565
  return `${desc.family}__${normalizedWeight}`;
566
566
  }
567
+ hasRegisteredFace(desc) {
568
+ return this.faces.has(this.key(desc));
569
+ }
567
570
  async registerFromBytes(bytes, desc) {
568
571
  try {
569
572
  if (!this.hb) await this.init();
@@ -3380,16 +3383,56 @@ async function createTextEngine(opts = {}) {
3380
3383
  );
3381
3384
  }
3382
3385
  const layout = new LayoutEngine(fonts);
3386
+ const customFontLoadCache = /* @__PURE__ */ new Map();
3387
+ const directRegistrationCache = /* @__PURE__ */ new Map();
3388
+ const normalizeWeight2 = (weight) => `${weight ?? "400"}`;
3389
+ const fontDescKey = (family, weight) => `${family}__${normalizeWeight2(weight)}`;
3390
+ const registerFontFromSource = async (source, desc) => {
3391
+ const normalizedDesc = {
3392
+ family: desc.family,
3393
+ weight: normalizeWeight2(desc.weight)
3394
+ };
3395
+ const cacheKey = `${source}|${fontDescKey(normalizedDesc.family, normalizedDesc.weight)}`;
3396
+ const cached = directRegistrationCache.get(cacheKey);
3397
+ if (cached) {
3398
+ return cached;
3399
+ }
3400
+ const registrationTask = (async () => {
3401
+ if (fonts.hasRegisteredFace(normalizedDesc)) {
3402
+ return;
3403
+ }
3404
+ const bytes = await fetchToArrayBuffer(source);
3405
+ await fonts.registerFromBytes(bytes, normalizedDesc);
3406
+ })();
3407
+ directRegistrationCache.set(cacheKey, registrationTask);
3408
+ return registrationTask;
3409
+ };
3410
+ const ensureCustomFontLoaded = async (customFont) => {
3411
+ const normalizedDesc = {
3412
+ family: customFont.family,
3413
+ weight: normalizeWeight2(customFont.weight)
3414
+ };
3415
+ const cacheKey = `${customFont.src}|${fontDescKey(normalizedDesc.family, normalizedDesc.weight)}`;
3416
+ const cached = customFontLoadCache.get(cacheKey);
3417
+ if (cached) {
3418
+ return cached;
3419
+ }
3420
+ const loadTask = (async () => {
3421
+ if (fonts.hasRegisteredFace(normalizedDesc)) {
3422
+ return;
3423
+ }
3424
+ const bytes = await fetchToArrayBuffer(customFont.src);
3425
+ await fonts.registerFromBytes(bytes, normalizedDesc);
3426
+ })();
3427
+ customFontLoadCache.set(cacheKey, loadTask);
3428
+ return loadTask;
3429
+ };
3383
3430
  async function ensureFonts(asset) {
3384
3431
  try {
3385
3432
  if (asset.customFonts) {
3386
3433
  for (const cf of asset.customFonts) {
3387
3434
  try {
3388
- const bytes = await fetchToArrayBuffer(cf.src);
3389
- await fonts.registerFromBytes(bytes, {
3390
- family: cf.family,
3391
- weight: cf.weight ?? "400"
3392
- });
3435
+ await ensureCustomFontLoaded(cf);
3393
3436
  } catch (err) {
3394
3437
  throw new Error(
3395
3438
  `Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
@@ -3411,11 +3454,13 @@ async function createTextEngine(opts = {}) {
3411
3454
  } catch {
3412
3455
  const wantsDefaultRoboto = (main.family || "Roboto").toLowerCase() === "roboto" && `${main.weight}` === "400";
3413
3456
  if (wantsDefaultRoboto) {
3414
- const bytes = await fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
3415
- await fonts.registerFromBytes(bytes, {
3416
- family: "Roboto",
3417
- weight: "400"
3418
- });
3457
+ if (!fonts.hasRegisteredFace({ family: "Roboto", weight: "400" })) {
3458
+ const bytes = await fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
3459
+ await fonts.registerFromBytes(bytes, {
3460
+ family: "Roboto",
3461
+ weight: "400"
3462
+ });
3463
+ }
3419
3464
  } else {
3420
3465
  throw new Error(`Font not registered for ${desc.family}__${desc.weight}`);
3421
3466
  }
@@ -3439,8 +3484,7 @@ async function createTextEngine(opts = {}) {
3439
3484
  },
3440
3485
  async registerFontFromUrl(url, desc) {
3441
3486
  try {
3442
- const bytes = await fetchToArrayBuffer(url);
3443
- await fonts.registerFromBytes(bytes, desc);
3487
+ await registerFontFromSource(url, desc);
3444
3488
  } catch (err) {
3445
3489
  throw new Error(
3446
3490
  `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
@@ -3451,13 +3495,8 @@ async function createTextEngine(opts = {}) {
3451
3495
  try {
3452
3496
  let bytes;
3453
3497
  if (typeof source === "string") {
3454
- try {
3455
- bytes = await fetchToArrayBuffer(source);
3456
- } catch (err) {
3457
- throw new Error(
3458
- `Failed to fetch font from ${source}: ${err instanceof Error ? err.message : String(err)}`
3459
- );
3460
- }
3498
+ await registerFontFromSource(source, desc);
3499
+ return;
3461
3500
  } else {
3462
3501
  try {
3463
3502
  bytes = await source.arrayBuffer();
@@ -3467,7 +3506,14 @@ async function createTextEngine(opts = {}) {
3467
3506
  );
3468
3507
  }
3469
3508
  }
3470
- await fonts.registerFromBytes(bytes, desc);
3509
+ const normalizedDesc = {
3510
+ family: desc.family,
3511
+ weight: normalizeWeight2(desc.weight)
3512
+ };
3513
+ if (fonts.hasRegisteredFace(normalizedDesc)) {
3514
+ return;
3515
+ }
3516
+ await fonts.registerFromBytes(bytes, normalizedDesc);
3471
3517
  } catch (err) {
3472
3518
  if (err instanceof Error) {
3473
3519
  throw err;
package/package.json CHANGED
@@ -1,65 +1,65 @@
1
- {
2
- "name": "@shotstack/shotstack-canvas",
3
- "version": "1.9.5",
4
- "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
- "type": "module",
6
- "main": "./dist/entry.node.cjs",
7
- "module": "./dist/entry.node.js",
8
- "browser": "./dist/entry.web.js",
9
- "types": "./dist/entry.node.d.ts",
10
- "exports": {
11
- ".": {
12
- "node": {
13
- "import": "./dist/entry.node.js",
14
- "require": "./dist/entry.node.cjs"
15
- },
16
- "browser": "./dist/entry.web.js",
17
- "default": "./dist/entry.web.js"
18
- }
19
- },
20
- "files": [
21
- "dist/**",
22
- "scripts/postinstall.js",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "scripts": {
27
- "dev": "tsup --watch",
28
- "build": "tsup",
29
- "postinstall": "node scripts/postinstall.js",
30
- "vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
31
- "example:node": "node examples/node-example.mjs",
32
- "example:video": "node examples/node-video.mjs",
33
- "example:web": "vite dev examples/web-example",
34
- "prepublishOnly": "npm run build"
35
- },
36
- "publishConfig": {
37
- "access": "public",
38
- "registry": "https://registry.npmjs.org/"
39
- },
40
- "engines": {
41
- "node": ">=18"
42
- },
43
- "sideEffects": false,
44
- "dependencies": {
45
- "@resvg/resvg-js": "^2.6.2",
46
- "@resvg/resvg-wasm": "^2.6.2",
47
- "@shotstack/schemas": "^1.5.4",
48
- "canvas": "npm:@napi-rs/canvas@^0.1.54",
49
- "ffmpeg-static": "^5.2.0",
50
- "fontkit": "^2.0.4",
51
- "harfbuzzjs": "0.4.12",
52
- "opentype.js": "^1.3.4",
53
- "zod": "^4.2.0"
54
- },
55
- "devDependencies": {
56
- "@types/fluent-ffmpeg": "2.1.27",
57
- "@types/node": "^20.14.10",
58
- "fluent-ffmpeg": "^2.1.3",
59
- "tsup": "^8.2.3",
60
- "typescript": "^5.5.3",
61
- "vite": "^5.3.3",
62
- "vite-plugin-top-level-await": "1.6.0",
63
- "vite-plugin-wasm": "3.5.0"
64
- }
65
- }
1
+ {
2
+ "name": "@shotstack/shotstack-canvas",
3
+ "version": "1.9.6",
4
+ "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
+ "type": "module",
6
+ "main": "./dist/entry.node.cjs",
7
+ "module": "./dist/entry.node.js",
8
+ "browser": "./dist/entry.web.js",
9
+ "types": "./dist/entry.node.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "node": {
13
+ "import": "./dist/entry.node.js",
14
+ "require": "./dist/entry.node.cjs"
15
+ },
16
+ "browser": "./dist/entry.web.js",
17
+ "default": "./dist/entry.web.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist/**",
22
+ "scripts/postinstall.js",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "dev": "tsup --watch",
28
+ "build": "tsup",
29
+ "postinstall": "node scripts/postinstall.js",
30
+ "vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
31
+ "example:node": "node examples/node-example.mjs",
32
+ "example:video": "node examples/node-video.mjs",
33
+ "example:web": "vite dev examples/web-example",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org/"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "sideEffects": false,
44
+ "dependencies": {
45
+ "@resvg/resvg-js": "^2.6.2",
46
+ "@resvg/resvg-wasm": "^2.6.2",
47
+ "@shotstack/schemas": "^1.5.4",
48
+ "canvas": "npm:@napi-rs/canvas@^0.1.54",
49
+ "ffmpeg-static": "^5.2.0",
50
+ "fontkit": "^2.0.4",
51
+ "harfbuzzjs": "0.4.12",
52
+ "opentype.js": "^1.3.4",
53
+ "zod": "^4.2.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/fluent-ffmpeg": "2.1.27",
57
+ "@types/node": "^20.14.10",
58
+ "fluent-ffmpeg": "^2.1.3",
59
+ "tsup": "^8.2.3",
60
+ "typescript": "^5.5.3",
61
+ "vite": "^5.3.3",
62
+ "vite-plugin-top-level-await": "1.6.0",
63
+ "vite-plugin-wasm": "3.5.0"
64
+ }
65
+ }
@@ -1,58 +1,58 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Postinstall script to verify native canvas bindings are available
5
- * This helps catch issues early and provides helpful guidance
6
- */
7
-
8
- import { platform as _platform, arch as _arch } from 'os';
9
- import { dirname } from 'path';
10
- import { readdirSync } from 'fs';
11
- import { createRequire } from 'module';
12
-
13
- const require = createRequire(import.meta.url);
14
-
15
- const platform = _platform();
16
- const arch = _arch();
17
-
18
- // Map platform/arch to package names
19
- const platformMap = {
20
- 'darwin-arm64': '@napi-rs/canvas-darwin-arm64',
21
- 'darwin-x64': '@napi-rs/canvas-darwin-x64',
22
- 'linux-arm64': '@napi-rs/canvas-linux-arm64-gnu',
23
- 'linux-x64': '@napi-rs/canvas-linux-x64-gnu',
24
- 'win32-x64': '@napi-rs/canvas-win32-x64-msvc',
25
- 'linux-arm': '@napi-rs/canvas-linux-arm-gnueabihf',
26
- 'android-arm64': '@napi-rs/canvas-android-arm64',
27
- };
28
-
29
- const platformKey = `${platform}-${arch}`;
30
- const requiredPackage = platformMap[platformKey];
31
-
32
- if (!requiredPackage) {
33
- console.warn(`\n⚠️ Warning: Unsupported platform ${platformKey} for @napi-rs/canvas`);
34
- console.warn(' Canvas rendering may not work on this platform.\n');
35
- process.exit(0);
36
- }
37
-
38
- // Check if the native binding package is installed
39
- try {
40
- const packagePath = require.resolve(`${requiredPackage}/package.json`);
41
- const packageDir = dirname(packagePath);
42
-
43
- // Verify the .node file exists
44
- const nodeFiles = readdirSync(packageDir).filter(f => f.endsWith('.node'));
45
-
46
- if (nodeFiles.length > 0) {
47
- console.log(`✅ @shotstack/shotstack-canvas: Native canvas binding found for ${platformKey}`);
48
- } else {
49
- throw new Error('No .node file found');
50
- }
51
- } catch (error) {
52
- console.warn(`\n⚠️ Warning: Native canvas binding not found for ${platformKey}`);
53
- console.warn(` Expected package: ${requiredPackage}`);
54
- console.warn('\n If you see "Cannot find native binding" errors, try:');
55
- console.warn(' 1. Delete node_modules and package-lock.json');
56
- console.warn(' 2. Run: npm install');
57
- console.warn(` 3. Or manually install: npm install ${requiredPackage}\n`);
58
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script to verify native canvas bindings are available
5
+ * This helps catch issues early and provides helpful guidance
6
+ */
7
+
8
+ import { platform as _platform, arch as _arch } from 'os';
9
+ import { dirname } from 'path';
10
+ import { readdirSync } from 'fs';
11
+ import { createRequire } from 'module';
12
+
13
+ const require = createRequire(import.meta.url);
14
+
15
+ const platform = _platform();
16
+ const arch = _arch();
17
+
18
+ // Map platform/arch to package names
19
+ const platformMap = {
20
+ 'darwin-arm64': '@napi-rs/canvas-darwin-arm64',
21
+ 'darwin-x64': '@napi-rs/canvas-darwin-x64',
22
+ 'linux-arm64': '@napi-rs/canvas-linux-arm64-gnu',
23
+ 'linux-x64': '@napi-rs/canvas-linux-x64-gnu',
24
+ 'win32-x64': '@napi-rs/canvas-win32-x64-msvc',
25
+ 'linux-arm': '@napi-rs/canvas-linux-arm-gnueabihf',
26
+ 'android-arm64': '@napi-rs/canvas-android-arm64',
27
+ };
28
+
29
+ const platformKey = `${platform}-${arch}`;
30
+ const requiredPackage = platformMap[platformKey];
31
+
32
+ if (!requiredPackage) {
33
+ console.warn(`\n⚠️ Warning: Unsupported platform ${platformKey} for @napi-rs/canvas`);
34
+ console.warn(' Canvas rendering may not work on this platform.\n');
35
+ process.exit(0);
36
+ }
37
+
38
+ // Check if the native binding package is installed
39
+ try {
40
+ const packagePath = require.resolve(`${requiredPackage}/package.json`);
41
+ const packageDir = dirname(packagePath);
42
+
43
+ // Verify the .node file exists
44
+ const nodeFiles = readdirSync(packageDir).filter(f => f.endsWith('.node'));
45
+
46
+ if (nodeFiles.length > 0) {
47
+ console.log(`✅ @shotstack/shotstack-canvas: Native canvas binding found for ${platformKey}`);
48
+ } else {
49
+ throw new Error('No .node file found');
50
+ }
51
+ } catch (error) {
52
+ console.warn(`\n⚠️ Warning: Native canvas binding not found for ${platformKey}`);
53
+ console.warn(` Expected package: ${requiredPackage}`);
54
+ console.warn('\n If you see "Cannot find native binding" errors, try:');
55
+ console.warn(' 1. Delete node_modules and package-lock.json');
56
+ console.warn(' 2. Run: npm install');
57
+ console.warn(` 3. Or manually install: npm install ${requiredPackage}\n`);
58
+ }