@jay-framework/wix-media 0.19.1

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.
@@ -0,0 +1,105 @@
1
+ # Using Wix Media
2
+
3
+ How to use images, video, documents, and audio from Wix Media Manager in jay-html templates.
4
+
5
+ ## Finding Media
6
+
7
+ See the generated `references/wix-media/MEDIA-INDEX.md` for all available media with ready-to-use URLs.
8
+ Look up media by slug, then use the URL from the table.
9
+
10
+ ## Image URLs
11
+
12
+ ### Basic usage
13
+
14
+ ```html
15
+ <img src="https://static.wixstatic.com/media/{mediaId}" alt="description" />
16
+ ```
17
+
18
+ ### Transformed (recommended for performance)
19
+
20
+ URL format: `https://static.wixstatic.com/media/{mediaId}/v1/{mode}/{params}/file.{ext}`
21
+
22
+ #### Modes
23
+
24
+ - **fit** — scale to fit within dimensions, preserve aspect ratio (may add padding)
25
+ `<img src="https://static.wixstatic.com/media/{mediaId}/v1/fit/w_800,h_600/file.jpg" alt="" />`
26
+ - **fill** — scale to fill dimensions exactly, crop from center
27
+ `<img src="https://static.wixstatic.com/media/{mediaId}/v1/fill/w_800,h_600/file.jpg" alt="" />`
28
+ - **crop** — extract a rectangle at specific coordinates
29
+ `<img src="https://static.wixstatic.com/media/{mediaId}/v1/crop/x_100,y_50,w_800,h_600/file.jpg" alt="" />`
30
+
31
+ #### Parameters
32
+
33
+ - `w_{width}` — target width (1–5000 px)
34
+ - `h_{height}` — target height (1–5000 px)
35
+ - `x_{x},y_{y}` — crop start position (crop mode only)
36
+ - `q_{quality}` — JPEG quality (1–100)
37
+
38
+ #### Output format (set via file extension)
39
+
40
+ - `file.jpg` — JPEG (photos)
41
+ - `file.webp` — WebP (modern, smaller)
42
+ - `file.png` — PNG (transparency)
43
+ - `file.gif` — GIF (animation)
44
+
45
+ #### Common sizes
46
+
47
+ - Thumbnail: `/v1/fill/w_100,h_100/file.jpg`
48
+ - Card: `/v1/fill/w_400,h_300/file.jpg`
49
+ - Hero: `/v1/fill/w_1920,h_600/file.jpg`
50
+ - Full width: `/v1/fit/w_1200,h_800/file.webp`
51
+
52
+ ## Responsive Images
53
+
54
+ Use `srcset` and `sizes` to let the browser pick the best image size for the viewport. Generate multiple sizes from the same Wix media URL:
55
+
56
+ ```html
57
+ <img
58
+ src="{url}/v1/fill/w_400,h_400/file.webp"
59
+ srcset="{url}/v1/fill/w_300,h_300/file.webp 300w,
60
+ {url}/v1/fill/w_400,h_400/file.webp 400w,
61
+ {url}/v1/fill/w_600,h_600/file.webp 600w,
62
+ {url}/v1/fill/w_800,h_800/file.webp 800w"
63
+ sizes="(max-width: 600px) 100vw, (max-width: 900px) 50vw, 25vw"
64
+ alt="description"
65
+ />
66
+ ```
67
+
68
+ - `src` — fallback for browsers that don't support srcset
69
+ - `srcset` — candidate images with their width in pixels (`w` descriptor)
70
+ - `sizes` — tells the browser how wide the image will be at each breakpoint
71
+
72
+ Match `sizes` to your CSS layout (grid columns, container widths). The browser combines `sizes` with the device pixel ratio to pick the smallest sufficient image from `srcset`.
73
+
74
+ For hero/banner images that span the full viewport:
75
+
76
+ ```html
77
+ <img
78
+ src="{url}/v1/fill/w_1200,h_600/file.webp"
79
+ srcset="{url}/v1/fill/w_600,h_300/file.webp 600w,
80
+ {url}/v1/fill/w_1200,h_600/file.webp 1200w,
81
+ {url}/v1/fill/w_1920,h_960/file.webp 1920w"
82
+ sizes="100vw"
83
+ alt="description"
84
+ />
85
+ ```
86
+
87
+ ## Video
88
+
89
+ Use the media URL directly in `<video>` tags.
90
+ Poster image: append `/v1/fit/w_{w},h_{h}/file.jpg` to video mediaId.
91
+
92
+ ## Documents
93
+
94
+ URL format: `https://static.wixstatic.com/ugd/{mediaId}`
95
+
96
+ ## Audio
97
+
98
+ URL format: `https://static.wixstatic.com/mp3/{mediaId}`
99
+
100
+ ## Limits
101
+
102
+ - Max dimension: 5000px per side
103
+ - WebP max: 16,383px per side
104
+ - Images are not upscaled beyond original size
105
+ - Transformations only work with public Wix-hosted media
@@ -0,0 +1,35 @@
1
+ import { JayHtmlValidatorFn } from '@jay-framework/compiler-shared';
2
+ import { PluginSetupContext, PluginSetupResult, PluginReferencesContext, PluginReferencesResult } from '@jay-framework/stack-server-runtime';
3
+ import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
4
+
5
+ declare const validate: JayHtmlValidatorFn;
6
+
7
+ interface MediaFileInfo {
8
+ id: string;
9
+ displayName: string;
10
+ slug: string;
11
+ url: string;
12
+ mediaType: string;
13
+ width?: number;
14
+ height?: number;
15
+ labels: string[];
16
+ folderId: string;
17
+ folderName: string;
18
+ }
19
+
20
+ declare function generateMediaIndex(files: MediaFileInfo[]): string;
21
+
22
+ declare function setupWixMedia(ctx: PluginSetupContext): Promise<PluginSetupResult>;
23
+ declare function generateWixMediaReferences(ctx: PluginReferencesContext): Promise<PluginReferencesResult>;
24
+
25
+ declare const rebuildIndex: _jay_framework_fullstack_component.JayCliCommand<{}> & _jay_framework_fullstack_component.JayCliCommandDefinition<{}, [_jay_framework_fullstack_component.ConsoleContext]>;
26
+
27
+ declare const uploadPublic: _jay_framework_fullstack_component.JayCliCommand<{
28
+ folder?: string;
29
+ dryRun?: boolean;
30
+ }> & _jay_framework_fullstack_component.JayCliCommandDefinition<{
31
+ folder?: string;
32
+ dryRun?: boolean;
33
+ }, [_jay_framework_fullstack_component.ConsoleContext]>;
34
+
35
+ export { generateMediaIndex, generateWixMediaReferences, rebuildIndex, setupWixMedia, uploadPublic, validate };
package/dist/index.js ADDED
@@ -0,0 +1,440 @@
1
+ import { walkElements, resolveBinding } from "@jay-framework/compiler-shared";
2
+ import { parseTemplateParts } from "@jay-framework/compiler-jay-html";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import { registerService, getService } from "@jay-framework/stack-server-runtime";
6
+ import { WIX_CLIENT_SERVICE } from "@jay-framework/wix-server-client";
7
+ import { files, folders } from "@wix/media";
8
+ import { createJayService, makeCliCommand, CONSOLE_CONTEXT } from "@jay-framework/fullstack-component";
9
+ import * as fs$1 from "node:fs";
10
+ import * as path$1 from "node:path";
11
+ const WIXSTATIC_MEDIA_RE = /static\.wixstatic\.com\/media\//;
12
+ const V1_TRANSFORM_RE = /\/v1\//;
13
+ const IMAGE_EXTENSIONS_RE = /\.(jpe?g|png|gif|webp|svg|bmp|ico)$/i;
14
+ const MEDIA_SRC_ATTRS = ["src", "poster"];
15
+ const MEDIA_ELEMENTS = /* @__PURE__ */ new Set(["img", "video", "source"]);
16
+ function checkStaticWixUrl(value) {
17
+ return WIXSTATIC_MEDIA_RE.test(value) && !V1_TRANSFORM_RE.test(value);
18
+ }
19
+ function isLocalImagePath(value) {
20
+ if (!IMAGE_EXTENSIONS_RE.test(value)) return false;
21
+ if (value.startsWith("http://") || value.startsWith("https://")) return false;
22
+ return true;
23
+ }
24
+ function resolveBindingWithHeadless(bindingPath, scope, ctx) {
25
+ const resolved = resolveBinding(bindingPath, scope);
26
+ if (resolved.tag) return resolved;
27
+ const segments = bindingPath.split(".");
28
+ if (segments.length < 2) return resolved;
29
+ const headlessKey = segments[0];
30
+ const headless = ctx.headlessImports.find((h) => h.key === headlessKey && h.contract);
31
+ if (!headless?.contract) return resolved;
32
+ const remainingPath = segments.slice(1).join(".");
33
+ const headlessScope = { tags: headless.contract.tags };
34
+ return resolveBinding(remainingPath, headlessScope);
35
+ }
36
+ const validate = (ctx) => {
37
+ const findings = [];
38
+ walkElements(ctx.body, ctx, (el, scope) => {
39
+ const tagName = el.rawTagName?.toLowerCase();
40
+ if (!tagName || !MEDIA_ELEMENTS.has(tagName)) return;
41
+ for (const attr of MEDIA_SRC_ATTRS) {
42
+ const value = el.getAttribute(attr);
43
+ if (!value) continue;
44
+ const parts = parseTemplateParts(value);
45
+ const isFullyStatic = parts.every((p) => p.kind === "static");
46
+ if (isFullyStatic) {
47
+ const staticValue = parts.map((p) => p.value).join("");
48
+ if (checkStaticWixUrl(staticValue)) {
49
+ findings.push({
50
+ severity: "error",
51
+ message: "Wix media URL missing image optimization parameters. See agent-kit/wix-media.md for transformation reference.",
52
+ element: `<${tagName}>`,
53
+ attribute: attr
54
+ });
55
+ } else if (isLocalImagePath(staticValue)) {
56
+ findings.push({
57
+ severity: "error",
58
+ message: "Local image reference — upload to Wix Media Manager and use a Wix media URL with optimization parameters. See agent-kit/wix-media.md.",
59
+ element: `<${tagName}>`,
60
+ attribute: attr
61
+ });
62
+ }
63
+ continue;
64
+ }
65
+ for (let i = 0; i < parts.length; i++) {
66
+ const part = parts[i];
67
+ if (part.kind !== "binding") continue;
68
+ const resolved = resolveBindingWithHeadless(part.value, scope, ctx);
69
+ if (resolved.tag?.meta?.mediaType !== "wix-image") continue;
70
+ const followingStatic = parts.slice(i + 1).filter((p) => p.kind === "static").map((p) => p.value).join("");
71
+ if (!V1_TRANSFORM_RE.test(followingStatic)) {
72
+ findings.push({
73
+ severity: "error",
74
+ message: `Image binding '{${part.value}}' produces a Wix media URL but no optimization parameters are applied. See agent-kit/wix-media.md for transformation reference.`,
75
+ element: `<${tagName}>`,
76
+ attribute: attr
77
+ });
78
+ }
79
+ }
80
+ }
81
+ });
82
+ return findings;
83
+ };
84
+ function formatDimensions(file) {
85
+ if (file.width && file.height) {
86
+ return `${file.width}x${file.height}`;
87
+ }
88
+ return "-";
89
+ }
90
+ function formatLabels(labels) {
91
+ return labels.length > 0 ? labels.join(", ") : "-";
92
+ }
93
+ function escapeMarkdownCell(value) {
94
+ return value.replace(/\|/g, "\\|");
95
+ }
96
+ function generateMediaIndex(files2) {
97
+ const lines = [];
98
+ lines.push("# Available Media");
99
+ lines.push("");
100
+ lines.push("| Folder | Slug | Display Name | Type | Dimensions | Labels | URL |");
101
+ lines.push("| ------ | ---- | ------------ | ---- | ---------- | ------ | --- |");
102
+ for (const file of files2) {
103
+ lines.push(
104
+ `| ${escapeMarkdownCell(file.folderName)} | ${file.slug} | ${escapeMarkdownCell(file.displayName)} | ${file.mediaType} | ${formatDimensions(file)} | ${formatLabels(file.labels)} | ${file.url} |`
105
+ );
106
+ }
107
+ lines.push("");
108
+ return lines.join("\n");
109
+ }
110
+ const WIX_MEDIA_SERVICE_MARKER = createJayService("Wix Media Service");
111
+ let filesClientInstance;
112
+ let foldersClientInstance;
113
+ function getFilesClient(wixClient) {
114
+ if (!filesClientInstance) {
115
+ filesClientInstance = wixClient.use(files);
116
+ }
117
+ return filesClientInstance;
118
+ }
119
+ function getFoldersClient(wixClient) {
120
+ if (!foldersClientInstance) {
121
+ foldersClientInstance = wixClient.use(folders);
122
+ }
123
+ return foldersClientInstance;
124
+ }
125
+ function toSlug(displayName) {
126
+ return displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
127
+ }
128
+ function deduplicateSlugs(items) {
129
+ const seen = /* @__PURE__ */ new Map();
130
+ for (const item of items) {
131
+ const count = seen.get(item.slug) ?? 0;
132
+ if (count > 0) {
133
+ item.slug = `${item.slug}-${count + 1}`;
134
+ }
135
+ seen.set(item.slug, count + 1);
136
+ }
137
+ }
138
+ function extractDimensions(file) {
139
+ const url = file.url ?? "";
140
+ const widthMatch = url.match(/originWidth=(\d+)/);
141
+ const heightMatch = url.match(/originHeight=(\d+)/);
142
+ if (widthMatch && heightMatch) {
143
+ return { width: parseInt(widthMatch[1], 10), height: parseInt(heightMatch[1], 10) };
144
+ }
145
+ return {};
146
+ }
147
+ async function fetchAllFolders(foldersClient) {
148
+ const folderMap = /* @__PURE__ */ new Map();
149
+ folderMap.set("media-root", "Media Root");
150
+ let cursor;
151
+ do {
152
+ const result = await foldersClient.searchFolders({
153
+ rootFolder: "MEDIA_ROOT",
154
+ paging: cursor ? { cursor, limit: 100 } : { limit: 100 }
155
+ });
156
+ for (const folder of result.folders ?? []) {
157
+ if (folder._id && folder.displayName) {
158
+ folderMap.set(folder._id, folder.displayName);
159
+ }
160
+ }
161
+ cursor = result.nextCursor?.cursors?.next;
162
+ } while (cursor);
163
+ return folderMap;
164
+ }
165
+ async function fetchAllPublicFiles(filesClient) {
166
+ const allFiles = [];
167
+ let cursor;
168
+ do {
169
+ const result = await filesClient.searchFiles({
170
+ rootFolder: "MEDIA_ROOT",
171
+ private: false,
172
+ paging: cursor ? { cursor, limit: 100 } : { limit: 100 },
173
+ sort: { fieldName: "displayName", order: "ASC" }
174
+ });
175
+ for (const file of result.files ?? []) {
176
+ if (file.state !== "OK" || !file._id) continue;
177
+ allFiles.push({
178
+ id: file._id,
179
+ displayName: file.displayName ?? file._id,
180
+ url: file.url ?? "",
181
+ mediaType: (file.mediaType ?? "UNKNOWN").toLowerCase(),
182
+ labels: file.labels ?? [],
183
+ folderId: file.parentFolderId ?? "media-root",
184
+ media: file.media
185
+ });
186
+ }
187
+ cursor = result.nextCursor?.cursors?.next;
188
+ } while (cursor);
189
+ return allFiles;
190
+ }
191
+ function provideWixMediaService(wixClient) {
192
+ const filesClient = getFilesClient(wixClient);
193
+ const foldersClient = getFoldersClient(wixClient);
194
+ const service = {
195
+ async generateUploadUrl(mimeType, fileName) {
196
+ const result = await filesClient.generateFileUploadUrl(mimeType, {
197
+ fileName
198
+ });
199
+ return { uploadUrl: result.uploadUrl ?? "" };
200
+ },
201
+ async listPublicFiles() {
202
+ const [folderMap, rawFiles] = await Promise.all([
203
+ fetchAllFolders(foldersClient),
204
+ fetchAllPublicFiles(filesClient)
205
+ ]);
206
+ const items = rawFiles.map((file) => {
207
+ const dims = extractDimensions(file);
208
+ return {
209
+ id: file.id,
210
+ displayName: file.displayName,
211
+ slug: toSlug(file.displayName),
212
+ url: file.url,
213
+ mediaType: file.mediaType,
214
+ width: dims.width,
215
+ height: dims.height,
216
+ labels: file.labels,
217
+ folderId: file.folderId,
218
+ folderName: folderMap.get(file.folderId) ?? "Unknown"
219
+ };
220
+ });
221
+ deduplicateSlugs(items);
222
+ items.sort((a, b) => {
223
+ const folderCmp = a.folderName.localeCompare(b.folderName);
224
+ if (folderCmp !== 0) return folderCmp;
225
+ return a.slug.localeCompare(b.slug);
226
+ });
227
+ return items;
228
+ }
229
+ };
230
+ registerService(WIX_MEDIA_SERVICE_MARKER, service);
231
+ return service;
232
+ }
233
+ function createMediaService() {
234
+ const wixClient = getService(WIX_CLIENT_SERVICE);
235
+ return provideWixMediaService(wixClient);
236
+ }
237
+ async function setupWixMedia(ctx) {
238
+ if (ctx.initError) {
239
+ return {
240
+ status: "needs-config",
241
+ message: "wix-media requires wix-server-client to be configured first. Run: jay-stack setup wix-server-client"
242
+ };
243
+ }
244
+ try {
245
+ createMediaService();
246
+ } catch {
247
+ return {
248
+ status: "needs-config",
249
+ message: "wix-media requires wix-server-client to be configured first. Run: jay-stack setup wix-server-client"
250
+ };
251
+ }
252
+ return {
253
+ status: "configured",
254
+ message: "Wix Media Manager access verified."
255
+ };
256
+ }
257
+ async function generateWixMediaReferences(ctx) {
258
+ if (ctx.initError) {
259
+ throw new Error(`init failed: ${ctx.initError.message}`);
260
+ }
261
+ let mediaService;
262
+ try {
263
+ mediaService = createMediaService();
264
+ } catch {
265
+ throw new Error("WixMediaService not available. Run jay-stack setup first.");
266
+ }
267
+ fs.mkdirSync(ctx.referencesDir, { recursive: true });
268
+ const mediaFiles = await mediaService.listPublicFiles();
269
+ const mediaIndexContent = generateMediaIndex(mediaFiles);
270
+ const mediaIndexPath = path.join(ctx.referencesDir, "MEDIA-INDEX.md");
271
+ fs.writeFileSync(mediaIndexPath, mediaIndexContent, "utf-8");
272
+ return {
273
+ referencesCreated: [`agent-kit/references/${ctx.pluginName}/MEDIA-INDEX.md`],
274
+ message: `${mediaFiles.length} media files indexed.`
275
+ };
276
+ }
277
+ const rebuildIndex = makeCliCommand("rebuild-index").withServices(CONSOLE_CONTEXT).withHandler(async (input, console) => {
278
+ const wixClient = getService(WIX_CLIENT_SERVICE);
279
+ const mediaService = provideWixMediaService(wixClient);
280
+ console.log("Fetching media from Wix Media Manager...");
281
+ const files2 = await mediaService.listPublicFiles();
282
+ console.log(`Found ${files2.length} public media files.`);
283
+ const referencesDir = path$1.join(
284
+ console.projectRoot,
285
+ "agent-kit",
286
+ "references",
287
+ "wix-media"
288
+ );
289
+ fs$1.mkdirSync(referencesDir, { recursive: true });
290
+ const indexContent = generateMediaIndex(files2);
291
+ const indexPath = path$1.join(referencesDir, "MEDIA-INDEX.md");
292
+ fs$1.writeFileSync(indexPath, indexContent, "utf-8");
293
+ console.log(`Media index written to ${indexPath}`);
294
+ return { success: true };
295
+ });
296
+ const UPLOAD_INDEX_FILE = ".wix-media-uploads.json";
297
+ const MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
298
+ ".jpg",
299
+ ".jpeg",
300
+ ".png",
301
+ ".gif",
302
+ ".webp",
303
+ ".svg",
304
+ ".bmp",
305
+ ".ico",
306
+ ".mp4",
307
+ ".webm",
308
+ ".mov",
309
+ ".avi",
310
+ ".mp3",
311
+ ".wav",
312
+ ".ogg",
313
+ ".aac",
314
+ ".pdf",
315
+ ".doc",
316
+ ".docx"
317
+ ]);
318
+ function getMimeType(ext) {
319
+ const map = {
320
+ ".jpg": "image/jpeg",
321
+ ".jpeg": "image/jpeg",
322
+ ".png": "image/png",
323
+ ".gif": "image/gif",
324
+ ".webp": "image/webp",
325
+ ".svg": "image/svg+xml",
326
+ ".bmp": "image/bmp",
327
+ ".ico": "image/x-icon",
328
+ ".mp4": "video/mp4",
329
+ ".webm": "video/webm",
330
+ ".mov": "video/quicktime",
331
+ ".avi": "video/x-msvideo",
332
+ ".mp3": "audio/mpeg",
333
+ ".wav": "audio/wav",
334
+ ".ogg": "audio/ogg",
335
+ ".aac": "audio/aac",
336
+ ".pdf": "application/pdf",
337
+ ".doc": "application/msword",
338
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
339
+ };
340
+ return map[ext.toLowerCase()] ?? "application/octet-stream";
341
+ }
342
+ function loadUploadIndex(configDir) {
343
+ const indexPath = path$1.join(configDir, UPLOAD_INDEX_FILE);
344
+ if (fs$1.existsSync(indexPath)) {
345
+ return JSON.parse(fs$1.readFileSync(indexPath, "utf-8"));
346
+ }
347
+ return {};
348
+ }
349
+ function saveUploadIndex(configDir, index) {
350
+ fs$1.mkdirSync(configDir, { recursive: true });
351
+ const indexPath = path$1.join(configDir, UPLOAD_INDEX_FILE);
352
+ fs$1.writeFileSync(indexPath, JSON.stringify(index, null, 2), "utf-8");
353
+ }
354
+ function scanMediaFiles(dir, baseDir) {
355
+ const results = [];
356
+ if (!fs$1.existsSync(dir)) return results;
357
+ const entries = fs$1.readdirSync(dir, { withFileTypes: true });
358
+ for (const entry of entries) {
359
+ const fullPath = path$1.join(dir, entry.name);
360
+ if (entry.isDirectory()) {
361
+ results.push(...scanMediaFiles(fullPath, baseDir));
362
+ } else {
363
+ const ext = path$1.extname(entry.name).toLowerCase();
364
+ if (MEDIA_EXTENSIONS.has(ext)) {
365
+ results.push(path$1.relative(baseDir, fullPath));
366
+ }
367
+ }
368
+ }
369
+ return results;
370
+ }
371
+ const uploadPublic = makeCliCommand("upload-public").withServices(CONSOLE_CONTEXT).withHandler(async (input, console) => {
372
+ const wixClient = getService(WIX_CLIENT_SERVICE);
373
+ const mediaService = provideWixMediaService(wixClient);
374
+ const publicDir = console.publicFolder;
375
+ const scanDir = input.folder ? path$1.join(publicDir, input.folder) : publicDir;
376
+ const configDir = path$1.join(console.projectRoot, "config");
377
+ console.log(`Scanning ${scanDir} for media files...`);
378
+ const mediaFiles = scanMediaFiles(scanDir, publicDir);
379
+ if (mediaFiles.length === 0) {
380
+ console.log("No media files found.");
381
+ return { success: true };
382
+ }
383
+ console.log(`Found ${mediaFiles.length} media files.`);
384
+ const uploadIndex = loadUploadIndex(configDir);
385
+ const toUpload = mediaFiles.filter((f) => {
386
+ const key = `public/${f}`;
387
+ return !uploadIndex[key] || uploadIndex[key].status !== "uploaded";
388
+ });
389
+ if (toUpload.length === 0) {
390
+ console.log("All files already uploaded.");
391
+ return { success: true };
392
+ }
393
+ console.log(
394
+ `${toUpload.length} files to upload (${mediaFiles.length - toUpload.length} already uploaded).`
395
+ );
396
+ if (input.dryRun) {
397
+ for (const file of toUpload) {
398
+ console.log(`[dry-run] Would upload: ${file}`);
399
+ }
400
+ return { success: true };
401
+ }
402
+ let uploaded = 0;
403
+ let failed = 0;
404
+ for (const file of toUpload) {
405
+ const key = `public/${file}`;
406
+ const filePath = path$1.join(publicDir, file);
407
+ const ext = path$1.extname(file);
408
+ const mimeType = getMimeType(ext);
409
+ const fileName = path$1.basename(file);
410
+ try {
411
+ const { uploadUrl } = await mediaService.generateUploadUrl(mimeType, fileName);
412
+ const fileBuffer = fs$1.readFileSync(filePath);
413
+ const response = await fetch(uploadUrl, {
414
+ method: "PUT",
415
+ headers: { "Content-Type": mimeType },
416
+ body: fileBuffer
417
+ });
418
+ if (!response.ok) {
419
+ throw new Error(`Upload failed with status ${response.status}`);
420
+ }
421
+ uploadIndex[key] = { status: "uploaded" };
422
+ uploaded++;
423
+ console.log(`Uploaded: ${file}`);
424
+ } catch (err) {
425
+ failed++;
426
+ console.error(`Failed to upload ${file}: ${err.message}`);
427
+ }
428
+ }
429
+ saveUploadIndex(configDir, uploadIndex);
430
+ console.log(`Upload complete: ${uploaded} uploaded, ${failed} failed.`);
431
+ return { success: failed === 0 };
432
+ });
433
+ export {
434
+ generateMediaIndex,
435
+ generateWixMediaReferences,
436
+ rebuildIndex,
437
+ setupWixMedia,
438
+ uploadPublic,
439
+ validate
440
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@jay-framework/wix-media",
3
+ "version": "0.19.1",
4
+ "type": "module",
5
+ "description": "Wix Media Manager plugin for Jay Framework — media validation, index generation, and agent-kit references",
6
+ "license": "Apache-2.0",
7
+ "main": "dist/index.js",
8
+ "files": [
9
+ "dist",
10
+ "plugin.yaml",
11
+ "agent-kit"
12
+ ],
13
+ "exports": {
14
+ ".": "./dist/index.js",
15
+ "./plugin.yaml": "./plugin.yaml"
16
+ },
17
+ "scripts": {
18
+ "build": "npm run clean && npm run build:server && npm run build:types && npm run validate",
19
+ "validate": "jay-stack-cli validate-plugin",
20
+ "build:server": "vite build --ssr",
21
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
22
+ "build:check-types": "tsc",
23
+ "clean": "rimraf dist",
24
+ "confirm": "npm run clean && npm run build && npm run test",
25
+ "test": "vitest run"
26
+ },
27
+ "dependencies": {
28
+ "@jay-framework/compiler-jay-html": "^0.19.1",
29
+ "@jay-framework/compiler-shared": "^0.19.1",
30
+ "@jay-framework/component": "^0.19.1",
31
+ "@jay-framework/fullstack-component": "^0.19.1",
32
+ "@jay-framework/stack-server-runtime": "^0.19.1",
33
+ "@jay-framework/wix-server-client": "^0.19.1",
34
+ "@jay-framework/wix-utils": "^0.19.1",
35
+ "@wix/media": "^1.0.247",
36
+ "@wix/sdk": "^1.21.5"
37
+ },
38
+ "devDependencies": {
39
+ "@jay-framework/compiler-jay-stack": "^0.19.1",
40
+ "@jay-framework/vite-plugin": "^0.19.1",
41
+ "@types/node": "^20.11.0",
42
+ "node-html-parser": "^6.1.0",
43
+ "rimraf": "^5.0.5",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^5.3.3",
46
+ "vite": "^5.0.11",
47
+ "vitest": "^1.2.0"
48
+ }
49
+ }
package/plugin.yaml ADDED
@@ -0,0 +1,17 @@
1
+ name: wix-media
2
+
3
+ validators:
4
+ - name: media-optimization
5
+ handler: validate
6
+ description: Validates Wix media URLs have optimization parameters
7
+
8
+ commands:
9
+ - name: upload-public
10
+ command: upload-public.jay-command
11
+ - name: rebuild-index
12
+ command: rebuild-index.jay-command
13
+
14
+ setup:
15
+ handler: setupWixMedia
16
+ references: generateWixMediaReferences
17
+ description: Validates Wix Media Manager access, generates agent-kit media index