@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 +78 -0
- package/bin/cli.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +467 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/index.ts +520 -0
- package/tsconfig.json +20 -0
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
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|