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('<', '&lt;')
19 .replaceAll('>', '&gt;')
20 .replaceAll('"', '&quot;')
21 .replaceAll("'", '&#39;');
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();