loopwind 0.25.6 → 0.25.7

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.
Files changed (90) hide show
  1. package/app/.astro/types.d.ts +1 -0
  2. package/app/dist/_astro/callback.Ci5gaEfJ.css +1 -0
  3. package/app/dist/auth/callback/index.html +81 -0
  4. package/app/dist/device/index.html +70 -0
  5. package/app/dist/index.html +327 -0
  6. package/app/package-lock.json +9239 -0
  7. package/app/package.json +23 -0
  8. package/app/wrangler.toml +8 -0
  9. package/dist/cli.js +54 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/login.d.ts +5 -0
  12. package/dist/commands/login.d.ts.map +1 -0
  13. package/dist/commands/login.js +60 -0
  14. package/dist/commands/login.js.map +1 -0
  15. package/dist/commands/logout.d.ts +5 -0
  16. package/dist/commands/logout.d.ts.map +1 -0
  17. package/dist/commands/logout.js +15 -0
  18. package/dist/commands/logout.js.map +1 -0
  19. package/dist/commands/publish.d.ts +10 -0
  20. package/dist/commands/publish.d.ts.map +1 -0
  21. package/dist/commands/publish.js +155 -0
  22. package/dist/commands/publish.js.map +1 -0
  23. package/dist/commands/templates.d.ts +5 -0
  24. package/dist/commands/templates.d.ts.map +1 -0
  25. package/dist/commands/templates.js +60 -0
  26. package/dist/commands/templates.js.map +1 -0
  27. package/dist/commands/unpublish.d.ts +5 -0
  28. package/dist/commands/unpublish.d.ts.map +1 -0
  29. package/dist/commands/unpublish.js +54 -0
  30. package/dist/commands/unpublish.js.map +1 -0
  31. package/dist/commands/whoami.d.ts +5 -0
  32. package/dist/commands/whoami.d.ts.map +1 -0
  33. package/dist/commands/whoami.js +30 -0
  34. package/dist/commands/whoami.js.map +1 -0
  35. package/dist/lib/api.d.ts +92 -0
  36. package/dist/lib/api.d.ts.map +1 -0
  37. package/dist/lib/api.js +149 -0
  38. package/dist/lib/api.js.map +1 -0
  39. package/dist/lib/auth.d.ts +41 -0
  40. package/dist/lib/auth.d.ts.map +1 -0
  41. package/dist/lib/auth.js +89 -0
  42. package/dist/lib/auth.js.map +1 -0
  43. package/dist/lib/bundler.d.ts +18 -0
  44. package/dist/lib/bundler.d.ts.map +1 -0
  45. package/dist/lib/bundler.js +105 -0
  46. package/dist/lib/bundler.js.map +1 -0
  47. package/dist/lib/helpers.d.ts +35 -2
  48. package/dist/lib/helpers.d.ts.map +1 -1
  49. package/dist/lib/helpers.js +91 -13
  50. package/dist/lib/helpers.js.map +1 -1
  51. package/dist/lib/utils.d.ts.map +1 -1
  52. package/dist/lib/utils.js +9 -0
  53. package/dist/lib/utils.js.map +1 -1
  54. package/dist/sdk/edge.d.ts +65 -0
  55. package/dist/sdk/edge.d.ts.map +1 -0
  56. package/dist/sdk/edge.js +329 -0
  57. package/dist/sdk/edge.js.map +1 -0
  58. package/dist/sdk/errors.d.ts +64 -0
  59. package/dist/sdk/errors.d.ts.map +1 -0
  60. package/dist/sdk/errors.js +94 -0
  61. package/dist/sdk/errors.js.map +1 -0
  62. package/dist/sdk/index.d.ts +29 -0
  63. package/dist/sdk/index.d.ts.map +1 -0
  64. package/dist/sdk/index.js +30 -0
  65. package/dist/sdk/index.js.map +1 -0
  66. package/dist/sdk/render.d.ts +52 -0
  67. package/dist/sdk/render.d.ts.map +1 -0
  68. package/dist/sdk/render.js +432 -0
  69. package/dist/sdk/render.js.map +1 -0
  70. package/dist/sdk/types.d.ts +185 -0
  71. package/dist/sdk/types.d.ts.map +1 -0
  72. package/dist/sdk/types.js +5 -0
  73. package/dist/sdk/types.js.map +1 -0
  74. package/dist/types/template.d.ts +18 -0
  75. package/dist/types/template.d.ts.map +1 -1
  76. package/package.json +26 -4
  77. package/plans/PLATFORM.md +1637 -237
  78. package/plans/PLATFORM_IMPLEMENTATION.md +1347 -530
  79. package/plans/SDK.md +797 -0
  80. package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite +0 -0
  81. package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite-shm +0 -0
  82. package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite-wal +0 -0
  83. package/platform/migrations/0001_initial.sql +90 -0
  84. package/platform/package-lock.json +3104 -0
  85. package/platform/package.json +30 -0
  86. package/platform/wrangler.toml +43 -0
  87. package/tests-sdk/createRenderer.test.ts +251 -0
  88. package/tests-sdk/errors.test.ts +230 -0
  89. package/tests-sdk/render.test.ts +241 -0
  90. package/tests-sdk/tw.test.ts +277 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * SDK Render Function Tests
3
+ */
4
+
5
+ import { test, describe } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import { render, ValidationError } from '../dist/sdk/index.js';
8
+ import React from 'react';
9
+
10
+ // Simple test template
11
+ const SimpleTemplate = ({ title, tw }: { title: string; tw: (c: string) => any }) => {
12
+ return React.createElement(
13
+ 'div',
14
+ { style: tw('w-full h-full bg-blue-500 flex items-center justify-center') },
15
+ React.createElement('h1', { style: tw('text-white text-4xl') }, title)
16
+ );
17
+ };
18
+
19
+ // Template with multiple props
20
+ const ComplexTemplate = ({
21
+ title,
22
+ subtitle,
23
+ showBadge,
24
+ tw,
25
+ }: {
26
+ title: string;
27
+ subtitle?: string;
28
+ showBadge?: boolean;
29
+ tw: (c: string) => any;
30
+ }) => {
31
+ return React.createElement(
32
+ 'div',
33
+ { style: tw('w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 p-12 flex flex-col') },
34
+ [
35
+ React.createElement('h1', { key: 'title', style: tw('text-white text-5xl font-bold') }, title),
36
+ subtitle && React.createElement('p', { key: 'sub', style: tw('text-white/80 text-xl mt-4') }, subtitle),
37
+ showBadge && React.createElement('span', { key: 'badge', style: tw('bg-white text-purple-500 px-4 py-2 rounded-full mt-4') }, 'NEW'),
38
+ ]
39
+ );
40
+ };
41
+
42
+ describe('render()', () => {
43
+ test('renders PNG with basic template', async () => {
44
+ const result = await render(SimpleTemplate, {
45
+ props: { title: 'Hello World' },
46
+ width: 800,
47
+ height: 400,
48
+ format: 'png',
49
+ });
50
+
51
+ assert.ok(result instanceof Buffer || result instanceof Uint8Array, 'Result should be a buffer');
52
+ assert.ok(result.length > 0, 'Result should not be empty');
53
+
54
+ // Check PNG magic bytes
55
+ const pngMagic = [0x89, 0x50, 0x4e, 0x47];
56
+ const header = Array.from(result.slice(0, 4));
57
+ assert.deepStrictEqual(header, pngMagic, 'Result should be valid PNG');
58
+ });
59
+
60
+ test('renders SVG with basic template', async () => {
61
+ const result = await render(SimpleTemplate, {
62
+ props: { title: 'Hello SVG' },
63
+ width: 800,
64
+ height: 400,
65
+ format: 'svg',
66
+ });
67
+
68
+ assert.ok(result instanceof Buffer || result instanceof Uint8Array, 'Result should be a buffer');
69
+
70
+ const svgString = result.toString('utf-8');
71
+ assert.ok(svgString.includes('<svg'), 'Result should contain SVG tag');
72
+ assert.ok(svgString.includes('</svg>'), 'Result should be valid SVG');
73
+ assert.ok(svgString.length > 500, 'SVG should have substantial content');
74
+ });
75
+
76
+ test('renders with complex props', async () => {
77
+ const result = await render(ComplexTemplate, {
78
+ props: {
79
+ title: 'Complex Test',
80
+ subtitle: 'With subtitle',
81
+ showBadge: true,
82
+ },
83
+ width: 1200,
84
+ height: 630,
85
+ format: 'png',
86
+ });
87
+
88
+ assert.ok(result instanceof Buffer || result instanceof Uint8Array);
89
+ assert.ok(result.length > 0);
90
+ });
91
+
92
+ test('uses default dimensions', async () => {
93
+ const result = await render(SimpleTemplate, {
94
+ props: { title: 'Default Size' },
95
+ format: 'svg',
96
+ });
97
+
98
+ const svgString = result.toString('utf-8');
99
+ // Default is 1200x630
100
+ assert.ok(svgString.includes('width="1200"'), 'Should use default width');
101
+ assert.ok(svgString.includes('height="630"'), 'Should use default height');
102
+ });
103
+
104
+ test('respects custom dimensions', async () => {
105
+ const result = await render(SimpleTemplate, {
106
+ props: { title: 'Custom Size' },
107
+ width: 500,
108
+ height: 250,
109
+ format: 'svg',
110
+ });
111
+
112
+ const svgString = result.toString('utf-8');
113
+ assert.ok(svgString.includes('width="500"'), 'Should use custom width');
114
+ assert.ok(svgString.includes('height="250"'), 'Should use custom height');
115
+ });
116
+
117
+ test('throws ValidationError for invalid width', async () => {
118
+ await assert.rejects(
119
+ async () => {
120
+ await render(SimpleTemplate, {
121
+ props: { title: 'Test' },
122
+ width: -100,
123
+ format: 'png',
124
+ });
125
+ },
126
+ (error: any) => {
127
+ assert.ok(error instanceof ValidationError);
128
+ assert.strictEqual(error.field, 'width');
129
+ return true;
130
+ }
131
+ );
132
+ });
133
+
134
+ test('throws ValidationError for invalid height', async () => {
135
+ await assert.rejects(
136
+ async () => {
137
+ await render(SimpleTemplate, {
138
+ props: { title: 'Test' },
139
+ height: 999999,
140
+ format: 'png',
141
+ });
142
+ },
143
+ (error: any) => {
144
+ assert.ok(error instanceof ValidationError);
145
+ assert.strictEqual(error.field, 'height');
146
+ return true;
147
+ }
148
+ );
149
+ });
150
+
151
+ test('throws ValidationError for invalid format', async () => {
152
+ await assert.rejects(
153
+ async () => {
154
+ await render(SimpleTemplate, {
155
+ props: { title: 'Test' },
156
+ format: 'invalid' as any,
157
+ });
158
+ },
159
+ (error: any) => {
160
+ assert.ok(error instanceof ValidationError);
161
+ assert.strictEqual(error.field, 'format');
162
+ return true;
163
+ }
164
+ );
165
+ });
166
+
167
+ test('renders JPG format', async () => {
168
+ const result = await render(SimpleTemplate, {
169
+ props: { title: 'JPEG Test' },
170
+ width: 400,
171
+ height: 200,
172
+ format: 'jpg',
173
+ quality: 80,
174
+ });
175
+
176
+ assert.ok(result instanceof Buffer || result instanceof Uint8Array);
177
+ assert.ok(result.length > 0);
178
+
179
+ // Check JPEG magic bytes
180
+ const jpegMagic = [0xff, 0xd8, 0xff];
181
+ const header = Array.from(result.slice(0, 3));
182
+ assert.deepStrictEqual(header, jpegMagic, 'Result should be valid JPEG');
183
+ });
184
+
185
+ test('renders WebP format', async () => {
186
+ const result = await render(SimpleTemplate, {
187
+ props: { title: 'WebP Test' },
188
+ width: 400,
189
+ height: 200,
190
+ format: 'webp',
191
+ quality: 85,
192
+ });
193
+
194
+ assert.ok(result instanceof Buffer || result instanceof Uint8Array);
195
+ assert.ok(result.length > 0);
196
+
197
+ // Check WebP magic bytes (RIFF....WEBP)
198
+ const riffMagic = result.slice(0, 4).toString('utf-8');
199
+ const webpMagic = result.slice(8, 12).toString('utf-8');
200
+ assert.strictEqual(riffMagic, 'RIFF', 'Should have RIFF header');
201
+ assert.strictEqual(webpMagic, 'WEBP', 'Should have WEBP identifier');
202
+ });
203
+
204
+ test('respects scale option', async () => {
205
+ const scale1 = await render(SimpleTemplate, {
206
+ props: { title: 'Scale 1' },
207
+ width: 100,
208
+ height: 100,
209
+ format: 'png',
210
+ scale: 1,
211
+ });
212
+
213
+ const scale2 = await render(SimpleTemplate, {
214
+ props: { title: 'Scale 2' },
215
+ width: 100,
216
+ height: 100,
217
+ format: 'png',
218
+ scale: 2,
219
+ });
220
+
221
+ // Scale 2 should produce larger file (more pixels)
222
+ assert.ok(scale2.length > scale1.length, 'Higher scale should produce larger file');
223
+ });
224
+ });
225
+
226
+ describe('render() with timeout', () => {
227
+ test('completes within timeout', async () => {
228
+ const result = await render(SimpleTemplate, {
229
+ props: { title: 'Fast Render' },
230
+ width: 100,
231
+ height: 100,
232
+ format: 'png',
233
+ timeout: 30000, // 30 seconds should be plenty
234
+ });
235
+
236
+ assert.ok(result.length > 0);
237
+ });
238
+
239
+ // Note: Testing actual timeout is tricky without a slow template
240
+ // In production, you'd want to test with a deliberately slow render
241
+ });
@@ -0,0 +1,277 @@
1
+ /**
2
+ * SDK tw() Function Tests
3
+ *
4
+ * Tests that Tailwind classes are properly converted to inline styles
5
+ */
6
+
7
+ import { test, describe } from 'node:test';
8
+ import assert from 'node:assert';
9
+ import { render } from '../dist/sdk/index.js';
10
+ import React from 'react';
11
+
12
+ // Helper to extract styles from SVG output
13
+ async function renderAndGetSvg(twClasses: string): Promise<string> {
14
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
15
+ return React.createElement('div', { style: tw(twClasses) }, 'Test');
16
+ };
17
+
18
+ const result = await render(Template, {
19
+ props: {},
20
+ width: 100,
21
+ height: 100,
22
+ format: 'svg',
23
+ });
24
+
25
+ return result.toString('utf-8');
26
+ }
27
+
28
+ describe('tw() function', () => {
29
+ test('converts flex classes', async () => {
30
+ const svg = await renderAndGetSvg('flex items-center justify-center');
31
+ // Satori renders successfully with flex - just verify output exists
32
+ assert.ok(svg.length > 0, 'Should render successfully with flex');
33
+ assert.ok(svg.includes('<svg'), 'Should contain SVG output');
34
+ });
35
+
36
+ test('converts padding classes', async () => {
37
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
38
+ const style = tw('p-4');
39
+ // p-4 = 16px padding (4 * 4)
40
+ return React.createElement('div', { style }, 'Padded');
41
+ };
42
+
43
+ const result = await render(Template, {
44
+ props: {},
45
+ format: 'svg',
46
+ });
47
+
48
+ assert.ok(result.length > 0);
49
+ });
50
+
51
+ test('converts margin classes', async () => {
52
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
53
+ const style = tw('m-8 mt-4');
54
+ return React.createElement('div', { style }, 'Margin');
55
+ };
56
+
57
+ const result = await render(Template, {
58
+ props: {},
59
+ format: 'svg',
60
+ });
61
+
62
+ assert.ok(result.length > 0);
63
+ });
64
+
65
+ test('converts width and height classes', async () => {
66
+ const svg = await renderAndGetSvg('w-full h-full');
67
+ assert.ok(svg.includes('100%') || svg.includes('width'), 'Should contain width');
68
+ });
69
+
70
+ test('converts background color classes', async () => {
71
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
72
+ return React.createElement(
73
+ 'div',
74
+ { style: tw('bg-blue-500') },
75
+ 'Blue Background'
76
+ );
77
+ };
78
+
79
+ const result = await render(Template, {
80
+ props: {},
81
+ format: 'svg',
82
+ });
83
+
84
+ // bg-blue-500 = #3b82f6
85
+ const svg = result.toString('utf-8');
86
+ assert.ok(
87
+ svg.includes('#3b82f6') || svg.includes('3b82f6') || svg.includes('rgb'),
88
+ 'Should contain blue color'
89
+ );
90
+ });
91
+
92
+ test('converts text color classes', async () => {
93
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
94
+ return React.createElement(
95
+ 'span',
96
+ { style: tw('text-white') },
97
+ 'White Text'
98
+ );
99
+ };
100
+
101
+ const result = await render(Template, {
102
+ props: {},
103
+ format: 'svg',
104
+ });
105
+
106
+ const svg = result.toString('utf-8');
107
+ // text-white = #ffffff or white
108
+ assert.ok(
109
+ svg.includes('#fff') || svg.includes('white') || svg.includes('#ffffff'),
110
+ 'Should contain white color'
111
+ );
112
+ });
113
+
114
+ test('converts font size classes', async () => {
115
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
116
+ return React.createElement('span', { style: tw('text-4xl') }, 'Large');
117
+ };
118
+
119
+ const result = await render(Template, {
120
+ props: {},
121
+ format: 'svg',
122
+ });
123
+
124
+ assert.ok(result.length > 0);
125
+ });
126
+
127
+ test('converts font weight classes', async () => {
128
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
129
+ return React.createElement('span', { style: tw('font-bold') }, 'Bold');
130
+ };
131
+
132
+ const result = await render(Template, {
133
+ props: {},
134
+ format: 'svg',
135
+ });
136
+
137
+ // font-bold = font-weight: 700, just verify rendering succeeds
138
+ assert.ok(result.length > 0, 'Should render successfully with font-bold');
139
+ });
140
+
141
+ test('converts border radius classes', async () => {
142
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
143
+ return React.createElement(
144
+ 'div',
145
+ { style: tw('rounded-lg') },
146
+ 'Rounded'
147
+ );
148
+ };
149
+
150
+ const result = await render(Template, {
151
+ props: {},
152
+ format: 'svg',
153
+ });
154
+
155
+ assert.ok(result.length > 0);
156
+ });
157
+
158
+ test('converts shadow classes', async () => {
159
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
160
+ return React.createElement('div', { style: tw('shadow-lg') }, 'Shadow');
161
+ };
162
+
163
+ const result = await render(Template, {
164
+ props: {},
165
+ format: 'svg',
166
+ });
167
+
168
+ assert.ok(result.length > 0);
169
+ });
170
+
171
+ test('handles multiple classes', async () => {
172
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
173
+ return React.createElement(
174
+ 'div',
175
+ {
176
+ style: tw(
177
+ 'w-full h-full flex items-center justify-center bg-gradient-to-r from-blue-500 to-purple-600 p-8 rounded-xl'
178
+ ),
179
+ },
180
+ 'Multi-class'
181
+ );
182
+ };
183
+
184
+ const result = await render(Template, {
185
+ props: {},
186
+ format: 'svg',
187
+ });
188
+
189
+ assert.ok(result.length > 0);
190
+ });
191
+
192
+ test('handles gap classes', async () => {
193
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
194
+ return React.createElement(
195
+ 'div',
196
+ { style: tw('flex gap-4') },
197
+ [
198
+ React.createElement('span', { key: '1' }, 'A'),
199
+ React.createElement('span', { key: '2' }, 'B'),
200
+ ]
201
+ );
202
+ };
203
+
204
+ const result = await render(Template, {
205
+ props: {},
206
+ format: 'svg',
207
+ });
208
+
209
+ assert.ok(result.length > 0);
210
+ });
211
+
212
+ test('handles opacity classes', async () => {
213
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
214
+ return React.createElement(
215
+ 'div',
216
+ { style: tw('opacity-50') },
217
+ 'Semi-transparent'
218
+ );
219
+ };
220
+
221
+ const result = await render(Template, {
222
+ props: {},
223
+ format: 'svg',
224
+ });
225
+
226
+ assert.ok(result.length > 0);
227
+ });
228
+
229
+ test('returns style object that can be spread', async () => {
230
+ const Template = ({ tw }: { tw: (c: string) => any }) => {
231
+ const baseStyles = tw('bg-white p-4');
232
+ const customStyles = { opacity: 0.8 };
233
+
234
+ return React.createElement(
235
+ 'div',
236
+ { style: { ...baseStyles, ...customStyles } },
237
+ 'Merged styles'
238
+ );
239
+ };
240
+
241
+ const result = await render(Template, {
242
+ props: {},
243
+ format: 'svg',
244
+ });
245
+
246
+ assert.ok(result.length > 0);
247
+ });
248
+
249
+ test('handles dynamic class construction', async () => {
250
+ const Template = ({
251
+ variant,
252
+ tw,
253
+ }: {
254
+ variant: 'primary' | 'secondary';
255
+ tw: (c: string) => any;
256
+ }) => {
257
+ const bgClass = variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500';
258
+ return React.createElement('div', { style: tw(`p-4 ${bgClass}`) }, variant);
259
+ };
260
+
261
+ const primary = await render(Template, {
262
+ props: { variant: 'primary' },
263
+ format: 'svg',
264
+ });
265
+
266
+ const secondary = await render(Template, {
267
+ props: { variant: 'secondary' },
268
+ format: 'svg',
269
+ });
270
+
271
+ // Both should render successfully
272
+ assert.ok(primary.length > 0);
273
+ assert.ok(secondary.length > 0);
274
+ // And they should be different (different colors)
275
+ assert.notStrictEqual(primary.toString(), secondary.toString());
276
+ });
277
+ });