ember.nvp 0.5.0 → 0.7.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 +17 -2
- package/.release-plan.json +84 -4
- package/CHANGELOG.md +68 -0
- package/README.md +29 -0
- package/package.json +4 -2
- package/packages/build-tooling-utils/README.md +37 -0
- package/packages/build-tooling-utils/index.ts +2 -0
- package/packages/build-tooling-utils/package.json +42 -0
- package/packages/build-tooling-utils/rolldown.config.js +18 -0
- package/packages/build-tooling-utils/src/extensions.ts +18 -0
- package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
- package/packages/rolldown/README.md +115 -0
- package/packages/rolldown/index.ts +56 -0
- package/packages/rolldown/package.json +59 -0
- package/packages/rolldown/rolldown.config.js +21 -0
- package/packages/rolldown/src/babel.ts +136 -0
- package/packages/rolldown/src/define-config.ts +32 -0
- package/packages/rolldown/src/externals.ts +83 -0
- package/packages/rolldown/src/isolated-declarations.ts +65 -0
- package/packages/rolldown/src/transform.ts +162 -0
- package/packages/vite/README.md +7 -21
- package/packages/vite/index.ts +1 -1
- package/packages/vite/package.json +4 -6
- package/packages/vite/src/ember.ts +129 -15
- package/packages/vite/src/test-html.ts +86 -0
- package/src/bases/minimal-app/files/app/router.ts +1 -0
- package/src/bases/minimal-app/files/tsconfig.json +1 -0
- package/src/bases/minimal-app/index.js +2 -2
- package/src/bases/minimal-library/files/README.md +41 -0
- package/src/bases/minimal-library/files/package.json +49 -0
- package/src/bases/minimal-library/files/tsconfig.json +10 -0
- package/src/bases/minimal-library/files/tsdown.config.js +6 -0
- package/src/bases/minimal-library/index.js +158 -2
- package/src/cli/args.js +7 -1
- package/src/cli/index.js +90 -16
- package/src/cli/questions/layers.js +5 -2
- package/src/cli/questions/project-type.js +0 -5
- package/src/cli/questions/write.js +169 -0
- package/src/index.js +1 -0
- package/src/layers/are-the-types-wrong/index.js +74 -0
- package/src/layers/git/files/gitignore +14 -1
- package/src/layers/git/index.js +6 -5
- package/src/layers/github-actions/index.js +12 -8
- package/src/layers/index.js +1 -1
- package/src/layers/inspector-support/index.js +101 -0
- package/src/layers/prettier/files/.prettierignore +4 -0
- package/src/layers/publint/index.js +68 -0
- package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
- package/src/layers/qunit/index.js +107 -7
- package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
- package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
- package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
- package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
- package/src/layers/qunit/library-files/vite.config.mjs +6 -0
- package/src/layers/typescript/index.js +43 -7
- package/src/layers/vitest/files/vitest.config.mjs +11 -11
- package/src/layers/vitest/index.js +70 -32
- package/src/utils/babel.js +9 -6
- package/src/utils/fs.js +44 -3
- package/src/utils/inspector-support.js +196 -0
- package/src/utils/npm.js +21 -7
- package/src/utils/remove-types.js +5 -0
- package/src/utils/rewrite-imports.js +157 -0
- package/src/utils/stage.js +317 -0
- package/src/utils/tsdown-config.js +85 -0
- package/src/utils/types.ts +5 -1
- package/test/ember.nvp/base-library.test.ts +232 -0
- package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
- package/test/ember.nvp/inspector-support.test.ts +179 -0
- package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
- package/test/ember.nvp/layer-publish-checks.test.ts +103 -0
- package/test/ember.nvp/layer-qunit.test.ts +376 -15
- package/test/ember.nvp/layer-vitest.test.ts +250 -0
- package/test/ember.nvp/layers.test.ts +16 -0
- package/test/ember.nvp/library-src-fixtures.ts +81 -0
- package/test/ember.nvp/permutations/app-1.test.ts +3 -0
- package/test/ember.nvp/permutations/app-2.test.ts +3 -0
- package/test/ember.nvp/permutations/library-1.test.ts +3 -0
- package/test/ember.nvp/permutations/library-2.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +239 -0
- package/test/ember.nvp/rewrite-imports.test.ts +235 -0
- package/test/ember.nvp/stage.test.ts +188 -0
- package/test/test-helpers/permutate.test.ts +17 -0
- package/test/test-helpers.ts +20 -7
- package/tsconfig.json +1 -0
- package/packages/vite/src/maybe-babel.ts +0 -219
- package/src/layers/qunit/files/run-tests.mjs +0 -30
- package/src/layers/vitest/files/tests/test-helper.ts +0 -5
- package/test/ember.nvp/permutations.test.ts +0 -200
- /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
- /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
|
@@ -1,10 +1,166 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getLatest } from "#utils/npm.js";
|
|
5
|
+
import { applyFolderTo } from "#utils/fs.js";
|
|
6
|
+
import { removeConfiguredPlugin } from "#utils/babel.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minimal Library Base
|
|
10
|
+
*
|
|
11
|
+
* Provides the absolute bare minimum for an Ember v2 library (addon):
|
|
12
|
+
* - type: "module" in package.json
|
|
13
|
+
* - built with tsdown + @nullvoxpopuli/ember-rolldown
|
|
14
|
+
* - a sample `<template>` component and a plain module
|
|
15
|
+
* - No babel.config.js: ember() covers TS stripping, template compilation,
|
|
16
|
+
* and decorators
|
|
17
|
+
* - No testing framework
|
|
18
|
+
* - No linting or formatting
|
|
19
|
+
*
|
|
20
|
+
* Testing, linting, formatting, etc. are opt-in via layers.
|
|
21
|
+
*/
|
|
1
22
|
export default {
|
|
2
23
|
label: "Minimal Library Base",
|
|
3
|
-
description: "Bare minimum library structure",
|
|
24
|
+
description: "Bare minimum Ember v2 library (addon) structure",
|
|
4
25
|
|
|
5
26
|
/**
|
|
27
|
+
* 1. Apply files
|
|
28
|
+
* a. Remove TS if needed
|
|
29
|
+
* 2. Update name(s)
|
|
30
|
+
* 3. Make publishable
|
|
31
|
+
* 4. Remove TS deps/files/config if needed
|
|
32
|
+
* 5. Upgrade in-range dependencies
|
|
33
|
+
* 6. Update an existing babel config, if any
|
|
6
34
|
*
|
|
7
35
|
* @param {import('#utils/project.js').Project} project
|
|
8
36
|
*/
|
|
9
|
-
async run(project) {
|
|
37
|
+
async run(project) {
|
|
38
|
+
await applyFiles(project);
|
|
39
|
+
await updateName(project);
|
|
40
|
+
await makePublishable(project);
|
|
41
|
+
await makeJavaScript(project);
|
|
42
|
+
await upgradeDependencies(project);
|
|
43
|
+
await updateBabelConfig(project);
|
|
44
|
+
},
|
|
10
45
|
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('#utils/project.js').Project} project
|
|
49
|
+
*/
|
|
50
|
+
async function applyFiles(project) {
|
|
51
|
+
let filePath = join(import.meta.dirname, "files");
|
|
52
|
+
|
|
53
|
+
await applyFolderTo(filePath, project);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Operates on known files where the name matters
|
|
58
|
+
*
|
|
59
|
+
* @param {import('#utils/project.js').Project} project
|
|
60
|
+
*/
|
|
61
|
+
async function updateName(project) {
|
|
62
|
+
await packageJson.modify((json) => {
|
|
63
|
+
json.name = project.desires.name;
|
|
64
|
+
}, project.directory);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The template's package.json is `private: true` so it can never be
|
|
69
|
+
* published from this repo; the generated library exists to be published,
|
|
70
|
+
* so the flag is removed entirely.
|
|
71
|
+
*
|
|
72
|
+
* @param {import('#utils/project.js').Project} project
|
|
73
|
+
*/
|
|
74
|
+
async function makePublishable(project) {
|
|
75
|
+
await packageJson.modify((json) => {
|
|
76
|
+
delete json.private;
|
|
77
|
+
}, project.directory);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* When the project is JavaScript (no typescript layer), remove the TS
|
|
82
|
+
* toolchain, drop the tsconfig, and point the build at the emitted `.js`
|
|
83
|
+
* entry (declarations can't be produced without types).
|
|
84
|
+
*
|
|
85
|
+
* @param {import('#utils/project.js').Project} project
|
|
86
|
+
*/
|
|
87
|
+
async function makeJavaScript(project) {
|
|
88
|
+
if (await project.hasOrWantsLayer("typescript")) return;
|
|
89
|
+
|
|
90
|
+
await packageJson.removeDevDependencies(
|
|
91
|
+
["@babel/plugin-transform-typescript", "@ember/library-tsconfig", "typescript"],
|
|
92
|
+
project.directory,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
await project.removeFile("tsconfig.json");
|
|
96
|
+
|
|
97
|
+
await removeTypesExports(project);
|
|
98
|
+
await pointBuildAtJavaScript(project);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* No declarations are emitted for a JavaScript library, so the `types`
|
|
103
|
+
* export conditions would point at files that never exist.
|
|
104
|
+
*
|
|
105
|
+
* @param {import('#utils/project.js').Project} project
|
|
106
|
+
*/
|
|
107
|
+
async function removeTypesExports(project) {
|
|
108
|
+
await packageJson.modify((json) => {
|
|
109
|
+
for (let condition of Object.values(json.exports ?? {})) {
|
|
110
|
+
if (condition && typeof condition === "object") {
|
|
111
|
+
delete condition.types;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}, project.directory);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Rewrites the tsdown config so it builds the JavaScript entry and stops
|
|
119
|
+
* emitting declarations (defineConfig defaults to `dts: true`).
|
|
120
|
+
*
|
|
121
|
+
* @param {import('#utils/project.js').Project} project
|
|
122
|
+
*/
|
|
123
|
+
async function pointBuildAtJavaScript(project) {
|
|
124
|
+
let configPath = project.path("tsdown.config.js");
|
|
125
|
+
let contents = await readFile(configPath, "utf-8");
|
|
126
|
+
|
|
127
|
+
contents = contents.replace(
|
|
128
|
+
`entry: ["./src/index.ts"],`,
|
|
129
|
+
`entry: ["./src/index.js"],\n dts: false,`,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await writeFile(configPath, contents);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Bumps in-range only.
|
|
137
|
+
* Majors will need to go through PR to this repo.
|
|
138
|
+
*
|
|
139
|
+
* @param {import('#utils/project.js').Project} project
|
|
140
|
+
*/
|
|
141
|
+
async function upgradeDependencies(project) {
|
|
142
|
+
let existing = await packageJson.read(project.directory);
|
|
143
|
+
|
|
144
|
+
await packageJson.modify(async (json) => {
|
|
145
|
+
if (json.dependencies) {
|
|
146
|
+
Object.assign(json.dependencies, await getLatest(existing.dependencies));
|
|
147
|
+
}
|
|
148
|
+
if (json.devDependencies) {
|
|
149
|
+
Object.assign(json.devDependencies, await getLatest(existing.devDependencies));
|
|
150
|
+
}
|
|
151
|
+
}, project.directory);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The base doesn't emit a babel.config.js, but this can run over an
|
|
156
|
+
* existing project that has one -- and a JavaScript project's config
|
|
157
|
+
* must not reference the TS plugin.
|
|
158
|
+
*
|
|
159
|
+
* @param {import('#utils/project.js').Project} project
|
|
160
|
+
*/
|
|
161
|
+
async function updateBabelConfig(project) {
|
|
162
|
+
if (await project.hasOrWantsLayer("typescript")) return;
|
|
163
|
+
if (!project.hasFile("babel.config.js")) return;
|
|
164
|
+
|
|
165
|
+
await removeConfiguredPlugin(project, "@babel/plugin-transform-typescript");
|
|
166
|
+
}
|
package/src/cli/args.js
CHANGED
|
@@ -35,10 +35,15 @@ const { values } = parseArgs({
|
|
|
35
35
|
type: "string",
|
|
36
36
|
choices: ["replace", "update"],
|
|
37
37
|
},
|
|
38
|
+
|
|
39
|
+
write: {
|
|
40
|
+
type: "string",
|
|
41
|
+
choices: ["yes", "no"],
|
|
42
|
+
},
|
|
38
43
|
},
|
|
39
44
|
});
|
|
40
45
|
|
|
41
|
-
const { replaceOrUpdate, name, type, layers = [], packageManager, path, confirm } = values;
|
|
46
|
+
const { replaceOrUpdate, name, type, layers = [], packageManager, path, confirm, write } = values;
|
|
42
47
|
|
|
43
48
|
export const answers = {
|
|
44
49
|
name,
|
|
@@ -48,6 +53,7 @@ export const answers = {
|
|
|
48
53
|
path,
|
|
49
54
|
confirm,
|
|
50
55
|
replaceOrUpdate,
|
|
56
|
+
write,
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
/**
|
package/src/cli/index.js
CHANGED
|
@@ -8,8 +8,12 @@ import { askProjectType } from "./questions/project-type.js";
|
|
|
8
8
|
import { askPackageManager } from "./questions/package-manager.js";
|
|
9
9
|
import { askPath } from "./questions/path.js";
|
|
10
10
|
import { askIfOK } from "./questions/ok.js";
|
|
11
|
+
import { askToWrite } from "./questions/write.js";
|
|
11
12
|
import { styleText } from "node:util";
|
|
13
|
+
import { readFile, rm } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
12
15
|
import { Project } from "#utils/project.js";
|
|
16
|
+
import { Stage } from "#utils/stage.js";
|
|
13
17
|
import { askReplaceOrUpdate } from "./questions/replace-or-update.js";
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -28,10 +32,29 @@ async function main() {
|
|
|
28
32
|
const projectPath = await askPath(projectName);
|
|
29
33
|
const replaceOrUpdate = await askReplaceOrUpdate(projectPath);
|
|
30
34
|
const projectType = await askProjectType();
|
|
31
|
-
const selectedLayers = await askLayers();
|
|
35
|
+
const selectedLayers = await askLayers(projectType);
|
|
32
36
|
const packageManager = await askPackageManager();
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
await askIfOK();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Everything is generated in a stage: a real directory (in the OS temp
|
|
42
|
+
* dir) acting as a writable overlay of the target directory.
|
|
43
|
+
*
|
|
44
|
+
* When updating an existing project, the stage is seeded with a full
|
|
45
|
+
* copy of it (sans node_modules/.git), so the bases and layers run
|
|
46
|
+
* against the project's current state -- isSetup checks, package.json
|
|
47
|
+
* reads, file existence checks, etc. all see the existing files, and
|
|
48
|
+
* the codemods modify them in the stage exactly as if they were
|
|
49
|
+
* operating in place. "replace" starts from an empty stage instead.
|
|
50
|
+
*
|
|
51
|
+
* The target itself is not touched until stage.commit() below, which
|
|
52
|
+
* applies the difference between the stage and the target (or the
|
|
53
|
+
* subset of it the user accepts during review).
|
|
54
|
+
*/
|
|
55
|
+
const stage = await Stage.create(projectPath, { seed: replaceOrUpdate !== "replace" });
|
|
56
|
+
|
|
57
|
+
let project = new Project(stage.directory, {
|
|
35
58
|
name: projectName,
|
|
36
59
|
type: projectType,
|
|
37
60
|
path: projectPath,
|
|
@@ -39,28 +62,19 @@ async function main() {
|
|
|
39
62
|
packageManager,
|
|
40
63
|
});
|
|
41
64
|
|
|
42
|
-
await askIfOK();
|
|
43
|
-
|
|
44
65
|
const s = p.spinner();
|
|
45
66
|
s.start("Creating your Ember app...");
|
|
46
67
|
|
|
47
68
|
try {
|
|
48
|
-
// Generate the project
|
|
49
|
-
await generateProject(project
|
|
50
|
-
|
|
51
|
-
s.stop("Project created!");
|
|
52
|
-
|
|
53
|
-
p.note(
|
|
54
|
-
`cd ${projectName}\n` +
|
|
55
|
-
`${packageManager} install\n` +
|
|
56
|
-
`${packageManager} ${packageManager === "npm" ? "run " : ""}start`,
|
|
57
|
-
"Next steps",
|
|
58
|
-
);
|
|
69
|
+
// Generate the project (into the stage)
|
|
70
|
+
await generateProject(project);
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
s.stop("Project generated");
|
|
61
73
|
} catch (err) {
|
|
62
74
|
s.stop("Failed to create project");
|
|
63
75
|
|
|
76
|
+
await stage.discard();
|
|
77
|
+
|
|
64
78
|
if (err instanceof Error) {
|
|
65
79
|
p.cancel(`Error: ${err.message}`);
|
|
66
80
|
if (err.stack) {
|
|
@@ -73,6 +87,66 @@ async function main() {
|
|
|
73
87
|
p.cancel(`Error: ${String(err)}`);
|
|
74
88
|
process.exit(1);
|
|
75
89
|
}
|
|
90
|
+
|
|
91
|
+
const changes = await stage.changes();
|
|
92
|
+
|
|
93
|
+
if (changes.length === 0 && replaceOrUpdate !== "replace") {
|
|
94
|
+
await stage.discard();
|
|
95
|
+
|
|
96
|
+
p.outro(styleText("green", "✓") + " Already up to date -- nothing to write.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* New projects (and "replace", which was already confirmed) are written
|
|
102
|
+
* without asking. Updates to an existing project must be confirmed:
|
|
103
|
+
* write all / review each change (accept/reject) / cancel.
|
|
104
|
+
*/
|
|
105
|
+
const isUpdate = !stage.isNew && replaceOrUpdate !== "replace";
|
|
106
|
+
|
|
107
|
+
const toApply = isUpdate ? await askToWrite(stage, changes) : changes;
|
|
108
|
+
|
|
109
|
+
if (!toApply || toApply.length === 0) {
|
|
110
|
+
await stage.discard();
|
|
111
|
+
|
|
112
|
+
p.cancel("Cancelled -- no files were written");
|
|
113
|
+
return process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (replaceOrUpdate === "replace") {
|
|
117
|
+
await rm(projectPath, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await stage.commit(toApply);
|
|
121
|
+
|
|
122
|
+
p.log.success(`Applied ${toApply.length} change${toApply.length === 1 ? "" : "s"}`);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Converted / generated files are written expecting the project's own
|
|
126
|
+
* formatting and lint pass to run right away (e.g. blank lines between
|
|
127
|
+
* import groups are `eslint --fix`'s job) -- so when the project has a
|
|
128
|
+
* lint:fix script, surface it as the step right after install.
|
|
129
|
+
*/
|
|
130
|
+
let lintFixStep = "";
|
|
131
|
+
try {
|
|
132
|
+
const manifest = JSON.parse(await readFile(join(projectPath, "package.json"), "utf-8"));
|
|
133
|
+
|
|
134
|
+
if (manifest.scripts?.["lint:fix"]) {
|
|
135
|
+
lintFixStep = `${packageManager} ${packageManager === "npm" ? "run " : ""}lint:fix\n`;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// no readable package.json -- skip the suggestion
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
p.note(
|
|
142
|
+
`cd ${projectName}\n` +
|
|
143
|
+
`${packageManager} install\n` +
|
|
144
|
+
lintFixStep +
|
|
145
|
+
`${packageManager} ${packageManager === "npm" ? "run " : ""}start`,
|
|
146
|
+
"Next steps",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
p.outro(styleText("green", "✓") + " Project ready! " + styleText("dim", "Happy coding!"));
|
|
76
150
|
}
|
|
77
151
|
|
|
78
152
|
main().catch((err) => {
|
|
@@ -3,7 +3,10 @@ import { discoverLayers } from "#layers";
|
|
|
3
3
|
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('#types').ProjectType} projectType
|
|
8
|
+
*/
|
|
9
|
+
export async function askLayers(projectType) {
|
|
7
10
|
const optionalLayers = (await discoverLayers()).filter(
|
|
8
11
|
(layer) => typeof layer.run === "function",
|
|
9
12
|
);
|
|
@@ -34,7 +37,7 @@ export async function askLayers() {
|
|
|
34
37
|
const defaultValues = (
|
|
35
38
|
await Promise.all(
|
|
36
39
|
optionalLayers.map(async (layer) => {
|
|
37
|
-
let result = await layer.defaultValue?.();
|
|
40
|
+
let result = await layer.defaultValue?.(projectType);
|
|
38
41
|
|
|
39
42
|
return result && layer.name;
|
|
40
43
|
}),
|
|
@@ -41,11 +41,6 @@ export async function askProjectType() {
|
|
|
41
41
|
],
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
if (answer === "library") {
|
|
45
|
-
p.cancel("Not implemented yet, apologies!");
|
|
46
|
-
return process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
44
|
if (p.isCancel(answer)) {
|
|
50
45
|
p.cancel("Operation cancelled");
|
|
51
46
|
return process.exit(0);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { styleText } from "node:util";
|
|
3
|
+
|
|
4
|
+
import { answers, printArgInUse } from "#args";
|
|
5
|
+
|
|
6
|
+
const SUPPORTED = new Set(["yes", "no"]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {string | undefined} selected
|
|
11
|
+
* @returns {selected is 'yes' | 'no'}
|
|
12
|
+
*/
|
|
13
|
+
function isValid(selected) {
|
|
14
|
+
if (!selected) return false;
|
|
15
|
+
|
|
16
|
+
return SUPPORTED.has(selected);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Nothing has been written to the target directory yet -- everything so
|
|
21
|
+
* far only exists in the stage. Ask whether to write the staged changes,
|
|
22
|
+
* with the option to review the diff change-by-change, accepting or
|
|
23
|
+
* rejecting each one.
|
|
24
|
+
*
|
|
25
|
+
* @param {import('#utils/stage.js').Stage} stage
|
|
26
|
+
* @param {import('#utils/stage.js').Change[]} changes
|
|
27
|
+
* @returns {Promise<import('#utils/stage.js').Change[] | null>}
|
|
28
|
+
* the changes to write, or null to cancel without writing anything
|
|
29
|
+
*/
|
|
30
|
+
export async function askToWrite(stage, changes) {
|
|
31
|
+
if (isValid(answers.write)) {
|
|
32
|
+
printArgInUse("write", answers.write);
|
|
33
|
+
|
|
34
|
+
return answers.write === "yes" ? changes : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
p.note(summarize(changes), `${changes.length} staged change${changes.length === 1 ? "" : "s"}`);
|
|
38
|
+
|
|
39
|
+
const answer = await p.select({
|
|
40
|
+
message: `Write these changes to ${stage.targetDirectory}?`,
|
|
41
|
+
options: [
|
|
42
|
+
{ value: "yes", label: "write the files", hint: "apply all staged changes" },
|
|
43
|
+
{
|
|
44
|
+
value: "review",
|
|
45
|
+
label: "review the diff",
|
|
46
|
+
hint: "step through each change, accepting or rejecting it",
|
|
47
|
+
},
|
|
48
|
+
{ value: "no", label: "cancel", hint: "discard the staged changes, touching nothing" },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (p.isCancel(answer) || answer === "no") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (answer === "yes") {
|
|
57
|
+
return changes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return reviewEachChange(stage, changes);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Show each change's diff and ask what to do with it. "accept remaining"
|
|
65
|
+
* and "reject remaining" finish the review early, so they require an
|
|
66
|
+
* extra confirmation; declining the confirmation returns to the change
|
|
67
|
+
* being reviewed.
|
|
68
|
+
*
|
|
69
|
+
* @param {import('#utils/stage.js').Stage} stage
|
|
70
|
+
* @param {import('#utils/stage.js').Change[]} changes
|
|
71
|
+
* @returns {Promise<import('#utils/stage.js').Change[] | null>}
|
|
72
|
+
* the accepted changes, or null to cancel the whole run
|
|
73
|
+
*/
|
|
74
|
+
async function reviewEachChange(stage, changes) {
|
|
75
|
+
/** @type {import('#utils/stage.js').Change[]} */
|
|
76
|
+
const accepted = [];
|
|
77
|
+
let index = 0;
|
|
78
|
+
|
|
79
|
+
while (index < changes.length) {
|
|
80
|
+
const change = changes[index];
|
|
81
|
+
|
|
82
|
+
if (!change) break;
|
|
83
|
+
|
|
84
|
+
const remaining = changes.length - index;
|
|
85
|
+
|
|
86
|
+
// Bypass clack for the diff itself: diffs are long, and clack's
|
|
87
|
+
// log decorations wrap every line.
|
|
88
|
+
console.log("\n" + (await stage.diff([change])));
|
|
89
|
+
|
|
90
|
+
const answer = await p.select({
|
|
91
|
+
message: `${change.path} (${index + 1} of ${changes.length})`,
|
|
92
|
+
options: [
|
|
93
|
+
{ value: "accept", label: "accept", hint: "write this change" },
|
|
94
|
+
{ value: "reject", label: "reject", hint: "skip this change" },
|
|
95
|
+
...(remaining > 1
|
|
96
|
+
? /** @type {const} */ ([
|
|
97
|
+
{
|
|
98
|
+
value: "accept-remaining",
|
|
99
|
+
label: "accept remaining",
|
|
100
|
+
hint: `accept this and the ${remaining - 1} unreviewed change${remaining - 1 === 1 ? "" : "s"} after it`,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: "reject-remaining",
|
|
104
|
+
label: "reject remaining",
|
|
105
|
+
hint: `reject this and the ${remaining - 1} unreviewed change${remaining - 1 === 1 ? "" : "s"} after it`,
|
|
106
|
+
},
|
|
107
|
+
])
|
|
108
|
+
: []),
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (p.isCancel(answer)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (answer === "accept") {
|
|
117
|
+
accepted.push(change);
|
|
118
|
+
index++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (answer === "reject") {
|
|
123
|
+
index++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const bulkAccept = answer === "accept-remaining";
|
|
128
|
+
const confirmed = await p.confirm({
|
|
129
|
+
message: `${bulkAccept ? "Accept" : "Reject"} all ${remaining} remaining change${remaining === 1 ? "" : "s"} without reviewing them?`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
133
|
+
// return to the change being reviewed
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (bulkAccept) {
|
|
138
|
+
accepted.push(...changes.slice(index));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
p.log.info(
|
|
145
|
+
`Accepted ${accepted.length} of ${changes.length} change${changes.length === 1 ? "" : "s"}`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return accepted;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {import('#utils/stage.js').Change[]} changes
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
function summarize(changes) {
|
|
156
|
+
const colors = /** @type {const} */ ({
|
|
157
|
+
added: "green",
|
|
158
|
+
modified: "yellow",
|
|
159
|
+
deleted: "red",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return changes
|
|
163
|
+
.map((change) => {
|
|
164
|
+
const letter = change.status[0]?.toUpperCase() ?? "?";
|
|
165
|
+
|
|
166
|
+
return `${styleText(colors[change.status], letter)} ${change.path}`;
|
|
167
|
+
})
|
|
168
|
+
.join("\n");
|
|
169
|
+
}
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { getLatest } from "#utils/npm.js";
|
|
3
|
+
import { addTsdownConfigProperty, hasTsdownConfigProperty } from "#utils/tsdown-config.js";
|
|
4
|
+
|
|
5
|
+
const deps = {
|
|
6
|
+
"@arethetypeswrong/core": "^0.18.5",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @type {import('#types').Layer}
|
|
11
|
+
*/
|
|
12
|
+
export default {
|
|
13
|
+
label: "Are The Types Wrong?",
|
|
14
|
+
hint: "validate the published declarations on every build",
|
|
15
|
+
|
|
16
|
+
async run(project) {
|
|
17
|
+
// Publish checks: apps aren't published
|
|
18
|
+
if (project.type !== "library") return;
|
|
19
|
+
|
|
20
|
+
// Declarations are the thing being checked
|
|
21
|
+
if (!(await project.hasOrWantsLayer("typescript"))) return;
|
|
22
|
+
|
|
23
|
+
// ESM-only packages don't offer the CJS resolution modes the strict
|
|
24
|
+
// profile requires
|
|
25
|
+
await addTsdownConfigProperty(project, "attw", `attw: { profile: "esm-only" }`);
|
|
26
|
+
await packageJson.addDevDependencies(await getLatest(deps), project.directory);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @overload
|
|
31
|
+
* @param {import('#utils/project.js').Project} project
|
|
32
|
+
* @param {true} explain
|
|
33
|
+
* @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* @overload
|
|
37
|
+
* @param {import('#utils/project.js').Project} project
|
|
38
|
+
* @param {boolean | undefined} [explain]
|
|
39
|
+
* @returns {Promise<boolean>}
|
|
40
|
+
*/
|
|
41
|
+
async isSetup(project, explain) {
|
|
42
|
+
/** @type {string[]} */
|
|
43
|
+
const reasons = [];
|
|
44
|
+
|
|
45
|
+
// Nothing to set up outside TypeScript libraries, so nothing can be
|
|
46
|
+
// missing
|
|
47
|
+
if (project.type !== "library" || !(await project.hasOrWantsLayer("typescript"))) {
|
|
48
|
+
return explain ? { isSetup: true, reasons } : true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!(await hasTsdownConfigProperty(project, "attw"))) {
|
|
52
|
+
if (!explain) return false;
|
|
53
|
+
|
|
54
|
+
reasons.push("tsdown.config.js does not configure attw");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let manifest = await packageJson.read(project.directory);
|
|
58
|
+
|
|
59
|
+
if (!manifest.devDependencies?.["@arethetypeswrong/core"]) {
|
|
60
|
+
if (!explain) return false;
|
|
61
|
+
|
|
62
|
+
reasons.push("package.json is missing @arethetypeswrong/core in devDependencies");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (explain) {
|
|
66
|
+
return {
|
|
67
|
+
isSetup: reasons.length === 0,
|
|
68
|
+
reasons,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return reasons.length === 0;
|
|
73
|
+
},
|
|
74
|
+
};
|
package/src/layers/git/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hasGit, initGit, isInGit } from "#utils/git.js";
|
|
2
2
|
import { formatLabel } from "#utils/cli.js";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { cp } from "node:fs/promises";
|
|
5
6
|
|
|
@@ -7,10 +8,7 @@ export default {
|
|
|
7
8
|
label: formatLabel("git init"),
|
|
8
9
|
hint: "set up this project as a fresh git repository",
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
* @param {import('#utils/project.js').Project} project
|
|
12
|
-
*/
|
|
13
|
-
async defaultValue(project) {
|
|
11
|
+
async defaultValue() {
|
|
14
12
|
return !isInGit();
|
|
15
13
|
},
|
|
16
14
|
|
|
@@ -27,7 +25,10 @@ export default {
|
|
|
27
25
|
let gitIgnorePath = join(import.meta.dirname, "files/gitignore");
|
|
28
26
|
let targetPath = join(project.directory, ".gitignore");
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
// Don't clobber a .gitignore provided by a base or by the user
|
|
29
|
+
if (!existsSync(targetPath)) {
|
|
30
|
+
await cp(gitIgnorePath, targetPath);
|
|
31
|
+
}
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
return initOk;
|