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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { js, packageJson } from "ember-apply";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cp } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { hasConfiguredTSBabel, prependPlugin } from "#utils/babel.js";
|
|
6
|
+
import { getLatest } from "#utils/npm.js";
|
|
7
|
+
|
|
8
|
+
const bases = join(import.meta.dirname, "../../bases");
|
|
9
|
+
const appBase = join(bases, "minimal-app/files");
|
|
10
|
+
const libraryBase = join(bases, "minimal-library/files");
|
|
11
|
+
|
|
12
|
+
const deps = {
|
|
13
|
+
"@babel/plugin-transform-typescript": "^7.28.5",
|
|
14
|
+
"@ember/app-tsconfig": "^2.0.0",
|
|
15
|
+
"@glint/ember-tsc": "^1.0.8",
|
|
16
|
+
"@glint/template": "^1.7.3",
|
|
17
|
+
"@glint/tsserver-plugin": "^2.0.8",
|
|
18
|
+
typescript: "^5.9.3",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @type {import('#types').Layer}
|
|
23
|
+
*/
|
|
24
|
+
export default {
|
|
25
|
+
label: "TypeScript",
|
|
26
|
+
|
|
27
|
+
async run(project) {
|
|
28
|
+
/**
|
|
29
|
+
* if jsconfig exists, switch to tsconfig
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* if tsconfig exists,
|
|
33
|
+
*/
|
|
34
|
+
await addTSConfig(project);
|
|
35
|
+
await updatePackageJson(project);
|
|
36
|
+
await updateBabelConfig(project);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @overload
|
|
41
|
+
* @param {import('#utils/project.js').Project} project
|
|
42
|
+
* @param {true} explain
|
|
43
|
+
* @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* @overload
|
|
47
|
+
* @param {import('#utils/project.js').Project} project
|
|
48
|
+
* @param {boolean | undefined} [explain]
|
|
49
|
+
* @returns {Promise<boolean>}
|
|
50
|
+
*/
|
|
51
|
+
async isSetup(project, explain) {
|
|
52
|
+
const reasons = [];
|
|
53
|
+
|
|
54
|
+
if (!project.hasFile("tsconfig.json")) {
|
|
55
|
+
if (!explain) return false;
|
|
56
|
+
|
|
57
|
+
reasons.push("tsconfig.json is missing");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!(await hasConfiguredTSBabel(project))) {
|
|
61
|
+
if (!explain) return false;
|
|
62
|
+
|
|
63
|
+
reasons.push(`babel.config is missing @babel/plugin-transform-typescript`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let manifest = await packageJson.read(project.directory);
|
|
67
|
+
|
|
68
|
+
if (!manifest.scripts?.["lint:types"]) {
|
|
69
|
+
if (!explain) return false;
|
|
70
|
+
|
|
71
|
+
reasons.push(`package.json is missing the "lint:types" script`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let dep of Object.keys(deps)) {
|
|
75
|
+
if (!manifest.devDependencies?.[dep]) {
|
|
76
|
+
if (!explain) return false;
|
|
77
|
+
|
|
78
|
+
reasons.push(`package.json is missing ${dep} in devDependencies`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (explain) {
|
|
83
|
+
return {
|
|
84
|
+
isSetup: reasons.length === 0,
|
|
85
|
+
reasons,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return reasons.length === 0;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {import('#utils/project.js').Project} project
|
|
95
|
+
*/
|
|
96
|
+
async function updatePackageJson(project) {
|
|
97
|
+
await packageJson.modify(async (json) => {
|
|
98
|
+
json.scripts ||= {};
|
|
99
|
+
json.devDependencies ||= {};
|
|
100
|
+
|
|
101
|
+
Object.assign(json.scripts, {
|
|
102
|
+
"lint:types": "ember-tsc --noEmit",
|
|
103
|
+
});
|
|
104
|
+
Object.assign(json.devDependencies, await getLatest(deps));
|
|
105
|
+
}, project.directory);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {import('#utils/project.js').Project} project
|
|
110
|
+
*/
|
|
111
|
+
async function addTSConfig(project) {
|
|
112
|
+
if (existsSync(project.path("tsconfig.json"))) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (project.type === "app") {
|
|
117
|
+
await cp(join(appBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (project.type === "library") {
|
|
122
|
+
await cp(join(libraryBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {import('#utils/project.js').Project} project
|
|
128
|
+
*/
|
|
129
|
+
async function updateBabelConfig(project) {
|
|
130
|
+
if (await hasConfiguredTSBabel(project)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await prependPlugin(
|
|
135
|
+
project,
|
|
136
|
+
`[
|
|
137
|
+
"@babel/plugin-transform-typescript",
|
|
138
|
+
{
|
|
139
|
+
allExtensions: true,
|
|
140
|
+
onlyRemoveTypeImports: true,
|
|
141
|
+
allowDeclareFields: true,
|
|
142
|
+
},
|
|
143
|
+
]`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -7,11 +7,7 @@ export default defineConfig({
|
|
|
7
7
|
ember(),
|
|
8
8
|
babel({
|
|
9
9
|
babelHelpers: "runtime",
|
|
10
|
-
extensions: extensions.flatMap((extension) => [
|
|
11
|
-
extension,
|
|
12
|
-
".gts",
|
|
13
|
-
".gjs",
|
|
14
|
-
]),
|
|
10
|
+
extensions: extensions.flatMap((extension) => [extension, ".gts", ".gjs"]),
|
|
15
11
|
}),
|
|
16
12
|
],
|
|
17
13
|
test: {
|
|
@@ -1,39 +1,31 @@
|
|
|
1
|
+
import { getLatest } from "#utils/npm.js";
|
|
1
2
|
import { packageJson, files } from "ember-apply";
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3
|
+
import { join } from "node:path";
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Adds Vitest testing support (modern, fast alternative to QUnit)
|
|
6
|
+
* @type {import('#types').Layer}
|
|
11
7
|
*/
|
|
12
8
|
export default {
|
|
13
9
|
label: "Vitest",
|
|
14
|
-
description: "Testing with Vitest (modern & fast)",
|
|
15
10
|
|
|
16
|
-
async run(
|
|
11
|
+
async run(project) {
|
|
17
12
|
// Apply test files
|
|
18
|
-
await files.applyFolder(join(
|
|
19
|
-
|
|
20
|
-
// Add dependencies
|
|
21
|
-
await packageJson.addDependencies(
|
|
22
|
-
{
|
|
23
|
-
"@ember/test-helpers": "^4.0.4",
|
|
24
|
-
},
|
|
25
|
-
targetDir,
|
|
26
|
-
);
|
|
13
|
+
await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
|
|
27
14
|
|
|
28
15
|
// Add devDependencies
|
|
29
16
|
await packageJson.addDevDependencies(
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
"@
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
await getLatest({
|
|
18
|
+
"@ember/test-helpers": "^5.4.1",
|
|
19
|
+
"@ember/test-waiters": "^4.1.1",
|
|
20
|
+
"@testing-library/dom": "^10.4.1",
|
|
21
|
+
"ember-vitest": "^0.3.3",
|
|
22
|
+
vitest: "^4.0.0",
|
|
23
|
+
"@vitest/ui": "^4.0.0",
|
|
24
|
+
"@vitest/browser": "^4.0.0",
|
|
25
|
+
"@vitest/browser-webdriverio": "^4.0.0",
|
|
26
|
+
webdriverio: "^9.23.2",
|
|
27
|
+
}),
|
|
28
|
+
project.directory,
|
|
37
29
|
);
|
|
38
30
|
|
|
39
31
|
// Add scripts
|
|
@@ -43,7 +35,32 @@ export default {
|
|
|
43
35
|
"test:watch": "vitest",
|
|
44
36
|
"test:ui": "vitest --ui",
|
|
45
37
|
},
|
|
46
|
-
|
|
38
|
+
project.directory,
|
|
47
39
|
);
|
|
48
40
|
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @overload
|
|
44
|
+
* @param {import('#utils/project.js').Project} project
|
|
45
|
+
* @param {true} explain
|
|
46
|
+
* @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* @overload
|
|
50
|
+
* @param {import('#utils/project.js').Project} project
|
|
51
|
+
* @param {boolean | undefined} [explain]
|
|
52
|
+
* @returns {Promise<boolean>}
|
|
53
|
+
*/
|
|
54
|
+
async isSetup(project, explain) {
|
|
55
|
+
const reasons = ["Vitest setup detection not implemented"];
|
|
56
|
+
|
|
57
|
+
if (explain) {
|
|
58
|
+
return {
|
|
59
|
+
isSetup: false,
|
|
60
|
+
reasons,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
},
|
|
49
66
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// uses jscodeshift
|
|
2
|
+
import { js } from "ember-apply";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('./project.js').Project} project
|
|
6
|
+
*/
|
|
7
|
+
export async function hasConfiguredTSBabel(project) {
|
|
8
|
+
return hasConfiguredPlugin(project, "@babel/plugin-transform-typescript");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('./project.js').Project} project
|
|
13
|
+
* @param {string} pluginName
|
|
14
|
+
*/
|
|
15
|
+
export async function hasConfiguredPlugin(project, pluginName) {
|
|
16
|
+
let hasPlugin = false;
|
|
17
|
+
|
|
18
|
+
await js.analyze(project.path("babel.config.js"), async ({ root, j }) => {
|
|
19
|
+
root
|
|
20
|
+
.find(j.ArrayExpression, {
|
|
21
|
+
elements: {
|
|
22
|
+
0: { value: pluginName },
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
.forEach(() => {
|
|
26
|
+
hasPlugin = true;
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return hasPlugin;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {import('./project.js').Project} project
|
|
35
|
+
* @param {string} pluginName
|
|
36
|
+
*/
|
|
37
|
+
export async function removeConfiguredPlugin(project, pluginName) {
|
|
38
|
+
await js.transform(project.path("babel.config.js"), async ({ root, j }) => {
|
|
39
|
+
root
|
|
40
|
+
.find(j.ArrayExpression, {
|
|
41
|
+
elements: {
|
|
42
|
+
0: { value: pluginName },
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
.forEach(
|
|
46
|
+
/**
|
|
47
|
+
* @param {unknown} path
|
|
48
|
+
*/
|
|
49
|
+
(path) => {
|
|
50
|
+
j(path).remove();
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import('./project.js').Project} project
|
|
58
|
+
* @param {string} plugin
|
|
59
|
+
*/
|
|
60
|
+
export async function prependPlugin(project, plugin) {
|
|
61
|
+
await js.transform(project.path("babel.config.js"), async ({ root, j }) => {
|
|
62
|
+
root
|
|
63
|
+
.find(j.Property, {
|
|
64
|
+
key: { name: "plugins" },
|
|
65
|
+
value: { type: "ArrayExpression" },
|
|
66
|
+
})
|
|
67
|
+
.forEach(
|
|
68
|
+
/**
|
|
69
|
+
* @param {any} path
|
|
70
|
+
*/
|
|
71
|
+
(path) => {
|
|
72
|
+
path.node.value.elements.unshift(plugin);
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* from https://github.com/ember-cli/ember-cli/blob/8ea5b2e22d37805f5749a4917e0ef25fac3c9cda/lib/models/blueprint.js#L1789
|
|
3
|
+
*
|
|
4
|
+
* Takes a substring of a string based on byte offsets.
|
|
5
|
+
|
|
6
|
+
* @param {string} value : The input string.
|
|
7
|
+
* @param {number} start : The byte index of the substring start.
|
|
8
|
+
* @param {number} end : The byte index of the substring end.
|
|
9
|
+
* @return {string} : The substring.
|
|
10
|
+
*/
|
|
11
|
+
export function substringBytes(value, start, end) {
|
|
12
|
+
const buf = Buffer.from(value);
|
|
13
|
+
|
|
14
|
+
return buf.subarray(start, end).toString();
|
|
15
|
+
}
|
package/src/utils/cli.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
|
|
3
|
+
export function isInteractive() {
|
|
4
|
+
const isTerminalInteractive = process.stdout.isTTY;
|
|
5
|
+
|
|
6
|
+
return isTerminalInteractive;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* I would like to have selecs look more like a table,
|
|
11
|
+
* but @clack/prompts makes this a bit hard.
|
|
12
|
+
*
|
|
13
|
+
* I tried using padding, but then when it prints your selection,
|
|
14
|
+
* I don't think there is a way to format just that, so
|
|
15
|
+
* there are a bunch of goofy spacings in the labels.
|
|
16
|
+
*/
|
|
17
|
+
const PADDING = 10;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @param {string} [subTitle]
|
|
22
|
+
*/
|
|
23
|
+
export function formatLabel(name, subTitle) {
|
|
24
|
+
if (!subTitle) {
|
|
25
|
+
return padToWidth(name, PADDING);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return padToWidth(name, PADDING) + styleText("magentaBright", subTitle);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} input
|
|
33
|
+
* @param {number} maxWidth
|
|
34
|
+
* @param {{ side?: 'right' | 'left' }} [options]
|
|
35
|
+
*/
|
|
36
|
+
function padToWidth(input, maxWidth, { side = "right" } = {}) {
|
|
37
|
+
let str = String(input);
|
|
38
|
+
let width = Math.max(0, maxWidth | 0);
|
|
39
|
+
|
|
40
|
+
if (str.length >= width) return str;
|
|
41
|
+
|
|
42
|
+
let spaces = " ".repeat(width - str.length);
|
|
43
|
+
|
|
44
|
+
return side === "left" ? spaces + str : str + spaces;
|
|
45
|
+
}
|
package/src/utils/cwd.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const cwd = process.cwd();
|
package/src/utils/fs.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { readFile, glob, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { parse as parsePath } from "node:path";
|
|
5
|
+
import { removeTypes } from "./remove-types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* Modified version of applyFolder from ember-apply
|
|
10
|
+
* where the caller gets to decide the final output filPath and contents - whereas the version from ember-apply only allows similar file paths in source and destination.
|
|
11
|
+
*
|
|
12
|
+
* We need customization because we will intake ts and output js
|
|
13
|
+
*
|
|
14
|
+
* @param {string} from
|
|
15
|
+
* @param {{to: import('#utils/project.js').Project, process: (data: { entry: string, contents: string }) => string | Promise<void>}} options sub folder within the target project to copy the contents to
|
|
16
|
+
*/
|
|
17
|
+
export async function applyFolder(from, options) {
|
|
18
|
+
for await (const entry of glob("**/*", { exclude: ["**/node_modules", "**/dist"], cwd: from })) {
|
|
19
|
+
let sourcePath = join(from, entry);
|
|
20
|
+
let targetPath = join(options.to.directory, entry);
|
|
21
|
+
let directory = dirname(targetPath);
|
|
22
|
+
|
|
23
|
+
if (directory) {
|
|
24
|
+
await mkdir(directory, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let stat = lstatSync(sourcePath);
|
|
28
|
+
|
|
29
|
+
if (stat.isDirectory()) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let buffer = await readFile(sourcePath);
|
|
34
|
+
let contents = buffer.toString();
|
|
35
|
+
|
|
36
|
+
await options.process({ entry: targetPath, contents });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} from
|
|
42
|
+
* @param {import('#utils/project.js').Project} project
|
|
43
|
+
*/
|
|
44
|
+
export async function applyFolderTo(from, project) {
|
|
45
|
+
await applyFolder(from, {
|
|
46
|
+
to: project,
|
|
47
|
+
async process({ entry, contents }) {
|
|
48
|
+
/**
|
|
49
|
+
* TODO: handle conflicts if files already exists
|
|
50
|
+
*
|
|
51
|
+
* (I believe we can do interactive here)
|
|
52
|
+
*/
|
|
53
|
+
let pathInfo = parsePath(entry);
|
|
54
|
+
let ext = pathInfo.ext;
|
|
55
|
+
if (ext === ".gts" || ext === ".ts") {
|
|
56
|
+
let wantsTS = await project.hasOrWantsLayer("typescript");
|
|
57
|
+
|
|
58
|
+
if (!wantsTS) {
|
|
59
|
+
let filePath = entry.replace(/\.gts$/, ".gjs").replace(/\.ts$/, ".js");
|
|
60
|
+
|
|
61
|
+
if (existsSync(filePath)) {
|
|
62
|
+
/**
|
|
63
|
+
* Skipping due to already existing
|
|
64
|
+
* TODO: need a flag to force overwrite?
|
|
65
|
+
*/
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let newContents = await removeTypes(ext, contents);
|
|
69
|
+
|
|
70
|
+
await writeFile(filePath, newContents);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (existsSync(entry)) {
|
|
76
|
+
/**
|
|
77
|
+
* Skipping due to already existing
|
|
78
|
+
* TODO: need a flag to force overwrite?
|
|
79
|
+
*/
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await writeFile(entry, contents);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
package/src/utils/git.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { cwd } from "#utils/cwd.js";
|
|
5
|
+
|
|
6
|
+
export function isInGit(directory = cwd) {
|
|
7
|
+
try {
|
|
8
|
+
const result = execSync("git rev-parse --is-inside-work-tree", {
|
|
9
|
+
cwd: directory,
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return result.toString().trim() === "true";
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param {string} directory
|
|
22
|
+
*/
|
|
23
|
+
export function initGit(directory) {
|
|
24
|
+
try {
|
|
25
|
+
execSync("git init --initial-branch=main", {
|
|
26
|
+
cwd: directory,
|
|
27
|
+
stdio: "pipe",
|
|
28
|
+
});
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param {string} directory
|
|
38
|
+
*/
|
|
39
|
+
export function hasGit(directory) {
|
|
40
|
+
return existsSync(join(directory, ".git"));
|
|
41
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isInGit, initGit, hasGit } from "./git.js";
|
|
3
|
+
import { cwd } from "./cwd.js";
|
|
4
|
+
import { mktemp } from "#test-helpers";
|
|
5
|
+
|
|
6
|
+
describe("isInGit", () => {
|
|
7
|
+
it("in this repo", async () => {
|
|
8
|
+
expect(isInGit(cwd)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("in a temp directory", async () => {
|
|
12
|
+
let tmp = await mktemp();
|
|
13
|
+
|
|
14
|
+
expect(isInGit(tmp)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("initGit + hasGit", () => {
|
|
19
|
+
it("creates in non-git directory", async () => {
|
|
20
|
+
let tmp = await mktemp();
|
|
21
|
+
|
|
22
|
+
expect(hasGit(tmp)).toBe(false);
|
|
23
|
+
initGit(tmp);
|
|
24
|
+
expect(hasGit(tmp)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("attempts in pre-existing git repo", async () => {
|
|
28
|
+
let tmp = await mktemp();
|
|
29
|
+
|
|
30
|
+
expect(hasGit(tmp)).toBe(false);
|
|
31
|
+
initGit(tmp);
|
|
32
|
+
expect(hasGit(tmp)).toBe(true);
|
|
33
|
+
initGit(tmp);
|
|
34
|
+
expect(hasGit(tmp)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { hasAllKeys } from "./object.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Record<string, any>} manifest
|
|
5
|
+
* @param {string[] | Record<string, unknown>} listOrObject
|
|
6
|
+
*/
|
|
7
|
+
export function hasDevDeps(manifest, listOrObject) {
|
|
8
|
+
return hasAllKeys(manifest.devDependencies ?? {}, listOrObject);
|
|
9
|
+
}
|
package/src/utils/npm.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import latestVersion from "latest-version";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @type {{ [name: string]: { [version: string]: string } }}
|
|
5
|
+
*/
|
|
6
|
+
const CACHE = {};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{ [name: string]: string }} deps map of dep name to semver range
|
|
10
|
+
*/
|
|
11
|
+
export async function getLatest(deps) {
|
|
12
|
+
let results = await Promise.all(
|
|
13
|
+
Object.entries(deps).map(async ([dep, range]) => {
|
|
14
|
+
let existing = CACHE[dep]?.[range];
|
|
15
|
+
if (existing) {
|
|
16
|
+
return [dep, existing];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let version = await latestVersion(dep, { version: range });
|
|
20
|
+
|
|
21
|
+
CACHE[dep] ||= {};
|
|
22
|
+
CACHE[dep][range] = version;
|
|
23
|
+
|
|
24
|
+
return [dep, version];
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return Object.fromEntries(results);
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Record<string, unknown>} object
|
|
3
|
+
* @param {string[] | Record<string, unknown>} keysOrObject
|
|
4
|
+
*/
|
|
5
|
+
export function hasAllKeys(object, keysOrObject) {
|
|
6
|
+
let keysToCheck = Array.isArray(keysOrObject) ? keysOrObject : Object.keys(keysOrObject);
|
|
7
|
+
let fromObject = new Set(Object.keys(object));
|
|
8
|
+
|
|
9
|
+
return keysToCheck.every((key) => fromObject.has(key));
|
|
10
|
+
}
|