@shipload/item-renderer 0.2.1 → 0.2.3

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.
Files changed (71) hide show
  1. package/.claude/settings.local.json +6 -0
  2. package/bun.lock +2 -2
  3. package/package.json +8 -4
  4. package/scripts/check-bundle-size.ts +21 -21
  5. package/scripts/copy-fonts.ts +19 -19
  6. package/scripts/preview.ts +13 -15
  7. package/src/assets/stardust-base64.ts +1 -1
  8. package/src/errors.ts +8 -8
  9. package/src/fonts/index.ts +25 -26
  10. package/src/fonts/load-bun.ts +9 -9
  11. package/src/index.ts +21 -21
  12. package/src/links.ts +11 -11
  13. package/src/meta.ts +16 -16
  14. package/src/payload/base64url.ts +16 -16
  15. package/src/payload/codec.ts +13 -13
  16. package/src/primitives/category-icon.ts +69 -48
  17. package/src/primitives/compact-row.ts +13 -13
  18. package/src/primitives/divider.ts +9 -9
  19. package/src/primitives/icon-hex.ts +18 -16
  20. package/src/primitives/module-slot.ts +73 -73
  21. package/src/primitives/panel.ts +10 -10
  22. package/src/primitives/quantity-badge.ts +13 -13
  23. package/src/primitives/span-paragraph.ts +48 -50
  24. package/src/primitives/stat-bar.ts +24 -24
  25. package/src/primitives/svg.ts +13 -13
  26. package/src/primitives/text.ts +25 -25
  27. package/src/primitives/wrap.ts +12 -12
  28. package/src/render.ts +15 -19
  29. package/src/templates/_shared.ts +6 -7
  30. package/src/templates/component.ts +68 -63
  31. package/src/templates/index.ts +17 -17
  32. package/src/templates/item-cell.ts +48 -41
  33. package/src/templates/module.ts +84 -83
  34. package/src/templates/packed-entity.ts +12 -14
  35. package/src/templates/resource.ts +63 -65
  36. package/src/templates/ship-panel.ts +67 -72
  37. package/src/templates/social-card.ts +27 -25
  38. package/src/tokens/colors.ts +29 -29
  39. package/src/tokens/index.ts +6 -6
  40. package/src/tokens/spacing.ts +1 -1
  41. package/src/tokens/typography.ts +1 -1
  42. package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
  43. package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
  44. package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
  45. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
  46. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  47. package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
  48. package/test/base64url.test.ts +22 -22
  49. package/test/codec.test.ts +26 -35
  50. package/test/errors.test.ts +21 -21
  51. package/test/fixtures/cargo-items.ts +43 -43
  52. package/test/fonts.test.ts +23 -23
  53. package/test/links-meta.test.ts +37 -37
  54. package/test/pixel.test.ts +44 -41
  55. package/test/primitives-category-icon.test.ts +74 -67
  56. package/test/primitives-compact-row.test.ts +29 -29
  57. package/test/primitives-domain.test.ts +61 -50
  58. package/test/primitives-layout.test.ts +47 -47
  59. package/test/primitives-module-slot.test.ts +58 -58
  60. package/test/render.test.ts +38 -35
  61. package/test/sanity.test.ts +5 -5
  62. package/test/sdk-link.test.ts +13 -13
  63. package/test/svg.test.ts +24 -22
  64. package/test/templates-component.test.ts +32 -32
  65. package/test/templates-dispatch.test.ts +29 -29
  66. package/test/templates-item-cell.test.ts +79 -79
  67. package/test/templates-module.test.ts +52 -52
  68. package/test/templates-packed-entity.test.ts +42 -42
  69. package/test/templates-resource.test.ts +61 -61
  70. package/test/templates-ship-panel.test.ts +69 -65
  71. package/test/tokens.test.ts +28 -26
@@ -1,33 +1,33 @@
1
- import { expect, test } from 'bun:test'
2
- import { bytesToBase64Url, base64UrlToBytes } from '../src/payload/base64url.ts'
3
- import { InvalidPayloadError } from '../src/errors.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { bytesToBase64Url, base64UrlToBytes } from "../src/payload/base64url.ts";
3
+ import { InvalidPayloadError } from "../src/errors.ts";
4
4
 
5
- test('round-trips arbitrary bytes', () => {
5
+ test("round-trips arbitrary bytes", () => {
6
6
  const inputs = [
7
7
  new Uint8Array([0]),
8
8
  new Uint8Array([255]),
9
9
  new Uint8Array([0, 1, 2, 3, 4, 5]),
10
10
  new Uint8Array(Array.from({ length: 256 }, (_, i) => i)),
11
- ]
11
+ ];
12
12
  for (const input of inputs) {
13
- const encoded = bytesToBase64Url(input)
14
- const decoded = base64UrlToBytes(encoded)
15
- expect(decoded).toEqual(input)
13
+ const encoded = bytesToBase64Url(input);
14
+ const decoded = base64UrlToBytes(encoded);
15
+ expect(decoded).toEqual(input);
16
16
  }
17
- })
17
+ });
18
18
 
19
- test('produces only URL-safe characters — no +, /, =', () => {
20
- const bytes = new Uint8Array(Array.from({ length: 256 }, (_, i) => i))
21
- const encoded = bytesToBase64Url(bytes)
22
- expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/)
23
- })
19
+ test("produces only URL-safe characters — no +, /, =", () => {
20
+ const bytes = new Uint8Array(Array.from({ length: 256 }, (_, i) => i));
21
+ const encoded = bytesToBase64Url(bytes);
22
+ expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/);
23
+ });
24
24
 
25
- test('rejects malformed base64url with InvalidPayloadError', () => {
26
- expect(() => base64UrlToBytes('!!!not-base64!!!')).toThrow(InvalidPayloadError)
27
- expect(() => base64UrlToBytes('has spaces')).toThrow(InvalidPayloadError)
28
- expect(() => base64UrlToBytes('with+plus/slash')).toThrow(InvalidPayloadError)
29
- })
25
+ test("rejects malformed base64url with InvalidPayloadError", () => {
26
+ expect(() => base64UrlToBytes("!!!not-base64!!!")).toThrow(InvalidPayloadError);
27
+ expect(() => base64UrlToBytes("has spaces")).toThrow(InvalidPayloadError);
28
+ expect(() => base64UrlToBytes("with+plus/slash")).toThrow(InvalidPayloadError);
29
+ });
30
30
 
31
- test('accepts known-good short string', () => {
32
- expect(base64UrlToBytes('AAA')).toEqual(new Uint8Array([0, 0]))
33
- })
31
+ test("accepts known-good short string", () => {
32
+ expect(base64UrlToBytes("AAA")).toEqual(new Uint8Array([0, 0]));
33
+ });
@@ -1,43 +1,34 @@
1
- import { expect, test } from 'bun:test'
2
- import { encodePayload, decodePayload } from '../src/payload/codec.ts'
3
- import { InvalidPayloadError } from '../src/errors.ts'
4
- import { FIXTURES } from './fixtures/cargo-items.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { encodePayload, decodePayload } from "../src/payload/codec.ts";
3
+ import { InvalidPayloadError } from "../src/errors.ts";
4
+ import { FIXTURES } from "./fixtures/cargo-items.ts";
5
5
 
6
- test('round-trips every fixture exactly', () => {
6
+ test("round-trips every fixture exactly", () => {
7
7
  for (const [name, item] of Object.entries(FIXTURES)) {
8
- const encoded = encodePayload(item)
9
- const decoded = decodePayload(encoded)
10
- expect(
11
- decoded.item_id.equals(item.item_id),
12
- `${name} item_id`,
13
- ).toBe(true)
14
- expect(
15
- decoded.quantity.equals(item.quantity),
16
- `${name} quantity`,
17
- ).toBe(true)
18
- expect(
19
- decoded.stats.equals(item.stats),
20
- `${name} stats`,
21
- ).toBe(true)
22
- expect(decoded.modules.length, `${name} modules length`).toBe(item.modules.length)
8
+ const encoded = encodePayload(item);
9
+ const decoded = decodePayload(encoded);
10
+ expect(decoded.item_id.equals(item.item_id), `${name} item_id`).toBe(true);
11
+ expect(decoded.quantity.equals(item.quantity), `${name} quantity`).toBe(true);
12
+ expect(decoded.stats.equals(item.stats), `${name} stats`).toBe(true);
13
+ expect(decoded.modules.length, `${name} modules length`).toBe(item.modules.length);
23
14
  }
24
- })
15
+ });
25
16
 
26
- test('encoded payload is URL-safe', () => {
17
+ test("encoded payload is URL-safe", () => {
27
18
  for (const item of Object.values(FIXTURES)) {
28
- const encoded = encodePayload(item)
29
- expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/)
19
+ const encoded = encodePayload(item);
20
+ expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/);
30
21
  }
31
- })
22
+ });
32
23
 
33
- test('decodePayload throws InvalidPayloadError on malformed input', () => {
34
- expect(() => decodePayload('!!!')).toThrow(InvalidPayloadError)
35
- expect(() => decodePayload('AAA')).toThrow(InvalidPayloadError)
36
- expect(() => decodePayload('')).toThrow(InvalidPayloadError)
37
- })
24
+ test("decodePayload throws InvalidPayloadError on malformed input", () => {
25
+ expect(() => decodePayload("!!!")).toThrow(InvalidPayloadError);
26
+ expect(() => decodePayload("AAA")).toThrow(InvalidPayloadError);
27
+ expect(() => decodePayload("")).toThrow(InvalidPayloadError);
28
+ });
38
29
 
39
- test('payload sizes are within expected ranges', () => {
40
- expect(encodePayload(FIXTURES.oreT1).length).toBeLessThan(30)
41
- expect(encodePayload(FIXTURES.shipT1NoModules).length).toBeLessThan(30)
42
- expect(encodePayload(FIXTURES.shipT1TwoModules).length).toBeLessThan(110)
43
- })
30
+ test("payload sizes are within expected ranges", () => {
31
+ expect(encodePayload(FIXTURES.oreT1).length).toBeLessThan(30);
32
+ expect(encodePayload(FIXTURES.shipT1NoModules).length).toBeLessThan(30);
33
+ expect(encodePayload(FIXTURES.shipT1TwoModules).length).toBeLessThan(110);
34
+ });
@@ -1,24 +1,24 @@
1
- import { expect, test } from 'bun:test'
2
- import { InvalidPayloadError, UnknownItemError, RenderError } from '../src/errors.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { InvalidPayloadError, UnknownItemError, RenderError } from "../src/errors.ts";
3
3
 
4
- test('InvalidPayloadError preserves the message and is an Error', () => {
5
- const e = new InvalidPayloadError('bad base64')
6
- expect(e).toBeInstanceOf(Error)
7
- expect(e).toBeInstanceOf(InvalidPayloadError)
8
- expect(e.message).toBe('bad base64')
9
- expect(e.name).toBe('InvalidPayloadError')
10
- })
4
+ test("InvalidPayloadError preserves the message and is an Error", () => {
5
+ const e = new InvalidPayloadError("bad base64");
6
+ expect(e).toBeInstanceOf(Error);
7
+ expect(e).toBeInstanceOf(InvalidPayloadError);
8
+ expect(e.message).toBe("bad base64");
9
+ expect(e.name).toBe("InvalidPayloadError");
10
+ });
11
11
 
12
- test('UnknownItemError carries the item id', () => {
13
- const e = new UnknownItemError(9999)
14
- expect(e).toBeInstanceOf(UnknownItemError)
15
- expect(e.itemId).toBe(9999)
16
- expect(e.message).toContain('9999')
17
- })
12
+ test("UnknownItemError carries the item id", () => {
13
+ const e = new UnknownItemError(9999);
14
+ expect(e).toBeInstanceOf(UnknownItemError);
15
+ expect(e.itemId).toBe(9999);
16
+ expect(e.message).toContain("9999");
17
+ });
18
18
 
19
- test('RenderError wraps a cause', () => {
20
- const cause = new Error('inner')
21
- const e = new RenderError('render failed', { cause })
22
- expect(e).toBeInstanceOf(RenderError)
23
- expect(e.cause).toBe(cause)
24
- })
19
+ test("RenderError wraps a cause", () => {
20
+ const cause = new Error("inner");
21
+ const e = new RenderError("render failed", { cause });
22
+ expect(e).toBeInstanceOf(RenderError);
23
+ expect(e.cause).toBe(cause);
24
+ });
@@ -1,122 +1,122 @@
1
- import { ServerContract } from '@shipload/sdk'
1
+ import { ServerContract } from "@shipload/sdk";
2
2
 
3
- export const ITEM_ORE_T1 = 101
4
- export const ITEM_GAS_T2 = 302
5
- export const ITEM_ENGINE_T1 = 10100
6
- export const ITEM_GENERATOR_T1 = 10101
7
- export const ITEM_GATHERER_T1 = 10102
8
- export const ITEM_LOADER_T1 = 10103
9
- export const ITEM_MANUFACTURING_T1 = 10104
10
- export const ITEM_STORAGE_T1 = 10105
11
- export const ITEM_HAULER_T1 = 10106
12
- export const ITEM_SHIP_T1_PACKED = 10201
3
+ export const ITEM_ORE_T1 = 101;
4
+ export const ITEM_GAS_T2 = 302;
5
+ export const ITEM_ENGINE_T1 = 10100;
6
+ export const ITEM_GENERATOR_T1 = 10101;
7
+ export const ITEM_GATHERER_T1 = 10102;
8
+ export const ITEM_LOADER_T1 = 10103;
9
+ export const ITEM_MANUFACTURING_T1 = 10104;
10
+ export const ITEM_STORAGE_T1 = 10105;
11
+ export const ITEM_HAULER_T1 = 10106;
12
+ export const ITEM_SHIP_T1_PACKED = 10201;
13
13
 
14
- export const MODULE_ENGINE = 1
15
- export const MODULE_GENERATOR = 2
14
+ export const MODULE_ENGINE = 1;
15
+ export const MODULE_GENERATOR = 2;
16
16
 
17
- export function cargoOreT1(stats = '0x123456789ABCDEF', quantity = 1) {
17
+ export function cargoOreT1(stats = "0x123456789ABCDEF", quantity = 1) {
18
18
  return ServerContract.Types.cargo_item.from({
19
19
  item_id: ITEM_ORE_T1,
20
20
  quantity,
21
21
  stats,
22
22
  modules: [],
23
- })
23
+ });
24
24
  }
25
25
 
26
26
  export function cargoShipT1Packed(opts?: {
27
- stats?: string
28
- engineStats?: string
29
- generatorStats?: string
30
- onlyEngine?: boolean
27
+ stats?: string;
28
+ engineStats?: string;
29
+ generatorStats?: string;
30
+ onlyEngine?: boolean;
31
31
  }) {
32
- const o = opts ?? {}
33
- const modules: unknown[] = []
32
+ const o = opts ?? {};
33
+ const modules: unknown[] = [];
34
34
  modules.push({
35
35
  type: MODULE_ENGINE,
36
- installed: { item_id: ITEM_ENGINE_T1, stats: o.engineStats ?? '0x2A4F6B8C' },
37
- })
36
+ installed: { item_id: ITEM_ENGINE_T1, stats: o.engineStats ?? "0x2A4F6B8C" },
37
+ });
38
38
  if (!o.onlyEngine) {
39
39
  modules.push({
40
40
  type: MODULE_GENERATOR,
41
- installed: { item_id: ITEM_GENERATOR_T1, stats: o.generatorStats ?? '0x1B2D4F' },
42
- })
41
+ installed: { item_id: ITEM_GENERATOR_T1, stats: o.generatorStats ?? "0x1B2D4F" },
42
+ });
43
43
  }
44
44
  return ServerContract.Types.cargo_item.from({
45
45
  item_id: ITEM_SHIP_T1_PACKED,
46
46
  quantity: 1,
47
- stats: o.stats ?? '0',
47
+ stats: o.stats ?? "0",
48
48
  modules,
49
- })
49
+ });
50
50
  }
51
51
 
52
- export const ITEM_HULL_PLATES = 10001
52
+ export const ITEM_HULL_PLATES = 10001;
53
53
 
54
54
  export const FIXTURES = {
55
- oreT1: cargoOreT1('0x123456789ABCDEF'),
56
- oreT1StackOf50: cargoOreT1('0x123456789ABCDEF', 50),
57
- oreT1ZeroStats: cargoOreT1('0'),
55
+ oreT1: cargoOreT1("0x123456789ABCDEF"),
56
+ oreT1StackOf50: cargoOreT1("0x123456789ABCDEF", 50),
57
+ oreT1ZeroStats: cargoOreT1("0"),
58
58
  gasT2: ServerContract.Types.cargo_item.from({
59
59
  item_id: ITEM_GAS_T2,
60
60
  quantity: 1,
61
- stats: '0xDEADBEEF1234',
61
+ stats: "0xDEADBEEF1234",
62
62
  modules: [],
63
63
  }),
64
64
  hullPlates: ServerContract.Types.cargo_item.from({
65
65
  item_id: ITEM_HULL_PLATES,
66
66
  quantity: 1,
67
- stats: '0x7FFF',
67
+ stats: "0x7FFF",
68
68
  modules: [],
69
69
  }),
70
70
  engineT1: ServerContract.Types.cargo_item.from({
71
71
  item_id: ITEM_ENGINE_T1,
72
72
  quantity: 1,
73
- stats: '358800',
73
+ stats: "358800",
74
74
  modules: [],
75
75
  }),
76
76
  generatorT1: ServerContract.Types.cargo_item.from({
77
77
  item_id: ITEM_GENERATOR_T1,
78
78
  quantity: 1,
79
- stats: '683908',
79
+ stats: "683908",
80
80
  modules: [],
81
81
  }),
82
82
  gathererT1: ServerContract.Types.cargo_item.from({
83
83
  item_id: ITEM_GATHERER_T1,
84
84
  quantity: 1,
85
- stats: '138255128433040',
85
+ stats: "138255128433040",
86
86
  modules: [],
87
87
  }),
88
88
  loaderT1: ServerContract.Types.cargo_item.from({
89
89
  item_id: ITEM_LOADER_T1,
90
90
  quantity: 1,
91
- stats: '512750',
91
+ stats: "512750",
92
92
  modules: [],
93
93
  }),
94
94
  crafterT1: ServerContract.Types.cargo_item.from({
95
95
  item_id: ITEM_MANUFACTURING_T1,
96
96
  quantity: 1,
97
- stats: '512600',
97
+ stats: "512600",
98
98
  modules: [],
99
99
  }),
100
100
  storageT1: ServerContract.Types.cargo_item.from({
101
101
  item_id: ITEM_STORAGE_T1,
102
102
  quantity: 1,
103
- stats: '537605632700',
103
+ stats: "537605632700",
104
104
  modules: [],
105
105
  }),
106
106
  haulerT1: ServerContract.Types.cargo_item.from({
107
107
  item_id: ITEM_HAULER_T1,
108
108
  quantity: 1,
109
- stats: '0x3E8',
109
+ stats: "0x3E8",
110
110
  modules: [],
111
111
  }),
112
112
  shipT1NoModules: ServerContract.Types.cargo_item.from({
113
113
  item_id: ITEM_SHIP_T1_PACKED,
114
114
  quantity: 1,
115
- stats: '0',
115
+ stats: "0",
116
116
  modules: [],
117
117
  }),
118
118
  shipT1TwoModules: cargoShipT1Packed(),
119
119
  shipT1OnlyEngine: cargoShipT1Packed({ onlyEngine: true }),
120
- } as const
120
+ } as const;
121
121
 
122
- export type FixtureName = keyof typeof FIXTURES
122
+ export type FixtureName = keyof typeof FIXTURES;
@@ -1,28 +1,28 @@
1
- import { expect, test } from 'bun:test'
2
- import { embedFontsInSvg } from '../src/fonts/index.ts'
3
- import { loadFontData } from '../src/fonts/load-bun.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { embedFontsInSvg } from "../src/fonts/index.ts";
3
+ import { loadFontData } from "../src/fonts/load-bun.ts";
4
4
 
5
- test('loadFontData returns all four faces with non-zero bytes', async () => {
6
- const data = await loadFontData()
5
+ test("loadFontData returns all four faces with non-zero bytes", async () => {
6
+ const data = await loadFontData();
7
7
  expect(Object.keys(data).sort()).toEqual([
8
- 'inter-400',
9
- 'inter-600',
10
- 'jetbrains-500',
11
- 'orbitron-700',
12
- ])
8
+ "inter-400",
9
+ "inter-600",
10
+ "jetbrains-500",
11
+ "orbitron-700",
12
+ ]);
13
13
  for (const [key, bytes] of Object.entries(data)) {
14
- expect(bytes.byteLength, `${key} bytes`).toBeGreaterThan(2000)
15
- expect(bytes.byteLength, `${key} bytes`).toBeLessThan(120_000)
14
+ expect(bytes.byteLength, `${key} bytes`).toBeGreaterThan(2000);
15
+ expect(bytes.byteLength, `${key} bytes`).toBeLessThan(120_000);
16
16
  }
17
- })
17
+ });
18
18
 
19
- test('embedFontsInSvg inlines @font-face blocks', async () => {
20
- const data = await loadFontData()
21
- const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>'
22
- const out = embedFontsInSvg(svg, data)
23
- expect(out).toContain('@font-face')
24
- expect(out).toContain('font-family: "Orbitron"')
25
- expect(out).toContain('font-family: "Inter"')
26
- expect(out).toContain('font-family: "JetBrains Mono"')
27
- expect(out).toContain('data:font/woff2;base64,')
28
- })
19
+ test("embedFontsInSvg inlines @font-face blocks", async () => {
20
+ const data = await loadFontData();
21
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>';
22
+ const out = embedFontsInSvg(svg, data);
23
+ expect(out).toContain("@font-face");
24
+ expect(out).toContain('font-family: "Orbitron"');
25
+ expect(out).toContain('font-family: "Inter"');
26
+ expect(out).toContain('font-family: "JetBrains Mono"');
27
+ expect(out).toContain("data:font/woff2;base64,");
28
+ });
@@ -1,43 +1,43 @@
1
- import { expect, test } from 'bun:test'
2
- import { resolveItem } from '@shipload/sdk'
3
- import { linkToItemImage, linkToItemPage, linkToItemSocial } from '../src/links.ts'
4
- import { itemPageMeta } from '../src/meta.ts'
5
- import { FIXTURES } from './fixtures/cargo-items.ts'
1
+ import { expect, test } from "bun:test";
2
+ import { resolveItem } from "@shipload/sdk";
3
+ import { linkToItemImage, linkToItemPage, linkToItemSocial } from "../src/links.ts";
4
+ import { itemPageMeta } from "../src/meta.ts";
5
+ import { FIXTURES } from "./fixtures/cargo-items.ts";
6
6
 
7
- test('linkToItemPage defaults to shiploadgame.com', () => {
8
- const url = linkToItemPage(FIXTURES.oreT1)
9
- expect(url).toMatch(/^https:\/\/shiploadgame\.com\/guide\/item\/[A-Za-z0-9_-]+$/)
10
- })
7
+ test("linkToItemPage defaults to shiploadgame.com", () => {
8
+ const url = linkToItemPage(FIXTURES.oreT1);
9
+ expect(url).toMatch(/^https:\/\/shiploadgame\.com\/guide\/item\/[A-Za-z0-9_-]+$/);
10
+ });
11
11
 
12
- test('linkToItemPage accepts a custom base URL', () => {
13
- const url = linkToItemPage(FIXTURES.oreT1, 'http://localhost:5173')
14
- expect(url.startsWith('http://localhost:5173/guide/item/')).toBe(true)
15
- })
12
+ test("linkToItemPage accepts a custom base URL", () => {
13
+ const url = linkToItemPage(FIXTURES.oreT1, "http://localhost:5173");
14
+ expect(url.startsWith("http://localhost:5173/guide/item/")).toBe(true);
15
+ });
16
16
 
17
- test('linkToItemImage builds a PNG URL', () => {
18
- const url = linkToItemImage(FIXTURES.oreT1, 'png')
19
- expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/item\/[A-Za-z0-9_-]+\.png$/)
20
- })
17
+ test("linkToItemImage builds a PNG URL", () => {
18
+ const url = linkToItemImage(FIXTURES.oreT1, "png");
19
+ expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/item\/[A-Za-z0-9_-]+\.png$/);
20
+ });
21
21
 
22
- test('linkToItemImage builds an SVG URL', () => {
23
- const url = linkToItemImage(FIXTURES.oreT1, 'svg')
24
- expect(url).toMatch(/\.svg$/)
25
- })
22
+ test("linkToItemImage builds an SVG URL", () => {
23
+ const url = linkToItemImage(FIXTURES.oreT1, "svg");
24
+ expect(url).toMatch(/\.svg$/);
25
+ });
26
26
 
27
- test('linkToItemSocial builds a social card URL', () => {
28
- const url = linkToItemSocial(FIXTURES.oreT1)
29
- expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/)
30
- })
27
+ test("linkToItemSocial builds a social card URL", () => {
28
+ const url = linkToItemSocial(FIXTURES.oreT1);
29
+ expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/);
30
+ });
31
31
 
32
- test('itemPageMeta produces title, description, and ogImage (social card)', () => {
33
- const item = FIXTURES.oreT1
34
- const resolved = resolveItem(item.item_id, item.stats, item.modules)
35
- const meta = itemPageMeta(item, resolved)
36
- expect(meta.title).toContain('Crude Ore')
37
- expect(meta.description).toContain('T1 Ore')
38
- expect(meta.description).toMatch(/Strength \d+/)
39
- expect(meta.description).toMatch(/\d+(\.\d+)? t$/)
40
- expect(meta.ogImage).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/)
41
- expect(meta.ogImageWidth).toBe(1200)
42
- expect(meta.ogImageHeight).toBe(630)
43
- })
32
+ test("itemPageMeta produces title, description, and ogImage (social card)", () => {
33
+ const item = FIXTURES.oreT1;
34
+ const resolved = resolveItem(item.item_id, item.stats, item.modules);
35
+ const meta = itemPageMeta(item, resolved);
36
+ expect(meta.title).toContain("Crude Ore");
37
+ expect(meta.description).toContain("T1 Ore");
38
+ expect(meta.description).toMatch(/Strength \d+/);
39
+ expect(meta.description).toMatch(/\d+(\.\d+)? t$/);
40
+ expect(meta.ogImage).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/);
41
+ expect(meta.ogImageWidth).toBe(1200);
42
+ expect(meta.ogImageHeight).toBe(630);
43
+ });
@@ -1,55 +1,58 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises'
2
- import { existsSync } from 'node:fs'
3
- import { resolve } from 'node:path'
4
- import { expect, test } from 'bun:test'
5
- import { Resvg } from '@resvg/resvg-js'
6
- import pixelmatch from 'pixelmatch'
7
- import { PNG } from 'pngjs'
8
- import { resolveItem } from '@shipload/sdk'
9
- import { renderItem } from '../src/render.ts'
10
- import { embedFontsInSvg } from '../src/fonts/index.ts'
11
- import { loadFontData } from '../src/fonts/load-bun.ts'
12
- import { FIXTURES } from './fixtures/cargo-items.ts'
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { expect, test } from "bun:test";
5
+ import { Resvg } from "@resvg/resvg-js";
6
+ import pixelmatch from "pixelmatch";
7
+ import { PNG } from "pngjs";
8
+ import { resolveItem } from "@shipload/sdk";
9
+ import { renderItem } from "../src/render.ts";
10
+ import { embedFontsInSvg } from "../src/fonts/index.ts";
11
+ import { loadFontData } from "../src/fonts/load-bun.ts";
12
+ import { FIXTURES } from "./fixtures/cargo-items.ts";
13
13
 
14
- const SNAP_DIR = resolve(import.meta.dir, '__image_snapshots__')
15
- const UPDATE = process.env.UPDATE_IMAGE_SNAPSHOTS === '1'
14
+ const SNAP_DIR = resolve(import.meta.dir, "__image_snapshots__");
15
+ const UPDATE = process.env.UPDATE_IMAGE_SNAPSHOTS === "1";
16
16
 
17
- await mkdir(SNAP_DIR, { recursive: true })
18
- const fontData = await loadFontData()
17
+ await mkdir(SNAP_DIR, { recursive: true });
18
+ const fontData = await loadFontData();
19
19
 
20
20
  async function renderPng(svg: string): Promise<Buffer> {
21
21
  const resvg = new Resvg(svg, {
22
- font: { loadSystemFonts: false, fontBuffers: Object.values(fontData).map((b) => Buffer.from(b)) },
23
- })
24
- return resvg.render().asPng()
22
+ font: {
23
+ loadSystemFonts: false,
24
+ fontBuffers: Object.values(fontData).map((b) => Buffer.from(b)),
25
+ },
26
+ });
27
+ return resvg.render().asPng();
25
28
  }
26
29
 
27
30
  const CASES = [
28
- { name: 'resource-ore-t1', fixture: FIXTURES.oreT1 },
29
- { name: 'packed-entity-ship-t1-two-modules', fixture: FIXTURES.shipT1TwoModules },
30
- { name: 'packed-entity-ship-t1-only-engine', fixture: FIXTURES.shipT1OnlyEngine },
31
- { name: 'component-hull-plates', fixture: FIXTURES.hullPlates },
32
- { name: 'module-engine-t1', fixture: FIXTURES.engineT1 },
33
- { name: 'module-storage-t1', fixture: FIXTURES.storageT1 },
34
- ] as const
31
+ { name: "resource-ore-t1", fixture: FIXTURES.oreT1 },
32
+ { name: "packed-entity-ship-t1-two-modules", fixture: FIXTURES.shipT1TwoModules },
33
+ { name: "packed-entity-ship-t1-only-engine", fixture: FIXTURES.shipT1OnlyEngine },
34
+ { name: "component-hull-plates", fixture: FIXTURES.hullPlates },
35
+ { name: "module-engine-t1", fixture: FIXTURES.engineT1 },
36
+ { name: "module-storage-t1", fixture: FIXTURES.storageT1 },
37
+ ] as const;
35
38
 
36
39
  for (const c of CASES) {
37
40
  test(`pixel golden — ${c.name}`, async () => {
38
- const resolved = resolveItem(c.fixture.item_id, c.fixture.stats, c.fixture.modules)
39
- const svg = embedFontsInSvg(renderItem(c.fixture, resolved), fontData)
40
- const png = await renderPng(svg)
41
- const goldPath = resolve(SNAP_DIR, `${c.name}.png`)
41
+ const resolved = resolveItem(c.fixture.item_id, c.fixture.stats, c.fixture.modules);
42
+ const svg = embedFontsInSvg(renderItem(c.fixture, resolved), fontData);
43
+ const png = await renderPng(svg);
44
+ const goldPath = resolve(SNAP_DIR, `${c.name}.png`);
42
45
 
43
46
  if (UPDATE || !existsSync(goldPath)) {
44
- await writeFile(goldPath, png)
45
- return
47
+ await writeFile(goldPath, png);
48
+ return;
46
49
  }
47
50
 
48
- const actual = PNG.sync.read(png)
49
- const expected = PNG.sync.read(await readFile(goldPath))
50
- expect(actual.width).toBe(expected.width)
51
- expect(actual.height).toBe(expected.height)
52
- const diff = new PNG({ width: actual.width, height: actual.height })
51
+ const actual = PNG.sync.read(png);
52
+ const expected = PNG.sync.read(await readFile(goldPath));
53
+ expect(actual.width).toBe(expected.width);
54
+ expect(actual.height).toBe(expected.height);
55
+ const diff = new PNG({ width: actual.width, height: actual.height });
53
56
  const diffCount = pixelmatch(
54
57
  actual.data,
55
58
  expected.data,
@@ -57,10 +60,10 @@ for (const c of CASES) {
57
60
  actual.width,
58
61
  actual.height,
59
62
  { threshold: 0.1 },
60
- )
63
+ );
61
64
  if (diffCount > 10) {
62
- await writeFile(resolve(SNAP_DIR, `${c.name}.diff.png`), PNG.sync.write(diff))
65
+ await writeFile(resolve(SNAP_DIR, `${c.name}.diff.png`), PNG.sync.write(diff));
63
66
  }
64
- expect(diffCount).toBeLessThanOrEqual(10)
65
- })
67
+ expect(diffCount).toBeLessThanOrEqual(10);
68
+ });
66
69
  }