@jskit-ai/uploads-image-web 0.1.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,80 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@jskit-ai/uploads-image-web",
4
+ version: "0.1.1",
5
+ kind: "runtime",
6
+ description: "Reusable client-side image upload runtime with pre-upload image editing.",
7
+ dependsOn: [
8
+ "@jskit-ai/uploads-runtime"
9
+ ],
10
+ capabilities: {
11
+ provides: [
12
+ "runtime.uploads.image-web"
13
+ ],
14
+ requires: []
15
+ },
16
+ runtime: {
17
+ server: {
18
+ providers: []
19
+ },
20
+ client: {
21
+ providers: []
22
+ }
23
+ },
24
+ metadata: {
25
+ client: {
26
+ optimizeDeps: {
27
+ include: [
28
+ "@uppy/core",
29
+ "@uppy/dashboard",
30
+ "@uppy/image-editor",
31
+ "@uppy/compressor",
32
+ "@uppy/xhr-upload"
33
+ ]
34
+ }
35
+ },
36
+ apiSummary: {
37
+ surfaces: [
38
+ {
39
+ subpath: "./client",
40
+ summary: "Exports reusable image upload client runtime helpers."
41
+ },
42
+ {
43
+ subpath: "./client/composables/createImageUploadRuntime",
44
+ summary: "Exports the reusable image upload runtime factory."
45
+ },
46
+ {
47
+ subpath: "./client/styles",
48
+ summary: "Exports Uppy CSS side effects for image upload UIs."
49
+ },
50
+ {
51
+ subpath: "./shared",
52
+ summary: "Exports shared image upload defaults."
53
+ }
54
+ ],
55
+ containerTokens: {
56
+ server: [],
57
+ client: []
58
+ }
59
+ }
60
+ },
61
+ mutations: {
62
+ dependencies: {
63
+ runtime: {
64
+ "@jskit-ai/uploads-runtime": "0.1.1",
65
+ "@uppy/compressor": "^3.1.0",
66
+ "@uppy/core": "^5.2.0",
67
+ "@uppy/dashboard": "^5.1.1",
68
+ "@uppy/image-editor": "^4.2.0",
69
+ "@uppy/xhr-upload": "^5.1.1"
70
+ },
71
+ dev: {}
72
+ },
73
+ packageJson: {
74
+ scripts: {}
75
+ },
76
+ procfile: {},
77
+ files: [],
78
+ text: []
79
+ }
80
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@jskit-ai/uploads-image-web",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ },
8
+ "exports": {
9
+ "./client": "./src/client/index.js",
10
+ "./client/composables/createImageUploadRuntime": "./src/client/composables/createImageUploadRuntime.js",
11
+ "./client/styles": "./src/client/styles/index.js",
12
+ "./shared": "./src/shared/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@jskit-ai/uploads-runtime": "0.1.1",
16
+ "@uppy/compressor": "^3.1.0",
17
+ "@uppy/core": "^5.2.0",
18
+ "@uppy/dashboard": "^5.1.1",
19
+ "@uppy/image-editor": "^4.2.0",
20
+ "@uppy/xhr-upload": "^5.1.1"
21
+ }
22
+ }
@@ -0,0 +1,245 @@
1
+ import Uppy from "@uppy/core";
2
+ import Dashboard from "@uppy/dashboard";
3
+ import ImageEditor from "@uppy/image-editor";
4
+ import Compressor from "@uppy/compressor";
5
+ import XHRUpload from "@uppy/xhr-upload";
6
+ import {
7
+ DEFAULT_IMAGE_COMPRESSOR_OPTIONS,
8
+ DEFAULT_IMAGE_DASHBOARD_OPTIONS,
9
+ DEFAULT_IMAGE_EDITOR_OPTIONS,
10
+ DEFAULT_IMAGE_UPLOAD_ALLOWED_MIME_TYPES,
11
+ DEFAULT_IMAGE_UPLOAD_MAX_BYTES
12
+ } from "../../shared/imageUploadDefaults.js";
13
+ import { parseUploadResponse } from "../support/parseUploadResponse.js";
14
+ import { stopImageEditor } from "../support/stopImageEditor.js";
15
+
16
+ function normalizeObject(value) {
17
+ return value && typeof value === "object" ? value : {};
18
+ }
19
+
20
+ function createImageUploadRuntime({
21
+ endpoint = "",
22
+ method = "POST",
23
+ fieldName = "file",
24
+ withCredentials = true,
25
+ maxNumberOfFiles = 1,
26
+ allowedMimeTypes = DEFAULT_IMAGE_UPLOAD_ALLOWED_MIME_TYPES,
27
+ maxUploadBytes = DEFAULT_IMAGE_UPLOAD_MAX_BYTES,
28
+ note = "",
29
+ dashboardOptions = {},
30
+ imageEditorOptions = {},
31
+ compressorOptions = {},
32
+ parseResponse = parseUploadResponse,
33
+ resolveRequestHeaders = null,
34
+ onSelectedFileNameChanged = null,
35
+ onUploadSuccess = null,
36
+ onInvalidResponse = null,
37
+ onUploadError = null,
38
+ onRestrictionFailed = null,
39
+ onUnavailable = null,
40
+ dependencies = {}
41
+ } = {}) {
42
+ const UppyClass = dependencies.UppyClass || Uppy;
43
+ const DashboardPlugin = dependencies.DashboardPlugin || Dashboard;
44
+ const ImageEditorPlugin = dependencies.ImageEditorPlugin || ImageEditor;
45
+ const CompressorPlugin = dependencies.CompressorPlugin || Compressor;
46
+ const XHRUploadPlugin = dependencies.XHRUploadPlugin || XHRUpload;
47
+ let uploadUppy = null;
48
+
49
+ const normalizedAllowedMimeTypes =
50
+ Array.isArray(allowedMimeTypes) && allowedMimeTypes.length > 0
51
+ ? allowedMimeTypes.map((value) => String(value || "").trim()).filter(Boolean)
52
+ : [...DEFAULT_IMAGE_UPLOAD_ALLOWED_MIME_TYPES];
53
+ const normalizedMaxUploadBytes =
54
+ Number.isInteger(maxUploadBytes) && maxUploadBytes > 0 ? maxUploadBytes : DEFAULT_IMAGE_UPLOAD_MAX_BYTES;
55
+ const normalizedEndpoint = String(endpoint || "").trim();
56
+ const normalizedFieldName = String(fieldName || "file").trim() || "file";
57
+ const normalizedMethod = String(method || "POST").trim().toUpperCase() || "POST";
58
+ const dashboardNote =
59
+ String(note || "").trim() ||
60
+ `Accepted: ${normalizedAllowedMimeTypes.join(", ")}, max ${Math.floor(normalizedMaxUploadBytes / (1024 * 1024))}MB`;
61
+
62
+ function emitSelectedFileName(fileName) {
63
+ if (typeof onSelectedFileNameChanged === "function") {
64
+ onSelectedFileNameChanged(String(fileName || ""));
65
+ }
66
+ }
67
+
68
+ function setup() {
69
+ if (typeof window === "undefined") {
70
+ return;
71
+ }
72
+
73
+ if (uploadUppy) {
74
+ return;
75
+ }
76
+
77
+ const uppy = new UppyClass({
78
+ autoProceed: false,
79
+ restrictions: {
80
+ maxNumberOfFiles: Number.isInteger(maxNumberOfFiles) && maxNumberOfFiles > 0 ? maxNumberOfFiles : 1,
81
+ allowedFileTypes: [...normalizedAllowedMimeTypes],
82
+ maxFileSize: normalizedMaxUploadBytes
83
+ }
84
+ });
85
+
86
+ uppy.use(DashboardPlugin, {
87
+ ...DEFAULT_IMAGE_DASHBOARD_OPTIONS,
88
+ ...normalizeObject(dashboardOptions),
89
+ note: dashboardNote,
90
+ doneButtonHandler: () => {
91
+ const dashboard = uppy.getPlugin("Dashboard");
92
+ if (dashboard && typeof dashboard.closeModal === "function") {
93
+ dashboard.closeModal();
94
+ }
95
+ }
96
+ });
97
+
98
+ uppy.use(ImageEditorPlugin, {
99
+ ...DEFAULT_IMAGE_EDITOR_OPTIONS,
100
+ ...normalizeObject(imageEditorOptions)
101
+ });
102
+
103
+ uppy.use(CompressorPlugin, {
104
+ ...DEFAULT_IMAGE_COMPRESSOR_OPTIONS,
105
+ ...normalizeObject(compressorOptions)
106
+ });
107
+
108
+ uppy.use(XHRUploadPlugin, {
109
+ endpoint: normalizedEndpoint,
110
+ method: normalizedMethod,
111
+ formData: true,
112
+ fieldName: normalizedFieldName,
113
+ withCredentials: withCredentials !== false,
114
+ onBeforeRequest: async (xhr) => {
115
+ const headers =
116
+ typeof resolveRequestHeaders === "function"
117
+ ? normalizeObject(await resolveRequestHeaders({ xhr, uppy }))
118
+ : {};
119
+ for (const [headerName, headerValue] of Object.entries(headers)) {
120
+ if (headerValue == null) {
121
+ continue;
122
+ }
123
+ xhr.setRequestHeader(headerName, String(headerValue));
124
+ }
125
+ },
126
+ getResponseData: parseResponse
127
+ });
128
+
129
+ uppy.on("file-added", (file) => {
130
+ emitSelectedFileName(file?.name || "");
131
+ });
132
+
133
+ uppy.on("file-removed", () => {
134
+ emitSelectedFileName("");
135
+ });
136
+
137
+ uppy.on("file-editor:complete", (file) => {
138
+ emitSelectedFileName(file?.name || "");
139
+ stopImageEditor(uppy);
140
+ });
141
+
142
+ uppy.on("file-editor:cancel", () => {
143
+ stopImageEditor(uppy);
144
+ });
145
+
146
+ uppy.on("dashboard:modal-closed", () => {
147
+ stopImageEditor(uppy);
148
+ });
149
+
150
+ uppy.on("upload-success", (file, response) => {
151
+ const data = response?.body;
152
+ if (!data || typeof data !== "object") {
153
+ if (typeof onInvalidResponse === "function") {
154
+ onInvalidResponse({
155
+ file,
156
+ response,
157
+ uppy
158
+ });
159
+ }
160
+ return;
161
+ }
162
+
163
+ if (typeof onUploadSuccess === "function") {
164
+ onUploadSuccess({
165
+ data,
166
+ file,
167
+ response,
168
+ uppy
169
+ });
170
+ }
171
+
172
+ emitSelectedFileName("");
173
+ });
174
+
175
+ uppy.on("upload-error", (file, error, response) => {
176
+ if (typeof onUploadError === "function") {
177
+ onUploadError({
178
+ file,
179
+ error,
180
+ response,
181
+ uppy
182
+ });
183
+ }
184
+ });
185
+
186
+ uppy.on("restriction-failed", (file, error) => {
187
+ if (typeof onRestrictionFailed === "function") {
188
+ onRestrictionFailed({
189
+ file,
190
+ error,
191
+ uppy
192
+ });
193
+ }
194
+ });
195
+
196
+ uppy.on("complete", (result) => {
197
+ const successfulCount = Array.isArray(result?.successful) ? result.successful.length : 0;
198
+ if (successfulCount <= 0) {
199
+ return;
200
+ }
201
+
202
+ try {
203
+ uppy.clear();
204
+ } catch {
205
+ // Upload succeeded; ignore clear timing issues.
206
+ }
207
+ });
208
+
209
+ uploadUppy = uppy;
210
+ }
211
+
212
+ function openEditor() {
213
+ setup();
214
+
215
+ const uppy = uploadUppy;
216
+ if (!uppy) {
217
+ if (typeof onUnavailable === "function") {
218
+ onUnavailable();
219
+ }
220
+ return;
221
+ }
222
+
223
+ const dashboard = uppy.getPlugin("Dashboard");
224
+ if (dashboard && typeof dashboard.openModal === "function") {
225
+ dashboard.openModal();
226
+ }
227
+ }
228
+
229
+ function destroy() {
230
+ if (!uploadUppy) {
231
+ return;
232
+ }
233
+
234
+ uploadUppy.destroy();
235
+ uploadUppy = null;
236
+ }
237
+
238
+ return Object.freeze({
239
+ destroy,
240
+ openEditor,
241
+ setup
242
+ });
243
+ }
244
+
245
+ export { createImageUploadRuntime };
@@ -0,0 +1,5 @@
1
+ export { createImageUploadRuntime } from "./composables/createImageUploadRuntime.js";
2
+
3
+ const clientProviders = Object.freeze([]);
4
+
5
+ export { clientProviders };
@@ -0,0 +1,5 @@
1
+ import "@uppy/core/css/style.min.css";
2
+ import "@uppy/dashboard/css/style.min.css";
3
+ import "@uppy/image-editor/css/style.min.css";
4
+
5
+ export {};
@@ -0,0 +1,13 @@
1
+ function parseUploadResponse(xhr) {
2
+ if (!xhr?.responseText) {
3
+ return {};
4
+ }
5
+
6
+ try {
7
+ return JSON.parse(xhr.responseText);
8
+ } catch {
9
+ return {};
10
+ }
11
+ }
12
+
13
+ export { parseUploadResponse };
@@ -0,0 +1,8 @@
1
+ function stopImageEditor(uppy) {
2
+ const imageEditor = uppy?.getPlugin?.("ImageEditor");
3
+ if (imageEditor && typeof imageEditor.stop === "function") {
4
+ imageEditor.stop();
5
+ }
6
+ }
7
+
8
+ export { stopImageEditor };
@@ -0,0 +1,29 @@
1
+ import {
2
+ DEFAULT_IMAGE_UPLOAD_ALLOWED_MIME_TYPES,
3
+ DEFAULT_IMAGE_UPLOAD_MAX_BYTES
4
+ } from "@jskit-ai/uploads-runtime/shared";
5
+
6
+ const DEFAULT_IMAGE_EDITOR_OPTIONS = Object.freeze({
7
+ quality: 0.9
8
+ });
9
+
10
+ const DEFAULT_IMAGE_COMPRESSOR_OPTIONS = Object.freeze({
11
+ quality: 0.84,
12
+ limit: 1
13
+ });
14
+
15
+ const DEFAULT_IMAGE_DASHBOARD_OPTIONS = Object.freeze({
16
+ inline: false,
17
+ closeAfterFinish: false,
18
+ showProgressDetails: true,
19
+ proudlyDisplayPoweredByUppy: false,
20
+ hideUploadButton: false
21
+ });
22
+
23
+ export {
24
+ DEFAULT_IMAGE_COMPRESSOR_OPTIONS,
25
+ DEFAULT_IMAGE_DASHBOARD_OPTIONS,
26
+ DEFAULT_IMAGE_EDITOR_OPTIONS,
27
+ DEFAULT_IMAGE_UPLOAD_ALLOWED_MIME_TYPES,
28
+ DEFAULT_IMAGE_UPLOAD_MAX_BYTES
29
+ };
@@ -0,0 +1,7 @@
1
+ export {
2
+ DEFAULT_IMAGE_COMPRESSOR_OPTIONS,
3
+ DEFAULT_IMAGE_DASHBOARD_OPTIONS,
4
+ DEFAULT_IMAGE_EDITOR_OPTIONS,
5
+ DEFAULT_IMAGE_UPLOAD_ALLOWED_MIME_TYPES,
6
+ DEFAULT_IMAGE_UPLOAD_MAX_BYTES
7
+ } from "./imageUploadDefaults.js";
@@ -0,0 +1,171 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createImageUploadRuntime } from "../src/client/composables/createImageUploadRuntime.js";
4
+
5
+ class FakeUppy {
6
+ constructor(options) {
7
+ this.options = options;
8
+ this.plugins = new Map();
9
+ this.pluginOptions = new Map();
10
+ this.eventHandlers = new Map();
11
+ this.clearCount = 0;
12
+ this.destroyCount = 0;
13
+ }
14
+
15
+ use(plugin, options) {
16
+ const pluginName = String(plugin?.name || plugin?.pluginName || "");
17
+ this.pluginOptions.set(pluginName, options);
18
+
19
+ if (pluginName === "Dashboard") {
20
+ this.plugins.set("Dashboard", {
21
+ openCount: 0,
22
+ closeCount: 0,
23
+ openModal() {
24
+ this.openCount += 1;
25
+ },
26
+ closeModal() {
27
+ this.closeCount += 1;
28
+ }
29
+ });
30
+ } else if (pluginName === "ImageEditor") {
31
+ this.plugins.set("ImageEditor", {
32
+ stopCount: 0,
33
+ stop() {
34
+ this.stopCount += 1;
35
+ }
36
+ });
37
+ }
38
+
39
+ return this;
40
+ }
41
+
42
+ on(eventName, handler) {
43
+ this.eventHandlers.set(eventName, handler);
44
+ return this;
45
+ }
46
+
47
+ getPlugin(name) {
48
+ return this.plugins.get(name) || null;
49
+ }
50
+
51
+ clear() {
52
+ this.clearCount += 1;
53
+ }
54
+
55
+ destroy() {
56
+ this.destroyCount += 1;
57
+ }
58
+ }
59
+
60
+ function setWindowStub() {
61
+ const previousWindow = globalThis.window;
62
+ globalThis.window = {};
63
+ return () => {
64
+ if (previousWindow === undefined) {
65
+ delete globalThis.window;
66
+ return;
67
+ }
68
+ globalThis.window = previousWindow;
69
+ };
70
+ }
71
+
72
+ test("createImageUploadRuntime wires headers and lifecycle callbacks", async () => {
73
+ const restoreWindow = setWindowStub();
74
+ let fakeUppy = null;
75
+
76
+ try {
77
+ const fileNames = [];
78
+ const successPayloads = [];
79
+ const invalidResponses = [];
80
+ const unavailableCalls = [];
81
+ const restrictionFailures = [];
82
+ const uploadErrors = [];
83
+
84
+ const runtime = createImageUploadRuntime({
85
+ endpoint: "/api/upload",
86
+ fieldName: "avatar",
87
+ resolveRequestHeaders: async () => ({
88
+ "csrf-token": "csrf-1"
89
+ }),
90
+ onSelectedFileNameChanged: (name) => {
91
+ fileNames.push(name);
92
+ },
93
+ onUploadSuccess: (payload) => {
94
+ successPayloads.push(payload.data);
95
+ },
96
+ onInvalidResponse: (payload) => {
97
+ invalidResponses.push(payload.response?.body || null);
98
+ },
99
+ onUploadError: (payload) => {
100
+ uploadErrors.push(payload.error?.message || "");
101
+ },
102
+ onRestrictionFailed: (payload) => {
103
+ restrictionFailures.push(payload.error?.message || "");
104
+ },
105
+ onUnavailable: () => {
106
+ unavailableCalls.push(true);
107
+ },
108
+ dependencies: {
109
+ UppyClass: class extends FakeUppy {
110
+ constructor(options) {
111
+ super(options);
112
+ fakeUppy = this;
113
+ }
114
+ },
115
+ DashboardPlugin: { name: "Dashboard" },
116
+ ImageEditorPlugin: { name: "ImageEditor" },
117
+ CompressorPlugin: { name: "Compressor" },
118
+ XHRUploadPlugin: { name: "XHRUpload" }
119
+ }
120
+ });
121
+
122
+ runtime.setup();
123
+ runtime.openEditor();
124
+
125
+ assert.ok(fakeUppy);
126
+ const pluginOptions = fakeUppy.pluginOptions || new Map();
127
+ const xhrOptions = pluginOptions.get("XHRUpload");
128
+
129
+ const headers = [];
130
+ await xhrOptions.onBeforeRequest({
131
+ setRequestHeader(name, value) {
132
+ headers.push([name, value]);
133
+ }
134
+ });
135
+ assert.deepEqual(headers, [["csrf-token", "csrf-1"]]);
136
+
137
+ fakeUppy.eventHandlers.get("file-added")({ name: "face.png" });
138
+ fakeUppy.eventHandlers.get("file-editor:complete")({ name: "face-edited.png" });
139
+ fakeUppy.eventHandlers.get("upload-success")({}, { body: { ok: true } });
140
+ fakeUppy.eventHandlers.get("upload-success")({}, { body: "" });
141
+ fakeUppy.eventHandlers.get("upload-error")({}, new Error("boom"), {});
142
+ fakeUppy.eventHandlers.get("restriction-failed")({}, new Error("nope"));
143
+ fakeUppy.eventHandlers.get("complete")({ successful: [{}] });
144
+
145
+ assert.deepEqual(fileNames, ["face.png", "face-edited.png", ""]);
146
+ assert.deepEqual(successPayloads, [{ ok: true }]);
147
+ assert.deepEqual(invalidResponses, [null]);
148
+ assert.deepEqual(uploadErrors, ["boom"]);
149
+ assert.deepEqual(restrictionFailures, ["nope"]);
150
+ assert.equal(fakeUppy.clearCount, 1);
151
+
152
+ runtime.destroy();
153
+ assert.equal(fakeUppy.destroyCount, 1);
154
+ assert.deepEqual(unavailableCalls, []);
155
+ } finally {
156
+ restoreWindow();
157
+ }
158
+ });
159
+
160
+ test("createImageUploadRuntime reports unavailable editor outside browser environments", () => {
161
+ const unavailableCalls = [];
162
+
163
+ const runtime = createImageUploadRuntime({
164
+ onUnavailable: () => {
165
+ unavailableCalls.push(true);
166
+ }
167
+ });
168
+
169
+ runtime.openEditor();
170
+ assert.deepEqual(unavailableCalls, [true]);
171
+ });
@@ -0,0 +1,23 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { evaluatePackageExportsContract } from "../../../tooling/test-support/exportsContract.mjs";
6
+
7
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
+ const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
9
+ const PACKAGE_DIR = path.join(REPO_ROOT, "packages", "uploads-image-web");
10
+
11
+ test("uploads-image-web exports are explicit and aligned with usage", () => {
12
+ const result = evaluatePackageExportsContract({
13
+ repoRoot: REPO_ROOT,
14
+ packageDir: PACKAGE_DIR,
15
+ packageId: "@jskit-ai/uploads-image-web",
16
+ requiredExports: ["./client", "./shared"]
17
+ });
18
+
19
+ assert.deepEqual(result.wildcardExports, []);
20
+ assert.deepEqual(result.missingRequiredExports, []);
21
+ assert.deepEqual(result.missingExports, []);
22
+ assert.deepEqual(result.staleExports, []);
23
+ });