@openlearning/create-widget 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @openlearning/create-widget
2
+
3
+ Scaffold a new OpenLearning widget project with all necessary boilerplate.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create @openlearning/widget@latest my-widget
9
+ ```
10
+
11
+ Or with pnpm:
12
+
13
+ ```bash
14
+ pnpm create @openlearning/widget my-widget
15
+ ```
16
+
17
+ ## What It Creates
18
+
19
+ The scaffolding tool generates a complete widget project structure:
20
+
21
+ ```
22
+ my-widget/
23
+ ├── src/
24
+ │ ├── components/
25
+ │ │ ├── LearnerView.tsx
26
+ │ │ └── SetupView.tsx
27
+ │ ├── entries/
28
+ │ │ ├── learner.tsx
29
+ │ │ └── setup.tsx
30
+ │ ├── data.ts
31
+ │ ├── types.ts
32
+ │ ├── DevApp.tsx
33
+ │ ├── devMain.tsx
34
+ │ └── index.css
35
+ ├── html/
36
+ │ ├── learner.html
37
+ │ └── setup.html
38
+ ├── index.html (dev server)
39
+ ├── package.json
40
+ ├── tsconfig.json
41
+ ├── vite.config.ts
42
+ ├── eslint.config.js
43
+ └── README.md
44
+ ```
45
+
46
+ ## Next Steps
47
+
48
+ After creating a new widget:
49
+
50
+ ```bash
51
+ cd my-widget
52
+ pnpm install
53
+ pnpm dev
54
+ ```
55
+
56
+ Then edit the components in `src/components/` to implement your widget's learner and setup interfaces.
57
+
58
+ ## Project Structure
59
+
60
+ - **`src/components/`** - React components for learner and setup views
61
+ - **`src/entries/`** - Widget entry points (created automatically by framework)
62
+ - **`src/types.ts`** - Define your widget's config type
63
+ - **`src/data.ts`** - Default configuration for your widget
64
+ - **`html/`** - Production HTML templates
65
+ - **`index.html`** - Development server entry point
66
+
67
+ The framework handles all parent communication, config state management, and DOM rendering automatically.
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ pnpm dev # Start dev server
73
+ pnpm build # Build for production
74
+ ```
75
+
76
+ ## Framework
77
+
78
+ This tool uses [@openlearning/widget-framework](../widget-framework) for all widget functionality. See its documentation for details on the parent message protocol, hooks, and component props.
package/bin/cli.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { createWidget } from "../dist/index.js";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const projectName = process.argv[2];
8
+
9
+ if (!projectName) {
10
+ console.log("Usage: create-widget <project-name>");
11
+ process.exit(1);
12
+ }
13
+
14
+ createWidget(projectName).catch((error) => {
15
+ console.error("Error creating widget:", error.message);
16
+ process.exit(1);
17
+ });
@@ -0,0 +1,2 @@
1
+ export declare function createWidget(projectName: string): Promise<void>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8dA,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCrE"}
package/dist/index.js ADDED
@@ -0,0 +1,467 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ function getTemplateFiles(projectName) {
6
+ const camelCase = projectName.replace(/-./g, (x) => x[1].toUpperCase());
7
+ return [
8
+ // package.json
9
+ {
10
+ path: "package.json",
11
+ content: JSON.stringify({
12
+ name: projectName,
13
+ version: "0.0.0",
14
+ type: "module",
15
+ scripts: {
16
+ dev: "vite",
17
+ build: "tsc -b && vite build",
18
+ preview: "vite preview",
19
+ },
20
+ dependencies: {
21
+ "@openlearning/widget-framework": "workspace:*",
22
+ react: "^19.2.0",
23
+ "react-dom": "^19.2.0",
24
+ },
25
+ devDependencies: {
26
+ "@types/node": "^20.0.0",
27
+ "@types/react": "^19.0.0",
28
+ "@types/react-dom": "^19.0.0",
29
+ "@vitejs/plugin-react": "^4.0.0",
30
+ eslint: "^8.0.0",
31
+ "eslint-plugin-react-hooks": "^4.6.0",
32
+ "eslint-plugin-react-refresh": "^0.4.0",
33
+ typescript: "^5.7.2",
34
+ vite: "^7.3.1",
35
+ },
36
+ }, null, 2),
37
+ },
38
+ // tsconfig.json
39
+ {
40
+ path: "tsconfig.json",
41
+ content: JSON.stringify({
42
+ compilerOptions: {
43
+ target: "ES2020",
44
+ useDefineForClassFields: true,
45
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
46
+ module: "ES2020",
47
+ skipLibCheck: true,
48
+ esModuleInterop: true,
49
+ jsx: "react-jsx",
50
+ noEmit: true,
51
+ },
52
+ include: ["src"],
53
+ references: [{ path: "./tsconfig.app.json" }],
54
+ }, null, 2),
55
+ },
56
+ // tsconfig.app.json
57
+ {
58
+ path: "tsconfig.app.json",
59
+ content: JSON.stringify({
60
+ extends: "./tsconfig.json",
61
+ compilerOptions: {
62
+ outDir: "./dist",
63
+ rootDir: "./src",
64
+ },
65
+ include: ["src"],
66
+ }, null, 2),
67
+ },
68
+ // tsconfig.node.json
69
+ {
70
+ path: "tsconfig.node.json",
71
+ content: JSON.stringify({
72
+ compilerOptions: {
73
+ composite: true,
74
+ skipLibCheck: true,
75
+ module: "ES2020",
76
+ moduleResolution: "bundler",
77
+ allowSyntheticDefaultImports: true,
78
+ },
79
+ include: ["vite.config.ts"],
80
+ }, null, 2),
81
+ },
82
+ // vite.config.ts
83
+ {
84
+ path: "vite.config.ts",
85
+ content: `import { defineConfig } from "vite";
86
+ import react from "@vitejs/plugin-react";
87
+
88
+ export default defineConfig({
89
+ plugins: [react()],
90
+ build: {
91
+ rollupOptions: {
92
+ input: {
93
+ learner: "html/learner.html",
94
+ setup: "html/setup.html",
95
+ },
96
+ output: {
97
+ entryFileNames: "[name].js",
98
+ dir: "dist",
99
+ },
100
+ },
101
+ },
102
+ });
103
+ `,
104
+ },
105
+ // .prettierrc
106
+ {
107
+ path: ".prettierrc",
108
+ content: JSON.stringify({
109
+ semi: true,
110
+ trailingComma: "es5",
111
+ singleQuote: false,
112
+ }),
113
+ },
114
+ // .gitignore
115
+ {
116
+ path: ".gitignore",
117
+ content: `# Logs
118
+ logs
119
+ *.log
120
+ npm-debug.log*
121
+
122
+ # Dependencies
123
+ node_modules
124
+ dist
125
+
126
+ # IDE
127
+ .vscode
128
+ .idea
129
+ *.swp
130
+ *.swo
131
+
132
+ # Environment
133
+ .env
134
+ .env.local
135
+ `,
136
+ },
137
+ // README.md
138
+ {
139
+ path: "README.md",
140
+ content: `# ${projectName}
141
+
142
+ A widget built with the OpenLearning widget framework.
143
+
144
+ ## Setup
145
+
146
+ \`\`\`bash
147
+ pnpm install
148
+ \`\`\`
149
+
150
+ ## Development
151
+
152
+ \`\`\`bash
153
+ pnpm dev
154
+ \`\`\`
155
+
156
+ ## Build
157
+
158
+ \`\`\`bash
159
+ pnpm build
160
+ \`\`\`
161
+
162
+ ## Project Structure
163
+
164
+ - \`src/components/\` - React components (LearnerView, SetupView)
165
+ - \`src/entries/\` - Widget entry points
166
+ - \`html/\` - HTML template files
167
+ - \`src/types.ts\` - Widget configuration types
168
+ - \`src/data.ts\` - Default configuration
169
+
170
+ ## Adding the Widget to a Parent
171
+
172
+ The widget exposes two HTML endpoints:
173
+ - \`/learner.html\` - Learner view
174
+ - \`/setup.html\` - Setup/admin view
175
+
176
+ The parent window communicates via \`postMessage\` protocol.
177
+ `,
178
+ },
179
+ // public/.gitkeep
180
+ {
181
+ path: "public/.gitkeep",
182
+ content: "",
183
+ },
184
+ // src/index.css
185
+ {
186
+ path: "src/index.css",
187
+ content: `* {
188
+ margin: 0;
189
+ padding: 0;
190
+ box-sizing: border-box;
191
+ }
192
+
193
+ body {
194
+ font-family: system-ui, -apple-system, sans-serif;
195
+ line-height: 1.5;
196
+ }
197
+ `,
198
+ },
199
+ // src/types.ts
200
+ {
201
+ path: "src/types.ts",
202
+ content: `import {
203
+ LearnerViewProps,
204
+ SetupViewProps,
205
+ JSONValue,
206
+ Attachment,
207
+ } from "@openlearning/widget-framework";
208
+
209
+ // Define your widget's configuration structure
210
+ export type WidgetConfig = JSONValue;
211
+
212
+ // Export framework types for use in your components
213
+ export type { LearnerViewProps, SetupViewProps, JSONValue, Attachment };
214
+ `,
215
+ },
216
+ // src/data.ts
217
+ {
218
+ path: "src/data.ts",
219
+ content: `import { WidgetConfig } from "./types";
220
+
221
+ // Default configuration for this widget
222
+ export const DEFAULT_CONFIG: WidgetConfig = {
223
+ // Add your default configuration here
224
+ };
225
+ `,
226
+ },
227
+ // src/DevApp.tsx
228
+ {
229
+ path: "src/DevApp.tsx",
230
+ content: `import { DevApp as FrameworkDevApp } from "@openlearning/widget-framework";
231
+ import { LearnerView } from "./components/LearnerView";
232
+ import { SetupView } from "./components/SetupView";
233
+ import { DEFAULT_CONFIG } from "./data";
234
+
235
+ export const DevApp = () => (
236
+ <FrameworkDevApp
237
+ LearnerViewComponent={LearnerView}
238
+ SetupViewComponent={SetupView}
239
+ defaultConfig={DEFAULT_CONFIG}
240
+ />
241
+ );
242
+ `,
243
+ },
244
+ // src/devMain.tsx
245
+ {
246
+ path: "src/devMain.tsx",
247
+ content: `import React from "react";
248
+ import ReactDOM from "react-dom/client";
249
+ import { DevApp } from "./DevApp";
250
+ import "./index.css";
251
+
252
+ ReactDOM.createRoot(document.getElementById("root")!).render(
253
+ <React.StrictMode>
254
+ <DevApp />
255
+ </React.StrictMode>
256
+ );
257
+ `,
258
+ },
259
+ // src/components/LearnerView.tsx
260
+ {
261
+ path: "src/components/LearnerView.tsx",
262
+ content: `import React from "react";
263
+ import type { LearnerViewProps } from "@openlearning/widget-framework";
264
+ import type { WidgetConfig } from "../types";
265
+
266
+ export const LearnerView: React.FC<LearnerViewProps<WidgetConfig>> = ({
267
+ config,
268
+ onComplete,
269
+ onShare,
270
+ onResize,
271
+ onSave,
272
+ onReinit,
273
+ }) => {
274
+ return (
275
+ <div style={{ padding: "1rem" }}>
276
+ <h1>Learner View</h1>
277
+ <p>Implement your learner interface here.</p>
278
+
279
+ <div style={{ marginTop: "1rem" }}>
280
+ <button onClick={() => onComplete?.()}>Complete</button>
281
+ <button onClick={() => onShare?.([])}>Share</button>
282
+ <button onClick={() => onResize?.(400)}>Resize</button>
283
+ </div>
284
+ </div>
285
+ );
286
+ };
287
+ `,
288
+ },
289
+ // src/components/SetupView.tsx
290
+ {
291
+ path: "src/components/SetupView.tsx",
292
+ content: `import React from "react";
293
+ import type { SetupViewProps } from "@openlearning/widget-framework";
294
+ import type { WidgetConfig } from "../types";
295
+
296
+ export const SetupView: React.FC<SetupViewProps<WidgetConfig>> = ({
297
+ config,
298
+ onChange,
299
+ onResize,
300
+ onReinit,
301
+ }) => {
302
+ return (
303
+ <div style={{ padding: "1rem" }}>
304
+ <h1>Setup View</h1>
305
+ <p>Implement your setup/configuration interface here.</p>
306
+
307
+ <div style={{ marginTop: "1rem" }}>
308
+ <button onClick={() => onChange?.(config)}>Save Configuration</button>
309
+ <button onClick={() => onResize?.(400)}>Resize</button>
310
+ </div>
311
+ </div>
312
+ );
313
+ };
314
+ `,
315
+ },
316
+ // src/entries/learner.tsx
317
+ {
318
+ path: "src/entries/learner.tsx",
319
+ content: `import { createLearnerEntry } from "@openlearning/widget-framework";
320
+ import { LearnerView } from "../components/LearnerView";
321
+ import "../index.css";
322
+
323
+ createLearnerEntry(LearnerView);
324
+ `,
325
+ },
326
+ // src/entries/setup.tsx
327
+ {
328
+ path: "src/entries/setup.tsx",
329
+ content: `import { createSetupEntry } from "@openlearning/widget-framework";
330
+ import { SetupView } from "../components/SetupView";
331
+ import { DEFAULT_CONFIG } from "../data";
332
+ import "../index.css";
333
+
334
+ createSetupEntry(SetupView, DEFAULT_CONFIG);
335
+ `,
336
+ },
337
+ // index.html (dev server)
338
+ {
339
+ path: "index.html",
340
+ content: `<!doctype html>
341
+ <html lang="en">
342
+ <head>
343
+ <meta charset="UTF-8" />
344
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
345
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
346
+ <title>${projectName}</title>
347
+ </head>
348
+ <body>
349
+ <div id="root"></div>
350
+ <script type="module" src="/src/devMain.tsx"></script>
351
+ </body>
352
+ </html>
353
+ `,
354
+ },
355
+ // html/learner.html
356
+ {
357
+ path: "html/learner.html",
358
+ content: `<!doctype html>
359
+ <html lang="en">
360
+ <head>
361
+ <meta charset="UTF-8" />
362
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
363
+ <title>Learner</title>
364
+ </head>
365
+ <body>
366
+ <div id="root"></div>
367
+ <script type="module" src="../src/entries/learner.tsx"></script>
368
+ </body>
369
+ </html>
370
+ `,
371
+ },
372
+ // html/setup.html
373
+ {
374
+ path: "html/setup.html",
375
+ content: `<!doctype html>
376
+ <html lang="en">
377
+ <head>
378
+ <meta charset="UTF-8" />
379
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
380
+ <title>Setup</title>
381
+ </head>
382
+ <body>
383
+ <div id="root"></div>
384
+ <script type="module" src="../src/entries/setup.tsx"></script>
385
+ </body>
386
+ </html>
387
+ `,
388
+ },
389
+ // eslint.config.js
390
+ {
391
+ path: "eslint.config.js",
392
+ content: `import js from "@eslint/js";
393
+ import globals from "globals";
394
+ import react from "eslint-plugin-react/configs/recommended.js";
395
+ import reactHooks from "eslint-plugin-react-hooks";
396
+ import reactRefresh from "eslint-plugin-react-refresh";
397
+
398
+ export default [
399
+ { ignores: ["dist"] },
400
+ {
401
+ files: ["**/*.{js,jsx,ts,tsx}"],
402
+ languageOptions: {
403
+ ecmaVersion: 2020,
404
+ globals: globals.browser,
405
+ parserOptions: {
406
+ ecmaVersion: "latest",
407
+ ecmaFeatures: { jsx: true },
408
+ sourceType: "module",
409
+ },
410
+ },
411
+ settings: { react: { version: "18.3" } },
412
+ plugins: {
413
+ react,
414
+ "react-hooks": reactHooks,
415
+ "react-refresh": reactRefresh,
416
+ },
417
+ rules: {
418
+ ...js.configs.recommended.rules,
419
+ ...react.rules,
420
+ ...reactHooks.configs.recommended.rules,
421
+ "react/react-in-jsx-scope": "off",
422
+ "react-refresh/only-export-components": [
423
+ "warn",
424
+ { allowConstantExport: true },
425
+ ],
426
+ },
427
+ },
428
+ ];
429
+ `,
430
+ },
431
+ ];
432
+ }
433
+ export async function createWidget(projectName) {
434
+ const projectDir = path.resolve(projectName);
435
+ // Check if directory already exists
436
+ if (fs.existsSync(projectDir)) {
437
+ throw new Error(`Directory '${projectName}' already exists`);
438
+ }
439
+ console.log(`Creating widget project: ${projectName}`);
440
+ // Create the project directory
441
+ fs.mkdirSync(projectDir, { recursive: true });
442
+ // Create all template files
443
+ const files = getTemplateFiles(projectName);
444
+ for (const file of files) {
445
+ const filePath = path.join(projectDir, file.path);
446
+ const dirname = path.dirname(filePath);
447
+ // Create directories if they don't exist
448
+ fs.mkdirSync(dirname, { recursive: true });
449
+ // Write file
450
+ fs.writeFileSync(filePath, file.content);
451
+ console.log(` created ${file.path}`);
452
+ }
453
+ const nextSteps = `
454
+ ✨ Widget project created!
455
+
456
+ Next steps:
457
+ cd ${projectName}
458
+ pnpm install
459
+ pnpm dev
460
+
461
+ The widget is ready to customize. Edit components in:
462
+ - src/components/LearnerView.tsx
463
+ - src/components/SetupView.tsx
464
+ `;
465
+ console.log(nextSteps);
466
+ }
467
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@openlearning/create-widget",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Scaffold a new OpenLearning widget project with all necessary boilerplate",
6
+ "license": "MIT",
7
+ "author": "OpenLearning",
8
+ "homepage": "https://github.com/openlearning/widgets#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/openlearning/widgets.git",
12
+ "directory": "libs/create-widget"
13
+ },
14
+ "bin": {
15
+ "create-widget": "./bin/cli.js"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "keywords": [
23
+ "widget",
24
+ "scaffold",
25
+ "template",
26
+ "openlearning"
27
+ ],
28
+ "devDependencies": {
29
+ "@types/node": "^20.0.0",
30
+ "typescript": "^5.7.2"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc",
34
+ "dev": "tsc -w"
35
+ }
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,520 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ interface TemplateFile {
8
+ path: string;
9
+ content: string;
10
+ }
11
+
12
+ function getTemplateFiles(projectName: string): TemplateFile[] {
13
+ const camelCase = projectName.replace(/-./g, (x) => x[1].toUpperCase());
14
+
15
+ return [
16
+ // package.json
17
+ {
18
+ path: "package.json",
19
+ content: JSON.stringify(
20
+ {
21
+ name: projectName,
22
+ version: "0.0.0",
23
+ type: "module",
24
+ scripts: {
25
+ dev: "vite",
26
+ build: "tsc -b && vite build",
27
+ preview: "vite preview",
28
+ },
29
+ dependencies: {
30
+ "@openlearning/widget-framework": "workspace:*",
31
+ react: "^19.2.0",
32
+ "react-dom": "^19.2.0",
33
+ },
34
+ devDependencies: {
35
+ "@types/node": "^20.0.0",
36
+ "@types/react": "^19.0.0",
37
+ "@types/react-dom": "^19.0.0",
38
+ "@vitejs/plugin-react": "^4.0.0",
39
+ eslint: "^8.0.0",
40
+ "eslint-plugin-react-hooks": "^4.6.0",
41
+ "eslint-plugin-react-refresh": "^0.4.0",
42
+ typescript: "^5.7.2",
43
+ vite: "^7.3.1",
44
+ },
45
+ },
46
+ null,
47
+ 2
48
+ ),
49
+ },
50
+
51
+ // tsconfig.json
52
+ {
53
+ path: "tsconfig.json",
54
+ content: JSON.stringify(
55
+ {
56
+ compilerOptions: {
57
+ target: "ES2020",
58
+ useDefineForClassFields: true,
59
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
60
+ module: "ES2020",
61
+ skipLibCheck: true,
62
+ esModuleInterop: true,
63
+ jsx: "react-jsx",
64
+ noEmit: true,
65
+ },
66
+ include: ["src"],
67
+ references: [{ path: "./tsconfig.app.json" }],
68
+ },
69
+ null,
70
+ 2
71
+ ),
72
+ },
73
+
74
+ // tsconfig.app.json
75
+ {
76
+ path: "tsconfig.app.json",
77
+ content: JSON.stringify(
78
+ {
79
+ extends: "./tsconfig.json",
80
+ compilerOptions: {
81
+ outDir: "./dist",
82
+ rootDir: "./src",
83
+ },
84
+ include: ["src"],
85
+ },
86
+ null,
87
+ 2
88
+ ),
89
+ },
90
+
91
+ // tsconfig.node.json
92
+ {
93
+ path: "tsconfig.node.json",
94
+ content: JSON.stringify(
95
+ {
96
+ compilerOptions: {
97
+ composite: true,
98
+ skipLibCheck: true,
99
+ module: "ES2020",
100
+ moduleResolution: "bundler",
101
+ allowSyntheticDefaultImports: true,
102
+ },
103
+ include: ["vite.config.ts"],
104
+ },
105
+ null,
106
+ 2
107
+ ),
108
+ },
109
+
110
+ // vite.config.ts
111
+ {
112
+ path: "vite.config.ts",
113
+ content: `import { defineConfig } from "vite";
114
+ import react from "@vitejs/plugin-react";
115
+
116
+ export default defineConfig({
117
+ plugins: [react()],
118
+ build: {
119
+ rollupOptions: {
120
+ input: {
121
+ learner: "html/learner.html",
122
+ setup: "html/setup.html",
123
+ },
124
+ output: {
125
+ entryFileNames: "[name].js",
126
+ dir: "dist",
127
+ },
128
+ },
129
+ },
130
+ });
131
+ `,
132
+ },
133
+
134
+ // .prettierrc
135
+ {
136
+ path: ".prettierrc",
137
+ content: JSON.stringify({
138
+ semi: true,
139
+ trailingComma: "es5",
140
+ singleQuote: false,
141
+ }),
142
+ },
143
+
144
+ // .gitignore
145
+ {
146
+ path: ".gitignore",
147
+ content: `# Logs
148
+ logs
149
+ *.log
150
+ npm-debug.log*
151
+
152
+ # Dependencies
153
+ node_modules
154
+ dist
155
+
156
+ # IDE
157
+ .vscode
158
+ .idea
159
+ *.swp
160
+ *.swo
161
+
162
+ # Environment
163
+ .env
164
+ .env.local
165
+ `,
166
+ },
167
+
168
+ // README.md
169
+ {
170
+ path: "README.md",
171
+ content: `# ${projectName}
172
+
173
+ A widget built with the OpenLearning widget framework.
174
+
175
+ ## Setup
176
+
177
+ \`\`\`bash
178
+ pnpm install
179
+ \`\`\`
180
+
181
+ ## Development
182
+
183
+ \`\`\`bash
184
+ pnpm dev
185
+ \`\`\`
186
+
187
+ ## Build
188
+
189
+ \`\`\`bash
190
+ pnpm build
191
+ \`\`\`
192
+
193
+ ## Project Structure
194
+
195
+ - \`src/components/\` - React components (LearnerView, SetupView)
196
+ - \`src/entries/\` - Widget entry points
197
+ - \`html/\` - HTML template files
198
+ - \`src/types.ts\` - Widget configuration types
199
+ - \`src/data.ts\` - Default configuration
200
+
201
+ ## Adding the Widget to a Parent
202
+
203
+ The widget exposes two HTML endpoints:
204
+ - \`/learner.html\` - Learner view
205
+ - \`/setup.html\` - Setup/admin view
206
+
207
+ The parent window communicates via \`postMessage\` protocol.
208
+ `,
209
+ },
210
+
211
+ // public/.gitkeep
212
+ {
213
+ path: "public/.gitkeep",
214
+ content: "",
215
+ },
216
+
217
+ // src/index.css
218
+ {
219
+ path: "src/index.css",
220
+ content: `* {
221
+ margin: 0;
222
+ padding: 0;
223
+ box-sizing: border-box;
224
+ }
225
+
226
+ body {
227
+ font-family: system-ui, -apple-system, sans-serif;
228
+ line-height: 1.5;
229
+ }
230
+ `,
231
+ },
232
+
233
+ // src/types.ts
234
+ {
235
+ path: "src/types.ts",
236
+ content: `import {
237
+ LearnerViewProps,
238
+ SetupViewProps,
239
+ JSONValue,
240
+ Attachment,
241
+ } from "@openlearning/widget-framework";
242
+
243
+ // Define your widget's configuration structure
244
+ export type WidgetConfig = JSONValue;
245
+
246
+ // Export framework types for use in your components
247
+ export type { LearnerViewProps, SetupViewProps, JSONValue, Attachment };
248
+ `,
249
+ },
250
+
251
+ // src/data.ts
252
+ {
253
+ path: "src/data.ts",
254
+ content: `import { WidgetConfig } from "./types";
255
+
256
+ // Default configuration for this widget
257
+ export const DEFAULT_CONFIG: WidgetConfig = {
258
+ // Add your default configuration here
259
+ };
260
+ `,
261
+ },
262
+
263
+ // src/DevApp.tsx
264
+ {
265
+ path: "src/DevApp.tsx",
266
+ content: `import { DevApp as FrameworkDevApp } from "@openlearning/widget-framework";
267
+ import { LearnerView } from "./components/LearnerView";
268
+ import { SetupView } from "./components/SetupView";
269
+ import { DEFAULT_CONFIG } from "./data";
270
+
271
+ export const DevApp = () => (
272
+ <FrameworkDevApp
273
+ LearnerViewComponent={LearnerView}
274
+ SetupViewComponent={SetupView}
275
+ defaultConfig={DEFAULT_CONFIG}
276
+ />
277
+ );
278
+ `,
279
+ },
280
+
281
+ // src/devMain.tsx
282
+ {
283
+ path: "src/devMain.tsx",
284
+ content: `import React from "react";
285
+ import ReactDOM from "react-dom/client";
286
+ import { DevApp } from "./DevApp";
287
+ import "./index.css";
288
+
289
+ ReactDOM.createRoot(document.getElementById("root")!).render(
290
+ <React.StrictMode>
291
+ <DevApp />
292
+ </React.StrictMode>
293
+ );
294
+ `,
295
+ },
296
+
297
+ // src/components/LearnerView.tsx
298
+ {
299
+ path: "src/components/LearnerView.tsx",
300
+ content: `import React from "react";
301
+ import type { LearnerViewProps } from "@openlearning/widget-framework";
302
+ import type { WidgetConfig } from "../types";
303
+
304
+ export const LearnerView: React.FC<LearnerViewProps<WidgetConfig>> = ({
305
+ config,
306
+ onComplete,
307
+ onShare,
308
+ onResize,
309
+ onSave,
310
+ onReinit,
311
+ }) => {
312
+ return (
313
+ <div style={{ padding: "1rem" }}>
314
+ <h1>Learner View</h1>
315
+ <p>Implement your learner interface here.</p>
316
+
317
+ <div style={{ marginTop: "1rem" }}>
318
+ <button onClick={() => onComplete?.()}>Complete</button>
319
+ <button onClick={() => onShare?.([])}>Share</button>
320
+ <button onClick={() => onResize?.(400)}>Resize</button>
321
+ </div>
322
+ </div>
323
+ );
324
+ };
325
+ `,
326
+ },
327
+
328
+ // src/components/SetupView.tsx
329
+ {
330
+ path: "src/components/SetupView.tsx",
331
+ content: `import React from "react";
332
+ import type { SetupViewProps } from "@openlearning/widget-framework";
333
+ import type { WidgetConfig } from "../types";
334
+
335
+ export const SetupView: React.FC<SetupViewProps<WidgetConfig>> = ({
336
+ config,
337
+ onChange,
338
+ onResize,
339
+ onReinit,
340
+ }) => {
341
+ return (
342
+ <div style={{ padding: "1rem" }}>
343
+ <h1>Setup View</h1>
344
+ <p>Implement your setup/configuration interface here.</p>
345
+
346
+ <div style={{ marginTop: "1rem" }}>
347
+ <button onClick={() => onChange?.(config)}>Save Configuration</button>
348
+ <button onClick={() => onResize?.(400)}>Resize</button>
349
+ </div>
350
+ </div>
351
+ );
352
+ };
353
+ `,
354
+ },
355
+
356
+ // src/entries/learner.tsx
357
+ {
358
+ path: "src/entries/learner.tsx",
359
+ content: `import { createLearnerEntry } from "@openlearning/widget-framework";
360
+ import { LearnerView } from "../components/LearnerView";
361
+ import "../index.css";
362
+
363
+ createLearnerEntry(LearnerView);
364
+ `,
365
+ },
366
+
367
+ // src/entries/setup.tsx
368
+ {
369
+ path: "src/entries/setup.tsx",
370
+ content: `import { createSetupEntry } from "@openlearning/widget-framework";
371
+ import { SetupView } from "../components/SetupView";
372
+ import { DEFAULT_CONFIG } from "../data";
373
+ import "../index.css";
374
+
375
+ createSetupEntry(SetupView, DEFAULT_CONFIG);
376
+ `,
377
+ },
378
+
379
+ // index.html (dev server)
380
+ {
381
+ path: "index.html",
382
+ content: `<!doctype html>
383
+ <html lang="en">
384
+ <head>
385
+ <meta charset="UTF-8" />
386
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
387
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
388
+ <title>${projectName}</title>
389
+ </head>
390
+ <body>
391
+ <div id="root"></div>
392
+ <script type="module" src="/src/devMain.tsx"></script>
393
+ </body>
394
+ </html>
395
+ `,
396
+ },
397
+
398
+ // html/learner.html
399
+ {
400
+ path: "html/learner.html",
401
+ content: `<!doctype html>
402
+ <html lang="en">
403
+ <head>
404
+ <meta charset="UTF-8" />
405
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
406
+ <title>Learner</title>
407
+ </head>
408
+ <body>
409
+ <div id="root"></div>
410
+ <script type="module" src="../src/entries/learner.tsx"></script>
411
+ </body>
412
+ </html>
413
+ `,
414
+ },
415
+
416
+ // html/setup.html
417
+ {
418
+ path: "html/setup.html",
419
+ content: `<!doctype html>
420
+ <html lang="en">
421
+ <head>
422
+ <meta charset="UTF-8" />
423
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
424
+ <title>Setup</title>
425
+ </head>
426
+ <body>
427
+ <div id="root"></div>
428
+ <script type="module" src="../src/entries/setup.tsx"></script>
429
+ </body>
430
+ </html>
431
+ `,
432
+ },
433
+
434
+ // eslint.config.js
435
+ {
436
+ path: "eslint.config.js",
437
+ content: `import js from "@eslint/js";
438
+ import globals from "globals";
439
+ import react from "eslint-plugin-react/configs/recommended.js";
440
+ import reactHooks from "eslint-plugin-react-hooks";
441
+ import reactRefresh from "eslint-plugin-react-refresh";
442
+
443
+ export default [
444
+ { ignores: ["dist"] },
445
+ {
446
+ files: ["**/*.{js,jsx,ts,tsx}"],
447
+ languageOptions: {
448
+ ecmaVersion: 2020,
449
+ globals: globals.browser,
450
+ parserOptions: {
451
+ ecmaVersion: "latest",
452
+ ecmaFeatures: { jsx: true },
453
+ sourceType: "module",
454
+ },
455
+ },
456
+ settings: { react: { version: "18.3" } },
457
+ plugins: {
458
+ react,
459
+ "react-hooks": reactHooks,
460
+ "react-refresh": reactRefresh,
461
+ },
462
+ rules: {
463
+ ...js.configs.recommended.rules,
464
+ ...react.rules,
465
+ ...reactHooks.configs.recommended.rules,
466
+ "react/react-in-jsx-scope": "off",
467
+ "react-refresh/only-export-components": [
468
+ "warn",
469
+ { allowConstantExport: true },
470
+ ],
471
+ },
472
+ },
473
+ ];
474
+ `,
475
+ },
476
+ ];
477
+ }
478
+
479
+ export async function createWidget(projectName: string): Promise<void> {
480
+ const projectDir = path.resolve(projectName);
481
+
482
+ // Check if directory already exists
483
+ if (fs.existsSync(projectDir)) {
484
+ throw new Error(`Directory '${projectName}' already exists`);
485
+ }
486
+
487
+ console.log(`Creating widget project: ${projectName}`);
488
+
489
+ // Create the project directory
490
+ fs.mkdirSync(projectDir, { recursive: true });
491
+
492
+ // Create all template files
493
+ const files = getTemplateFiles(projectName);
494
+
495
+ for (const file of files) {
496
+ const filePath = path.join(projectDir, file.path);
497
+ const dirname = path.dirname(filePath);
498
+
499
+ // Create directories if they don't exist
500
+ fs.mkdirSync(dirname, { recursive: true });
501
+
502
+ // Write file
503
+ fs.writeFileSync(filePath, file.content);
504
+ console.log(` created ${file.path}`);
505
+ }
506
+
507
+ const nextSteps = `
508
+ ✨ Widget project created!
509
+
510
+ Next steps:
511
+ cd ${projectName}
512
+ pnpm install
513
+ pnpm dev
514
+
515
+ The widget is ready to customize. Edit components in:
516
+ - src/components/LearnerView.tsx
517
+ - src/components/SetupView.tsx
518
+ `;
519
+ console.log(nextSteps);
520
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020"],
5
+ "module": "ES2020",
6
+ "moduleResolution": "bundler",
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "esModuleInterop": true,
15
+ "resolveJsonModule": true,
16
+ "types": ["node"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }