@jay-framework/jay-stack-cli 0.16.0 → 0.16.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.
|
@@ -81,6 +81,37 @@ export const page = makeJayStackComponent<ProductPageContract>()
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Calling File Upload Actions
|
|
85
|
+
|
|
86
|
+
Actions created with `.withFiles()` accept browser `File` objects directly. Use `oninput` events on file inputs to drive signals, then pass them to the action:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { createSignal } from '@jay-framework/component';
|
|
90
|
+
import { uploadPhoto } from '../actions/upload.actions';
|
|
91
|
+
|
|
92
|
+
.withInteractive(function UploadPage(props, refs, fastViewState) {
|
|
93
|
+
const [result, setResult] = createSignal('');
|
|
94
|
+
const [selectedFile, setSelectedFile] = createSignal<File | undefined>(undefined);
|
|
95
|
+
|
|
96
|
+
refs.fileInput.oninput(({ event }) => {
|
|
97
|
+
setSelectedFile((event.target as HTMLInputElement).files?.[0]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
refs.uploadBtn.onclick(async () => {
|
|
101
|
+
const file = selectedFile();
|
|
102
|
+
if (!file) return;
|
|
103
|
+
const res = await uploadPhoto({ caption: 'My photo', photo: file });
|
|
104
|
+
setResult(res.message);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
render: () => ({ result: result() }),
|
|
109
|
+
};
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No casting needed — browser `File` is assignable to `JayFile`.
|
|
114
|
+
|
|
84
115
|
## Combining with Headless Plugins
|
|
85
116
|
|
|
86
117
|
A page component handles page-level data. Plugin headless components handle their own data independently. Both render into the same page:
|
|
@@ -38,12 +38,99 @@ makeJayAction('name')
|
|
|
38
38
|
.withServices(SERVICE1, SERVICE2) // Inject services
|
|
39
39
|
.withMethod('PUT') // Override HTTP method (default: POST for actions)
|
|
40
40
|
.withCaching({ maxAge: 60 }) // Enable caching (queries only)
|
|
41
|
+
.withFiles({ maxFileSize: 5_000_000 }) // Accept file uploads (multipart/form-data)
|
|
41
42
|
.withHandler(async (input, svc1, svc2) => {
|
|
42
43
|
// Define handler
|
|
43
44
|
return result;
|
|
44
45
|
});
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
## .withFiles() — File Uploads
|
|
49
|
+
|
|
50
|
+
Actions can receive binary files (images, documents, etc.) via multipart/form-data.
|
|
51
|
+
Add `.withFiles()` to the builder chain to enable file uploads.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { makeJayAction, type JayFile } from '@jay-framework/fullstack-component';
|
|
55
|
+
import fs from 'fs';
|
|
56
|
+
|
|
57
|
+
export const uploadPhoto = makeJayAction('photos.upload')
|
|
58
|
+
.withFiles({ maxFileSize: 5 * 1024 * 1024 }) // 5MB limit, 10 files max (default)
|
|
59
|
+
.withHandler(async (input: { caption: string; photo: JayFile }) => {
|
|
60
|
+
// JayFile provides: name, type, size, path (temp file on disk)
|
|
61
|
+
const data = fs.readFileSync(input.photo.path);
|
|
62
|
+
// Process file...
|
|
63
|
+
return { fileName: input.photo.name, size: input.photo.size };
|
|
64
|
+
// Temp file is automatically cleaned up after handler returns
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### JayFile type
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
interface JayFile {
|
|
72
|
+
name: string; // Original filename
|
|
73
|
+
type: string; // MIME type (e.g., 'image/png')
|
|
74
|
+
size: number; // File size in bytes
|
|
75
|
+
path: string; // Absolute path to temp file on disk
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Multiple files
|
|
80
|
+
|
|
81
|
+
Use an array type for the field — files with the same field name are grouped:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
.withHandler(async (input: { images: JayFile[] }) => {
|
|
85
|
+
for (const img of input.images) {
|
|
86
|
+
// Process each file
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**File fields must be top-level properties** — not nested inside objects. Dynamic file fields via index signatures are supported:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
{ notes: string; screenshot: JayFile; extras: JayFile[] } // OK
|
|
95
|
+
{ notes: string; [key: string]: string | JayFile | undefined } // OK
|
|
96
|
+
{ meta: { photo: JayFile } } // NOT supported
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Streaming with files
|
|
100
|
+
|
|
101
|
+
`makeJayStream` also supports `.withFiles()`:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
export const processImages = makeJayStream('images.process')
|
|
105
|
+
.withFiles()
|
|
106
|
+
.withHandler(async function* (input: { images: JayFile[] }) {
|
|
107
|
+
for (const img of input.images) {
|
|
108
|
+
yield { step: 'processing', fileName: img.name };
|
|
109
|
+
}
|
|
110
|
+
yield { step: 'done' };
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Client-side usage
|
|
115
|
+
|
|
116
|
+
The client automatically sends `FormData` when `File` or `Blob` objects are present:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
refs.uploadBtn.onClick(async () => {
|
|
120
|
+
const fileInput = refs.fileInput.element as HTMLInputElement;
|
|
121
|
+
const file = fileInput.files?.[0];
|
|
122
|
+
const result = await uploadPhoto({ caption: 'My photo', photo: file });
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### FileUploadOptions
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
.withFiles() // Defaults: 10MB per file, 10 files max
|
|
130
|
+
.withFiles({ maxFileSize: 2 * 1024 * 1024 }) // 2MB limit
|
|
131
|
+
.withFiles({ maxFileSize: 20_000_000, maxFiles: 5 }) // 20MB, 5 files
|
|
132
|
+
```
|
|
133
|
+
|
|
47
134
|
## ActionError
|
|
48
135
|
|
|
49
136
|
Throw typed errors from action handlers:
|
|
@@ -109,6 +196,8 @@ outputSchema:
|
|
|
109
196
|
| `prop: string` | Required string |
|
|
110
197
|
| `prop?: number` | Optional number |
|
|
111
198
|
| `prop: boolean` | Required boolean |
|
|
199
|
+
| `prop: file` | File upload (`JayFile`) |
|
|
200
|
+
| `prop: file[]` | Multiple file uploads |
|
|
112
201
|
| `prop: enum(a \| b \| c)` | Required enum |
|
|
113
202
|
| `prop:` + nested block | Nested object |
|
|
114
203
|
| `prop:` + `- child: type` | Array of objects |
|
|
@@ -167,6 +256,34 @@ outputSchema:
|
|
|
167
256
|
- slug: string
|
|
168
257
|
```
|
|
169
258
|
|
|
259
|
+
### .jay-action for file uploads
|
|
260
|
+
|
|
261
|
+
Use `file` type for upload fields. The generated TypeScript uses `JayFile`:
|
|
262
|
+
|
|
263
|
+
```yaml
|
|
264
|
+
name: uploadPhoto
|
|
265
|
+
description: Upload a product photo with caption
|
|
266
|
+
inputSchema:
|
|
267
|
+
caption: string
|
|
268
|
+
photo: file
|
|
269
|
+
attachments?: file[]
|
|
270
|
+
outputSchema:
|
|
271
|
+
fileId: string
|
|
272
|
+
message: string
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For dynamic file fields, use `record(file)`:
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
name: submitTask
|
|
279
|
+
description: Submit task with named file attachments
|
|
280
|
+
inputSchema:
|
|
281
|
+
notes: string
|
|
282
|
+
files: record(file)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
This generates `files: Record<string, JayFile>`.
|
|
286
|
+
|
|
170
287
|
## Type Helpers
|
|
171
288
|
|
|
172
289
|
```typescript
|
package/dist/index.js
CHANGED
|
@@ -4352,15 +4352,29 @@ async function runAction(actionRef, options, projectRoot, initializeServices) {
|
|
|
4352
4352
|
async function runParams(contractRef, options, projectRoot, initializeServices) {
|
|
4353
4353
|
let viteServer;
|
|
4354
4354
|
try {
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
)
|
|
4360
|
-
|
|
4355
|
+
let pluginName;
|
|
4356
|
+
let contractName;
|
|
4357
|
+
if (contractRef.startsWith("@")) {
|
|
4358
|
+
const secondSlash = contractRef.indexOf("/", contractRef.indexOf("/") + 1);
|
|
4359
|
+
if (secondSlash === -1) {
|
|
4360
|
+
getLogger().error(
|
|
4361
|
+
chalk.red("❌ Invalid contract reference. Use format: @scope/plugin/contract")
|
|
4362
|
+
);
|
|
4363
|
+
process.exit(1);
|
|
4364
|
+
}
|
|
4365
|
+
pluginName = contractRef.substring(0, secondSlash);
|
|
4366
|
+
contractName = contractRef.substring(secondSlash + 1);
|
|
4367
|
+
} else {
|
|
4368
|
+
const slashIndex = contractRef.indexOf("/");
|
|
4369
|
+
if (slashIndex === -1) {
|
|
4370
|
+
getLogger().error(
|
|
4371
|
+
chalk.red("❌ Invalid contract reference. Use format: <plugin>/<contract>")
|
|
4372
|
+
);
|
|
4373
|
+
process.exit(1);
|
|
4374
|
+
}
|
|
4375
|
+
pluginName = contractRef.substring(0, slashIndex);
|
|
4376
|
+
contractName = contractRef.substring(slashIndex + 1);
|
|
4361
4377
|
}
|
|
4362
|
-
const pluginName = contractRef.substring(0, slashIndex);
|
|
4363
|
-
const contractName = contractRef.substring(slashIndex + 1);
|
|
4364
4378
|
if (options.verbose) {
|
|
4365
4379
|
getLogger().info("Starting Vite for TypeScript support...");
|
|
4366
4380
|
}
|
|
@@ -4395,21 +4409,21 @@ async function runParams(contractRef, options, projectRoot, initializeServices)
|
|
|
4395
4409
|
}
|
|
4396
4410
|
const { resolveServices } = await import("@jay-framework/stack-server-runtime");
|
|
4397
4411
|
const resolvedServices = resolveServices(component.services || []);
|
|
4398
|
-
const allParams = [];
|
|
4399
4412
|
const paramsGenerator = component.loadParams(resolvedServices);
|
|
4413
|
+
let total = 0;
|
|
4400
4414
|
for await (const batch of paramsGenerator) {
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4415
|
+
for (const params of batch) {
|
|
4416
|
+
if (options.yaml) {
|
|
4417
|
+
getLogger().important(YAML.stringify([params]).trimEnd());
|
|
4418
|
+
} else {
|
|
4419
|
+
getLogger().important(JSON.stringify(params));
|
|
4420
|
+
}
|
|
4421
|
+
total++;
|
|
4422
|
+
}
|
|
4407
4423
|
}
|
|
4408
4424
|
if (!options.yaml) {
|
|
4409
|
-
getLogger().important(
|
|
4410
|
-
|
|
4411
|
-
✅ Found ${allParams.length} param combination(s)`)
|
|
4412
|
-
);
|
|
4425
|
+
getLogger().important(chalk.green(`
|
|
4426
|
+
✅ Found ${total} param combination(s)`));
|
|
4413
4427
|
}
|
|
4414
4428
|
} catch (error) {
|
|
4415
4429
|
getLogger().error(chalk.red("❌ Failed to discover params:") + " " + error.message);
|
|
@@ -4784,8 +4798,8 @@ program.command("action <plugin/action>").description(
|
|
|
4784
4798
|
await runAction(actionRef, options, process.cwd(), initializeServicesForCli);
|
|
4785
4799
|
});
|
|
4786
4800
|
program.command("params <plugin/contract>").description(
|
|
4787
|
-
"Discover load param values for a contract (
|
|
4788
|
-
).option("--yaml", "Output
|
|
4801
|
+
"Discover load param values for a contract. Streams results as NDJSON (one JSON object per line) or YAML."
|
|
4802
|
+
).option("--yaml", "Output as YAML instead of NDJSON").option("-v, --verbose", "Show detailed output").action(async (contractRef, options) => {
|
|
4789
4803
|
await runParams(contractRef, options, process.cwd(), initializeServicesForCli);
|
|
4790
4804
|
});
|
|
4791
4805
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/jay-stack-cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"test:watch": "vitest"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@jay-framework/compiler-jay-html": "^0.16.
|
|
28
|
-
"@jay-framework/compiler-shared": "^0.16.
|
|
29
|
-
"@jay-framework/dev-server": "^0.16.
|
|
30
|
-
"@jay-framework/editor-server": "^0.16.
|
|
31
|
-
"@jay-framework/fullstack-component": "^0.16.
|
|
32
|
-
"@jay-framework/logger": "^0.16.
|
|
33
|
-
"@jay-framework/plugin-validator": "^0.16.
|
|
34
|
-
"@jay-framework/stack-server-runtime": "^0.16.
|
|
27
|
+
"@jay-framework/compiler-jay-html": "^0.16.1",
|
|
28
|
+
"@jay-framework/compiler-shared": "^0.16.1",
|
|
29
|
+
"@jay-framework/dev-server": "^0.16.1",
|
|
30
|
+
"@jay-framework/editor-server": "^0.16.1",
|
|
31
|
+
"@jay-framework/fullstack-component": "^0.16.1",
|
|
32
|
+
"@jay-framework/logger": "^0.16.1",
|
|
33
|
+
"@jay-framework/plugin-validator": "^0.16.1",
|
|
34
|
+
"@jay-framework/stack-server-runtime": "^0.16.1",
|
|
35
35
|
"chalk": "^4.1.2",
|
|
36
36
|
"commander": "^14.0.0",
|
|
37
37
|
"express": "^5.0.1",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"yaml": "^2.3.4"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@jay-framework/dev-environment": "^0.16.
|
|
45
|
+
"@jay-framework/dev-environment": "^0.16.1",
|
|
46
46
|
"@types/express": "^5.0.2",
|
|
47
47
|
"@types/node": "^22.15.21",
|
|
48
48
|
"nodemon": "^3.0.3",
|