@adbayb/stack 2.32.1 → 2.33.0-next-4976f1e
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/configs/eslint/constants.js +2 -0
- package/configs/eslint/index.js +14 -14
- package/configs/eslint/presets/dependencies.js +14 -0
- package/configs/eslint/presets/eslint.js +3 -4
- package/configs/eslint/presets/import.js +9 -21
- package/configs/eslint/presets/jsdoc.js +1 -1
- package/configs/eslint/presets/node.js +6 -4
- package/configs/eslint/presets/react.js +64 -65
- package/configs/eslint/presets/sonar.js +1 -3
- package/configs/eslint/presets/stylistic.js +74 -71
- package/configs/eslint/presets/test.js +30 -5
- package/configs/eslint/presets/typescript.js +1 -2
- package/configs/eslint/presets/unicorn.js +13 -1
- package/dist/index.js +403 -409
- package/package.json +26 -28
- package/templates/multi-projects/.github/renovate.json +0 -3
- package/templates/multi-projects/.github/workflows/continuous_delivery.yml +6 -13
- package/templates/multi-projects/.github/workflows/conventional_commit.yml +2 -3
- package/templates/multi-projects/.github/workflows/dependency_changelog.yml +2 -2
- package/templates/multi-projects/.github/workflows/workflow.yml +3 -13
- package/templates/multi-projects/package.json.tmpl +11 -3
- package/templates/single-project/.github/renovate.json +0 -3
- package/templates/single-project/.github/workflows/continuous_delivery.yml +6 -13
- package/templates/single-project/.github/workflows/conventional_commit.yml +2 -3
- package/templates/single-project/.github/workflows/dependency_changelog.yml +2 -2
- package/templates/single-project/.github/workflows/workflow.yml +3 -13
- package/templates/single-project/package.json.tmpl +11 -3
- package/configs/eslint/presets/uncategorized.js +0 -42
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { helpers, termost } from 'termost';
|
|
2
|
-
import { resolve, join } from 'node:path';
|
|
3
2
|
import { createRequire } from 'node:module';
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync, cpSync, readFileSync, renameSync, writeFileSync
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { existsSync, readdirSync, cpSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { rm, mkdir, symlink, writeFile, chmod } from 'node:fs/promises';
|
|
6
6
|
import { fdir } from 'fdir';
|
|
7
7
|
|
|
8
8
|
const require$1 = createRequire(import.meta.url);
|
|
@@ -41,9 +41,9 @@ function assert(expectedCondition, createError) {
|
|
|
41
41
|
│ ◠ ◠ ${input.title}
|
|
42
42
|
│ ${type === "error" ? "◠" : "◡"} │ ${input.description}
|
|
43
43
|
╰─────╯
|
|
44
|
-
${
|
|
44
|
+
${input.body ? `
|
|
45
45
|
${input.body}
|
|
46
|
-
`}`, {
|
|
46
|
+
` : ""}`, {
|
|
47
47
|
color: colorByType[type]
|
|
48
48
|
}));
|
|
49
49
|
};
|
|
@@ -73,13 +73,6 @@ const createError = (bin, error)=>{
|
|
|
73
73
|
}
|
|
74
74
|
return new Error(errorMessage);
|
|
75
75
|
};
|
|
76
|
-
const checkPackageManager = async ()=>{
|
|
77
|
-
try {
|
|
78
|
-
await helpers.exec("npx only-allow pnpm");
|
|
79
|
-
} catch {
|
|
80
|
-
throw createError("pnpm", "The project must use `pnpm` as a node package manager tool. Follow this installation guide https://pnpm.io/installation");
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
76
|
const getNpmVersion = async ()=>{
|
|
84
77
|
try {
|
|
85
78
|
return await helpers.exec("pnpm -v");
|
|
@@ -87,17 +80,8 @@ const getNpmVersion = async ()=>{
|
|
|
87
80
|
throw createError("pnpm", "The project must use `pnpm` as a node package manager tool. Follow this installation guide https://pnpm.io/installation");
|
|
88
81
|
}
|
|
89
82
|
};
|
|
90
|
-
const getStackCommand = (command
|
|
91
|
-
|
|
92
|
-
* `isNodeRuntime` allows executing node bin executables in a non node environment such as in git hooks context
|
|
93
|
-
* Npx is used to make executable resolution independent from the build tool (npx is the built-in Node tool)
|
|
94
|
-
* `--no` flag to prevent installation prompt and throw an error if the binary is not installed.
|
|
95
|
-
*/ return [
|
|
96
|
-
...isNodeRuntime ? [] : [
|
|
97
|
-
"npx --no"
|
|
98
|
-
],
|
|
99
|
-
`stack ${command}`
|
|
100
|
-
].join(" ");
|
|
83
|
+
const getStackCommand = (command)=>{
|
|
84
|
+
return `pnpm stack ${command}`;
|
|
101
85
|
};
|
|
102
86
|
const hasDependency = (packageName)=>{
|
|
103
87
|
return Boolean(require$1.resolve(packageName));
|
|
@@ -197,190 +181,308 @@ const ESLINT_EXTENSIONS = [
|
|
|
197
181
|
"mdx"
|
|
198
182
|
];
|
|
199
183
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const createWatchCommand = (program)=>{
|
|
184
|
+
const createBuildCommand = (program)=>{
|
|
203
185
|
program.command({
|
|
204
|
-
|
|
205
|
-
|
|
186
|
+
description: "Build the project in production mode",
|
|
187
|
+
name: "build"
|
|
206
188
|
}).task({
|
|
207
189
|
async handler () {
|
|
208
|
-
await turbo("
|
|
190
|
+
await turbo("build");
|
|
209
191
|
}
|
|
210
192
|
});
|
|
211
193
|
};
|
|
212
194
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
await turbo("test");
|
|
220
|
-
}
|
|
221
|
-
});
|
|
195
|
+
const checkChangelog = async ()=>{
|
|
196
|
+
try {
|
|
197
|
+
return await helpers.exec("changeset status --since=origin/main");
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw createError("changeset", error);
|
|
200
|
+
}
|
|
222
201
|
};
|
|
223
202
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
description: "Start the project in production mode"
|
|
228
|
-
}).task({
|
|
229
|
-
async handler () {
|
|
230
|
-
await turbo("start");
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
};
|
|
203
|
+
const checkCode = eslint({
|
|
204
|
+
isFixMode: false
|
|
205
|
+
});
|
|
234
206
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
name: "log",
|
|
242
|
-
description: "Add a new changelog entry"
|
|
243
|
-
}).option({
|
|
244
|
-
key: "tag",
|
|
245
|
-
name: "tag",
|
|
246
|
-
description: "Bump the package(s) version"
|
|
247
|
-
}).option({
|
|
248
|
-
key: "publish",
|
|
249
|
-
name: "publish",
|
|
250
|
-
description: "Publish package(s) to the registry"
|
|
251
|
-
}).task({
|
|
252
|
-
async handler () {
|
|
253
|
-
helpers.message("New changelog entry\n");
|
|
254
|
-
await changeset("changeset");
|
|
255
|
-
},
|
|
256
|
-
skip: ifNotEqualTo("log")
|
|
257
|
-
}).task({
|
|
258
|
-
async handler () {
|
|
259
|
-
helpers.message("Bumping the package(s) version\n");
|
|
260
|
-
await changeset("changeset version && pnpm install --no-frozen-lockfile");
|
|
261
|
-
},
|
|
262
|
-
skip: ifNotEqualTo("tag")
|
|
263
|
-
}).task({
|
|
264
|
-
async handler () {
|
|
265
|
-
helpers.message("Publishing package(s) to the registry\n");
|
|
266
|
-
await changeset("stack build && pnpm changeset publish");
|
|
267
|
-
},
|
|
268
|
-
skip: ifNotEqualTo("publish")
|
|
269
|
-
});
|
|
207
|
+
const checkCommit = async ()=>{
|
|
208
|
+
try {
|
|
209
|
+
return await helpers.exec('commitlint --extends "@commitlint/config-conventional" --edit');
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw createError("commitlint", error);
|
|
212
|
+
}
|
|
270
213
|
};
|
|
271
|
-
const ifNotEqualTo = (validOption)=>(context)=>{
|
|
272
|
-
return !context[validOption];
|
|
273
|
-
};
|
|
274
214
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
}).task({
|
|
292
|
-
label: label$4("Check the package manager"),
|
|
293
|
-
async handler () {
|
|
294
|
-
await checkPackageManager();
|
|
295
|
-
}
|
|
215
|
+
const checkDependency = async ()=>{
|
|
216
|
+
const stdout = await helpers.exec("pnpm recursive ls --json");
|
|
217
|
+
const checkDependencyVersionMismatch = createPackagesVersionMismatchChecker();
|
|
218
|
+
const packages = JSON.parse(stdout).map((package_)=>{
|
|
219
|
+
const packagePath = join(package_.path, "package.json");
|
|
220
|
+
assert(package_.name, ()=>createPackageError(`\`${packagePath}\` must have a name field.`));
|
|
221
|
+
const packageContent = require$1(packagePath);
|
|
222
|
+
const peerDependencies = packageContent.peerDependencies ?? {};
|
|
223
|
+
const devDependencies = packageContent.devDependencies ?? {};
|
|
224
|
+
const dependencies = packageContent.dependencies ?? {};
|
|
225
|
+
return {
|
|
226
|
+
dependencies,
|
|
227
|
+
devDependencies,
|
|
228
|
+
name: package_.name,
|
|
229
|
+
peerDependencies
|
|
230
|
+
};
|
|
296
231
|
});
|
|
232
|
+
for (const package_ of packages){
|
|
233
|
+
// Check version mismatches to guarantee a single copy for a given package in the monorepo (use case: prevent singleton-like issues with React contexts)
|
|
234
|
+
checkDependencyVersionMismatch(package_);
|
|
235
|
+
// Check version range accordingly to our dependency guidelines (ie. dev dependencies must be pinned and dependencies must have caret)
|
|
236
|
+
checkDependencyVersionRange(package_);
|
|
237
|
+
}
|
|
297
238
|
};
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const PRETTIER_IGNORE_FILES = [
|
|
310
|
-
"pnpm-lock.yaml"
|
|
311
|
-
];
|
|
312
|
-
const fixFormatting = async (files)=>{
|
|
313
|
-
let prettierFiles = [];
|
|
314
|
-
if (files.length === 0) {
|
|
315
|
-
prettierFiles.push(`"**/!(${PRETTIER_IGNORE_FILES.join("|")})"`);
|
|
316
|
-
} else {
|
|
317
|
-
prettierFiles = files.filter((file)=>{
|
|
318
|
-
return !PRETTIER_IGNORE_FILES.some((filename)=>file.endsWith(filename));
|
|
239
|
+
const checkDependencyVersionRange = ({ dependencies, devDependencies, name, peerDependencies })=>{
|
|
240
|
+
for (const dependencyName of Object.keys(devDependencies)){
|
|
241
|
+
const version = devDependencies[dependencyName];
|
|
242
|
+
assertVersion(version, {
|
|
243
|
+
consumedBy: name,
|
|
244
|
+
name: dependencyName
|
|
245
|
+
});
|
|
246
|
+
if (version !== "workspace:*" && !isExcluded(version) && !/^\d/.test(version)) throw createPackageError(`As a dev dependency, \`${dependencyName}\` version must be fixed (or set as "workspace:*" for local packages) to reduce accidental breaking change risks due to an implicit semver upgrade.`, {
|
|
247
|
+
consumedBy: name,
|
|
248
|
+
name: dependencyName
|
|
319
249
|
});
|
|
320
|
-
if (prettierFiles.length === 0) return;
|
|
321
250
|
}
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
251
|
+
for (const dependencyName of Object.keys(dependencies)){
|
|
252
|
+
const version = dependencies[dependencyName];
|
|
253
|
+
assertVersion(version, {
|
|
254
|
+
consumedBy: name,
|
|
255
|
+
name: dependencyName
|
|
256
|
+
});
|
|
257
|
+
if (version !== "workspace:^" && !hasCaret(version) && !isExcluded(version)) throw createPackageError(`As a dependency, \`${dependencyName}\` version must be prefixed with a caret (or set as "workspace:^" for local packages) to optimize the size (whether of installation or bundle output) on the consumer side.`, {
|
|
258
|
+
consumedBy: name,
|
|
259
|
+
name: dependencyName
|
|
260
|
+
});
|
|
327
261
|
}
|
|
328
|
-
|
|
262
|
+
for (const dependencyName of Object.keys(peerDependencies)){
|
|
263
|
+
const version = peerDependencies[dependencyName];
|
|
264
|
+
assertVersion(version, {
|
|
265
|
+
consumedBy: name,
|
|
266
|
+
name: dependencyName
|
|
267
|
+
});
|
|
268
|
+
if (!hasCaret(version) && !isExcluded(version)) /*
|
|
269
|
+
* Why disallowing workspace protocol as a version resolver?
|
|
270
|
+
* To reduce the update frequency needs consumer-side and guarantee on our side the minimum compatible version,
|
|
271
|
+
* the best practice should be to keeping an explicit number version which represents the lowest compatible version from an API perspective.
|
|
272
|
+
*/ throw createPackageError(`As a peer dependency, \`${dependencyName}\` version must be explicit (i.e. the "workspace:^" protocol a version resolver is not allowed) and prefixed with a caret to optimize the size (whether of installation or bundle output) on the consumer side.`, {
|
|
273
|
+
consumedBy: name,
|
|
274
|
+
name: dependencyName
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const createPackagesVersionMismatchChecker = ()=>{
|
|
279
|
+
const monorepoDependencies = new Map();
|
|
280
|
+
const monorepoDevelopmentDependencies = new Map();
|
|
281
|
+
const lint = (package_, type)=>{
|
|
282
|
+
const packageName = package_.name;
|
|
283
|
+
const isDevelopment = type === "development";
|
|
284
|
+
const store = isDevelopment ? monorepoDevelopmentDependencies : monorepoDependencies;
|
|
285
|
+
const dependencies = isDevelopment ? package_.devDependencies : package_.dependencies;
|
|
286
|
+
for (const dependencyName of Object.keys(dependencies)){
|
|
287
|
+
const depVersion = dependencies[dependencyName];
|
|
288
|
+
if (!depVersion) continue;
|
|
289
|
+
const storedVersion = store.get(dependencyName);
|
|
290
|
+
if (!storedVersion) {
|
|
291
|
+
store.set(dependencyName, depVersion);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const isSameMonorepoVersion = depVersion === storedVersion;
|
|
295
|
+
if (!isSameMonorepoVersion) {
|
|
296
|
+
throw createPackageError(`Mismatched versions: received version \`${depVersion}\` while others use \`${storedVersion}\`. To prevent issues with singleton-like code (React contexts, …), please make sure to update all packages to use the same \`${dependencyName}\` version (either \`${storedVersion}\` or \`${depVersion}\`).`, {
|
|
297
|
+
consumedBy: packageName,
|
|
298
|
+
name: dependencyName
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
return (package_)=>{
|
|
304
|
+
lint(package_, "development");
|
|
305
|
+
lint(package_, "production");
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
const createPackageError = (message, context)=>{
|
|
309
|
+
return createError("stack check", context ? `\`${context.name}\` consumed by \`${context.consumedBy}\` doesn't conform to package guidelines.\n${message}` : message);
|
|
310
|
+
};
|
|
311
|
+
function assertVersion(version, { consumedBy, name }) {
|
|
312
|
+
assert(version, ()=>createPackageError(`\`${name}\` must have a valid version specified (current version equals to \`${String(version)}\`).`, {
|
|
313
|
+
consumedBy,
|
|
314
|
+
name
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
const isExcluded = (version)=>{
|
|
318
|
+
const isPreReleaseVersion = /\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/.exec(version);
|
|
319
|
+
const isNpmProtocol = version.startsWith("npm:");
|
|
320
|
+
return isNpmProtocol || isPreReleaseVersion;
|
|
321
|
+
};
|
|
322
|
+
const hasCaret = (version)=>version.startsWith("^");
|
|
323
|
+
|
|
324
|
+
const checkType = async ()=>{
|
|
329
325
|
try {
|
|
330
|
-
return await helpers.exec(
|
|
326
|
+
return await helpers.exec("pnpm --parallel exec tsc --noEmit");
|
|
331
327
|
} catch (error) {
|
|
332
|
-
throw createError("
|
|
328
|
+
throw createError("tsc", error);
|
|
333
329
|
}
|
|
334
330
|
};
|
|
335
331
|
|
|
336
|
-
const
|
|
332
|
+
const ONLY_VALUES = [
|
|
333
|
+
"changelog",
|
|
334
|
+
"commit",
|
|
335
|
+
"code",
|
|
336
|
+
"dependency",
|
|
337
|
+
"type"
|
|
338
|
+
];
|
|
339
|
+
const createCheckCommand = (program)=>{
|
|
337
340
|
program.command({
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
description: "Check code health (static analysis)",
|
|
342
|
+
name: "check"
|
|
343
|
+
}).option({
|
|
344
|
+
defaultValue: undefined,
|
|
345
|
+
description: `Filter the compliance check to run (accepted value: ${ONLY_VALUES.join(", ")})`,
|
|
346
|
+
key: "filter",
|
|
347
|
+
name: "filter"
|
|
340
348
|
}).task({
|
|
341
349
|
handler (_, argv) {
|
|
342
350
|
logCheckableFiles(argv.operands);
|
|
343
|
-
}
|
|
351
|
+
},
|
|
352
|
+
skip: ifFilterDefinedAndNotEqualTo("code")
|
|
344
353
|
}).task({
|
|
345
|
-
label: label$3("Prepare the project"),
|
|
346
354
|
async handler () {
|
|
347
355
|
await turbo("build", {
|
|
348
356
|
excludeExamples: true,
|
|
349
357
|
hasLiveOutput: false
|
|
350
358
|
});
|
|
359
|
+
},
|
|
360
|
+
label: label$4("Prepare the project"),
|
|
361
|
+
skip ({ filter }) {
|
|
362
|
+
return filter === "commit"; // No need to build if only commit is checked
|
|
351
363
|
}
|
|
352
364
|
}).task({
|
|
353
|
-
|
|
365
|
+
async handler () {
|
|
366
|
+
await checkChangelog();
|
|
367
|
+
},
|
|
368
|
+
label: label$4("Check changelog compliance"),
|
|
369
|
+
skip: ifFilterDefinedAndNotEqualTo("changelog")
|
|
370
|
+
}).task({
|
|
371
|
+
async handler () {
|
|
372
|
+
await checkDependency();
|
|
373
|
+
},
|
|
374
|
+
label: label$4("Check dependency compliance"),
|
|
375
|
+
skip: ifFilterDefinedAndNotEqualTo("dependency")
|
|
376
|
+
}).task({
|
|
354
377
|
async handler (_, argv) {
|
|
355
|
-
|
|
378
|
+
const filenames = argv.operands;
|
|
379
|
+
await checkCode(filenames);
|
|
380
|
+
},
|
|
381
|
+
label: label$4("Check code compliance"),
|
|
382
|
+
skip: ifFilterDefinedAndNotEqualTo("code")
|
|
383
|
+
}).task({
|
|
384
|
+
async handler () {
|
|
385
|
+
await checkType();
|
|
386
|
+
},
|
|
387
|
+
label: label$4("Check type compliance"),
|
|
388
|
+
skip (context, argv) {
|
|
389
|
+
return ifFilterDefinedAndNotEqualTo("type")(context) || !hasDependency("typescript") || /**
|
|
390
|
+
* For now, skip type-checking if some files are passed down.
|
|
391
|
+
* @see https://github.com/microsoft/TypeScript/issues/27379
|
|
392
|
+
*/ argv.operands.length > 0;
|
|
356
393
|
}
|
|
357
394
|
}).task({
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
395
|
+
async handler () {
|
|
396
|
+
await checkCommit();
|
|
397
|
+
},
|
|
398
|
+
label: label$4("Check commit compliance"),
|
|
399
|
+
skip (context) {
|
|
400
|
+
return context.filter !== "commit";
|
|
361
401
|
}
|
|
362
402
|
});
|
|
363
403
|
};
|
|
364
|
-
const label$
|
|
404
|
+
const label$4 = (message)=>`${message} 🧐`;
|
|
405
|
+
const ifFilterDefinedAndNotEqualTo = (filter)=>(context)=>{
|
|
406
|
+
return context.filter !== undefined && context.filter !== filter;
|
|
407
|
+
};
|
|
365
408
|
|
|
366
|
-
const
|
|
409
|
+
const createCleanCommand = (program)=>{
|
|
367
410
|
program.command({
|
|
368
|
-
|
|
369
|
-
|
|
411
|
+
description: "Clean the project",
|
|
412
|
+
name: "clean"
|
|
370
413
|
}).task({
|
|
371
|
-
handler () {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
414
|
+
async handler () {
|
|
415
|
+
const cachePath = "node_modules/.cache";
|
|
416
|
+
const files = await retrieveIgnoredFiles();
|
|
417
|
+
if (isDirectoryExistAndNotEmpty(resolveFromProjectDirectory(cachePath))) {
|
|
418
|
+
files.push(cachePath);
|
|
419
|
+
}
|
|
420
|
+
return files;
|
|
421
|
+
},
|
|
422
|
+
key: "files",
|
|
423
|
+
label: label$3("Retrieve removable files")
|
|
424
|
+
}).task({
|
|
425
|
+
async handler ({ files }) {
|
|
426
|
+
if (files.length === 0) return;
|
|
427
|
+
await cleanFiles(files);
|
|
428
|
+
},
|
|
429
|
+
label ({ files }) {
|
|
430
|
+
return files.length > 0 ? label$3("Clean assets") : "Already clean ✨";
|
|
431
|
+
}
|
|
432
|
+
}).task({
|
|
433
|
+
handler ({ files }) {
|
|
434
|
+
helpers.message(files.join("\n "), {
|
|
435
|
+
label: "Removed assets",
|
|
436
|
+
lineBreak: {
|
|
437
|
+
end: false,
|
|
438
|
+
start: true
|
|
439
|
+
},
|
|
440
|
+
type: "information"
|
|
441
|
+
});
|
|
442
|
+
},
|
|
443
|
+
skip ({ files }) {
|
|
444
|
+
return files.length === 0;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
const label$3 = (message)=>`${message} 🧹`;
|
|
449
|
+
const cleanFiles = async (files)=>{
|
|
450
|
+
return Promise.all(files.map(async (file)=>rm(file, {
|
|
451
|
+
force: true,
|
|
452
|
+
recursive: true
|
|
453
|
+
})));
|
|
454
|
+
};
|
|
455
|
+
const isDirectoryExistAndNotEmpty = (path)=>{
|
|
456
|
+
return existsSync(path) && readdirSync(path).length > 0;
|
|
457
|
+
};
|
|
458
|
+
const retrieveIgnoredFiles = async ()=>{
|
|
459
|
+
const rawFiles = await helpers.exec("git clean -fdXn");
|
|
460
|
+
return rawFiles.split(/\n|\r\n/).filter((cleanOutput)=>!PRESERVE_FILES.some((excludedFile)=>cleanOutput.includes(excludedFile))).map((cleanOutput)=>cleanOutput.split(" ").at(-1));
|
|
461
|
+
};
|
|
462
|
+
const PRESERVE_FILES = [
|
|
463
|
+
"node_modules"
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
var version = "2.33.0-next-4976f1e";
|
|
467
|
+
|
|
468
|
+
const createCreateCommand = (program)=>{
|
|
469
|
+
program.command({
|
|
470
|
+
description: "Scaffold a new project",
|
|
471
|
+
name: "create"
|
|
472
|
+
}).task({
|
|
473
|
+
handler () {
|
|
474
|
+
botMessage({
|
|
475
|
+
description: "I can guarantee you a project creation in under 1 minute 🚀",
|
|
476
|
+
title: `I'm Stack v${version}, your bot assistant`,
|
|
375
477
|
type: "information"
|
|
376
478
|
});
|
|
377
479
|
}
|
|
378
480
|
}).task({
|
|
379
|
-
label: label$2("Check pre-requisites"),
|
|
380
481
|
async handler () {
|
|
381
482
|
// Check pnpm availability by verifying its version
|
|
382
483
|
await getNpmVersion();
|
|
383
|
-
}
|
|
484
|
+
},
|
|
485
|
+
label: label$2("Check pre-requisites")
|
|
384
486
|
}).input({
|
|
385
487
|
key: "inputName",
|
|
386
488
|
label: "What's your project name?",
|
|
@@ -390,22 +492,20 @@ const createCreateCommand = (program)=>{
|
|
|
390
492
|
label: "How would you describe it?",
|
|
391
493
|
type: "text"
|
|
392
494
|
}).input({
|
|
495
|
+
defaultValue: "git@github.com:adbayb/xxx.git",
|
|
393
496
|
key: "inputUrl",
|
|
394
497
|
label: "Where will it be stored? (Git remote URL)",
|
|
395
|
-
defaultValue: "git@github.com:adbayb/xxx.git",
|
|
396
498
|
type: "text"
|
|
397
499
|
}).input({
|
|
500
|
+
defaultValue: "single-project",
|
|
398
501
|
key: "inputTemplate",
|
|
399
502
|
label: "Which template you would like to apply?",
|
|
400
|
-
defaultValue: "single-project",
|
|
401
503
|
options: [
|
|
402
504
|
"single-project",
|
|
403
505
|
"multi-projects"
|
|
404
506
|
],
|
|
405
507
|
type: "select"
|
|
406
508
|
}).task({
|
|
407
|
-
key: "data",
|
|
408
|
-
label: label$2("Check and format input"),
|
|
409
509
|
async handler ({ inputDescription, inputName, inputUrl }) {
|
|
410
510
|
if (!inputName) {
|
|
411
511
|
throw createError("stack create", "The project name is not optional. Make sure to provide a valid value (non-empty string).");
|
|
@@ -414,50 +514,51 @@ const createCreateCommand = (program)=>{
|
|
|
414
514
|
if (!repoOwner || !repoName) {
|
|
415
515
|
throw createError("git", "The owner and repository name can not be extracted. Please make sure to follow either `/^git@.*:(?<repoOwner>.*)/(?<repoName>.*).git$/` or `/^https?://.*/(?<repoOwner>.*)/(?<repoName>.*).git$/` pattern.");
|
|
416
516
|
}
|
|
417
|
-
const nodeVersion =
|
|
418
|
-
const npmVersion =
|
|
517
|
+
const nodeVersion = await request.get("https://resolve-node.vercel.app/lts", "text");
|
|
518
|
+
const { version: npmVersion } = await request.get("https://registry.npmjs.org/pnpm/latest", "json");
|
|
419
519
|
return {
|
|
420
520
|
licenseYear: new Date().getFullYear().toString(),
|
|
421
|
-
nodeVersion,
|
|
422
|
-
npmVersion,
|
|
521
|
+
nodeVersion: nodeVersion.replace("v", ""),
|
|
522
|
+
npmVersion: String(npmVersion),
|
|
423
523
|
projectDescription: inputDescription.charAt(0).toUpperCase() + inputDescription.slice(1),
|
|
424
524
|
projectName: inputName.toLowerCase(),
|
|
425
525
|
projectUrl: inputUrl,
|
|
426
526
|
repoId: `${repoOwner}/${repoName}`
|
|
427
527
|
};
|
|
428
|
-
}
|
|
429
|
-
}).task({
|
|
430
|
-
label ({ data }) {
|
|
431
|
-
return label$2(`Create \`${data.projectName}\` folder`);
|
|
432
528
|
},
|
|
529
|
+
key: "data",
|
|
530
|
+
label: label$2("Check and format input")
|
|
531
|
+
}).task({
|
|
433
532
|
async handler ({ data }) {
|
|
434
533
|
const projectPath = resolve(process.cwd(), data.projectName);
|
|
435
534
|
await mkdir(projectPath);
|
|
436
535
|
process.chdir(projectPath);
|
|
536
|
+
},
|
|
537
|
+
label ({ data }) {
|
|
538
|
+
return label$2(`Create \`${data.projectName}\` folder`);
|
|
437
539
|
}
|
|
438
540
|
}).task({
|
|
439
|
-
label: label$2("Initialize `git`"),
|
|
440
541
|
async handler ({ data }) {
|
|
441
542
|
await helpers.exec("git init");
|
|
442
543
|
await helpers.exec(`git remote add origin ${data.projectUrl}`);
|
|
443
|
-
}
|
|
544
|
+
},
|
|
545
|
+
label: label$2("Initialize `git`")
|
|
444
546
|
}).task({
|
|
445
|
-
label: label$2("Apply template"),
|
|
446
547
|
handler ({ data, inputTemplate }) {
|
|
447
548
|
applyTemplate(inputTemplate, data);
|
|
448
|
-
}
|
|
549
|
+
},
|
|
550
|
+
label: label$2("Apply template")
|
|
449
551
|
}).task({
|
|
450
|
-
label: label$2("Create a symlink to `README.md` file"),
|
|
451
552
|
async handler ({ data: { projectName }, inputTemplate }) {
|
|
452
553
|
await symlink(join(inputTemplate === "single-project" ? projectName : join("libraries", projectName), "README.md"), "./README.md");
|
|
453
|
-
}
|
|
554
|
+
},
|
|
555
|
+
label: label$2("Create a symlink to `README.md` file")
|
|
454
556
|
}).task({
|
|
455
|
-
label: label$2("Set up the package manager"),
|
|
456
557
|
async handler () {
|
|
457
558
|
await setPackageManager();
|
|
458
|
-
}
|
|
559
|
+
},
|
|
560
|
+
label: label$2("Set up the package manager")
|
|
459
561
|
}).task({
|
|
460
|
-
label: label$2("Install dependencies"),
|
|
461
562
|
async handler ({ data }) {
|
|
462
563
|
const localDevelopmentDependencies = [
|
|
463
564
|
"quickbundle",
|
|
@@ -473,23 +574,24 @@ const createCreateCommand = (program)=>{
|
|
|
473
574
|
} catch (error) {
|
|
474
575
|
throw createError("pnpm", error);
|
|
475
576
|
}
|
|
476
|
-
}
|
|
577
|
+
},
|
|
578
|
+
label: label$2("Install dependencies")
|
|
477
579
|
}).task({
|
|
478
|
-
label: label$2("Run `stack install`"),
|
|
479
580
|
async handler () {
|
|
480
581
|
await helpers.exec("stack install");
|
|
481
|
-
}
|
|
582
|
+
},
|
|
583
|
+
label: label$2("Run `stack install`")
|
|
482
584
|
}).task({
|
|
483
|
-
label: label$2("Commit"),
|
|
484
585
|
async handler () {
|
|
485
586
|
await helpers.exec("git add -A");
|
|
486
587
|
await helpers.exec('git commit -m "chore: initial commit"');
|
|
487
|
-
}
|
|
588
|
+
},
|
|
589
|
+
label: label$2("Commit")
|
|
488
590
|
}).task({
|
|
489
591
|
handler ({ data }) {
|
|
490
592
|
botMessage({
|
|
491
|
-
title: "The project was successfully created",
|
|
492
593
|
description: `Run \`cd ./${data.projectName}\` and Enjoy 🚀`,
|
|
594
|
+
title: "The project was successfully created",
|
|
493
595
|
type: "success"
|
|
494
596
|
});
|
|
495
597
|
}
|
|
@@ -533,281 +635,173 @@ const label$2 = (message)=>`${message} 🔨`;
|
|
|
533
635
|
});
|
|
534
636
|
};
|
|
535
637
|
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
name: "clean",
|
|
539
|
-
description: "Clean the project"
|
|
540
|
-
}).task({
|
|
541
|
-
key: "files",
|
|
542
|
-
label: label$1("Retrieve removable files"),
|
|
543
|
-
async handler () {
|
|
544
|
-
const cachePath = "node_modules/.cache";
|
|
545
|
-
const files = await retrieveIgnoredFiles();
|
|
546
|
-
if (isDirectoryExistAndNotEmpty(resolveFromProjectDirectory(cachePath))) {
|
|
547
|
-
files.push(cachePath);
|
|
548
|
-
}
|
|
549
|
-
return files;
|
|
550
|
-
}
|
|
551
|
-
}).task({
|
|
552
|
-
label ({ files }) {
|
|
553
|
-
return files.length > 0 ? label$1("Clean assets") : "Already clean ✨";
|
|
554
|
-
},
|
|
555
|
-
async handler ({ files }) {
|
|
556
|
-
if (files.length === 0) return;
|
|
557
|
-
await cleanFiles(files);
|
|
558
|
-
}
|
|
559
|
-
}).task({
|
|
560
|
-
handler ({ files }) {
|
|
561
|
-
helpers.message(files.join("\n "), {
|
|
562
|
-
label: "Removed assets",
|
|
563
|
-
lineBreak: {
|
|
564
|
-
end: false,
|
|
565
|
-
start: true
|
|
566
|
-
},
|
|
567
|
-
type: "information"
|
|
568
|
-
});
|
|
569
|
-
},
|
|
570
|
-
skip ({ files }) {
|
|
571
|
-
return files.length === 0;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
};
|
|
575
|
-
const label$1 = (message)=>`${message} 🧹`;
|
|
576
|
-
const cleanFiles = async (files)=>{
|
|
577
|
-
return Promise.all(files.map(async (file)=>rm(file, {
|
|
578
|
-
force: true,
|
|
579
|
-
recursive: true
|
|
580
|
-
})));
|
|
581
|
-
};
|
|
582
|
-
const isDirectoryExistAndNotEmpty = (path)=>{
|
|
583
|
-
return existsSync(path) && readdirSync(path).length > 0;
|
|
584
|
-
};
|
|
585
|
-
const retrieveIgnoredFiles = async ()=>{
|
|
586
|
-
const rawFiles = await helpers.exec("git clean -fdXn");
|
|
587
|
-
return rawFiles.split(/\n|\r\n/).filter((cleanOutput)=>!PRESERVE_FILES.some((excludedFile)=>cleanOutput.includes(excludedFile))).map((cleanOutput)=>cleanOutput.split(" ").at(-1));
|
|
588
|
-
};
|
|
589
|
-
const PRESERVE_FILES = [
|
|
590
|
-
"node_modules"
|
|
638
|
+
const PRETTIER_IGNORE_FILES = [
|
|
639
|
+
"pnpm-lock.yaml"
|
|
591
640
|
];
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
const checkDependency = async ()=>{
|
|
602
|
-
const stdout = await helpers.exec("pnpm recursive ls --json");
|
|
603
|
-
const checkDependencyVersionMismatch = createPackagesVersionMismatchChecker();
|
|
604
|
-
const packages = JSON.parse(stdout).map((package_)=>{
|
|
605
|
-
const packagePath = join(package_.path, "package.json");
|
|
606
|
-
assert(package_.name, ()=>createPackageError(`\`${packagePath}\` must have a name field.`));
|
|
607
|
-
const packageContent = require$1(packagePath);
|
|
608
|
-
const peerDependencies = packageContent.peerDependencies ?? {};
|
|
609
|
-
const devDependencies = packageContent.devDependencies ?? {};
|
|
610
|
-
const dependencies = packageContent.dependencies ?? {};
|
|
611
|
-
return {
|
|
612
|
-
name: package_.name,
|
|
613
|
-
dependencies,
|
|
614
|
-
devDependencies,
|
|
615
|
-
peerDependencies
|
|
616
|
-
};
|
|
617
|
-
});
|
|
618
|
-
for (const package_ of packages){
|
|
619
|
-
// Check version mismatches to guarantee a single copy for a given package in the monorepo (use case: prevent singleton-like issues with React contexts)
|
|
620
|
-
checkDependencyVersionMismatch(package_);
|
|
621
|
-
// Check version range accordingly to our dependency guidelines (ie. dev dependencies must be pinned and dependencies must have caret)
|
|
622
|
-
checkDependencyVersionRange(package_);
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
const checkDependencyVersionRange = ({ name, dependencies, devDependencies, peerDependencies })=>{
|
|
626
|
-
for (const dependencyName of Object.keys(devDependencies)){
|
|
627
|
-
const version = devDependencies[dependencyName];
|
|
628
|
-
assertVersion(version, {
|
|
629
|
-
name: dependencyName,
|
|
630
|
-
consumedBy: name
|
|
631
|
-
});
|
|
632
|
-
if (version !== "workspace:*" && !isExcluded(version) && !/^\d/.test(version)) throw createPackageError(`As a dev dependency, \`${dependencyName}\` version must be fixed (or set as "workspace:*" for local packages) to reduce accidental breaking change risks due to an implicit semver upgrade.`, {
|
|
633
|
-
name: dependencyName,
|
|
634
|
-
consumedBy: name
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
for (const dependencyName of Object.keys(dependencies)){
|
|
638
|
-
const version = dependencies[dependencyName];
|
|
639
|
-
assertVersion(version, {
|
|
640
|
-
name: dependencyName,
|
|
641
|
-
consumedBy: name
|
|
642
|
-
});
|
|
643
|
-
if (version !== "workspace:^" && !hasCaret(version) && !isExcluded(version)) throw createPackageError(`As a dependency, \`${dependencyName}\` version must be prefixed with a caret (or set as "workspace:^" for local packages) to optimize the size (whether of installation or bundle output) on the consumer side.`, {
|
|
644
|
-
name: dependencyName,
|
|
645
|
-
consumedBy: name
|
|
641
|
+
const fixFormatting = async (files)=>{
|
|
642
|
+
let prettierFiles = [];
|
|
643
|
+
if (files.length === 0) {
|
|
644
|
+
prettierFiles.push(`"**/!(${PRETTIER_IGNORE_FILES.join("|")})"`);
|
|
645
|
+
} else {
|
|
646
|
+
prettierFiles = files.filter((file)=>{
|
|
647
|
+
return !PRETTIER_IGNORE_FILES.some((filename)=>file.endsWith(filename));
|
|
646
648
|
});
|
|
649
|
+
if (prettierFiles.length === 0) return;
|
|
647
650
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
});
|
|
654
|
-
if (!hasCaret(version) && !isExcluded(version)) /*
|
|
655
|
-
* Why disallowing workspace protocol as a version resolver?
|
|
656
|
-
* To reduce the update frequency needs consumer-side and guarantee on our side the minimum compatible version,
|
|
657
|
-
* the best practice should be to keeping an explicit number version which represents the lowest compatible version from an API perspective.
|
|
658
|
-
*/ throw createPackageError(`As a peer dependency, \`${dependencyName}\` version must be explicit (i.e. the "workspace:^" protocol a version resolver is not allowed) and prefixed with a caret to optimize the size (whether of installation or bundle output) on the consumer side.`, {
|
|
659
|
-
name: dependencyName,
|
|
660
|
-
consumedBy: name
|
|
661
|
-
});
|
|
651
|
+
const arguments_ = [
|
|
652
|
+
...prettierFiles
|
|
653
|
+
];
|
|
654
|
+
if (existsSync(resolveFromProjectDirectory(".gitignore"))) {
|
|
655
|
+
arguments_.push("--ignore-path .gitignore");
|
|
662
656
|
}
|
|
663
|
-
|
|
664
|
-
const createPackagesVersionMismatchChecker = ()=>{
|
|
665
|
-
const monorepoDependencies = new Map();
|
|
666
|
-
const monorepoDevelopmentDependencies = new Map();
|
|
667
|
-
const lint = (package_, type)=>{
|
|
668
|
-
const packageName = package_.name;
|
|
669
|
-
const isDevelopment = type === "development";
|
|
670
|
-
const store = isDevelopment ? monorepoDevelopmentDependencies : monorepoDependencies;
|
|
671
|
-
const dependencies = isDevelopment ? package_.devDependencies : package_.dependencies;
|
|
672
|
-
for (const dependencyName of Object.keys(dependencies)){
|
|
673
|
-
const depVersion = dependencies[dependencyName];
|
|
674
|
-
if (!depVersion) continue;
|
|
675
|
-
const storedVersion = store.get(dependencyName);
|
|
676
|
-
if (!storedVersion) {
|
|
677
|
-
store.set(dependencyName, depVersion);
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
const isSameMonorepoVersion = depVersion === storedVersion;
|
|
681
|
-
if (!isSameMonorepoVersion) {
|
|
682
|
-
throw createPackageError(`Mismatched versions: received version \`${depVersion}\` while others use \`${storedVersion}\`. To prevent issues with singleton-like code (React contexts, …), please make sure to update all packages to use the same \`${dependencyName}\` version (either \`${storedVersion}\` or \`${depVersion}\`).`, {
|
|
683
|
-
name: dependencyName,
|
|
684
|
-
consumedBy: packageName
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
};
|
|
689
|
-
return (package_)=>{
|
|
690
|
-
lint(package_, "development");
|
|
691
|
-
lint(package_, "production");
|
|
692
|
-
};
|
|
693
|
-
};
|
|
694
|
-
const createPackageError = (message, context)=>{
|
|
695
|
-
return createError("stack check", !context ? message : `\`${context.name}\` consumed by \`${context.consumedBy}\` doesn't conform to package guidelines.\n${message}`);
|
|
696
|
-
};
|
|
697
|
-
function assertVersion(version, { name, consumedBy }) {
|
|
698
|
-
assert(version, ()=>createPackageError(`\`${name}\` must have a valid version specified (current version equals to \`${String(version)}\`).`, {
|
|
699
|
-
name,
|
|
700
|
-
consumedBy
|
|
701
|
-
}));
|
|
702
|
-
}
|
|
703
|
-
const isExcluded = (version)=>{
|
|
704
|
-
const isPreReleaseVersion = /\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/.exec(version);
|
|
705
|
-
const isNpmProtocol = version.startsWith("npm:");
|
|
706
|
-
return isNpmProtocol || isPreReleaseVersion;
|
|
707
|
-
};
|
|
708
|
-
const hasCaret = (version)=>version.startsWith("^");
|
|
709
|
-
|
|
710
|
-
const checkCommit = async ()=>{
|
|
657
|
+
arguments_.push("--write", "--ignore-unknown", "--no-error-on-unmatched-pattern");
|
|
711
658
|
try {
|
|
712
|
-
return await helpers.exec(
|
|
659
|
+
return await helpers.exec(`prettier ${arguments_.join(" ")}`);
|
|
713
660
|
} catch (error) {
|
|
714
|
-
throw createError("
|
|
661
|
+
throw createError("prettier", error);
|
|
715
662
|
}
|
|
716
663
|
};
|
|
717
664
|
|
|
718
|
-
const
|
|
719
|
-
isFixMode:
|
|
665
|
+
const fixLinter = eslint({
|
|
666
|
+
isFixMode: true
|
|
720
667
|
});
|
|
721
668
|
|
|
722
|
-
const
|
|
723
|
-
"commit",
|
|
724
|
-
"code",
|
|
725
|
-
"dependency",
|
|
726
|
-
"type"
|
|
727
|
-
];
|
|
728
|
-
const createCheckCommand = (program)=>{
|
|
669
|
+
const createFixCommand = (program)=>{
|
|
729
670
|
program.command({
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}).option({
|
|
733
|
-
key: "filter",
|
|
734
|
-
name: "filter",
|
|
735
|
-
description: `Filter the compliance check to run (accepted value: ${ONLY_VALUES.join(", ")})`,
|
|
736
|
-
defaultValue: undefined
|
|
671
|
+
description: "Fix auto-fixable issues",
|
|
672
|
+
name: "fix"
|
|
737
673
|
}).task({
|
|
738
674
|
handler (_, argv) {
|
|
739
675
|
logCheckableFiles(argv.operands);
|
|
740
|
-
}
|
|
741
|
-
skip: ifFilterDefinedAndNotEqualTo("code")
|
|
676
|
+
}
|
|
742
677
|
}).task({
|
|
743
|
-
label: label("Prepare the project"),
|
|
744
678
|
async handler () {
|
|
745
679
|
await turbo("build", {
|
|
746
680
|
excludeExamples: true,
|
|
747
681
|
hasLiveOutput: false
|
|
748
682
|
});
|
|
749
683
|
},
|
|
750
|
-
|
|
751
|
-
return filter === "commit"; // No need to build if only commit is checked
|
|
752
|
-
}
|
|
684
|
+
label: label$1("Prepare the project")
|
|
753
685
|
}).task({
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
await checkDependency();
|
|
686
|
+
async handler (_, argv) {
|
|
687
|
+
await fixLinter(argv.operands);
|
|
757
688
|
},
|
|
758
|
-
|
|
689
|
+
label: label$1("Fix linter issues")
|
|
759
690
|
}).task({
|
|
760
|
-
label: label("Check code compliance"),
|
|
761
691
|
async handler (_, argv) {
|
|
762
|
-
|
|
763
|
-
await checkCode(filenames);
|
|
692
|
+
await fixFormatting(argv.operands);
|
|
764
693
|
},
|
|
765
|
-
|
|
694
|
+
label: label$1("Fix formatting issues")
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
const label$1 = (message)=>`${message} 🚑`;
|
|
698
|
+
|
|
699
|
+
const createInstallCommand = (program)=>{
|
|
700
|
+
program.command({
|
|
701
|
+
description: "Install required setup",
|
|
702
|
+
name: "install"
|
|
766
703
|
}).task({
|
|
767
|
-
label: label("Check type compliance"),
|
|
768
704
|
async handler () {
|
|
769
|
-
|
|
705
|
+
const lineBreakMatcher = String.raw`\n|\r\n`;
|
|
706
|
+
const stackCommand = getStackCommand(`fix $(node -e 'console.log(require("child_process").execSync("git status --porcelain", {encoding: "utf8"}).split(/${lineBreakMatcher}/).filter(Boolean).map(item => item.split(" ").at(-1)).join(" "))')`);
|
|
707
|
+
await installGitHook("pre-commit", `${stackCommand} && git add -A`);
|
|
770
708
|
},
|
|
771
|
-
|
|
772
|
-
return ifFilterDefinedAndNotEqualTo("type")(context) || !hasDependency("typescript") || /**
|
|
773
|
-
* For now, skip type-checking if some files are passed down.
|
|
774
|
-
* @see https://github.com/microsoft/TypeScript/issues/27379
|
|
775
|
-
*/ argv.operands.length > 0;
|
|
776
|
-
}
|
|
709
|
+
label: label("Install `git.pre-commit` hook")
|
|
777
710
|
}).task({
|
|
778
|
-
label: label("Check commit compliance"),
|
|
779
711
|
async handler () {
|
|
780
|
-
await
|
|
712
|
+
await installGitHook("commit-msg", getStackCommand("check --filter commit"));
|
|
781
713
|
},
|
|
782
|
-
|
|
783
|
-
return context.filter !== "commit";
|
|
784
|
-
}
|
|
714
|
+
label: label("Install `git.commit-msg` hook")
|
|
785
715
|
});
|
|
786
716
|
};
|
|
787
|
-
const label = (message)=>`${message}
|
|
788
|
-
const
|
|
789
|
-
|
|
717
|
+
const label = (message)=>`${message} 📲`;
|
|
718
|
+
const installGitHook = async (hook, content)=>{
|
|
719
|
+
const filename = resolveFromProjectDirectory(`.git/hooks/${hook}`);
|
|
720
|
+
await writeFile(filename, content);
|
|
721
|
+
return chmod(filename, "0755");
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const createReleaseCommand = (program)=>{
|
|
725
|
+
program.command({
|
|
726
|
+
description: "Log, version, and publish package(s)",
|
|
727
|
+
name: "release"
|
|
728
|
+
}).option({
|
|
729
|
+
description: "Add a new changelog entry",
|
|
730
|
+
key: "log",
|
|
731
|
+
name: "log"
|
|
732
|
+
}).option({
|
|
733
|
+
description: "Bump the package(s) version",
|
|
734
|
+
key: "tag",
|
|
735
|
+
name: "tag"
|
|
736
|
+
}).option({
|
|
737
|
+
description: "Publish package(s) to the registry",
|
|
738
|
+
key: "publish",
|
|
739
|
+
name: "publish"
|
|
740
|
+
}).task({
|
|
741
|
+
async handler () {
|
|
742
|
+
helpers.message("New changelog entry\n");
|
|
743
|
+
await changeset("changeset");
|
|
744
|
+
},
|
|
745
|
+
skip: ifNotEqualTo("log")
|
|
746
|
+
}).task({
|
|
747
|
+
async handler () {
|
|
748
|
+
helpers.message("Bumping the package(s) version\n");
|
|
749
|
+
await changeset("changeset version && pnpm install --no-frozen-lockfile");
|
|
750
|
+
},
|
|
751
|
+
skip: ifNotEqualTo("tag")
|
|
752
|
+
}).task({
|
|
753
|
+
async handler () {
|
|
754
|
+
helpers.message("Publishing package(s) to the registry\n");
|
|
755
|
+
await changeset("stack build && pnpm changeset publish");
|
|
756
|
+
},
|
|
757
|
+
skip: ifNotEqualTo("publish")
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
|
+
const ifNotEqualTo = (validOption)=>(context)=>{
|
|
761
|
+
return !context[validOption];
|
|
790
762
|
};
|
|
791
763
|
|
|
792
|
-
const
|
|
764
|
+
const createStartCommand = (program)=>{
|
|
793
765
|
program.command({
|
|
794
|
-
|
|
795
|
-
|
|
766
|
+
description: "Start the project in production mode",
|
|
767
|
+
name: "start"
|
|
796
768
|
}).task({
|
|
797
769
|
async handler () {
|
|
798
|
-
await turbo("
|
|
770
|
+
await turbo("start");
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const createTestCommand = (program)=>{
|
|
776
|
+
program.command({
|
|
777
|
+
description: "Test the code execution",
|
|
778
|
+
name: "test"
|
|
779
|
+
}).task({
|
|
780
|
+
async handler () {
|
|
781
|
+
await turbo("test");
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const createWatchCommand = (program)=>{
|
|
787
|
+
program.command({
|
|
788
|
+
description: "Build and start the project in development mode",
|
|
789
|
+
name: "watch"
|
|
790
|
+
}).task({
|
|
791
|
+
async handler () {
|
|
792
|
+
await turbo("watch");
|
|
799
793
|
}
|
|
800
794
|
});
|
|
801
795
|
};
|
|
802
796
|
|
|
803
797
|
const createProgram = (...commandFactories)=>{
|
|
804
798
|
const program = termost({
|
|
805
|
-
name: "stack",
|
|
806
799
|
description: "Toolbox to easily scaffold and maintain a project",
|
|
800
|
+
name: "stack",
|
|
807
801
|
onException () {
|
|
808
802
|
botMessage({
|
|
809
|
-
title: "Oops, an error occurred",
|
|
810
803
|
description: "Keep calm and carry on with some coffee ☕️",
|
|
804
|
+
title: "Oops, an error occurred",
|
|
811
805
|
type: "error"
|
|
812
806
|
});
|
|
813
807
|
},
|