Skip to content

Commit edeb1f1

Browse files
authored
feat: graduate upload capability to unified Project.upload() method surface and bump version to 0.3.0 stable (#352)
1 parent 88ff68b commit edeb1f1

10 files changed

Lines changed: 100 additions & 144 deletions

File tree

packages/sdk/generated/domain-map.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
],
3737
"sideEffects": [
3838
{
39-
"method": "uploadImage",
39+
"method": "upload",
4040
"reason": "private_rest",
4141
"specPath": "src/spec/upload.ts"
4242
},

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@google/stitch-sdk",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"type": "module",
55
"private": false,
66
"description": "Generate UI screens from text prompts and extract their HTML and screenshots programmatically.",

packages/sdk/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ export type {
6363

6464
// Upload types
6565
export type {
66-
UploadImageInput,
67-
UploadImageResult,
68-
UploadImageErrorCode,
66+
UploadInput,
67+
UploadResult,
68+
UploadErrorCode,
6969
} from "./spec/upload.js";
7070

7171
// Download types

packages/sdk/src/project-ext.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,35 +31,26 @@ import { DownloadAssetsHandler } from './download-handler.js';
3131
import { DownloadAssetsInputSchema } from './spec/download.js';
3232
import type { DownloadAssetsOutput } from './spec/download.js';
3333
import {
34-
UploadImageInputSchema,
35-
type UploadImageInput,
34+
UploadInputSchema,
35+
type UploadInput,
3636
} from './spec/upload.js';
37-
import { UploadImageHandler } from './upload-handler.js';
37+
import { UploadHandler } from './upload-handler.js';
3838

3939
export class Project extends GeneratedProject {
40+
4041
/**
41-
* Upload an image file to the project and create a new Screen from it.
42-
*
43-
* WHY THIS IS NOT GENERATED:
44-
* BatchCreateScreens is a private REST endpoint — it has no MCP tool
45-
* in tools-manifest.json. It also requires reading a file from disk and
46-
* base64-encoding it, which the codegen arg-routing model cannot express.
47-
*
48-
* @param filePath - Absolute or relative path to the image (PNG, JPG, JPEG, WEBP).
49-
* @param opts - Optional screen title and createScreenInstances flag.
50-
* @returns An array of Screen objects created from the upload.
51-
* @throws {StitchError} on file not found, unsupported format, or upload failure.
42+
* Upload any supported design or document file asset (PNG, JPG, WEBP, HTML) into the project.
43+
* Creates a new screen canvas entity from the file contents.
5244
*
53-
* @example
54-
* const [screen] = await project.uploadImage('./mockup.png', { title: 'Home Screen' });
55-
* const html = await screen.getHtml();
45+
* @param filePath - Absolute or relative path to the asset file.
46+
* @param opts - Optional parameter overrides.
5647
*/
57-
async uploadImage(
48+
async upload(
5849
filePath: string,
59-
opts?: Partial<Omit<UploadImageInput, 'filePath'>>,
50+
opts?: Partial<Omit<UploadInput, 'filePath'>>,
6051
): Promise<Screen[]> {
61-
const input = UploadImageInputSchema.parse({ filePath, ...opts });
62-
const handler = new UploadImageHandler(this.client);
52+
const input = UploadInputSchema.parse({ filePath, ...opts });
53+
const handler = new UploadHandler(this.client);
6354
const result = await handler.execute(this.projectId, input);
6455

6556
if (!result.success) {

packages/sdk/src/spec/upload.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,43 +27,45 @@ export const SUPPORTED_MIME_TYPES = {
2727
'.jpg': 'image/jpeg',
2828
'.jpeg': 'image/jpeg',
2929
'.webp': 'image/webp',
30+
'.html': 'text/html',
31+
'.htm': 'text/html',
3032
} as const;
3133

3234
export type SupportedExtension = keyof typeof SUPPORTED_MIME_TYPES;
3335

3436
// ── Input ──────────────────────────────────────────────────────────────────────
3537

36-
export const UploadImageInputSchema = z.object({
37-
/** Absolute or relative path to the image file on disk. */
38+
export const UploadInputSchema = z.object({
39+
/** Absolute or relative path to the asset file on disk. */
3840
filePath: z.string().min(1),
3941
/** Optional display title for the created screen. */
4042
title: z.string().optional(),
4143
/** If true (default), creates screen instances on the project canvas. */
4244
createScreenInstances: z.boolean().default(true),
4345
});
4446

45-
export type UploadImageInput = z.infer<typeof UploadImageInputSchema>;
47+
export type UploadInput = z.infer<typeof UploadInputSchema>;
4648

4749
// ── Error Codes ────────────────────────────────────────────────────────────────
4850

49-
export const UploadImageErrorCode = z.enum([
51+
export const UploadErrorCode = z.enum([
5052
'FILE_NOT_FOUND',
5153
'UNSUPPORTED_FORMAT',
5254
'UPLOAD_FAILED',
5355
'AUTH_FAILED',
5456
'UNKNOWN_ERROR',
5557
]);
5658

57-
export type UploadImageErrorCode = z.infer<typeof UploadImageErrorCode>;
59+
export type UploadErrorCode = z.infer<typeof UploadErrorCode>;
5860

5961
// ── Result ─────────────────────────────────────────────────────────────────────
6062

61-
export type UploadImageResult =
63+
export type UploadResult =
6264
| { success: true; screens: Screen[] }
6365
| {
6466
success: false;
6567
error: {
66-
code: UploadImageErrorCode;
68+
code: UploadErrorCode;
6769
message: string;
6870
recoverable: boolean;
6971
};
@@ -72,9 +74,11 @@ export type UploadImageResult =
7274
// ── Interface ──────────────────────────────────────────────────────────────────
7375

7476
/**
75-
* Contract for the uploadImage operation.
76-
* Implementations must never throw — all failures return UploadImageResult.
77+
* Contract for the upload operation.
78+
* Implementations must never throw — all failures return UploadResult.
7779
*/
78-
export interface UploadImageSpec {
79-
execute(projectId: string, input: UploadImageInput): Promise<UploadImageResult>;
80+
export interface UploadSpec {
81+
execute(projectId: string, input: UploadInput): Promise<UploadResult>;
8082
}
83+
84+

packages/sdk/src/upload-handler.ts

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ import type { StitchToolClientSpec } from './spec/client.js';
4141
import {
4242
SUPPORTED_MIME_TYPES,
4343
type SupportedExtension,
44-
type UploadImageInput,
45-
type UploadImageResult,
46-
type UploadImageErrorCode,
47-
type UploadImageSpec,
44+
type UploadInput,
45+
type UploadResult,
46+
type UploadErrorCode,
47+
type UploadSpec,
4848
} from './spec/upload.js';
4949
import { Screen } from '../generated/src/screen.js';
5050

@@ -53,17 +53,25 @@ function buildBatchCreateScreensBody(
5353
projectId: string,
5454
fileContentBase64: string,
5555
mimeType: string,
56-
input: UploadImageInput,
56+
input: UploadInput,
5757
) {
58+
const fileObj = {
59+
fileContentBase64,
60+
mimeType,
61+
};
62+
63+
const isHtml = mimeType === 'text/html';
5864
const screen: Record<string, unknown> = {
59-
screenshot: {
60-
fileContentBase64,
61-
mimeType,
62-
},
63-
screenType: 'IMAGE',
65+
screenType: isHtml ? 'DOCUMENT' : 'IMAGE',
6466
isCreatedByClient: true,
6567
};
6668

69+
if (isHtml) {
70+
screen['htmlCode'] = fileObj;
71+
} else {
72+
screen['screenshot'] = fileObj;
73+
}
74+
6775
if (input.title) {
6876
screen['title'] = input.title;
6977
}
@@ -76,16 +84,14 @@ function buildBatchCreateScreensBody(
7684
}
7785

7886
/**
79-
* Handler for uploadImage — implements UploadImageSpec.
80-
*
81-
* Never throws. All failures are returned as UploadImageResult with a typed
82-
* error code. The caller (Project.uploadImage) surfaces failures as StitchError.
87+
* Handler for the upload capability — implements UploadSpec.
88+
* Never throws. All failures return an UploadResult value with a classified error code.
8389
*/
84-
export class UploadImageHandler implements UploadImageSpec {
90+
export class UploadHandler implements UploadSpec {
8591
constructor(private readonly client: StitchToolClientSpec) {}
8692

87-
async execute(projectId: string, input: UploadImageInput): Promise<UploadImageResult> {
88-
// ── Step 1: Validate extension → typed error code ────────────────────────
93+
async execute(projectId: string, input: UploadInput): Promise<UploadResult> {
94+
// ── Step 1: Validate extension ───────────────────────────────────────────
8995
const ext = path.extname(input.filePath).toLowerCase();
9096
const mimeType = SUPPORTED_MIME_TYPES[ext as SupportedExtension];
9197
if (!mimeType) {
@@ -117,12 +123,10 @@ export class UploadImageHandler implements UploadImageSpec {
117123
body,
118124
);
119125

120-
// ── Step 3: Project the response into Screen[] ───────────────────────
121-
// BatchCreateScreens returns { results: [{ screen: { ... } }] }
126+
// ── Step 3: Project the response into Screen[] ─────────────────────────
122127
const results: Array<{ screen: any }> = raw?.results ?? [];
123128
const screens: Screen[] = results.map((r) => {
124129
const screenData = { ...r.screen, projectId };
125-
// If API didn't return an ID but returned a file name, extract ID from it
126130
if (!screenData.id && screenData.screenshot?.name) {
127131
const parts = screenData.screenshot.name.split('/files/');
128132
if (parts.length === 2) {
@@ -132,7 +136,6 @@ export class UploadImageHandler implements UploadImageSpec {
132136
return new Screen(this.client as any, screenData);
133137
});
134138

135-
136139
return { success: true, screens };
137140
} catch (err) {
138141
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
@@ -146,7 +149,7 @@ export class UploadImageHandler implements UploadImageSpec {
146149
};
147150
}
148151
const msg = err instanceof Error ? err.message : String(err);
149-
const code: UploadImageErrorCode =
152+
const code: UploadErrorCode =
150153
msg.includes('401') || msg.includes('403') || msg.toLowerCase().includes('auth')
151154
? 'AUTH_FAILED'
152155
: 'UPLOAD_FAILED';
@@ -158,3 +161,5 @@ export class UploadImageHandler implements UploadImageSpec {
158161
}
159162
}
160163
}
164+
165+

packages/sdk/src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// Auto-generated by scripts/inject-version.ts — do not edit.
2-
export const SDK_VERSION = '0.2.0';
2+
export const SDK_VERSION = '0.3.0';

packages/sdk/test/unit/extension-resolution.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ describe('SDK Extension Resolution', () => {
1818
const project = projects[0];
1919

2020
// 4. Assert that the returned object is actually the extended subclass
21-
// By verifying the existence of the handwritten uploadImage method
21+
// By verifying the existence of the handwritten upload method
2222
expect(project).toBeDefined();
23-
expect(typeof project!.uploadImage).toBe('function');
23+
expect(typeof project!.upload).toBe('function');
2424
});
2525
});

packages/sdk/test/unit/sdk.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ describe("SDK Unit Tests", () => {
379379
expect(result).toEqual([]);
380380
});
381381

382+
382383
it("generate should throw StitchError on failure", async () => {
383384
const project = new Project(mockClient, projectId);
384385

0 commit comments

Comments
 (0)