|
1
|
'use strict'; |
|
2
|
|
|
3
|
const fs = require('fs'); |
|
4
|
const path = require('path'); |
|
5
|
const { parseArgs: parseNodeArgs } = require('node:util'); |
|
6
|
|
|
7
|
const TEMPLATE_SUFFIX = '.template'; |
|
8
|
const HTML_FILE_EXTENSIONS = new Set(['.html', '.htm']); |
|
9
|
const TOKEN_VALUES = { |
|
10
|
__REPLIT_ARTIFACT_SLUG__: (slug) => slug, |
|
11
|
__REPLIT_ARTIFACT_TITLE__: (_, title) => title, |
|
12
|
__REPLIT_ARTIFACT_PACKAGE_NAME__: (slug) => `@workspace/${slug}`, |
|
13
|
}; |
|
14
|
|
|
15
|
function escapeHtml(value) { |
|
16
|
return value |
|
17
|
.replaceAll('&', '&') |
|
18
|
.replaceAll('<', '<') |
|
19
|
.replaceAll('>', '>') |
|
20
|
.replaceAll('"', '"') |
|
21
|
.replaceAll("'", '''); |
|
22
|
} |
|
23
|
|
|
24
|
function parseArgs(argv) { |
|
25
|
const { values, positionals } = parseNodeArgs({ |
|
26
|
args: argv.slice(2), |
|
27
|
allowPositionals: true, |
|
28
|
options: { |
|
29
|
slug: { type: 'string' }, |
|
30
|
title: { type: 'string' }, |
|
31
|
}, |
|
32
|
}); |
|
33
|
const [artifactType] = positionals; |
|
34
|
const slug = values.slug; |
|
35
|
const title = values.title; |
|
36
|
|
|
37
|
if (!artifactType || !slug || !title) { |
|
38
|
console.error( |
|
39
|
'Usage: node bootstrap.js <artifactType> --slug=<slug> --title=<title>', |
|
40
|
); |
|
41
|
process.exit(1); |
|
42
|
} |
|
43
|
|
|
44
|
return { artifactType, slug, title }; |
|
45
|
} |
|
46
|
|
|
47
|
function interpolate(content, slug, title, { isHtml }) { |
|
48
|
let rendered = content; |
|
49
|
for (const [token, resolver] of Object.entries(TOKEN_VALUES)) { |
|
50
|
const value = resolver(slug, title); |
|
51
|
const replacement = isHtml ? escapeHtml(value) : value; |
|
52
|
rendered = rendered.replaceAll(token, replacement); |
|
53
|
} |
|
54
|
return rendered; |
|
55
|
} |
|
56
|
|
|
57
|
function copyDir(src, dest, slug, title) { |
|
58
|
fs.mkdirSync(dest, { recursive: true }); |
|
59
|
|
|
60
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) { |
|
61
|
const srcPath = path.join(src, entry.name); |
|
62
|
let destName = entry.name; |
|
63
|
|
|
64
|
if (entry.isDirectory()) { |
|
65
|
copyDir(srcPath, path.join(dest, destName), slug, title); |
|
66
|
continue; |
|
67
|
} |
|
68
|
|
|
69
|
const isTemplate = destName.endsWith(TEMPLATE_SUFFIX); |
|
70
|
if (isTemplate) { |
|
71
|
destName = destName.slice(0, -TEMPLATE_SUFFIX.length); |
|
72
|
} |
|
73
|
|
|
74
|
const destPath = path.join(dest, destName); |
|
75
|
|
|
76
|
if (isTemplate) { |
|
77
|
const raw = fs.readFileSync(srcPath, 'utf8'); |
|
78
|
const isHtml = HTML_FILE_EXTENSIONS.has(path.extname(destName).toLowerCase()); |
|
79
|
fs.writeFileSync(destPath, interpolate(raw, slug, title, { isHtml })); |
|
80
|
continue; |
|
81
|
} |
|
82
|
|
|
83
|
fs.copyFileSync(srcPath, destPath); |
|
84
|
} |
|
85
|
} |
|
86
|
|
|
87
|
function main() { |
|
88
|
const { artifactType, slug, title } = parseArgs(process.argv); |
|
89
|
const workspaceRoot = process.cwd(); |
|
90
|
|
|
91
|
|
|
92
|
let artifactFilesDir = artifactType; |
|
93
|
if (artifactType === 'data-visualization') { |
|
94
|
artifactFilesDir = 'react-vite'; |
|
95
|
} |
|
96
|
|
|
97
|
const filesDir = path.join(__dirname, 'artifacts', artifactFilesDir, 'files'); |
|
98
|
const destDir = path.join(workspaceRoot, 'artifacts', slug); |
|
99
|
|
|
100
|
if (!fs.existsSync(filesDir)) { |
|
101
|
console.error(`Error: missing template directory for ${artifactFilesDir}`); |
|
102
|
process.exit(1); |
|
103
|
} |
|
104
|
|
|
105
|
if (fs.existsSync(destDir)) { |
|
106
|
console.error(`Error: artifacts/${slug}/ already exists`); |
|
107
|
process.exit(1); |
|
108
|
} |
|
109
|
|
|
110
|
console.log(`Bootstrapping ${artifactType} artifact: ${slug}`); |
|
111
|
|
|
112
|
copyDir(filesDir, destDir, slug, title); |
|
113
|
console.log(` Copied files to artifacts/${slug}/`); |
|
114
|
|
|
115
|
console.log('Done.'); |
|
116
|
} |
|
117
|
|
|
118
|
main(); |