ember.nvp 0.1.0 → 0.2.0
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/.github/workflows/ci.yml +41 -0
- package/.github/workflows/plan-release.yml +2 -2
- package/.github/workflows/publish.yml +2 -2
- package/.prettierignore +2 -1
- package/.prettierrc.js +10 -0
- package/.release-plan.json +11 -3
- package/CHANGELOG.md +37 -0
- package/README.md +79 -142
- package/package.json +28 -5
- package/pnpm-workspace.yaml +3 -0
- package/src/bases/README.md +5 -0
- package/src/bases/minimal-app/app.test.ts +203 -0
- package/src/bases/minimal-app/files/.env.development +8 -0
- package/src/bases/minimal-app/files/app/app.ts +21 -0
- package/src/bases/minimal-app/files/app/config.ts +30 -0
- package/src/bases/minimal-app/files/app/router.ts +51 -0
- package/src/bases/minimal-app/files/app/templates/application.gts +5 -0
- package/src/{layers/minimal/files-ts → bases/minimal-app/files}/babel.config.js +1 -4
- package/src/{layers/minimal → bases/minimal-app}/files/index.html +3 -5
- package/src/bases/minimal-app/files/package.json +57 -0
- package/src/{layers/minimal → bases/minimal-app}/files/tsconfig.json +0 -6
- package/src/bases/minimal-app/index.js +111 -0
- package/src/bases/minimal-library/index.js +10 -0
- package/src/cli/args.js +57 -0
- package/src/cli/generator.js +66 -11
- package/src/cli/index.js +44 -13
- package/src/cli/questions/layers.js +39 -50
- package/src/cli/questions/name.js +22 -6
- package/src/cli/questions/ok.js +38 -0
- package/src/cli/questions/package-manager.js +23 -2
- package/src/cli/questions/path.js +59 -0
- package/src/cli/questions/project-type.js +55 -0
- package/src/consolidators/linting.js +51 -0
- package/src/index.js +2 -0
- package/src/layers/eslint-bundled-ember/files/eslint.config.js +14 -0
- package/src/layers/eslint-bundled-ember/index.js +91 -0
- package/src/layers/eslint-bundled-nvp/app.test.ts +91 -0
- package/src/layers/eslint-bundled-nvp/files/eslint.config.js +16 -0
- package/src/layers/eslint-bundled-nvp/index.js +91 -0
- package/src/layers/eslint-ejected/index.js +95 -0
- package/src/layers/git/index.js +33 -0
- package/src/layers/github-actions/app.test.ts +73 -0
- package/src/layers/github-actions/index.js +200 -0
- package/src/layers/index.js +47 -0
- package/src/layers/prettier/files/.prettierrc.js +4 -0
- package/src/layers/prettier/index.js +45 -18
- package/src/layers/qunit/app.test.ts +90 -0
- package/src/layers/qunit/files/testem.cjs +26 -0
- package/src/layers/qunit/files/tests/index.html +16 -7
- package/src/layers/qunit/files/tests/test-helper.ts +61 -7
- package/src/layers/qunit/index.js +83 -34
- package/src/layers/release-plan/index.js +5 -0
- package/src/layers/renovate/files/app.json5 +14 -0
- package/src/layers/renovate/files/library.json5 +16 -0
- package/src/layers/renovate/index.js +68 -0
- package/src/layers/typescript/app.test.ts +93 -0
- package/src/layers/typescript/index.js +145 -0
- package/src/layers/vitest/files/vitest.config.mjs +1 -5
- package/src/layers/vitest/index.js +43 -26
- package/src/utils/babel.js +76 -0
- package/src/utils/buffer.js +15 -0
- package/src/utils/cli.js +45 -0
- package/src/utils/cwd.js +1 -0
- package/src/utils/fs.js +85 -0
- package/src/utils/git.js +41 -0
- package/src/utils/git.test.ts +36 -0
- package/src/utils/manifest.js +9 -0
- package/src/utils/npm.js +29 -0
- package/src/utils/object.js +10 -0
- package/src/utils/project.js +249 -0
- package/src/utils/remove-types.js +90 -0
- package/src/utils/types.ts +48 -0
- package/test/ember.nvp/cli.test.ts +12 -0
- package/test/ember.nvp/layers.test.ts +35 -0
- package/test/ember.nvp/permutations.test.ts +200 -0
- package/test/manual.sh +11 -0
- package/test/test-helpers/permutate.test.ts +70 -0
- package/test/test-helpers.ts +175 -0
- package/tsconfig.json +10 -2
- package/vitest.config.js +9 -0
- package/src/layers/eslint/index.js +0 -42
- package/src/layers/minimal/files/app/app.ts +0 -10
- package/src/layers/minimal/files/app/config.ts +0 -18
- package/src/layers/minimal/files/app/router.ts +0 -9
- package/src/layers/minimal/files/app/templates/application.gts +0 -7
- package/src/layers/minimal/files-js/babel.config.js +0 -38
- package/src/layers/minimal/index.js +0 -89
- package/src/layers/prettier/files/.prettierrc.cjs +0 -15
- /package/src/{layers/minimal → bases/minimal-app}/files/vite.config.mjs +0 -0
- /package/src/layers/{eslint/files/eslint.config.mjs → eslint-ejected/files/eslint.config.js} +0 -0
- /package/src/layers/{minimal/files/app/routes → qunit/files/tests/application}/.gitkeep +0 -0
- /package/src/layers/qunit/files/tests/{integration → rendering}/.gitkeep +0 -0
package/src/cli/generator.js
CHANGED
|
@@ -1,19 +1,74 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
+
import baseApp from "#bases/minimal-app";
|
|
4
|
+
import baseLibrary from "#bases/minimal-library";
|
|
5
|
+
import { consolidateLintingScripts } from "../consolidators/linting.js";
|
|
6
|
+
import { hasGit } from "#utils/git.js";
|
|
3
7
|
/**
|
|
4
8
|
* Generate project files by running layer functions
|
|
9
|
+
*
|
|
10
|
+
* @param {import('#utils/project.js').Project} project
|
|
5
11
|
*/
|
|
6
|
-
export async function generateProject(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
export async function generateProject(project) {
|
|
13
|
+
await mkdir(project.directory, { recursive: true });
|
|
14
|
+
|
|
15
|
+
switch (project.desires.type) {
|
|
16
|
+
case "app":
|
|
17
|
+
await baseApp.run(project);
|
|
18
|
+
break;
|
|
19
|
+
case "library":
|
|
20
|
+
await baseLibrary.run(project);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* We could run these in a loop until there is no git diff
|
|
26
|
+
*/
|
|
27
|
+
await runLap(project);
|
|
28
|
+
|
|
29
|
+
if (hasGit(project.directory)) {
|
|
30
|
+
await project.gitAdd();
|
|
31
|
+
|
|
32
|
+
if (await project.gitHasDiff()) {
|
|
33
|
+
let layerNames = project.desires.layers.map((l) => l.name).join(", ");
|
|
34
|
+
await project.gitCommit(
|
|
35
|
+
`[ember.nvp] Applied ${layerNames} to ${project.type}: ${project.name} -- Please report issues to https://github.com/NullVoxPopuli/ember.nvp/`,
|
|
36
|
+
);
|
|
17
37
|
}
|
|
18
38
|
}
|
|
39
|
+
|
|
40
|
+
await runLap(project);
|
|
41
|
+
|
|
42
|
+
if (hasGit(project.directory) && (await project.gitHasDiff())) {
|
|
43
|
+
await project.gitAdd();
|
|
44
|
+
await project.gitCommit(
|
|
45
|
+
`[ember.nvp] Consolidation commit -- Please report issues to https://github.com/NullVoxPopuli/ember.nvp/`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await runLap(project);
|
|
50
|
+
|
|
51
|
+
if (hasGit(project.directory) && (await project.gitHasDiff())) {
|
|
52
|
+
await project.gitAdd();
|
|
53
|
+
await project.gitCommit(
|
|
54
|
+
`[ember.nvp] Consolidation commit -- Please report issues to https://github.com/NullVoxPopuli/ember.nvp/`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
*
|
|
61
|
+
* @param {import('#utils/project.js').Project} project
|
|
62
|
+
*/
|
|
63
|
+
async function runLap(project) {
|
|
64
|
+
for (const layer of project.desires.layers) {
|
|
65
|
+
if (typeof layer.run !== "function") {
|
|
66
|
+
console.warn(`${layer.name} is not implemented`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await layer.run(project);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await consolidateLintingScripts(project);
|
|
19
74
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -1,44 +1,75 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import pc from "picocolors";
|
|
6
4
|
import { generateProject } from "./generator.js";
|
|
7
|
-
import { askName } from
|
|
8
|
-
import { askLayers } from
|
|
9
|
-
import {
|
|
5
|
+
import { askName } from "./questions/name.js";
|
|
6
|
+
import { askLayers } from "./questions/layers.js";
|
|
7
|
+
import { askProjectType } from "./questions/project-type.js";
|
|
8
|
+
import { askPackageManager } from "./questions/package-manager.js";
|
|
9
|
+
import { askPath } from "./questions/path.js";
|
|
10
|
+
import { askIfOK } from "./questions/ok.js";
|
|
11
|
+
import { styleText } from "node:util";
|
|
12
|
+
import { Project } from "#utils/project.js";
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* This whole file's primary purpose is to be an interactive CLI
|
|
16
|
+
* for `generateProject`.
|
|
17
|
+
*
|
|
18
|
+
* Other tools can call `generateProject` themselves if they wish
|
|
19
|
+
* if they want to provide different TUI/GUIs.
|
|
20
|
+
*/
|
|
11
21
|
async function main() {
|
|
12
22
|
console.clear();
|
|
13
23
|
|
|
14
|
-
p.intro(
|
|
24
|
+
p.intro(styleText(["bgCyan", "black"], " ember.nvp "));
|
|
15
25
|
|
|
16
26
|
const projectName = await askName();
|
|
27
|
+
const projectPath = await askPath(projectName);
|
|
28
|
+
const projectType = await askProjectType();
|
|
17
29
|
const selectedLayers = await askLayers();
|
|
18
30
|
const packageManager = await askPackageManager();
|
|
19
31
|
|
|
32
|
+
let project = new Project(projectPath, {
|
|
33
|
+
name: projectName,
|
|
34
|
+
type: projectType,
|
|
35
|
+
path: projectPath,
|
|
36
|
+
layers: selectedLayers,
|
|
37
|
+
packageManager,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await askIfOK();
|
|
41
|
+
|
|
20
42
|
const s = p.spinner();
|
|
21
43
|
s.start("Creating your Ember app...");
|
|
22
44
|
|
|
23
45
|
try {
|
|
24
46
|
// Generate the project
|
|
25
|
-
|
|
26
|
-
await generateProject(projectPath, projectName, selectedLayers);
|
|
47
|
+
await generateProject(project);
|
|
27
48
|
|
|
28
49
|
s.stop("Project created!");
|
|
29
50
|
|
|
30
51
|
p.note(
|
|
31
52
|
`cd ${projectName}\n` +
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
`${packageManager} install\n` +
|
|
54
|
+
`${packageManager} ${packageManager === "npm" ? "run " : ""}start`,
|
|
34
55
|
"Next steps",
|
|
35
56
|
);
|
|
36
57
|
|
|
37
|
-
p.outro(
|
|
58
|
+
p.outro(styleText("green", "✓") + " Project ready! " + styleText("dim", "Happy coding!"));
|
|
38
59
|
} catch (err) {
|
|
39
60
|
s.stop("Failed to create project");
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
|
|
62
|
+
if (err instanceof Error) {
|
|
63
|
+
p.cancel(`Error: ${err.message}`);
|
|
64
|
+
if (err.stack) {
|
|
65
|
+
console.error(err.stack);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
p.cancel(`Error: ${String(err)}`);
|
|
72
|
+
process.exit(1);
|
|
42
73
|
}
|
|
43
74
|
}
|
|
44
75
|
|
|
@@ -1,31 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { answers, printArgInUse } from "#args";
|
|
2
|
+
import { discoverLayers } from "#layers";
|
|
3
3
|
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
|
|
6
6
|
export async function askLayers() {
|
|
7
|
+
const optionalLayers = (await discoverLayers()).filter(
|
|
8
|
+
(layer) => typeof layer.run === "function",
|
|
9
|
+
);
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
const layers = await discoverLayers();
|
|
11
|
+
const supported = new Set(optionalLayers.map((layer) => layer.name));
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param {string[] | undefined} selected
|
|
16
|
+
* @returns {selected is string[]}
|
|
17
|
+
*/
|
|
18
|
+
function isValid(selected) {
|
|
19
|
+
if (!selected) return false;
|
|
20
|
+
if (Array.isArray(selected) && selected.length === 0) return false;
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
return selected.every((name) => supported.has(name));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isValid(answers.layers)) {
|
|
26
|
+
let result = optionalLayers.filter((layer) => answers.layers.includes(layer.name));
|
|
27
|
+
|
|
28
|
+
printArgInUse(`layers`, result.map((layer) => layer.name).join(", "));
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Whenever we have a yes/no option, we use string values rather than true/false
|
|
34
|
+
const defaultValues = (
|
|
35
|
+
await Promise.all(
|
|
36
|
+
optionalLayers.map(async (layer) => {
|
|
37
|
+
let result = await layer.defaultValue?.();
|
|
38
|
+
|
|
39
|
+
return result && layer.name;
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
).filter(Boolean);
|
|
21
43
|
|
|
22
|
-
// Let user select additional features
|
|
23
44
|
const selectedFeatures = await p.multiselect({
|
|
24
45
|
message: "Select additional features:",
|
|
46
|
+
initialValues: defaultValues,
|
|
25
47
|
options: optionalLayers.map((layer) => ({
|
|
26
48
|
value: layer.name,
|
|
27
|
-
label: layer.label
|
|
28
|
-
hint: layer.
|
|
49
|
+
label: layer.label ?? layer.name,
|
|
50
|
+
hint: layer.hint,
|
|
29
51
|
})),
|
|
30
52
|
required: false,
|
|
31
53
|
});
|
|
@@ -36,40 +58,7 @@ export async function askLayers() {
|
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
// Build the final configuration
|
|
39
|
-
const selectedLayers =
|
|
40
|
-
minimalLayer,
|
|
41
|
-
...optionalLayers.filter((l) => selectedFeatures.includes(l.name)),
|
|
42
|
-
].filter(Boolean);
|
|
61
|
+
const selectedLayers = optionalLayers.filter((layer) => selectedFeatures.includes(layer.name));
|
|
43
62
|
|
|
44
63
|
return selectedLayers;
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function discoverLayers() {
|
|
49
|
-
const layersDir = join(import.meta.dirname, '..', "..", "layers");
|
|
50
|
-
const entries = await readdir(layersDir, { withFileTypes: true });
|
|
51
|
-
|
|
52
|
-
const layers = [];
|
|
53
|
-
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
if (entry.isDirectory()) {
|
|
56
|
-
const layerPath = join(layersDir, entry.name, "index.js");
|
|
57
|
-
try {
|
|
58
|
-
const layer = await import(layerPath);
|
|
59
|
-
layers.push({
|
|
60
|
-
name: entry.name,
|
|
61
|
-
...layer.default,
|
|
62
|
-
});
|
|
63
|
-
} catch (err) {
|
|
64
|
-
// Skip layers that don't have proper exports
|
|
65
|
-
console.warn(
|
|
66
|
-
`Warning: Could not load layer ${entry.name}:`,
|
|
67
|
-
err.message,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return layers;
|
|
74
64
|
}
|
|
75
|
-
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
|
|
3
|
+
import packageNameRegex from "package-name-regex";
|
|
4
|
+
import { answers, printArgInUse } from "#args";
|
|
5
|
+
|
|
6
|
+
const DEFAULT = "my-app";
|
|
7
|
+
|
|
3
8
|
export async function askName() {
|
|
9
|
+
if (answers.name) {
|
|
10
|
+
let isValid = packageNameRegex.test(answers.name);
|
|
11
|
+
if (isValid) {
|
|
12
|
+
printArgInUse("name", answers.name);
|
|
13
|
+
|
|
14
|
+
return answers.name;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
4
17
|
const projectName = await p.text({
|
|
5
18
|
message: "What is your project name?",
|
|
6
|
-
placeholder:
|
|
7
|
-
defaultValue:
|
|
19
|
+
placeholder: DEFAULT,
|
|
20
|
+
defaultValue: DEFAULT,
|
|
8
21
|
validate(value) {
|
|
9
|
-
if (value.length === 0) return
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
if (value.length === 0) return;
|
|
23
|
+
|
|
24
|
+
let isValid = packageNameRegex.test(value);
|
|
25
|
+
|
|
26
|
+
if (!isValid) {
|
|
27
|
+
return "Project name must be a valid npm package-name";
|
|
12
28
|
}
|
|
13
29
|
},
|
|
14
30
|
});
|
|
@@ -18,5 +34,5 @@ export async function askName() {
|
|
|
18
34
|
return process.exit(0);
|
|
19
35
|
}
|
|
20
36
|
|
|
21
|
-
return projectName;
|
|
37
|
+
return projectName ?? DEFAULT;
|
|
22
38
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { answers, printArgInUse } from "#args";
|
|
3
|
+
|
|
4
|
+
const SUPPORTED = new Set(["yes", "no"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {string | undefined} selected
|
|
9
|
+
* @returns {selected is 'yes' | 'no'}
|
|
10
|
+
*/
|
|
11
|
+
function isValid(selected) {
|
|
12
|
+
if (!selected) return false;
|
|
13
|
+
|
|
14
|
+
return SUPPORTED.has(selected);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function askIfOK() {
|
|
18
|
+
if (isValid(answers.confirm)) {
|
|
19
|
+
printArgInUse("confirm", answers.confirm);
|
|
20
|
+
|
|
21
|
+
return answers.confirm;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const answer = await p.select({
|
|
25
|
+
message: "Do all the selections above look ok?",
|
|
26
|
+
options: [
|
|
27
|
+
{ value: "yes", label: "yes, generate the project" },
|
|
28
|
+
{ value: "no", label: "no, cancel" },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (p.isCancel(answer) || answer === "no") {
|
|
33
|
+
p.cancel("Operation cancelled");
|
|
34
|
+
return process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return answer;
|
|
38
|
+
}
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
+
import { answers, printArgInUse } from "#args";
|
|
2
3
|
|
|
4
|
+
const SUPPORTED = new Set(["pnpm", "npm"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {string | undefined} selected
|
|
9
|
+
* @returns {selected is 'pnpm' | 'npm'}
|
|
10
|
+
*/
|
|
11
|
+
function isValid(selected) {
|
|
12
|
+
if (!selected) return false;
|
|
13
|
+
|
|
14
|
+
return SUPPORTED.has(selected);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
*
|
|
19
|
+
* @returns {Promise<import('#types').PackageManager>}
|
|
20
|
+
*/
|
|
3
21
|
export async function askPackageManager() {
|
|
22
|
+
if (isValid(answers.packageManager)) {
|
|
23
|
+
printArgInUse("packageManager", answers.packageManager);
|
|
24
|
+
|
|
25
|
+
return answers.packageManager;
|
|
26
|
+
}
|
|
4
27
|
|
|
5
28
|
const packageManager = await p.select({
|
|
6
29
|
message: "Which package manager?",
|
|
7
30
|
options: [
|
|
8
31
|
{ value: "pnpm", label: "pnpm", hint: "Fast, disk space efficient" },
|
|
9
32
|
{ value: "npm", label: "npm", hint: "Node default" },
|
|
10
|
-
{ value: "yarn", label: "yarn", hint: "Classic alternative" },
|
|
11
33
|
],
|
|
12
34
|
});
|
|
13
35
|
|
|
@@ -18,4 +40,3 @@ export async function askPackageManager() {
|
|
|
18
40
|
|
|
19
41
|
return packageManager;
|
|
20
42
|
}
|
|
21
|
-
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { answers, printArgInUse } from "#args";
|
|
5
|
+
import { cwd } from "#utils/cwd.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param {string | undefined} value
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
function isValid(value) {
|
|
13
|
+
if (!value) return false;
|
|
14
|
+
|
|
15
|
+
return value.startsWith(".") || value.startsWith("/");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} name -- the projectName
|
|
20
|
+
*/
|
|
21
|
+
export async function askPath(name) {
|
|
22
|
+
if (answers.path) {
|
|
23
|
+
if (isValid(answers.path)) {
|
|
24
|
+
printArgInUse("path", answers.path);
|
|
25
|
+
|
|
26
|
+
return answers.path;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultValue = join(cwd, name);
|
|
31
|
+
|
|
32
|
+
const answer = await p.text({
|
|
33
|
+
message: "Where would you like to place your project?",
|
|
34
|
+
placeholder: defaultValue.replace(cwd, "."),
|
|
35
|
+
defaultValue: defaultValue,
|
|
36
|
+
validate(value) {
|
|
37
|
+
if (value.length === 0) return;
|
|
38
|
+
|
|
39
|
+
if (!isValid(value)) {
|
|
40
|
+
return `Path must start with a '.' or '/' (be either a relative or absolute path)`;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (p.isCancel(answer)) {
|
|
46
|
+
p.cancel("Operation cancelled");
|
|
47
|
+
return process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (answer) {
|
|
51
|
+
if (answer.startsWith(".")) {
|
|
52
|
+
return join(cwd, answer);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return answer;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return defaultValue;
|
|
59
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { answers, printArgInUse } from "#args";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* addon is alias for library
|
|
6
|
+
*/
|
|
7
|
+
const SUPPORTED = new Set(["app", "library", "addon"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string | undefined} selected
|
|
11
|
+
* @returns {selected is "app" | "library" | "addon"}
|
|
12
|
+
*/
|
|
13
|
+
function isValid(selected) {
|
|
14
|
+
if (!selected) return false;
|
|
15
|
+
|
|
16
|
+
return SUPPORTED.has(selected);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @return {Promise<import('#types').ProjectType>}
|
|
22
|
+
*/
|
|
23
|
+
export async function askProjectType() {
|
|
24
|
+
if (isValid(answers.type)) {
|
|
25
|
+
printArgInUse("type", answers.type);
|
|
26
|
+
|
|
27
|
+
if (answers.type === "addon") return "library";
|
|
28
|
+
|
|
29
|
+
return answers.type;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const answer = await p.select({
|
|
33
|
+
message: "Which type of project?",
|
|
34
|
+
options: [
|
|
35
|
+
{ value: "app", label: "web app", hint: "generates html, js, and css to deploy to the web" },
|
|
36
|
+
{
|
|
37
|
+
value: "library",
|
|
38
|
+
label: "library",
|
|
39
|
+
hint: "sharable code, usable by other libraries and apps alike",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (answer === "library") {
|
|
45
|
+
p.cancel("Not implemented yet, apologies!");
|
|
46
|
+
return process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (p.isCancel(answer)) {
|
|
50
|
+
p.cancel("Operation cancelled");
|
|
51
|
+
return process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return answer;
|
|
55
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getLatest } from "#utils/npm.js";
|
|
2
|
+
import { packageJson } from "ember-apply";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('#utils/project.js').Project} project
|
|
6
|
+
*/
|
|
7
|
+
export async function consolidateLintingScripts(project) {
|
|
8
|
+
// todo
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('#utils/project.js').Project} project
|
|
13
|
+
*/
|
|
14
|
+
export async function maybeLintWithConcurrently(project) {
|
|
15
|
+
/** @type {Record<string, boolean>} */
|
|
16
|
+
let linting = {
|
|
17
|
+
prettier: false,
|
|
18
|
+
eslint: false,
|
|
19
|
+
"ember-template-lint": false,
|
|
20
|
+
stylelint: false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let count = 0;
|
|
24
|
+
let manifest = await packageJson.read(project.directory);
|
|
25
|
+
|
|
26
|
+
for (let script of Object.values(manifest.scripts)) {
|
|
27
|
+
for (let bin of Object.keys(linting)) {
|
|
28
|
+
if (script.includes(bin)) {
|
|
29
|
+
linting[bin] = true;
|
|
30
|
+
count++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (count > 1) {
|
|
36
|
+
await packageJson.modify(async (json) => {
|
|
37
|
+
json.scripts ||= {};
|
|
38
|
+
json.devDependencies ||= {};
|
|
39
|
+
json.scripts.lint = 'concurrently "pnpm:lint:*(!fix)" --names "lint:" --prefixColors auto';
|
|
40
|
+
json.scripts["lint:fix"] =
|
|
41
|
+
'concurrently "pnpm:lint:*:fix" --names "fix:" --prefixColors auto && pnpm format';
|
|
42
|
+
|
|
43
|
+
Object.assign(
|
|
44
|
+
json.devDependencies,
|
|
45
|
+
await getLatest({
|
|
46
|
+
concurrently: "^9.2.1",
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}, project.directory);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This tries to follow what the official blueprints are doing more closely
|
|
3
|
+
* but in a way that abstracts the dependencies and configuration
|
|
4
|
+
* out of your project.
|
|
5
|
+
*
|
|
6
|
+
* Handles: (G)TS + (G)JS, QUnit, Supporting Node files
|
|
7
|
+
*/
|
|
8
|
+
import { ember } from "ember-eslint";
|
|
9
|
+
|
|
10
|
+
export default [
|
|
11
|
+
...ember.recommended(import.meta.dirname),
|
|
12
|
+
// your modifications here
|
|
13
|
+
// see: https://eslint.org/docs/user-guide/configuring/configuration-files#how-do-overrides-work
|
|
14
|
+
];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getLatest } from "#utils/npm.js";
|
|
2
|
+
import { files, packageJson } from "ember-apply";
|
|
3
|
+
import { formatLabel } from "#utils/cli.js";
|
|
4
|
+
import { maybeLintWithConcurrently } from "#consolidators/linting.js";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { hasDevDeps } from "#utils/manifest.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @type {import('#types').Layer}
|
|
11
|
+
*/
|
|
12
|
+
export default {
|
|
13
|
+
label: formatLabel("ESLint", "ember's encapsulated config"),
|
|
14
|
+
hint: `Minimal dependencies added to package.json. Not official, but follows the official configuration`,
|
|
15
|
+
|
|
16
|
+
async run(project) {
|
|
17
|
+
await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
|
|
18
|
+
|
|
19
|
+
await packageJson.addDevDependencies(
|
|
20
|
+
await getLatest({
|
|
21
|
+
"ember-eslint": "^0.6.1",
|
|
22
|
+
eslint: "^9.39.2",
|
|
23
|
+
}),
|
|
24
|
+
project.directory,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
await packageJson.addScripts(
|
|
28
|
+
{
|
|
29
|
+
"lint:eslint": "eslint . --cache",
|
|
30
|
+
"lint:eslint:fix": "eslint . --fix",
|
|
31
|
+
},
|
|
32
|
+
project.directory,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await maybeLintWithConcurrently(project);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @overload
|
|
40
|
+
* @param {import('#utils/project.js').Project} project
|
|
41
|
+
* @param {true} explain
|
|
42
|
+
* @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* @overload
|
|
46
|
+
* @param {import('#utils/project.js').Project} project
|
|
47
|
+
* @param {boolean | undefined} [explain]
|
|
48
|
+
* @returns {Promise<boolean>}
|
|
49
|
+
*/
|
|
50
|
+
async isSetup(project, explain) {
|
|
51
|
+
const reasons = [];
|
|
52
|
+
|
|
53
|
+
if (!existsSync(join(project.directory, "eslint.config.js"))) {
|
|
54
|
+
reasons.push("eslint.config.js is missing");
|
|
55
|
+
|
|
56
|
+
if (!explain) return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let manifest = await packageJson.read(project.directory);
|
|
60
|
+
|
|
61
|
+
if (!hasDevDeps(manifest, ["ember-eslint", "eslint"])) {
|
|
62
|
+
reasons.push("missing required dependencies: ember-eslint, eslint");
|
|
63
|
+
|
|
64
|
+
if (!explain) return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let scripts = Object.values(manifest.scripts ?? {}).filter((script) =>
|
|
68
|
+
script.includes("eslint"),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (!scripts.some((script) => script.includes("--cache"))) {
|
|
72
|
+
reasons.push("missing eslint script with --cache flag");
|
|
73
|
+
|
|
74
|
+
if (!explain) return false;
|
|
75
|
+
}
|
|
76
|
+
if (!scripts.some((script) => script.includes("--fix"))) {
|
|
77
|
+
reasons.push("missing eslint script with --fix flag");
|
|
78
|
+
|
|
79
|
+
if (!explain) return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (explain) {
|
|
83
|
+
return {
|
|
84
|
+
isSetup: reasons.length === 0,
|
|
85
|
+
reasons,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return reasons.length === 0;
|
|
90
|
+
},
|
|
91
|
+
};
|