@zinejs/pdf 0.1.0 → 0.2.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/README.md CHANGED
@@ -75,9 +75,11 @@ new PdfSource(await getDocument(...).promise); // a pdf.js document you created;
75
75
  ```ts
76
76
  new PdfSource(src, {
77
77
  workerSrc, // string — override the auto-resolved worker URL (see above)
78
- renderScale: 1, // pages rasterize at renderScale × devicePixelRatio
79
- preload: 1, // adjacent pages to prefetch around a requested page
80
- maxCacheBytes, // soft cap on cached page bytes (default ~256 MB); LRU-evicts beyond it
78
+ renderScale: 1, // pages rasterize at renderScale × devicePixelRatio
79
+ preload: 1, // adjacent pages to prefetch around a requested page
80
+ maxCacheBytes, // soft cap on cached page bytes (default ~256 MB); LRU-evicts beyond it
81
+ progressive: true, // paint a low-res page first, then swap to crisp (faster first paint)
82
+ disableAutoFetch: true, // fetch only the byte ranges visible pages need (range-capable servers)
81
83
  });
82
84
  ```
83
85
 
package/dist/index.js CHANGED
@@ -9,22 +9,28 @@ var t = class {
9
9
  #r;
10
10
  #i = null;
11
11
  #a;
12
- #o = /* @__PURE__ */ new Map();
13
- #s = 0;
12
+ #o;
13
+ #s;
14
+ #c = /* @__PURE__ */ new Map();
15
+ #l = 0;
16
+ #u = null;
14
17
  constructor(e, t = {}) {
15
- this.#e = e, this.#t = t.workerSrc, this.#n = t.renderScale ?? 1, this.#r = t.preload ?? 1, this.#a = t.maxCacheBytes ?? 268435456;
18
+ this.#e = e, this.#t = t.workerSrc, this.#n = t.renderScale ?? 1, this.#r = t.preload ?? 1, this.#a = t.maxCacheBytes ?? 268435456, this.#o = t.progressive ?? !1, this.#s = t.disableAutoFetch ?? !1;
19
+ }
20
+ onPageUpdate(e) {
21
+ this.#u = e;
16
22
  }
17
23
  async open() {
18
24
  if (e(this.#e)) this.#i = this.#e;
19
25
  else {
20
26
  let e = await import("pdfjs-dist");
21
- e.GlobalWorkerOptions.workerSrc = this.#c(e.GlobalWorkerOptions.workerSrc);
27
+ e.GlobalWorkerOptions.workerSrc = this.#d(e.GlobalWorkerOptions.workerSrc);
22
28
  let t = typeof this.#e == "string" ? { url: this.#e } : { data: this.#e };
23
- this.#i = await e.getDocument(t).promise;
29
+ this.#s && (t.disableAutoFetch = !0), this.#i = await e.getDocument(t).promise;
24
30
  }
25
31
  this.pageCount = this.#i.numPages;
26
32
  }
27
- #c(e) {
33
+ #d(e) {
28
34
  if (this.#t) return this.#t;
29
35
  if (e) return e;
30
36
  try {
@@ -34,37 +40,42 @@ var t = class {
34
40
  }
35
41
  }
36
42
  get(e) {
37
- let t = this.#l(e);
43
+ let t = this.#f(e);
38
44
  for (let t = 1; t <= this.#r; t++) this.prefetch([e - t, e + t]);
39
45
  return t;
40
46
  }
41
47
  prefetch(e) {
42
- for (let t of e) t >= 0 && t < this.pageCount && this.#l(t).catch(() => {});
48
+ for (let t of e) t >= 0 && t < this.pageCount && this.#f(t).catch(() => {});
43
49
  }
44
50
  destroy() {
45
- this.#i?.destroy?.(), this.#o.clear(), this.#s = 0, this.#i = null;
51
+ this.#i?.destroy?.(), this.#c.clear(), this.#l = 0, this.#i = null;
46
52
  }
47
- #l(e) {
48
- let t = this.#o.get(e);
49
- if (t) return this.#o.delete(e), this.#o.set(e, t), t.promise;
50
- let n = {
53
+ #f(e) {
54
+ let t = this.#c.get(e);
55
+ if (t) return this.#c.delete(e), this.#c.set(e, t), t.promise;
56
+ let n = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, r = this.#n * n, i = this.#o ? r * .35 : r, a = {
51
57
  promise: void 0,
52
58
  bytes: 0
53
59
  };
54
- return n.promise = this.#d(e).then((t) => (n.bytes = t.width * t.height * 4, this.#s += n.bytes, this.#u(e), t), (t) => {
55
- throw this.#o.delete(e), t;
56
- }), this.#o.set(e, n), n.promise;
60
+ return a.promise = this.#h(e, i).then((t) => (a.bytes = t.width * t.height * 4, this.#l += a.bytes, this.#m(e), this.#o && this.#p(e, a, r), t), (t) => {
61
+ throw this.#c.delete(e), t;
62
+ }), this.#c.set(e, a), a.promise;
63
+ }
64
+ #p(e, t, n) {
65
+ this.#h(e, n).then((n) => {
66
+ this.#c.get(e) === t && (this.#l -= t.bytes, t.promise = Promise.resolve(n), t.bytes = n.width * n.height * 4, this.#l += t.bytes, this.#m(e), this.#u?.(e));
67
+ }).catch(() => {});
57
68
  }
58
- #u(e) {
59
- for (let [t, n] of this.#o) {
60
- if (this.#s <= this.#a) break;
61
- t === e || n.bytes === 0 || (this.#o.delete(t), this.#s -= n.bytes);
69
+ #m(e) {
70
+ for (let [t, n] of this.#c) {
71
+ if (this.#l <= this.#a) break;
72
+ t === e || n.bytes === 0 || (this.#c.delete(t), this.#l -= n.bytes);
62
73
  }
63
74
  }
64
- async #d(e) {
75
+ async #h(e, t) {
65
76
  if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
66
- let t = await this.#i.getPage(e + 1), n = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, r = t.getViewport({ scale: this.#n * n }), i = document.createElement("canvas");
67
- return i.width = Math.ceil(r.width), i.height = Math.ceil(r.height), await t.render({
77
+ let n = await this.#i.getPage(e + 1), r = n.getViewport({ scale: t }), i = document.createElement("canvas");
78
+ return i.width = Math.ceil(r.width), i.height = Math.ceil(r.height), await n.render({
68
79
  canvasContext: i.getContext("2d"),
69
80
  viewport: r
70
81
  }).promise, i;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#doc","#resolveWorkerSrc","#renderPage","#cache","#cachedBytes","#rasterize","#evictToFit"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type { PageContent, Source } from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n render(opts: { canvasContext: unknown; viewport: unknown }): { promise: Promise<void> };\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: { url: string } | { data: ArrayBuffer | Uint8Array }): { promise: Promise<PdfDocumentLike> };\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a peer dependency, loaded\n * lazily so it never lands in the core bundle. Async — pass it to `new Zine`,\n * which calls `open()` (learning the page count) before building spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = (await import('pdfjs-dist')) as unknown as PdfjsModule;\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params = typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n this.#doc = await pdfjs.getDocument(params).promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number): Promise<PageContent> {\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n async #rasterize(index: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const viewport = page.getViewport({ scale: this.#renderScale * dpr });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#onUpdate","#doc","#resolveWorkerSrc","#renderPage","#cache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type { PageContent, Source } from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n render(opts: { canvasContext: unknown; viewport: unknown }): { promise: Promise<void> };\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): { promise: Promise<PdfDocumentLike> };\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a peer dependency, loaded\n * lazily so it never lands in the core bundle. Async — pass it to `new Zine`,\n * which calls `open()` (learning the page count) before building spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #progressive: boolean;\n #disableAutoFetch: boolean;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = (await import('pdfjs-dist')) as unknown as PdfjsModule;\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n this.#doc = await pdfjs.getDocument(params).promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number): Promise<PageContent> {\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n async #rasterize(index: number, scale: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
@@ -30,6 +30,10 @@ export interface PdfSourceOptions {
30
30
  preload?: number;
31
31
  /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */
32
32
  maxCacheBytes?: number;
33
+ /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */
34
+ progressive?: boolean;
35
+ /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */
36
+ disableAutoFetch?: boolean;
33
37
  }
34
38
  /**
35
39
  * Renders PDF pages to canvases for zinejs. pdf.js is a peer dependency, loaded
@@ -40,6 +44,8 @@ export declare class PdfSource implements Source {
40
44
  #private;
41
45
  pageCount: number;
42
46
  constructor(src: PdfSrc, options?: PdfSourceOptions);
47
+ /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */
48
+ onPageUpdate(handler: (index: number) => void): void;
43
49
  open(): Promise<void>;
44
50
  get(index: number): Promise<PageContent>;
45
51
  prefetch(indices: number[]): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zinejs/pdf",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "PDF content source for zinejs, powered by pdf.js.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -36,10 +36,10 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "pdfjs-dist": ">=4",
39
- "@zinejs/core": "^0.1.0"
39
+ "@zinejs/core": "^0.2.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@zinejs/core": "^0.1.0"
42
+ "@zinejs/core": "^0.2.0"
43
43
  },
44
44
  "size-limit": [
45
45
  {