-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcreate-tutorial.js
More file actions
186 lines (153 loc) Β· 6.74 KB
/
create-tutorial.js
File metadata and controls
186 lines (153 loc) Β· 6.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import fs from 'fs/promises';
import path from 'path';
import readline from 'readline/promises';
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TUTORIALS_DIR = path.join(__dirname, 'tutorials');
const STARTER_DIR = path.join(TUTORIALS_DIR, '00_Starter-Tutorial');
// Folders/files to skip when copying the starter template
const SKIP = new Set(['.astro', 'node_modules', 'vonage-toolbar', 'dist']);
const NAME_PATTERN = /^[a-z0-9]+(?:[_-][a-z0-9]+)*$/;
const NAME_EXAMPLE = 'product_name-language-topic (e.g. messages_api-node-sms)';
function toTitle(name) {
return name
.replace(/[_-]+/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
async function copyDir(src, dest) {
await fs.mkdir(dest, { recursive: true });
for (const entry of await fs.readdir(src, { withFileTypes: true })) {
if (SKIP.has(entry.name)) continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDir(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}
async function replaceInFile(filePath, replacements) {
let content = await fs.readFile(filePath, 'utf8');
for (const [from, to] of replacements) {
content = content.replaceAll(from, to);
}
await fs.writeFile(filePath, content);
}
async function writeTutorialConfig(dest, config) {
await fs.writeFile(
path.join(dest, 'tutorial-config.json'),
JSON.stringify(config, null, 2)
);
}
async function main() {
console.log('\nπ Vonage Tutorial Creator\n');
// Single readline instance β collect ALL answers before any async scaffolding
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => rl.question(q);
const yesNo = async (question, defaultYes = true) => {
const hint = defaultYes ? '(Y/n)' : '(y/N)';
const answer = (await ask(`${question} ${hint}: `)).trim().toLowerCase();
if (answer === '') return defaultYes;
return answer === 'y' || answer === 'yes';
};
const collectList = async (prompt) => {
const items = [];
while (true) {
const value = (await ask(prompt)).trim();
if (!value) break;
items.push(value);
}
return items;
};
let name, title, dest, config, startEditing;
try {
// --- Tutorial name ---
name = process.argv[2];
if (name) {
if (!NAME_PATTERN.test(name)) {
console.error(`\nβ Invalid name "${name}". Use lowercase letters, numbers, hyphens, and underscores.`);
console.error(` Format: ${NAME_EXAMPLE}\n`);
process.exit(1);
}
} else {
while (true) {
name = (await ask(`Tutorial name (${NAME_EXAMPLE}): `)).trim();
if (!name) { console.log(' Name cannot be empty.'); continue; }
if (!NAME_PATTERN.test(name)) { console.log(' Use only lowercase letters, numbers, hyphens, and underscores.'); continue; }
break;
}
}
title = toTitle(name);
dest = path.join(TUTORIALS_DIR, name);
// Guard: already exists
try {
await fs.access(dest);
console.error(`\nβ "${name}" already exists at tutorials/${name}\n`);
process.exit(1);
} catch { /* expected */ }
// --- Toolbar config: steps 1β4 (ask before scaffolding so stdin stays open) ---
console.log(`\nπ Tutorial: ${name} (${title})`);
console.log('βοΈ Answer a few questions to configure it:\n');
// Step 1: Panels
const panels = [];
if (await yesNo('Step 1: Does this tutorial need a Terminal panel?')) panels.push('terminal');
if (await yesNo('Step 1: Does this tutorial need a Preview Browser panel?', false)) panels.push('browser');
// Step 2: External repository
console.log('');
const repository = (await ask('Step 2: External repository URL? (press Enter to skip): ')).trim();
// Step 3: Starter files
console.log('\nStep 3: Starter files (pre-populated files with scaffolding code).');
console.log(' Press Enter with no input when done.');
const starterFiles = await collectList(' Starter file: ');
// Step 4: Files to open
console.log('\nStep 4: Files to open in the editor when the tutorial starts.');
console.log(' Press Enter with no input when done.');
const openFiles = await collectList(' File to open: ');
config = { files: [], openFiles, panels, repository, starterFiles, capabilities: [], version: '0.0.1', filename: name };
// Ask about editing before closing readline
console.log('');
startEditing = await yesNo('π Start editing when ready?');
} finally {
rl.close();
}
// --- Scaffold (after all input collected) ---
console.log(`\nπ Creating tutorials/${name}...\n`);
await copyDir(STARTER_DIR, dest);
console.log('β
Copied starter template');
await replaceInFile(path.join(dest, 'astro.config.mjs'), [['Vonage Onboarding', title]]);
console.log('β
Updated astro.config.mjs');
await replaceInFile(path.join(dest, 'src/content/docs/index.mdx'), [
['Starter Tutorial', title],
['Get started building a tutorial.', `Get started with ${title}.`],
['Starting a tutorial!', `Starting ${title}!`],
]);
console.log('β
Updated index.mdx');
await replaceInFile(path.join(dest, 'src/content/docs/01-welcome.md'), [
['Starter Tutorial', title],
['This is a paragraph about what the tutorial contains.', `Welcome to the ${title} tutorial.`],
]);
console.log('β
Updated 01-welcome.md');
await replaceInFile(path.join(dest, 'README.md'), [
['Starter Onboarding Tutorial', title],
['This is a starter tutorial.', `Tutorial: ${title}`],
]);
console.log('β
Updated README.md');
await replaceInFile(path.join(dest, 'package.json'), [
['"vonage-interactive-tutorial"', `"${name}"`],
]);
console.log('β
Updated package.json');
await writeTutorialConfig(dest, config);
console.log('β
Created tutorial-config.json');
console.log('\nβ
Tutorial created successfully!\n');
if (startEditing) {
spawn('node', ['edit-tutorial.js', name], { cwd: __dirname, stdio: 'inherit' });
} else {
console.log(`Next steps:\n\n npm run edit-tutorial -- ${name}\n`);
}
}
main().catch(err => {
console.error('\nβ', err.message);
process.exit(1);
});