@jay-framework/jay-stack-cli 0.16.0 → 0.16.2

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
- const slashIndex = contractRef.indexOf("/");
4356
- if (slashIndex === -1) {
4357
- getLogger().error(
4358
- chalk.red(" Invalid contract reference. Use format: <plugin>/<contract>")
4359
- );
4360
- process.exit(1);
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
- allParams.push(...batch);
4402
- }
4403
- if (options.yaml) {
4404
- getLogger().important(YAML.stringify(allParams));
4405
- } else {
4406
- getLogger().important(JSON.stringify(allParams, null, 2));
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
- chalk.green(`
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 (e.g., jay-stack params wix-stores/product-page)"
4788
- ).option("--yaml", "Output result as YAML instead of JSON").option("-v, --verbose", "Show detailed output").action(async (contractRef, options) => {
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.0",
3
+ "version": "0.16.2",
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.0",
28
- "@jay-framework/compiler-shared": "^0.16.0",
29
- "@jay-framework/dev-server": "^0.16.0",
30
- "@jay-framework/editor-server": "^0.16.0",
31
- "@jay-framework/fullstack-component": "^0.16.0",
32
- "@jay-framework/logger": "^0.16.0",
33
- "@jay-framework/plugin-validator": "^0.16.0",
34
- "@jay-framework/stack-server-runtime": "^0.16.0",
27
+ "@jay-framework/compiler-jay-html": "^0.16.2",
28
+ "@jay-framework/compiler-shared": "^0.16.2",
29
+ "@jay-framework/dev-server": "^0.16.2",
30
+ "@jay-framework/editor-server": "^0.16.2",
31
+ "@jay-framework/fullstack-component": "^0.16.2",
32
+ "@jay-framework/logger": "^0.16.2",
33
+ "@jay-framework/plugin-validator": "^0.16.2",
34
+ "@jay-framework/stack-server-runtime": "^0.16.2",
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.0",
45
+ "@jay-framework/dev-environment": "^0.16.2",
46
46
  "@types/express": "^5.0.2",
47
47
  "@types/node": "^22.15.21",
48
48
  "nodemon": "^3.0.3",