-
Notifications
You must be signed in to change notification settings - Fork 352
Expand file tree
/
Copy pathmigrate.cjs
More file actions
269 lines (224 loc) · 9.07 KB
/
Copy pathmigrate.cjs
File metadata and controls
269 lines (224 loc) · 9.07 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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
const fs = require('fs');
const path = require('path');
// 1. Require the old VuePress configuration
console.log('⚡ Loading old VuePress configuration...');
const oldConfig = require('./docs/.vuepress/config.cjs');
const sidebar = oldConfig.themeConfig.sidebar;
// 2. Define directory paths
const srcDocsDir = path.join(__dirname, 'src', 'content', 'docs');
const srcComponentsDir = path.join(__dirname, 'src', 'components');
const publicDir = path.join(__dirname, 'public');
console.log('⚡ Creating Astro Starlight directories...');
fs.mkdirSync(srcDocsDir, { recursive: true });
fs.mkdirSync(srcComponentsDir, { recursive: true });
fs.mkdirSync(publicDir, { recursive: true });
// Helper to escape string for frontmatter
function escapeFrontmatterString(str) {
if (!str) return '';
return str.replace(/"/g, '\\"');
}
// 3. Process each sidebar / language directory
const starlightSidebar = [];
for (const key of Object.keys(sidebar)) {
const languagePath = key.replace(/^\/|\/$/g, ''); // E.g., 'sql', 'typescript'
if (!languagePath) continue;
const configGroup = sidebar[key][0];
const title = configGroup.title;
const children = configGroup.children;
console.log(`\n📦 Processing language: ${title} (${languagePath})...`);
const starlightGroup = {
label: title,
items: []
};
const oldLangDir = path.join(__dirname, 'docs', languagePath);
const newLangDir = path.join(srcDocsDir, languagePath);
if (!fs.existsSync(oldLangDir)) {
console.log(`⚠️ Warning: Directory docs/${languagePath} does not exist. Skipping file migration for this topic.`);
continue;
}
// Create new directory in src/content/docs
fs.mkdirSync(newLangDir, { recursive: true });
// Move files and process frontmatter
const titleMap = {};
const files = fs.readdirSync(oldLangDir);
for (const file of files) {
const filePath = path.join(oldLangDir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) continue;
let targetFileName = file;
if (file.toLowerCase() === 'readme.md') {
targetFileName = 'index.md';
}
const newFilePath = path.join(newLangDir, targetFileName);
// Read and process markdown file
let content = fs.readFileSync(filePath, 'utf8');
// Parse existing frontmatter and title
let titleVal = '';
let descriptionVal = '';
// Check if the file has frontmatter
const frontmatterRegex = /^---([\s\S]*?)---/;
const frontmatterMatch = content.match(frontmatterRegex);
let frontmatterContent = '';
let bodyContent = content;
if (frontmatterMatch) {
frontmatterContent = frontmatterMatch[1];
bodyContent = content.replace(frontmatterRegex, '').trim();
// Extract existing metaTitle or title
const metaTitleMatch = frontmatterContent.match(/metaTitle:\s*"([^"]+)"/);
if (metaTitleMatch) {
titleVal = metaTitleMatch[1];
// Clean up leading language prefix in metaTitle (e.g. "SQL - Select" -> "Select")
if (titleVal.includes(' - ')) {
titleVal = titleVal.split(' - ').slice(1).join(' - ');
}
}
const descMatch = frontmatterContent.match(/description:\s*"([^"]+)"/);
if (descMatch) {
descriptionVal = descMatch[1];
}
}
// If we didn't find a title in frontmatter, look for the first # H1 heading
if (!titleVal) {
const h1Match = bodyContent.match(/^#\s+(.+)$/m);
if (h1Match) {
titleVal = h1Match[1].trim();
}
}
// Fallback if no title is found at all
if (!titleVal) {
titleVal = targetFileName.replace(/\.md$/, '').replace(/-/g, ' ');
// Capitalize
titleVal = titleVal.charAt(0).toUpperCase() + titleVal.slice(1);
}
// Store in lookup map for sidebar labels
const fileBase = file.replace(/\.md$/, '');
titleMap[fileBase] = titleVal;
// Rebuild the frontmatter for Astro Starlight
const cleanTitle = escapeFrontmatterString(titleVal);
const cleanDesc = escapeFrontmatterString(descriptionVal || `Tutorial about ${titleVal}`);
const newFrontmatter = `---
title: "${cleanTitle}"
description: "${cleanDesc}"
---
`;
// Ensure we don't keep duplicate high-level H1 title that Starlight renders automatically
// (Starlight renders the 'title' frontmatter as the main H1, so we should strip the first H1 in body if it exists)
let cleanBody = bodyContent;
const h1Regex = /^#\s+.+$/m;
if (h1Regex.test(cleanBody)) {
cleanBody = cleanBody.replace(h1Regex, '').trim();
}
// Convert VuePress ::: container syntax to standard Markdown blockquotes
cleanBody = cleanBody.replace(/:::\s*(\w+)([^\n]*)\n([\s\S]*?)\n:::/g, (match, type, title, content) => {
const capType = type.charAt(0).toUpperCase() + type.slice(1);
const header = title.trim() ? title.trim() : capType;
return `> **${header}:**\n` + content.split('\n').map(line => `> ${line}`).join('\n');
});
fs.writeFileSync(newFilePath, newFrontmatter + cleanBody, 'utf8');
}
// Construct Starlight sidebar items based on the old config order
for (const child of children) {
if (Array.isArray(child)) {
// It's like ["", "Disclaimer"]
const link = `/${languagePath}/`;
starlightGroup.items.push({ label: child[1], link: link });
} else {
// It's a string representing a file
const fileBase = child;
const link = `/${languagePath}/${fileBase}/`;
const resolvedTitle = titleMap[fileBase] || fileBase.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
starlightGroup.items.push({ label: resolvedTitle, link: link });
}
}
starlightSidebar.push(starlightGroup);
}
// 4. Save the sidebars configuration as a JSON file
console.log('\n💾 Saving Starlight sidebar configuration...');
fs.writeFileSync(
path.join(__dirname, 'sidebars.json'),
JSON.stringify(starlightSidebar, null, 2),
'utf8'
);
// 5. Copy public assets
console.log('⚡ Copying public assets...');
const oldPublicDir = path.join(__dirname, 'docs', '.vuepress', 'public');
if (fs.existsSync(oldPublicDir)) {
const publicFiles = fs.readdirSync(oldPublicDir);
for (const file of publicFiles) {
fs.copyFileSync(
path.join(oldPublicDir, file),
path.join(publicDir, file)
);
}
}
// 5.5 Copy logo to src/assets for Astro Starlight processing
console.log('⚡ Processing logo asset...');
const srcAssetsDir = path.join(__dirname, 'src', 'assets');
fs.mkdirSync(srcAssetsDir, { recursive: true });
if (fs.existsSync(path.join(publicDir, 'logo.png'))) {
fs.copyFileSync(
path.join(publicDir, 'logo.png'),
path.join(srcAssetsDir, 'logo.png')
);
}
// 6. Migrate and convert LanguageSearch.vue to Vue 3 compatible syntax
console.log('⚡ Migrating LanguageSearch.vue...');
const oldSearchComponent = path.join(__dirname, 'docs', '.vuepress', 'components', 'LanguageSearch.vue');
const newSearchComponent = path.join(srcComponentsDir, 'LanguageSearch.vue');
if (fs.existsSync(oldSearchComponent)) {
let searchContent = fs.readFileSync(oldSearchComponent, 'utf8');
// Replace <router-link> with <a> tag
searchContent = searchContent.replace(/<router-link\s+([^>]*?)to="\{ path: `\$\{language.url\}` \}">/g, '<a :href="language.url">');
searchContent = searchContent.replace(/<\/router-link>/g, '</a>');
// Replace background-size and absolute paths for images if needed
searchContent = searchContent.replace(/\/assets\/img\/search\.83621669\.svg/g, '/search.svg');
fs.writeFileSync(newSearchComponent, searchContent, 'utf8');
}
// Create a mock search.svg if it doesn't exist
const searchSvgPath = path.join(publicDir, 'search.svg');
if (!fs.existsSync(searchSvgPath)) {
fs.writeFileSync(
searchSvgPath,
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#cfd4db" width="24" height="24">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>`,
'utf8'
);
}
// 7. Write the homepage
console.log('⚡ Writing Astro homepage...');
const newHomepagePath = path.join(srcDocsDir, 'index.mdx');
const homepageContent = `---
title: DevTut
description: Example based programming tutorials for solid developers. Master 45+ programming topics.
template: splash
hero:
title: DevTut
tagline: Example based programming tutorials for solid developers. Master 45+ programming topics.
image:
file: ../../assets/logo.png
---
import LanguageSearch from '../../components/LanguageSearch.vue';
<LanguageSearch client:load />
`;
fs.writeFileSync(newHomepagePath, homepageContent, 'utf8');
// 8. Generate TSConfig
console.log('⚡ Creating modern tsconfig.json...');
const tsConfig = {
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
}
};
fs.writeFileSync(
path.join(__dirname, 'tsconfig.json'),
JSON.stringify(tsConfig, null, 2),
'utf8'
);
console.log('🎉 Starlight migration script complete!');
console.log('To complete migration:');
console.log('1. Run the script: node migrate.cjs');
console.log('2. Replace package.json with Astro Starlight version.');
console.log('3. Run yarn install');
console.log('4. Run yarn dev');