create-stylus 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/base/.gitignore.template.mjs +2 -2
- package/templates/base/dist/cli.js +683 -0
- package/templates/base/dist/cli.js.map +1 -0
- package/templates/base/package.json +1 -0
- package/templates/base/packages/nextjs/.gitignore.template.mjs +3 -3
- package/templates/base/packages/nextjs/scaffold.config.ts +2 -2
- package/templates/base/packages/stylus/.env.example +5 -2
- package/templates/base/packages/stylus/.gitignore.template.mjs +3 -3
- package/templates/base/packages/stylus/package.json +0 -1
- package/templates/base/packages/stylus/scripts/deploy.ts +29 -12
- package/templates/base/packages/stylus/scripts/deploy_contract.ts +34 -43
- package/templates/base/packages/stylus/scripts/deploy_wrapper.ts +4 -44
- package/templates/base/packages/stylus/scripts/export_abi.ts +13 -13
- package/templates/base/packages/stylus/scripts/utils/command.ts +18 -31
- package/templates/base/packages/stylus/scripts/utils/contract.ts +23 -14
- package/templates/base/packages/stylus/scripts/utils/deployment.ts +169 -45
- package/templates/base/packages/stylus/scripts/utils/network.ts +27 -7
- package/templates/base/packages/stylus/scripts/utils/type.ts +13 -10
- package/templates/base/packages/stylus/your-contract/Cargo.lock +35 -18
- package/templates/base/packages/stylus/your-contract/Cargo.toml +3 -1
- package/templates/base/packages/stylus/your-contract/src/lib.rs +51 -21
- package/templates/base/readme.md +208 -43
- package/templates/base/yarn.lock +1 -2
- package/templates/base/packages/stylus/README.md +0 -263
- package/templates/base/packages/stylus/header.png +0 -0
- package/templates/base/packages/stylus/scripts/deploy_all_contracts.ts +0 -59
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import url, { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs, { lstatSync, readdirSync, existsSync, promises } from 'fs';
|
|
5
|
+
import mergeJsonStr from 'merge-packages';
|
|
6
|
+
import ncp from 'ncp';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { projectInstall } from 'pkg-install';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import Listr from 'listr';
|
|
11
|
+
import arg from 'arg';
|
|
12
|
+
import inquirer from 'inquirer';
|
|
13
|
+
|
|
14
|
+
const isExtension = (item) => item !== null;
|
|
15
|
+
/**
|
|
16
|
+
* This function makes sure that the `T` generic type is narrowed down to
|
|
17
|
+
* whatever `extensions` are passed in the question prop. That way we can type
|
|
18
|
+
* check the `default` prop is not using any valid extension, but only one
|
|
19
|
+
* already provided in the `extensions` prop.
|
|
20
|
+
*
|
|
21
|
+
* Questions can be created without this function, just using a normal object,
|
|
22
|
+
* but `default` type will be any valid Extension.
|
|
23
|
+
*/
|
|
24
|
+
const typedQuestion = (question) => question;
|
|
25
|
+
const isDefined = (item) => item !== undefined && item !== null;
|
|
26
|
+
const extensionWithSubextensions = (extension) => {
|
|
27
|
+
return Object.prototype.hasOwnProperty.call(extension, "extensions");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const baseDir = "base";
|
|
31
|
+
|
|
32
|
+
const extensionDict = {};
|
|
33
|
+
const currentFileUrl = import.meta.url;
|
|
34
|
+
const templatesDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../templates");
|
|
35
|
+
/**
|
|
36
|
+
* This function has side effects. It generates the extensionDict.
|
|
37
|
+
*
|
|
38
|
+
* @param basePath the path at which to start the traverse
|
|
39
|
+
* @returns the extensions found in this path. Useful for the recursion
|
|
40
|
+
*/
|
|
41
|
+
const traverseExtensions = async (basePath) => {
|
|
42
|
+
const extensionsPath = path.resolve(basePath, "extensions");
|
|
43
|
+
let extensions;
|
|
44
|
+
try {
|
|
45
|
+
extensions = fs.readdirSync(extensionsPath);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
await Promise.all(extensions.map(async (ext) => {
|
|
51
|
+
const extPath = path.resolve(extensionsPath, ext);
|
|
52
|
+
const configPath = path.resolve(extPath, "config.json");
|
|
53
|
+
let config = {};
|
|
54
|
+
try {
|
|
55
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (fs.existsSync(configPath)) {
|
|
59
|
+
throw new Error(`Couldn't parse existing config.json file.
|
|
60
|
+
Extension: ${ext};
|
|
61
|
+
Config file path: ${configPath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let name = config.name ?? ext;
|
|
65
|
+
let value = ext;
|
|
66
|
+
const subExtensions = await traverseExtensions(extPath);
|
|
67
|
+
const hasSubExtensions = subExtensions.length !== 0;
|
|
68
|
+
const extDescriptor = {
|
|
69
|
+
name,
|
|
70
|
+
value,
|
|
71
|
+
path: extPath,
|
|
72
|
+
extensions: subExtensions,
|
|
73
|
+
extends: config.extends,
|
|
74
|
+
};
|
|
75
|
+
if (!hasSubExtensions) {
|
|
76
|
+
delete extDescriptor.extensions;
|
|
77
|
+
}
|
|
78
|
+
extensionDict[ext] = extDescriptor;
|
|
79
|
+
return subExtensions;
|
|
80
|
+
}));
|
|
81
|
+
return extensions;
|
|
82
|
+
};
|
|
83
|
+
await traverseExtensions(templatesDirectory);
|
|
84
|
+
|
|
85
|
+
const findFilesRecursiveSync = (baseDir, criteriaFn = () => true) => {
|
|
86
|
+
const subPaths = fs.readdirSync(baseDir);
|
|
87
|
+
const files = subPaths.map((relativePath) => {
|
|
88
|
+
const fullPath = path.resolve(baseDir, relativePath);
|
|
89
|
+
return fs.lstatSync(fullPath).isDirectory()
|
|
90
|
+
? [...findFilesRecursiveSync(fullPath, criteriaFn)]
|
|
91
|
+
: criteriaFn(fullPath)
|
|
92
|
+
? [fullPath]
|
|
93
|
+
: [];
|
|
94
|
+
});
|
|
95
|
+
return files.flat();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// @ts-expect-error We don't have types for this probably add .d.ts file
|
|
99
|
+
function mergePackageJson(targetPackageJsonPath, secondPackageJsonPath, isDev) {
|
|
100
|
+
const existsTarget = fs.existsSync(targetPackageJsonPath);
|
|
101
|
+
const existsSecond = fs.existsSync(secondPackageJsonPath);
|
|
102
|
+
if (!existsTarget && !existsSecond) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const targetPackageJson = existsTarget ? fs.readFileSync(targetPackageJsonPath, "utf8") : '{}';
|
|
106
|
+
const secondPackageJson = existsSecond ? fs.readFileSync(secondPackageJsonPath, "utf8") : '{}';
|
|
107
|
+
const mergedPkgStr = mergeJsonStr.default(targetPackageJson, secondPackageJson);
|
|
108
|
+
fs.writeFileSync(targetPackageJsonPath, mergedPkgStr, "utf8");
|
|
109
|
+
if (isDev) {
|
|
110
|
+
const devStr = `TODO: write relevant information for the contributor`;
|
|
111
|
+
fs.writeFileSync(`${targetPackageJsonPath}.dev`, devStr, "utf8");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { mkdir, link } = promises;
|
|
116
|
+
/**
|
|
117
|
+
* The goal is that this function has the same API as ncp, so they can be used
|
|
118
|
+
* interchangeably.
|
|
119
|
+
*
|
|
120
|
+
* - clobber not implemented
|
|
121
|
+
*/
|
|
122
|
+
const linkRecursive = async (source, destination, options) => {
|
|
123
|
+
const passesFilter = options?.filter === undefined
|
|
124
|
+
? true // no filter
|
|
125
|
+
: typeof options.filter === 'function'
|
|
126
|
+
? options.filter(source) // filter is function
|
|
127
|
+
: options.filter.test(source); // filter is regex
|
|
128
|
+
if (!passesFilter) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (lstatSync(source).isDirectory()) {
|
|
132
|
+
const subPaths = readdirSync(source);
|
|
133
|
+
await Promise.all(subPaths.map(async (subPath) => {
|
|
134
|
+
const sourceSubpath = path.join(source, subPath);
|
|
135
|
+
const isSubPathAFolder = lstatSync(sourceSubpath).isDirectory();
|
|
136
|
+
const destSubPath = path.join(destination, subPath);
|
|
137
|
+
const existsDestSubPath = existsSync(destSubPath);
|
|
138
|
+
if (isSubPathAFolder && !existsDestSubPath) {
|
|
139
|
+
await mkdir(destSubPath);
|
|
140
|
+
}
|
|
141
|
+
await linkRecursive(sourceSubpath, destSubPath, options);
|
|
142
|
+
}));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
return link(source, destination);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const copy = promisify(ncp);
|
|
149
|
+
let copyOrLink = copy;
|
|
150
|
+
const expandExtensions = (options) => {
|
|
151
|
+
const expandedExtensions = options.extensions
|
|
152
|
+
.map((extension) => extensionDict[extension])
|
|
153
|
+
.map((extDescriptor) => [extDescriptor.extends, extDescriptor.value].filter(isDefined))
|
|
154
|
+
.flat()
|
|
155
|
+
// this reduce just removes duplications
|
|
156
|
+
.reduce((exts, ext) => (exts.includes(ext) ? exts : [...exts, ext]), []);
|
|
157
|
+
return expandedExtensions;
|
|
158
|
+
};
|
|
159
|
+
const isTemplateRegex = /([^\/\\]*?)\.template\./;
|
|
160
|
+
const isPackageJsonRegex = /package\.json/;
|
|
161
|
+
const isYarnLockRegex = /yarn\.lock/;
|
|
162
|
+
const isNextGeneratedRegex = /packages\/nextjs\/generated/;
|
|
163
|
+
const isConfigRegex = /([^\/\\]*?)\\config\.json/;
|
|
164
|
+
const isArgsRegex = /([^\/\\]*?)\.args\./;
|
|
165
|
+
const isExtensionFolderRegex = /extensions$/;
|
|
166
|
+
const isPackagesFolderRegex = /packages$/;
|
|
167
|
+
const copyBaseFiles = async ({ dev: isDev }, basePath, targetDir) => {
|
|
168
|
+
await copyOrLink(basePath, targetDir, {
|
|
169
|
+
clobber: false,
|
|
170
|
+
filter: (fileName) => {
|
|
171
|
+
const isTemplate = isTemplateRegex.test(fileName);
|
|
172
|
+
const isPackageJson = isPackageJsonRegex.test(fileName);
|
|
173
|
+
const isYarnLock = isYarnLockRegex.test(fileName);
|
|
174
|
+
const isNextGenerated = isNextGeneratedRegex.test(fileName);
|
|
175
|
+
const skipAlways = isTemplate || isPackageJson;
|
|
176
|
+
const skipDevOnly = isYarnLock || isNextGenerated;
|
|
177
|
+
const shouldSkip = skipAlways || (isDev && skipDevOnly);
|
|
178
|
+
return !shouldSkip;
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const basePackageJsonPaths = findFilesRecursiveSync(basePath, path => isPackageJsonRegex.test(path));
|
|
182
|
+
basePackageJsonPaths.forEach(packageJsonPath => {
|
|
183
|
+
const partialPath = packageJsonPath.split(basePath)[1];
|
|
184
|
+
mergePackageJson(path.join(targetDir, partialPath), path.join(basePath, partialPath), isDev);
|
|
185
|
+
});
|
|
186
|
+
if (isDev) {
|
|
187
|
+
const baseYarnLockPaths = findFilesRecursiveSync(basePath, path => isYarnLockRegex.test(path));
|
|
188
|
+
baseYarnLockPaths.forEach(yarnLockPath => {
|
|
189
|
+
const partialPath = yarnLockPath.split(basePath)[1];
|
|
190
|
+
copy(path.join(basePath, partialPath), path.join(targetDir, partialPath));
|
|
191
|
+
});
|
|
192
|
+
const nextGeneratedPaths = findFilesRecursiveSync(basePath, path => isNextGeneratedRegex.test(path));
|
|
193
|
+
nextGeneratedPaths.forEach(nextGeneratedPath => {
|
|
194
|
+
const partialPath = nextGeneratedPath.split(basePath)[1];
|
|
195
|
+
copy(path.join(basePath, partialPath), path.join(targetDir, partialPath));
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const copyExtensionsFiles = async ({ extensions, dev: isDev }, targetDir) => {
|
|
200
|
+
await Promise.all(extensions.map(async (extension) => {
|
|
201
|
+
const extensionPath = extensionDict[extension].path;
|
|
202
|
+
// copy (or link if dev) root files
|
|
203
|
+
await copyOrLink(extensionPath, path.join(targetDir), {
|
|
204
|
+
clobber: false,
|
|
205
|
+
filter: (path) => {
|
|
206
|
+
const isConfig = isConfigRegex.test(path);
|
|
207
|
+
const isArgs = isArgsRegex.test(path);
|
|
208
|
+
const isExtensionFolder = isExtensionFolderRegex.test(path) && fs.lstatSync(path).isDirectory();
|
|
209
|
+
const isPackagesFolder = isPackagesFolderRegex.test(path) && fs.lstatSync(path).isDirectory();
|
|
210
|
+
const isTemplate = isTemplateRegex.test(path);
|
|
211
|
+
// PR NOTE: this wasn't needed before because ncp had the clobber: false
|
|
212
|
+
const isPackageJson = isPackageJsonRegex.test(path);
|
|
213
|
+
const shouldSkip = isConfig ||
|
|
214
|
+
isArgs ||
|
|
215
|
+
isTemplate ||
|
|
216
|
+
isPackageJson ||
|
|
217
|
+
isExtensionFolder ||
|
|
218
|
+
isPackagesFolder;
|
|
219
|
+
return !shouldSkip;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
// merge root package.json
|
|
223
|
+
mergePackageJson(path.join(targetDir, "package.json"), path.join(extensionPath, "package.json"), isDev);
|
|
224
|
+
const extensionPackagesPath = path.join(extensionPath, "packages");
|
|
225
|
+
const hasPackages = fs.existsSync(extensionPackagesPath);
|
|
226
|
+
if (hasPackages) {
|
|
227
|
+
// copy extension packages files
|
|
228
|
+
await copyOrLink(extensionPackagesPath, path.join(targetDir, "packages"), {
|
|
229
|
+
clobber: false,
|
|
230
|
+
filter: (path) => {
|
|
231
|
+
const isArgs = isArgsRegex.test(path);
|
|
232
|
+
const isTemplate = isTemplateRegex.test(path);
|
|
233
|
+
const isPackageJson = isPackageJsonRegex.test(path);
|
|
234
|
+
const shouldSkip = isArgs || isTemplate || isPackageJson;
|
|
235
|
+
return !shouldSkip;
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
// copy each package's package.json
|
|
239
|
+
const extensionPackages = fs.readdirSync(extensionPackagesPath);
|
|
240
|
+
extensionPackages.forEach((packageName) => {
|
|
241
|
+
mergePackageJson(path.join(targetDir, "packages", packageName, "package.json"), path.join(extensionPath, "packages", packageName, "package.json"), isDev);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}));
|
|
245
|
+
};
|
|
246
|
+
const processTemplatedFiles = async ({ extensions, dev: isDev }, basePath, targetDir) => {
|
|
247
|
+
const baseTemplatedFileDescriptors = findFilesRecursiveSync(basePath, (path) => isTemplateRegex.test(path)).map((baseTemplatePath) => ({
|
|
248
|
+
path: baseTemplatePath,
|
|
249
|
+
fileUrl: url.pathToFileURL(baseTemplatePath).href,
|
|
250
|
+
relativePath: baseTemplatePath.split(basePath)[1],
|
|
251
|
+
source: "base",
|
|
252
|
+
}));
|
|
253
|
+
const extensionsTemplatedFileDescriptors = extensions
|
|
254
|
+
.map((ext) => findFilesRecursiveSync(extensionDict[ext].path, (filePath) => isTemplateRegex.test(filePath)).map((extensionTemplatePath) => ({
|
|
255
|
+
path: extensionTemplatePath,
|
|
256
|
+
fileUrl: url.pathToFileURL(extensionTemplatePath).href,
|
|
257
|
+
relativePath: extensionTemplatePath.split(extensionDict[ext].path)[1],
|
|
258
|
+
source: `extension ${extensionDict[ext].name}`,
|
|
259
|
+
})))
|
|
260
|
+
.flat();
|
|
261
|
+
await Promise.all([
|
|
262
|
+
...baseTemplatedFileDescriptors,
|
|
263
|
+
...extensionsTemplatedFileDescriptors,
|
|
264
|
+
].map(async (templateFileDescriptor) => {
|
|
265
|
+
const templateTargetName = templateFileDescriptor.path.match(isTemplateRegex)?.[1];
|
|
266
|
+
const argsPath = templateFileDescriptor.relativePath.replace(isTemplateRegex, `${templateTargetName}.args.`);
|
|
267
|
+
const argsFileUrls = extensions
|
|
268
|
+
.map((extension) => {
|
|
269
|
+
const argsFilePath = path.join(extensionDict[extension].path, argsPath);
|
|
270
|
+
const fileExists = fs.existsSync(argsFilePath);
|
|
271
|
+
if (!fileExists) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
return url.pathToFileURL(argsFilePath).href;
|
|
275
|
+
})
|
|
276
|
+
.flat();
|
|
277
|
+
const args = await Promise.all(argsFileUrls.map(async (argsFileUrl) => await import(argsFileUrl)));
|
|
278
|
+
const template = (await import(templateFileDescriptor.fileUrl)).default;
|
|
279
|
+
if (!template) {
|
|
280
|
+
throw new Error(`Template ${templateTargetName} from ${templateFileDescriptor.source} doesn't have a default export`);
|
|
281
|
+
}
|
|
282
|
+
if (typeof template !== "function") {
|
|
283
|
+
throw new Error(`Template ${templateTargetName} from ${templateFileDescriptor.source} is not exporting a function by default`);
|
|
284
|
+
}
|
|
285
|
+
const freshArgs = Object.fromEntries(Object.keys(args[0] ?? {}).map((key) => [
|
|
286
|
+
key,
|
|
287
|
+
[], // INFO: initial value for the freshArgs object
|
|
288
|
+
]));
|
|
289
|
+
const combinedArgs = args.reduce((accumulated, arg) => {
|
|
290
|
+
Object.entries(arg).map(([key, value]) => {
|
|
291
|
+
accumulated[key].push(value);
|
|
292
|
+
});
|
|
293
|
+
return accumulated;
|
|
294
|
+
}, freshArgs);
|
|
295
|
+
// TODO test: if first arg file found only uses 1 name, I think the rest are not used?
|
|
296
|
+
const output = template(combinedArgs);
|
|
297
|
+
const targetPath = path.join(targetDir, templateFileDescriptor.relativePath.split(templateTargetName)[0], templateTargetName);
|
|
298
|
+
fs.writeFileSync(targetPath, output);
|
|
299
|
+
if (isDev) {
|
|
300
|
+
const hasCombinedArgs = Object.keys(combinedArgs).length > 0;
|
|
301
|
+
const hasArgsPaths = argsFileUrls.length > 0;
|
|
302
|
+
const devOutput = `--- TEMPLATE FILE
|
|
303
|
+
templates/${templateFileDescriptor.source}${templateFileDescriptor.relativePath}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
--- ARGS FILES
|
|
307
|
+
${hasArgsPaths
|
|
308
|
+
? argsFileUrls.map(url => `\t- ${path.join('templates', url.split('templates')[1])}`).join('\n')
|
|
309
|
+
: '(no args files writing to the template)'}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
--- RESULTING ARGS
|
|
313
|
+
${hasCombinedArgs
|
|
314
|
+
? Object.entries(combinedArgs)
|
|
315
|
+
.map(([argName, argValue]) => `\t- ${argName}:\t[${argValue.join(',')}]`)
|
|
316
|
+
// TODO improvement: figure out how to add the values added by each args file
|
|
317
|
+
.join('\n')
|
|
318
|
+
: '(no args sent for the template)'}
|
|
319
|
+
`;
|
|
320
|
+
fs.writeFileSync(`${targetPath}.dev`, devOutput);
|
|
321
|
+
}
|
|
322
|
+
}));
|
|
323
|
+
};
|
|
324
|
+
async function copyTemplateFiles(options, templateDir, targetDir) {
|
|
325
|
+
copyOrLink = options.dev ? linkRecursive : copy;
|
|
326
|
+
const basePath = path.join(templateDir, baseDir);
|
|
327
|
+
// 1. Copy base template to target directory
|
|
328
|
+
await copyBaseFiles(options, basePath, targetDir);
|
|
329
|
+
// 2. Add "parent" extensions (set via config.json#extend field)
|
|
330
|
+
const expandedExtension = expandExtensions(options);
|
|
331
|
+
options.extensions = expandedExtension;
|
|
332
|
+
// 3. Copy extensions folders
|
|
333
|
+
await copyExtensionsFiles(options, targetDir);
|
|
334
|
+
// 4. Process templated files and generate output
|
|
335
|
+
await processTemplatedFiles(options, basePath, targetDir);
|
|
336
|
+
// 5. Initialize git repo to avoid husky error
|
|
337
|
+
await execa("git", ["init"], { cwd: targetDir });
|
|
338
|
+
await execa("git", ["checkout", "-b", "main"], { cwd: targetDir });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function createProjectDirectory(projectName) {
|
|
342
|
+
try {
|
|
343
|
+
const result = await execa("mkdir", [projectName]);
|
|
344
|
+
if (result.failed) {
|
|
345
|
+
throw new Error("There was a problem running the mkdir command");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
throw new Error("Failed to create directory", { cause: error });
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function installPackages(targetDir) {
|
|
355
|
+
return projectInstall({
|
|
356
|
+
cwd: targetDir,
|
|
357
|
+
prefer: "yarn",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Checkout the latest release tag in a git submodule
|
|
362
|
+
async function checkoutLatestTag(submodulePath) {
|
|
363
|
+
try {
|
|
364
|
+
const { stdout } = await execa("git", ["tag", "-l", "--sort=-v:refname"], {
|
|
365
|
+
cwd: submodulePath,
|
|
366
|
+
});
|
|
367
|
+
const tagLines = stdout.split("\n");
|
|
368
|
+
if (tagLines.length > 0) {
|
|
369
|
+
const latestTag = tagLines[0];
|
|
370
|
+
await execa("git", ["-C", `${submodulePath}`, "checkout", latestTag]);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
throw new Error(`No tags found in submodule at ${submodulePath}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
console.error("Error checking out latest tag:", error);
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function createFirstGitCommit(targetDir, options) {
|
|
382
|
+
try {
|
|
383
|
+
// TODO: Move the logic for adding submodules to tempaltes
|
|
384
|
+
if (options.extensions?.includes("foundry")) {
|
|
385
|
+
const foundryWorkSpacePath = path.resolve(targetDir, "packages", "foundry");
|
|
386
|
+
await execa("git", [
|
|
387
|
+
"submodule",
|
|
388
|
+
"add",
|
|
389
|
+
"https://github.com/foundry-rs/forge-std",
|
|
390
|
+
"lib/forge-std",
|
|
391
|
+
], {
|
|
392
|
+
cwd: foundryWorkSpacePath,
|
|
393
|
+
});
|
|
394
|
+
await execa("git", [
|
|
395
|
+
"submodule",
|
|
396
|
+
"add",
|
|
397
|
+
"https://github.com/OpenZeppelin/openzeppelin-contracts",
|
|
398
|
+
"lib/openzeppelin-contracts",
|
|
399
|
+
], {
|
|
400
|
+
cwd: foundryWorkSpacePath,
|
|
401
|
+
});
|
|
402
|
+
await execa("git", [
|
|
403
|
+
"submodule",
|
|
404
|
+
"add",
|
|
405
|
+
"https://github.com/gnsps/solidity-bytes-utils",
|
|
406
|
+
"lib/solidity-bytes-utils",
|
|
407
|
+
], {
|
|
408
|
+
cwd: foundryWorkSpacePath,
|
|
409
|
+
});
|
|
410
|
+
await execa("git", ["submodule", "update", "--init", "--recursive"], {
|
|
411
|
+
cwd: foundryWorkSpacePath,
|
|
412
|
+
});
|
|
413
|
+
await checkoutLatestTag(path.resolve(foundryWorkSpacePath, "lib", "forge-std"));
|
|
414
|
+
await checkoutLatestTag(path.resolve(foundryWorkSpacePath, "lib", "openzeppelin-contracts"));
|
|
415
|
+
}
|
|
416
|
+
await execa("git", ["add", "-A"], { cwd: targetDir });
|
|
417
|
+
await execa("git", ["commit", "-m", "Initial commit with 🏗️ Scaffold-Stylus", "--no-verify"], { cwd: targetDir });
|
|
418
|
+
// Update the submodule, since we have checked out the latest tag in the previous step of foundry
|
|
419
|
+
if (options.extensions?.includes("foundry")) {
|
|
420
|
+
await execa("git", ["submodule", "update", "--init", "--recursive"], {
|
|
421
|
+
cwd: path.resolve(targetDir, "packages", "foundry"),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
// cast error as ExecaError to get stderr
|
|
427
|
+
throw new Error("Failed to initialize git repository", {
|
|
428
|
+
cause: e?.stderr ?? e,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// TODO: Instead of using execa, use prettier package from cli to format targetDir
|
|
434
|
+
async function prettierFormat(targetDir) {
|
|
435
|
+
try {
|
|
436
|
+
const result = await execa("yarn", ["format"], { cwd: targetDir });
|
|
437
|
+
if (result.failed) {
|
|
438
|
+
throw new Error("There was a problem running the format command");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
throw new Error("Failed to create directory", { cause: error });
|
|
443
|
+
}
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function renderOutroMessage(options) {
|
|
448
|
+
let message = `
|
|
449
|
+
\n
|
|
450
|
+
${chalk.bold.green("Congratulations!")} Your project has been scaffolded! 🎉
|
|
451
|
+
|
|
452
|
+
${chalk.bold("Next steps:")}
|
|
453
|
+
|
|
454
|
+
${chalk.dim("cd")} ${options.project}
|
|
455
|
+
`;
|
|
456
|
+
if (options.extensions.includes("hardhat") ||
|
|
457
|
+
options.extensions.includes("foundry")) {
|
|
458
|
+
message += `
|
|
459
|
+
\t${chalk.bold("Start the local development node")}
|
|
460
|
+
\t${chalk.dim("yarn")} chain
|
|
461
|
+
`;
|
|
462
|
+
if (options.extensions.includes("foundry")) {
|
|
463
|
+
try {
|
|
464
|
+
await execa("foundryup", ["-h"]);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
message += `
|
|
468
|
+
\t${chalk.bold.yellow("(NOTE: Foundryup is not installed in your system)")}
|
|
469
|
+
\t${chalk.dim("Checkout: https://getfoundry.sh")}
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
message += `
|
|
474
|
+
\t${chalk.bold("In a new terminal window, deploy your contracts")}
|
|
475
|
+
\t${chalk.dim("yarn")} deploy
|
|
476
|
+
`;
|
|
477
|
+
}
|
|
478
|
+
message += `
|
|
479
|
+
\t${chalk.bold("In a new terminal window, start the frontend")}
|
|
480
|
+
\t${chalk.dim("yarn")} start
|
|
481
|
+
`;
|
|
482
|
+
message += `
|
|
483
|
+
${chalk.bold.green("Thanks for using Scaffold-Stylus 🙏, Happy Building!")}
|
|
484
|
+
`;
|
|
485
|
+
console.log(message);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function createProject(options) {
|
|
489
|
+
console.log(`\n`);
|
|
490
|
+
const currentFileUrl = import.meta.url;
|
|
491
|
+
const templateDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../templates");
|
|
492
|
+
const targetDirectory = path.resolve(process.cwd(), options.project);
|
|
493
|
+
const tasks = new Listr([
|
|
494
|
+
{
|
|
495
|
+
title: `📁 Create project directory ${targetDirectory}`,
|
|
496
|
+
task: () => createProjectDirectory(options.project),
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
title: `🚀 Creating a new Scaffold-Stylus app in ${chalk.green.bold(options.project)}`,
|
|
500
|
+
task: () => copyTemplateFiles(options, templateDirectory, targetDirectory),
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
title: `📦 Installing dependencies with yarn, this could take a while`,
|
|
504
|
+
task: () => installPackages(targetDirectory),
|
|
505
|
+
skip: () => {
|
|
506
|
+
if (!options.install) {
|
|
507
|
+
return "Manually skipped";
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
title: "🪄 Formatting files with prettier",
|
|
513
|
+
task: () => prettierFormat(targetDirectory),
|
|
514
|
+
skip: () => {
|
|
515
|
+
if (!options.install) {
|
|
516
|
+
return "Skipping because prettier install was skipped";
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
title: `📡 Initializing Git repository ${options.extensions.includes("foundry") ? "and submodules" : ""}`,
|
|
522
|
+
task: () => createFirstGitCommit(targetDirectory, options),
|
|
523
|
+
},
|
|
524
|
+
]);
|
|
525
|
+
try {
|
|
526
|
+
await tasks.run();
|
|
527
|
+
renderOutroMessage(options);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
console.log("%s Error occurred", chalk.red.bold("ERROR"), error);
|
|
531
|
+
console.log("%s Exiting...", chalk.red.bold("Uh oh! 😕 Sorry about that!"));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// TODO update smartContractFramework code with general extensions
|
|
536
|
+
function parseArgumentsIntoOptions(rawArgs) {
|
|
537
|
+
const args = arg({
|
|
538
|
+
"--install": Boolean,
|
|
539
|
+
"-i": "--install",
|
|
540
|
+
"--skip-install": Boolean,
|
|
541
|
+
"--skip": "--skip-install",
|
|
542
|
+
"-s": "--skip-install",
|
|
543
|
+
"--dev": Boolean,
|
|
544
|
+
}, {
|
|
545
|
+
argv: rawArgs.slice(2).map((a) => a.toLowerCase()),
|
|
546
|
+
});
|
|
547
|
+
const install = args["--install"] ?? null;
|
|
548
|
+
const skipInstall = args["--skip-install"] ?? null;
|
|
549
|
+
const hasInstallRelatedFlag = install || skipInstall;
|
|
550
|
+
const dev = args["--dev"] ?? false; // info: use false avoid asking user
|
|
551
|
+
const project = args._[0] ?? null;
|
|
552
|
+
return {
|
|
553
|
+
project,
|
|
554
|
+
install: hasInstallRelatedFlag ? install || !skipInstall : null,
|
|
555
|
+
dev,
|
|
556
|
+
extensions: null, // TODO add extensions flags
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const config = {
|
|
561
|
+
questions: [
|
|
562
|
+
typedQuestion({
|
|
563
|
+
type: "single-select",
|
|
564
|
+
name: "solidity-framework",
|
|
565
|
+
message: "What solidity framework do you want to use?",
|
|
566
|
+
extensions: ["hardhat", "foundry", null],
|
|
567
|
+
default: "hardhat",
|
|
568
|
+
}),
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// default values for unspecified args
|
|
573
|
+
const defaultOptions = {
|
|
574
|
+
project: "my-dapp-example",
|
|
575
|
+
install: true,
|
|
576
|
+
dev: false,
|
|
577
|
+
extensions: [],
|
|
578
|
+
};
|
|
579
|
+
const invalidQuestionNames = ["project", "install"];
|
|
580
|
+
const nullExtensionChoice = {
|
|
581
|
+
name: 'None',
|
|
582
|
+
value: null
|
|
583
|
+
};
|
|
584
|
+
async function promptForMissingOptions(options) {
|
|
585
|
+
const cliAnswers = Object.fromEntries(Object.entries(options).filter(([key, value]) => value !== null));
|
|
586
|
+
const questions = [];
|
|
587
|
+
questions.push({
|
|
588
|
+
type: "input",
|
|
589
|
+
name: "project",
|
|
590
|
+
message: "Your project name:",
|
|
591
|
+
default: defaultOptions.project,
|
|
592
|
+
validate: (value) => value.length > 0,
|
|
593
|
+
});
|
|
594
|
+
const recurringAddFollowUps = (extensions, relatedQuestion) => {
|
|
595
|
+
extensions.filter(extensionWithSubextensions).forEach((ext) => {
|
|
596
|
+
const nestedExtensions = ext.extensions.map((nestedExt) => extensionDict[nestedExt]);
|
|
597
|
+
questions.push({
|
|
598
|
+
// INFO: assuming nested extensions are all optional. To change this,
|
|
599
|
+
// update ExtensionDescriptor adding type, and update code here.
|
|
600
|
+
type: "checkbox",
|
|
601
|
+
name: `${ext.value}-extensions`,
|
|
602
|
+
message: `Select optional extensions for ${ext.name}`,
|
|
603
|
+
choices: nestedExtensions,
|
|
604
|
+
when: (answers) => {
|
|
605
|
+
const relatedResponse = answers[relatedQuestion];
|
|
606
|
+
const wasMultiselectResponse = Array.isArray(relatedResponse);
|
|
607
|
+
return wasMultiselectResponse
|
|
608
|
+
? relatedResponse.includes(ext.value)
|
|
609
|
+
: relatedResponse === ext.value;
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
recurringAddFollowUps(nestedExtensions, `${ext.value}-extensions`);
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
config.questions.forEach((question) => {
|
|
616
|
+
if (invalidQuestionNames.includes(question.name)) {
|
|
617
|
+
throw new Error(`The name of the question can't be "${question.name}". The invalid names are: ${invalidQuestionNames
|
|
618
|
+
.map((w) => `"${w}"`)
|
|
619
|
+
.join(", ")}`);
|
|
620
|
+
}
|
|
621
|
+
const extensions = question.extensions
|
|
622
|
+
.filter(isExtension)
|
|
623
|
+
.map((ext) => extensionDict[ext])
|
|
624
|
+
.filter(isDefined);
|
|
625
|
+
const hasNoneOption = question.extensions.includes(null);
|
|
626
|
+
questions.push({
|
|
627
|
+
type: question.type === "multi-select" ? "checkbox" : "list",
|
|
628
|
+
name: question.name,
|
|
629
|
+
message: question.message,
|
|
630
|
+
choices: hasNoneOption ? [...extensions, nullExtensionChoice] : extensions,
|
|
631
|
+
});
|
|
632
|
+
recurringAddFollowUps(extensions, question.name);
|
|
633
|
+
});
|
|
634
|
+
questions.push({
|
|
635
|
+
type: "confirm",
|
|
636
|
+
name: "install",
|
|
637
|
+
message: "Install packages?",
|
|
638
|
+
default: defaultOptions.install,
|
|
639
|
+
});
|
|
640
|
+
const answers = await inquirer.prompt(questions, cliAnswers);
|
|
641
|
+
const mergedOptions = {
|
|
642
|
+
project: options.project ?? answers.project,
|
|
643
|
+
install: options.install ?? answers.install,
|
|
644
|
+
dev: options.dev ?? defaultOptions.dev,
|
|
645
|
+
extensions: [],
|
|
646
|
+
};
|
|
647
|
+
config.questions.forEach((question) => {
|
|
648
|
+
const { name } = question;
|
|
649
|
+
const choice = [answers[name]].flat().filter(isDefined);
|
|
650
|
+
mergedOptions.extensions.push(...choice);
|
|
651
|
+
});
|
|
652
|
+
const recurringAddNestedExtensions = (baseExtensions) => {
|
|
653
|
+
baseExtensions.forEach((extValue) => {
|
|
654
|
+
const nestedExtKey = `${extValue}-extensions`;
|
|
655
|
+
const nestedExtensions = answers[nestedExtKey];
|
|
656
|
+
if (nestedExtensions) {
|
|
657
|
+
mergedOptions.extensions.push(...nestedExtensions);
|
|
658
|
+
recurringAddNestedExtensions(nestedExtensions);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
recurringAddNestedExtensions(mergedOptions.extensions);
|
|
663
|
+
return mergedOptions;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const TITLE_TEXT = `
|
|
667
|
+
${chalk.bold.blue("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+")}
|
|
668
|
+
${chalk.bold.blue("| Create Scaffold-Stylus app |")}
|
|
669
|
+
${chalk.bold.blue("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+")}
|
|
670
|
+
`;
|
|
671
|
+
function renderIntroMessage() {
|
|
672
|
+
console.log(TITLE_TEXT);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function cli(args) {
|
|
676
|
+
renderIntroMessage();
|
|
677
|
+
const rawOptions = parseArgumentsIntoOptions(args);
|
|
678
|
+
const options = await promptForMissingOptions(rawOptions);
|
|
679
|
+
await createProject(options);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export { cli };
|
|
683
|
+
//# sourceMappingURL=cli.js.map
|