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