ember.nvp 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.github/workflows/ci.yml +41 -0
  2. package/.github/workflows/plan-release.yml +2 -2
  3. package/.github/workflows/publish.yml +4 -5
  4. package/.prettierignore +2 -1
  5. package/.prettierrc.js +10 -0
  6. package/.release-plan.json +6 -6
  7. package/CHANGELOG.md +53 -0
  8. package/README.md +79 -142
  9. package/package.json +28 -5
  10. package/pnpm-workspace.yaml +3 -0
  11. package/src/bases/README.md +5 -0
  12. package/src/bases/minimal-app/app.test.ts +203 -0
  13. package/src/bases/minimal-app/files/.env.development +8 -0
  14. package/src/bases/minimal-app/files/app/app.ts +21 -0
  15. package/src/bases/minimal-app/files/app/config.ts +17 -0
  16. package/src/bases/minimal-app/files/app/router.ts +51 -0
  17. package/src/bases/minimal-app/files/app/templates/application.gts +5 -0
  18. package/src/{layers/minimal/files-ts → bases/minimal-app/files}/babel.config.js +8 -5
  19. package/src/{layers/minimal → bases/minimal-app}/files/index.html +3 -5
  20. package/src/bases/minimal-app/files/package.json +57 -0
  21. package/src/{layers/minimal → bases/minimal-app}/files/tsconfig.json +0 -6
  22. package/src/bases/minimal-app/index.js +111 -0
  23. package/src/bases/minimal-library/index.js +10 -0
  24. package/src/cli/args.js +57 -0
  25. package/src/cli/generator.js +66 -11
  26. package/src/cli/index.js +44 -13
  27. package/src/cli/questions/layers.js +39 -50
  28. package/src/cli/questions/name.js +22 -6
  29. package/src/cli/questions/ok.js +38 -0
  30. package/src/cli/questions/package-manager.js +23 -2
  31. package/src/cli/questions/path.js +59 -0
  32. package/src/cli/questions/project-type.js +55 -0
  33. package/src/consolidators/linting.js +51 -0
  34. package/src/index.js +2 -0
  35. package/src/layers/eslint-bundled-ember/files/eslint.config.js +14 -0
  36. package/src/layers/eslint-bundled-ember/index.js +91 -0
  37. package/src/layers/eslint-bundled-nvp/app.test.ts +91 -0
  38. package/src/layers/eslint-bundled-nvp/files/eslint.config.js +16 -0
  39. package/src/layers/eslint-bundled-nvp/index.js +91 -0
  40. package/src/layers/eslint-ejected/index.js +95 -0
  41. package/src/layers/git/index.js +33 -0
  42. package/src/layers/github-actions/app.test.ts +73 -0
  43. package/src/layers/github-actions/index.js +200 -0
  44. package/src/layers/index.js +47 -0
  45. package/src/layers/prettier/files/.prettierrc.js +4 -0
  46. package/src/layers/prettier/index.js +45 -18
  47. package/src/layers/qunit/app.test.ts +90 -0
  48. package/src/layers/qunit/files/testem.cjs +26 -0
  49. package/src/layers/qunit/files/tests/index.html +16 -7
  50. package/src/layers/qunit/files/tests/test-helper.ts +65 -7
  51. package/src/layers/qunit/files/tests/unit/macros-test.ts +26 -0
  52. package/src/layers/qunit/index.js +83 -34
  53. package/src/layers/release-plan/index.js +5 -0
  54. package/src/layers/renovate/files/app.json5 +14 -0
  55. package/src/layers/renovate/files/library.json5 +16 -0
  56. package/src/layers/renovate/index.js +68 -0
  57. package/src/layers/typescript/app.test.ts +93 -0
  58. package/src/layers/typescript/index.js +145 -0
  59. package/src/layers/vitest/files/vitest.config.mjs +1 -5
  60. package/src/layers/vitest/index.js +43 -26
  61. package/src/utils/babel.js +76 -0
  62. package/src/utils/buffer.js +15 -0
  63. package/src/utils/cli.js +45 -0
  64. package/src/utils/cwd.js +1 -0
  65. package/src/utils/fs.js +85 -0
  66. package/src/utils/git.js +41 -0
  67. package/src/utils/git.test.ts +36 -0
  68. package/src/utils/manifest.js +9 -0
  69. package/src/utils/npm.js +29 -0
  70. package/src/utils/object.js +10 -0
  71. package/src/utils/project.js +249 -0
  72. package/src/utils/remove-types.js +90 -0
  73. package/src/utils/types.ts +48 -0
  74. package/test/ember.nvp/cli.test.ts +12 -0
  75. package/test/ember.nvp/layers.test.ts +35 -0
  76. package/test/ember.nvp/permutations.test.ts +200 -0
  77. package/test/manual.sh +11 -0
  78. package/test/test-helpers/permutate.test.ts +70 -0
  79. package/test/test-helpers.ts +175 -0
  80. package/tsconfig.json +10 -2
  81. package/vitest.config.js +9 -0
  82. package/src/layers/eslint/index.js +0 -42
  83. package/src/layers/minimal/files/app/app.ts +0 -10
  84. package/src/layers/minimal/files/app/config.ts +0 -18
  85. package/src/layers/minimal/files/app/router.ts +0 -9
  86. package/src/layers/minimal/files/app/templates/application.gts +0 -7
  87. package/src/layers/minimal/files-js/babel.config.js +0 -38
  88. package/src/layers/minimal/index.js +0 -89
  89. package/src/layers/prettier/files/.prettierrc.cjs +0 -15
  90. package/src/layers/qunit/files/tests/unit/.gitkeep +0 -0
  91. /package/src/{layers/minimal → bases/minimal-app}/files/vite.config.mjs +0 -0
  92. /package/src/layers/{eslint/files/eslint.config.mjs → eslint-ejected/files/eslint.config.js} +0 -0
  93. /package/src/layers/{minimal/files/app/routes → qunit/files/tests/application}/.gitkeep +0 -0
  94. /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 { dirname, join } from "node:path";
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
- * Vitest Layer
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({ targetDir }) {
11
+ async run(project) {
17
12
  // Apply test files
18
- await files.applyFolder(join(__dirname, "files"), targetDir);
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
- vitest: "^2.1.8",
32
- "@vitest/ui": "^2.1.8",
33
- "happy-dom": "^15.11.7",
34
- "@ember/test-waiters": "^3.1.0",
35
- },
36
- targetDir,
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
- targetDir,
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
+ }
@@ -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
+ }
@@ -0,0 +1 @@
1
+ export const cwd = process.cwd();
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }