zone5 0.0.0 → 1.0.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/dist/cli/index.js +33 -0
  4. package/dist/cli/index2.js +238 -0
  5. package/dist/cli/index3.js +53 -0
  6. package/dist/cli/templates/.zone5.toml +4 -0
  7. package/dist/cli/templates/app.css +3 -0
  8. package/dist/cli/templates/layout.svelte +15 -0
  9. package/dist/cli/templates/layout.ts +1 -0
  10. package/dist/components/Zone5.svelte +153 -0
  11. package/dist/components/Zone5.svelte.d.ts +12 -0
  12. package/dist/components/Zone5Img.svelte +103 -0
  13. package/dist/components/Zone5Img.svelte.d.ts +10 -0
  14. package/dist/components/Zone5Lightbox.svelte +131 -0
  15. package/dist/components/Zone5Lightbox.svelte.d.ts +12 -0
  16. package/dist/components/Zone5Provider.svelte +68 -0
  17. package/dist/components/Zone5Provider.svelte.d.ts +9 -0
  18. package/dist/components/atoms/Button.svelte +40 -0
  19. package/dist/components/atoms/Button.svelte.d.ts +13 -0
  20. package/dist/components/atoms/CloseButton.svelte +18 -0
  21. package/dist/components/atoms/CloseButton.svelte.d.ts +9 -0
  22. package/dist/components/atoms/NextButton.svelte +19 -0
  23. package/dist/components/atoms/NextButton.svelte.d.ts +10 -0
  24. package/dist/components/atoms/PrevButton.svelte +19 -0
  25. package/dist/components/atoms/PrevButton.svelte.d.ts +10 -0
  26. package/dist/components/atoms/index.d.ts +4 -0
  27. package/dist/components/atoms/index.js +5 -0
  28. package/dist/components/constants.d.ts +17 -0
  29. package/dist/components/constants.js +17 -0
  30. package/dist/components/index.d.ts +6 -0
  31. package/dist/components/index.js +7 -0
  32. package/dist/components/portal.d.ts +4 -0
  33. package/dist/components/portal.js +26 -0
  34. package/dist/components/types.d.ts +7 -0
  35. package/dist/components/types.js +1 -0
  36. package/dist/config.d.ts +51 -0
  37. package/dist/config.js +56 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +4 -0
  40. package/dist/module.d.ts +19 -0
  41. package/dist/processor/blurhash.d.ts +7 -0
  42. package/dist/processor/blurhash.js +37 -0
  43. package/dist/processor/color.d.ts +5 -0
  44. package/dist/processor/color.js +32 -0
  45. package/dist/processor/config.d.ts +12 -0
  46. package/dist/processor/config.js +9 -0
  47. package/dist/processor/exif/converters.d.ts +7 -0
  48. package/dist/processor/exif/converters.js +38 -0
  49. package/dist/processor/exif/defaults.d.ts +17 -0
  50. package/dist/processor/exif/defaults.js +17 -0
  51. package/dist/processor/exif/exif.d.ts +34 -0
  52. package/dist/processor/exif/exif.js +43 -0
  53. package/dist/processor/exif/index.d.ts +1 -0
  54. package/dist/processor/exif/index.js +1 -0
  55. package/dist/processor/exif/types.d.ts +4 -0
  56. package/dist/processor/exif/types.js +1 -0
  57. package/dist/processor/file.d.ts +3 -0
  58. package/dist/processor/file.js +28 -0
  59. package/dist/processor/index.d.ts +27 -0
  60. package/dist/processor/index.js +70 -0
  61. package/dist/processor/test-data/canon-m6-22mm.jpg +0 -0
  62. package/dist/processor/test-data/iphone-15pro.jpg +0 -0
  63. package/dist/processor/test-data/nikon-z6iii-40mm.jpg +0 -0
  64. package/dist/processor/test-data/ricoh-gr-iiix.jpg +0 -0
  65. package/dist/processor/variants.d.ts +13 -0
  66. package/dist/processor/variants.js +83 -0
  67. package/dist/remark.d.ts +5 -0
  68. package/dist/remark.js +268 -0
  69. package/dist/stores/index.d.ts +1 -0
  70. package/dist/stores/index.js +1 -0
  71. package/dist/stores/registry.svelte.d.ts +22 -0
  72. package/dist/stores/registry.svelte.js +100 -0
  73. package/dist/test-setup.d.ts +9 -0
  74. package/dist/test-setup.js +25 -0
  75. package/dist/vite.d.ts +2 -0
  76. package/dist/vite.js +158 -0
  77. package/package.json +143 -5
@@ -0,0 +1,13 @@
1
+ import type { BaseConfigType } from '../config.js';
2
+ import type { ProcessorConfig } from './config.js';
3
+ export interface GeneratedVariant {
4
+ width: number;
5
+ path: string;
6
+ }
7
+ export declare function generateImageVariants(options: {
8
+ base: BaseConfigType;
9
+ processor: ProcessorConfig;
10
+ sourceFile: string;
11
+ clear?: boolean;
12
+ forceOverwrite?: boolean;
13
+ }): Promise<GeneratedVariant[]>;
@@ -0,0 +1,83 @@
1
+ import { SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { rm } from 'fs/promises';
3
+ import { join, parse } from 'path';
4
+ import sharp from 'sharp';
5
+ import { ensureDirectoryExists, fileExists, sourceFileHash } from './file.js';
6
+ const tracer = trace.getTracer('zone5-processor-variants');
7
+ const addDebugText = async (img, width, height) => {
8
+ const svg = `<svg height="100" width="300">
9
+ <text x="0" y="50" font-size="50" fill="#fff">${width}×${height}</text>
10
+ </svg>`;
11
+ return img.composite([{ input: Buffer.from(svg), gravity: sharp.gravity.center }]);
12
+ };
13
+ export async function generateImageVariants(options) {
14
+ return tracer.startActiveSpan('zone5.generateImageVariants', async (span) => {
15
+ try {
16
+ const { base, processor, sourceFile, clear = false, forceOverwrite = false } = options;
17
+ // Parse file path components
18
+ const { name: fileBasename, ext: fileExtension } = parse(sourceFile);
19
+ // Get source image metadata to check dimensions
20
+ const sourceImage = sharp(sourceFile);
21
+ const { width: sourceWidth } = await sourceImage.metadata();
22
+ // Filter out widths that would be wider than the source image
23
+ const validWidths = processor.variants.filter((width) => width <= sourceWidth);
24
+ span.setAttributes({
25
+ 'zone5.sourceFile': sourceFile,
26
+ 'zone5.sourceWidth': sourceWidth,
27
+ 'zone5.validWidthsCount': validWidths.length,
28
+ 'zone5.clear': clear,
29
+ 'zone5.forceOverwrite': forceOverwrite,
30
+ });
31
+ // Create cache subdirectory
32
+ const sourceHash = sourceFileHash(base.root, sourceFile);
33
+ const cacheSubDir = join(base.cache, `${fileBasename}-${sourceHash}`);
34
+ if (clear) {
35
+ await rm(cacheSubDir, { recursive: true, force: true });
36
+ }
37
+ await ensureDirectoryExists(cacheSubDir);
38
+ // Generate variants for each valid width
39
+ const variants = [];
40
+ let generatedCount = 0;
41
+ for (const width of validWidths) {
42
+ const variantFilename = `${fileBasename}-${width}${fileExtension}`;
43
+ const variantPath = join(cacheSubDir, variantFilename);
44
+ // Check if variant already exists and should be overwritten
45
+ const variantExists = await fileExists(variantPath);
46
+ if (!variantExists || forceOverwrite) {
47
+ let img = sharp(sourceFile).gamma(processor.resize_gamma).resize(width, null, {
48
+ fit: 'inside',
49
+ kernel: processor.resize_kernel,
50
+ });
51
+ if (process.env.ZONE5_DEBUG) {
52
+ const { width: w, height: h } = await img.metadata();
53
+ const scale = w / width;
54
+ img = await addDebugText(img, width, Math.ceil(h * scale));
55
+ }
56
+ await img.toFile(variantPath);
57
+ generatedCount++;
58
+ }
59
+ variants.push({
60
+ width,
61
+ path: variantPath,
62
+ });
63
+ }
64
+ span.setAttributes({
65
+ 'zone5.variantsGenerated': generatedCount,
66
+ 'zone5.variantsTotal': variants.length,
67
+ });
68
+ span.setStatus({ code: SpanStatusCode.OK });
69
+ return variants;
70
+ }
71
+ catch (error) {
72
+ span.setStatus({
73
+ code: SpanStatusCode.ERROR,
74
+ message: error instanceof Error ? error.message : String(error),
75
+ });
76
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
77
+ throw error;
78
+ }
79
+ finally {
80
+ span.end();
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from 'unified';
2
+ /**
3
+ * Remark plugin to process Zone5 images
4
+ */
5
+ export declare const remarkZ5Images: Plugin;
package/dist/remark.js ADDED
@@ -0,0 +1,268 @@
1
+ import { compile } from 'svast-stringify/dist/main.es.js';
2
+ import { visit } from 'unist-util-visit';
3
+ /**
4
+ * Convert svast svelteComponent node to HTML using svast-stringify
5
+ */
6
+ function svelteComponentToHTML(node) {
7
+ return compile({
8
+ type: 'root',
9
+ children: [node],
10
+ });
11
+ }
12
+ /**
13
+ * Check if a paragraph is a newline (contains only whitespace)
14
+ */
15
+ function isNewlineParagraph(node) {
16
+ return (node.type === 'paragraph' &&
17
+ node.children.length === 1 &&
18
+ node.children[0].type === 'text' &&
19
+ /^\s*$/.test(node.children[0].value));
20
+ }
21
+ /**
22
+ * Build the expression value for the images property
23
+ */
24
+ function buildImagesExpression(imageData) {
25
+ const imageExpressions = imageData.map((img) => `{...${img.key}, properties: {...${img.key}.properties, alt: ${JSON.stringify(img.alt)}, title: ${JSON.stringify(img.title)}}}`);
26
+ return '[' + imageExpressions.join(',') + ']';
27
+ }
28
+ /**
29
+ * Create a Zone5 Svelte component node
30
+ */
31
+ function createZone5Component(imageData, mode) {
32
+ const properties = [
33
+ {
34
+ type: 'svelteProperty',
35
+ name: 'images',
36
+ shorthand: 'none',
37
+ value: [
38
+ {
39
+ type: 'svelteDynamicContent',
40
+ expression: {
41
+ type: 'svelteExpression',
42
+ value: buildImagesExpression(imageData),
43
+ },
44
+ },
45
+ ],
46
+ modifiers: [],
47
+ },
48
+ ];
49
+ if (mode) {
50
+ properties.push({
51
+ type: 'svelteProperty',
52
+ name: 'mode',
53
+ shorthand: 'none',
54
+ value: [
55
+ {
56
+ type: 'text',
57
+ value: mode,
58
+ },
59
+ ],
60
+ modifiers: [],
61
+ });
62
+ }
63
+ return {
64
+ type: 'svelteComponent',
65
+ tagName: 'Zone5',
66
+ properties,
67
+ selfClosing: true,
68
+ children: [],
69
+ };
70
+ }
71
+ /**
72
+ * Collect image data from Z5 images
73
+ */
74
+ function collectImageData(images, existingKeys) {
75
+ return images.map((imageNode) => {
76
+ const importKey = generateImportKey(imageNode.url, existingKeys);
77
+ return {
78
+ key: importKey,
79
+ alt: imageNode.alt || '',
80
+ title: imageNode.title || undefined,
81
+ };
82
+ });
83
+ }
84
+ /**
85
+ * Generate a unique import key for an image URL
86
+ */
87
+ function generateImportKey(url, existingKeys) {
88
+ const baseName = url
89
+ .split('/')
90
+ .pop()
91
+ ?.replace(/\?z5$/, '')
92
+ .replace(/\.[^.]*$/, '')
93
+ .replace(/[^a-zA-Z0-9]/g, '_')
94
+ .replace(/^(\d)/, '_$1') || 'image';
95
+ let key = baseName;
96
+ let counter = 1;
97
+ while (existingKeys.has(key)) {
98
+ key = `${baseName}_${counter}`;
99
+ counter++;
100
+ }
101
+ existingKeys.add(key);
102
+ return key;
103
+ }
104
+ /**
105
+ * Check if a node is a Zone5 image (ends with ?z5)
106
+ */
107
+ function isZ5Image(node) {
108
+ return node.type === 'image' && typeof node.url === 'string' && node.url.endsWith('?z5');
109
+ }
110
+ /**
111
+ * Check if a paragraph contains only a single Z5 image
112
+ */
113
+ function isZ5ImageParagraph(node) {
114
+ return node.type === 'paragraph' && node.children.length === 1 && isZ5Image(node.children[0]);
115
+ }
116
+ /**
117
+ * Check if a paragraph contains multiple consecutive Z5 images
118
+ */
119
+ function isMultiZ5ImageParagraph(node) {
120
+ if (node.type !== 'paragraph')
121
+ return false;
122
+ const paragraph = node;
123
+ const z5Images = paragraph.children.filter((child) => isZ5Image(child));
124
+ return z5Images.length >= 2;
125
+ }
126
+ /**
127
+ * Extract the image from a Z5 image paragraph
128
+ */
129
+ function getImageFromParagraph(node) {
130
+ if (isZ5ImageParagraph(node) && node.type === 'paragraph') {
131
+ return node.children[0];
132
+ }
133
+ return null;
134
+ }
135
+ /**
136
+ * Find groups of consecutive Zone5 images
137
+ */
138
+ function findConsecutiveImageGroups(children) {
139
+ const groups = [];
140
+ let currentGroup = [];
141
+ for (let i = 0; i < children.length; i++) {
142
+ const node = children[i];
143
+ if (isZ5ImageParagraph(node)) {
144
+ currentGroup.push(i);
145
+ }
146
+ else if (!isNewlineParagraph(node) && currentGroup.length > 0) {
147
+ // End of consecutive group (non-newline, non-image node)
148
+ if (currentGroup.length >= 2) {
149
+ groups.push([...currentGroup]);
150
+ }
151
+ currentGroup = [];
152
+ }
153
+ }
154
+ // Handle group at end of children
155
+ if (currentGroup.length >= 2) {
156
+ groups.push(currentGroup);
157
+ }
158
+ return groups;
159
+ }
160
+ /**
161
+ * Create a script element with TypeScript imports
162
+ */
163
+ function createScriptElement(importMap) {
164
+ const imports = Object.entries(importMap)
165
+ .map(([key, url]) => `import ${key} from '${url}';`)
166
+ .join('\n');
167
+ // Use string concatenation to prevent the bundler from transforming the import path
168
+ const zone5Import = 'import { Zone5 } from ' + '"zone5/components"';
169
+ const lines = ['<script lang="ts">', zone5Import, imports, '</script>'];
170
+ return {
171
+ type: 'raw',
172
+ value: lines.join('\n'),
173
+ };
174
+ }
175
+ /**
176
+ * Remark plugin to process Zone5 images
177
+ */
178
+ export const remarkZ5Images = () => {
179
+ return (tree, file) => {
180
+ const rootTree = tree;
181
+ const importMap = {};
182
+ const existingKeys = new Set();
183
+ const fm = file.data.fm;
184
+ const zone5mode = fm?.zone5mode;
185
+ // First, collect all Z5 images for the import map
186
+ visit(rootTree, 'image', (node) => {
187
+ if (isZ5Image(node)) {
188
+ const importKey = generateImportKey(node.url, existingKeys);
189
+ importMap[importKey] = node.url;
190
+ }
191
+ });
192
+ // Reset keys for consistent grouping
193
+ existingKeys.clear();
194
+ visit(rootTree, 'root', (node) => {
195
+ // First, handle multi-image paragraphs (images on consecutive lines without blank lines)
196
+ for (let i = node.children.length - 1; i >= 0; i--) {
197
+ const child = node.children[i];
198
+ if (isMultiZ5ImageParagraph(child) && child.type === 'paragraph') {
199
+ const paragraph = child;
200
+ const z5Images = paragraph.children.filter((ch) => isZ5Image(ch));
201
+ const imageData = collectImageData(z5Images, existingKeys);
202
+ const svelteComponent = createZone5Component(imageData, zone5mode);
203
+ // Replace the multi-image paragraph with the svelte component
204
+ node.children.splice(i, 1, svelteComponent);
205
+ }
206
+ }
207
+ // Then handle consecutive single-image paragraphs
208
+ const groups = findConsecutiveImageGroups(node.children);
209
+ // Process groups in reverse order to maintain correct indices
210
+ for (let i = groups.length - 1; i >= 0; i--) {
211
+ const group = groups[i];
212
+ // Collect image nodes from the group
213
+ const imageNodes = group
214
+ .map((index) => getImageFromParagraph(node.children[index]))
215
+ .filter((img) => img !== null);
216
+ const imageData = collectImageData(imageNodes, existingKeys);
217
+ const svelteComponent = createZone5Component(imageData, zone5mode);
218
+ // Calculate how many nodes to remove (including newlines)
219
+ const startIndex = group[0];
220
+ const endIndex = group[group.length - 1];
221
+ const removeCount = endIndex - startIndex + 1;
222
+ // Count newline paragraphs between images
223
+ let actualRemoveCount = removeCount;
224
+ for (let j = startIndex + 1; j <= endIndex; j++) {
225
+ if (isNewlineParagraph(node.children[j])) {
226
+ actualRemoveCount++;
227
+ }
228
+ }
229
+ node.children.splice(startIndex, actualRemoveCount, svelteComponent);
230
+ }
231
+ // Finally, handle remaining single Z5 images
232
+ for (let i = node.children.length - 1; i >= 0; i--) {
233
+ const child = node.children[i];
234
+ if (isZ5ImageParagraph(child) && child.type === 'paragraph') {
235
+ const imageNode = child.children[0];
236
+ const imageData = collectImageData([imageNode], existingKeys);
237
+ const svelteComponent = createZone5Component(imageData, zone5mode);
238
+ // Replace the single image paragraph with the svelte component
239
+ node.children.splice(i, 1, svelteComponent);
240
+ }
241
+ }
242
+ });
243
+ // Store import map on the VFile's data
244
+ if (!file.data) {
245
+ file.data = {};
246
+ }
247
+ // Convert all svelteComponent nodes to HTML using svast-stringify
248
+ visit(rootTree, 'svelteComponent', (node, index, parent) => {
249
+ if (parent && typeof index === 'number') {
250
+ // Convert the svelteComponent node to HTML using svast-stringify
251
+ const componentHTML = svelteComponentToHTML(node);
252
+ // Create an HTML node with the stringified component
253
+ const htmlNode = {
254
+ type: 'html',
255
+ value: componentHTML,
256
+ };
257
+ // Replace the svelteComponent node with the HTML node
258
+ parent.children[index] = htmlNode;
259
+ }
260
+ });
261
+ // add script tag at beginning of markdown
262
+ if (Object.keys(importMap).length) {
263
+ // TODO: use existing script tag if any
264
+ const scriptNode = createScriptElement(importMap);
265
+ rootTree.children.unshift(scriptNode);
266
+ }
267
+ };
268
+ };
@@ -0,0 +1 @@
1
+ export { type Registry, default as registry } from './registry.svelte.js';
@@ -0,0 +1 @@
1
+ export { default as registry } from './registry.svelte.js';
@@ -0,0 +1,22 @@
1
+ import { type Readable } from 'svelte/store';
2
+ import type { ImageData } from '../components/types';
3
+ export type Registry = Readable<{
4
+ images: ImageData[];
5
+ current: ImageData | null;
6
+ currentOffset: number | null;
7
+ offsets: Map<symbol, {
8
+ start: number;
9
+ count: number;
10
+ }>;
11
+ }> & {
12
+ register: (componentId: symbol, images: ImageData[]) => void;
13
+ remove: (componentId: symbol) => void;
14
+ clear: () => void;
15
+ setCurrent: (componentId: symbol, offset: number) => void;
16
+ findCurrent: (id: string) => boolean;
17
+ next: () => void;
18
+ prev: () => void;
19
+ clearCurrent: () => void;
20
+ };
21
+ declare const registry: Registry;
22
+ export default registry;
@@ -0,0 +1,100 @@
1
+ import { SvelteMap } from 'svelte/reactivity';
2
+ import { get, writable } from 'svelte/store';
3
+ const store = writable({
4
+ images: [],
5
+ current: null,
6
+ currentOffset: null,
7
+ offsets: new Map(),
8
+ });
9
+ const mod = (n, m) => ((n % m) + m) % m;
10
+ const registry = {
11
+ subscribe: store.subscribe,
12
+ register: (componentId, images) => {
13
+ store.update((previous) => {
14
+ const offset = previous.offsets.get(componentId);
15
+ const start = offset?.start ?? previous.images.length;
16
+ const deleteCount = offset?.count ?? 0;
17
+ previous.offsets.set(componentId, { start, count: images.length });
18
+ return {
19
+ ...previous,
20
+ images: previous.images.toSpliced(start, deleteCount, ...images),
21
+ current: null,
22
+ };
23
+ });
24
+ },
25
+ remove: (componentId) => {
26
+ store.update((previous) => {
27
+ const offset = previous.offsets.get(componentId);
28
+ if (!offset) {
29
+ return previous;
30
+ }
31
+ return {
32
+ ...previous,
33
+ images: previous.images.toSpliced(offset.start, offset.count),
34
+ current: null,
35
+ };
36
+ });
37
+ },
38
+ clear: () => {
39
+ store.set({ images: [], current: null, currentOffset: null, offsets: new SvelteMap() });
40
+ },
41
+ setCurrent: (componentId, offset) => {
42
+ const registered = get(store).offsets.get(componentId);
43
+ if (!registered) {
44
+ throw new Error(`no component registered under given key.`);
45
+ }
46
+ if (offset < 0 || offset >= registered.count) {
47
+ throw new Error(`offset not within registered image count for component`);
48
+ }
49
+ const newCurrentIndex = registered.start + offset;
50
+ store.update((current) => ({
51
+ ...current,
52
+ current: current.images[newCurrentIndex],
53
+ currentOffset: newCurrentIndex,
54
+ }));
55
+ },
56
+ findCurrent: (id) => {
57
+ const value = get(store);
58
+ const index = value.images.findIndex((img) => img.id === id);
59
+ if (index >= 0 && value.currentOffset !== index) {
60
+ store.update((current) => ({
61
+ ...current,
62
+ current: current.images[index],
63
+ currentOffset: index,
64
+ }));
65
+ }
66
+ return index >= 0;
67
+ },
68
+ next: () => {
69
+ const current = get(store);
70
+ if (current.currentOffset === null) {
71
+ throw new Error('can not call next with not current image');
72
+ }
73
+ const newCurrentIndex = mod(current.currentOffset + 1, current.images.length);
74
+ store.update((current) => ({
75
+ ...current,
76
+ current: current.images[newCurrentIndex],
77
+ currentOffset: newCurrentIndex,
78
+ }));
79
+ },
80
+ prev: () => {
81
+ const current = get(store);
82
+ if (current.currentOffset === null) {
83
+ throw new Error('can not call prev with not current image');
84
+ }
85
+ const newCurrentIndex = mod(current.currentOffset - 1, current.images.length);
86
+ store.update((current) => ({
87
+ ...current,
88
+ current: current.images[newCurrentIndex],
89
+ currentOffset: newCurrentIndex,
90
+ }));
91
+ },
92
+ clearCurrent: () => {
93
+ store.update((current) => ({
94
+ ...current,
95
+ current: null,
96
+ currentOffset: null,
97
+ }));
98
+ },
99
+ };
100
+ export default registry;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Test setup file for Vitest
3
+ * Configures the environment for component testing
4
+ */
5
+ declare class ResizeObserverMock {
6
+ observe(): void;
7
+ unobserve(): void;
8
+ disconnect(): void;
9
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /**
3
+ * Test setup file for Vitest
4
+ * Configures the environment for component testing
5
+ */
6
+ // Mock ResizeObserver for jsdom
7
+ class ResizeObserverMock {
8
+ observe() { }
9
+ unobserve() { }
10
+ disconnect() { }
11
+ }
12
+ // Set up DOM environment
13
+ if (typeof global !== 'undefined') {
14
+ global.window = global.window || {};
15
+ global.ResizeObserver = ResizeObserverMock;
16
+ }
17
+ // Ensure we're in a browser-like environment
18
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
19
+ // Add ResizeObserver polyfill for jsdom
20
+ if (!window.ResizeObserver) {
21
+ window.ResizeObserver = ResizeObserverMock;
22
+ }
23
+ // Browser environment is ready
24
+ console.log('Test environment: Browser (jsdom)');
25
+ }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function zone5(cwd?: string): Plugin;