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.
Files changed (92) 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 +2 -2
  4. package/.prettierignore +2 -1
  5. package/.prettierrc.js +10 -0
  6. package/.release-plan.json +11 -3
  7. package/CHANGELOG.md +37 -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 +30 -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 +1 -4
  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 +61 -7
  51. package/src/layers/qunit/index.js +83 -34
  52. package/src/layers/release-plan/index.js +5 -0
  53. package/src/layers/renovate/files/app.json5 +14 -0
  54. package/src/layers/renovate/files/library.json5 +16 -0
  55. package/src/layers/renovate/index.js +68 -0
  56. package/src/layers/typescript/app.test.ts +93 -0
  57. package/src/layers/typescript/index.js +145 -0
  58. package/src/layers/vitest/files/vitest.config.mjs +1 -5
  59. package/src/layers/vitest/index.js +43 -26
  60. package/src/utils/babel.js +76 -0
  61. package/src/utils/buffer.js +15 -0
  62. package/src/utils/cli.js +45 -0
  63. package/src/utils/cwd.js +1 -0
  64. package/src/utils/fs.js +85 -0
  65. package/src/utils/git.js +41 -0
  66. package/src/utils/git.test.ts +36 -0
  67. package/src/utils/manifest.js +9 -0
  68. package/src/utils/npm.js +29 -0
  69. package/src/utils/object.js +10 -0
  70. package/src/utils/project.js +249 -0
  71. package/src/utils/remove-types.js +90 -0
  72. package/src/utils/types.ts +48 -0
  73. package/test/ember.nvp/cli.test.ts +12 -0
  74. package/test/ember.nvp/layers.test.ts +35 -0
  75. package/test/ember.nvp/permutations.test.ts +200 -0
  76. package/test/manual.sh +11 -0
  77. package/test/test-helpers/permutate.test.ts +70 -0
  78. package/test/test-helpers.ts +175 -0
  79. package/tsconfig.json +10 -2
  80. package/vitest.config.js +9 -0
  81. package/src/layers/eslint/index.js +0 -42
  82. package/src/layers/minimal/files/app/app.ts +0 -10
  83. package/src/layers/minimal/files/app/config.ts +0 -18
  84. package/src/layers/minimal/files/app/router.ts +0 -9
  85. package/src/layers/minimal/files/app/templates/application.gts +0 -7
  86. package/src/layers/minimal/files-js/babel.config.js +0 -38
  87. package/src/layers/minimal/index.js +0 -89
  88. package/src/layers/prettier/files/.prettierrc.cjs +0 -15
  89. /package/src/{layers/minimal → bases/minimal-app}/files/vite.config.mjs +0 -0
  90. /package/src/layers/{eslint/files/eslint.config.mjs → eslint-ejected/files/eslint.config.js} +0 -0
  91. /package/src/layers/{minimal/files/app/routes → qunit/files/tests/application}/.gitkeep +0 -0
  92. /package/src/layers/qunit/files/tests/{integration → rendering}/.gitkeep +0 -0
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { permutate } from "#test-helpers";
3
+
4
+ describe("permutate", () => {
5
+ it("generates all permutations", () => {
6
+ const result = permutate(["a", "b", "c"]);
7
+
8
+ expect(result).toEqual([
9
+ ["a"],
10
+ ["a", "b"],
11
+ ["a", "b", "c"],
12
+ ["a", "c"],
13
+ ["b"],
14
+ ["b", "c"],
15
+ ["c"],
16
+ ]);
17
+ });
18
+
19
+ it("skips duplicate entries", () => {
20
+ const result = permutate(["a", "b", "b"]);
21
+
22
+ // Should not include ["a", "b"] twice
23
+ const count = result.filter((perm) => perm.join(",") === "a,b").length;
24
+ expect(count).toBe(1);
25
+ });
26
+
27
+ it("enforces mutual exclusivity for eslint-prefixed entries", () => {
28
+ const result = permutate([
29
+ "eslint-bundled-ember",
30
+ "eslint-bundled-nvp",
31
+ "eslint-ejected",
32
+ "other",
33
+ ]);
34
+
35
+ // Check that no permutation has more than one eslint-prefixed entry
36
+ for (const permutation of result) {
37
+ const eslintCount = permutation.filter((item) => item.startsWith("eslint-")).length;
38
+ expect(eslintCount).toBeLessThanOrEqual(1);
39
+ }
40
+ });
41
+
42
+ it("allows permutations with 0 or 1 eslint entry", () => {
43
+ const result = permutate(["eslint-bundled-ember", "eslint-bundled-nvp", "other"]);
44
+
45
+ const hasNoEslint = result.some((perm) => !perm.some((item) => item.startsWith("eslint-")));
46
+ const hasOneEslint = result.some(
47
+ (perm) => perm.filter((item) => item.startsWith("eslint-")).length === 1,
48
+ );
49
+
50
+ expect(hasNoEslint).toBe(true);
51
+ expect(hasOneEslint).toBe(true);
52
+ });
53
+
54
+ it("generates valid permutations with mixed items", () => {
55
+ const result = permutate(["a", "eslint-foo", "eslint-bar", "b"]);
56
+
57
+ // Verify all permutations contain at most 1 eslint entry
58
+ for (const permutation of result) {
59
+ const eslintCount = permutation.filter((item) => item.startsWith("eslint-")).length;
60
+ expect(eslintCount).toBeLessThanOrEqual(1);
61
+ }
62
+
63
+ // Should have permutations like: ["a"], ["a", "eslint-foo"], ["a", "b"], etc.
64
+ // but NOT ["a", "eslint-foo", "eslint-bar"]
65
+ const hasMultipleEslint = result.some(
66
+ (perm) => perm.filter((item) => item.startsWith("eslint-")).length > 1,
67
+ );
68
+ expect(hasMultipleEslint).toBe(false);
69
+ });
70
+ });
@@ -0,0 +1,175 @@
1
+ import { execa } from "execa";
2
+ import { join } from "node:path";
3
+ import { styleText } from "node:util";
4
+ import { Readable, Writable } from "node:stream";
5
+ import { mkdtemp } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { generateProject, Project } from "ember.nvp";
8
+ import { discoverLayers } from "#layers";
9
+ import type { DiscoveredLayer } from "#types";
10
+ import { expect } from "vitest";
11
+
12
+ const minimalApp = "minimal-app";
13
+ const minimalAddon = "minimal-library";
14
+
15
+ export const bases = [minimalApp, minimalAddon];
16
+
17
+ export const layers = await discoverLayers();
18
+
19
+ export function permutate(toPermutate: string[]): string[][] {
20
+ const out: string[][] = [];
21
+
22
+ function backtrack(startIndex: number, prefix: string[], hasEslint: boolean) {
23
+ if (prefix.length > 0) out.push(prefix.slice());
24
+
25
+ for (let i = startIndex; i < toPermutate.length; i++) {
26
+ if (i > startIndex && toPermutate[i] === toPermutate[i - 1]) {
27
+ // skip duplicates (a,b,c == c,b,a)
28
+ continue;
29
+ }
30
+
31
+ let option = toPermutate[i];
32
+
33
+ // something has gone wrong if this is undefined
34
+ // (silly TS not understanding loop bounds)
35
+ if (!option) continue;
36
+
37
+ const isEslint = option.startsWith("eslint-");
38
+
39
+ // Skip this option if it's an eslint entry and we already have one
40
+ // (enforce mutual exclusivity for eslint-prefixed entries)
41
+ if (isEslint && hasEslint) continue;
42
+
43
+ prefix.push(option);
44
+ backtrack(i + 1, prefix, hasEslint || isEslint);
45
+ prefix.pop();
46
+ }
47
+ }
48
+
49
+ backtrack(0, [], false);
50
+ return out;
51
+ }
52
+
53
+ export async function mktemp(name = "ember.nvp_test-") {
54
+ return await mkdtemp(join(tmpdir(), `${name}-`));
55
+ }
56
+
57
+ export async function reapply(project: Project, layers: string[]) {
58
+ return await generate({
59
+ directory: project.directory,
60
+ layers,
61
+ name: project.name,
62
+ type: project.type,
63
+ packageManager: project.packageManager,
64
+ });
65
+ }
66
+
67
+ export async function generate({
68
+ layers: layerNames = [],
69
+ name = "my-app",
70
+ type = "app",
71
+ packageManager = "pnpm",
72
+ directory,
73
+ }: {
74
+ directory?: string;
75
+ layers?: string[];
76
+ name?: string;
77
+ type?: "app" | "library";
78
+ packageManager?: "pnpm" | "npm";
79
+ }): Promise<Project> {
80
+ const tempDir = directory ?? (await mktemp(name));
81
+
82
+ let selectedLayers = layers.filter((layer) => layerNames.includes(layer.name));
83
+ let project = new Project(tempDir, {
84
+ name,
85
+ type,
86
+ path: tempDir,
87
+ layers: selectedLayers,
88
+ packageManager,
89
+ });
90
+
91
+ await generateProject(project);
92
+
93
+ return project;
94
+ }
95
+
96
+ const cliPath = join(import.meta.dirname, "../src/cli/index.js");
97
+
98
+ export function cli(args: string[] = []) {
99
+ let cmd = `node ${cliPath} ${args.join(" ")}`;
100
+
101
+ console.log(`Running '${styleText("cyan", cmd)}'`);
102
+
103
+ let output = new MockWritable();
104
+ let input = new MockReadable();
105
+
106
+ let execaPromise = execa("node", args, { shell: true, stdout: output, stdin: input });
107
+
108
+ return {
109
+ execaPromise,
110
+ output,
111
+ input,
112
+ };
113
+ }
114
+
115
+ export class MockWritable extends Writable {
116
+ public buffer: string[] = [];
117
+ public isTTY = false;
118
+ public columns = 80;
119
+ public rows = 20;
120
+
121
+ _write(
122
+ chunk: any,
123
+ _encoding: BufferEncoding,
124
+ callback: (error?: Error | null | undefined) => void,
125
+ ): void {
126
+ this.buffer.push(chunk.toString());
127
+ callback();
128
+ }
129
+ }
130
+
131
+ export class MockReadable extends Readable {
132
+ protected _buffer: unknown[] | null = [];
133
+
134
+ _read() {
135
+ if (this._buffer === null) {
136
+ this.push(null);
137
+ return;
138
+ }
139
+
140
+ for (const val of this._buffer) {
141
+ this.push(val);
142
+ }
143
+
144
+ this._buffer = [];
145
+ }
146
+
147
+ pushValue(val: unknown): void {
148
+ this._buffer?.push(val);
149
+ }
150
+
151
+ close(): void {
152
+ this._buffer = null;
153
+ }
154
+ }
155
+
156
+ export async function build(project: Project, mode = "development") {
157
+ return await execa("pnpm", ["vite", "build", "--mode", mode], {
158
+ cwd: project.directory,
159
+ env: {
160
+ NODE_ENV: mode,
161
+ },
162
+ });
163
+ }
164
+
165
+ export async function expectIsSetup(project: Project, layer: DiscoveredLayer) {
166
+ let result = await layer.isSetup(project, true);
167
+
168
+ if (typeof result === "object") {
169
+ expect(result.reasons, `${layer.name} is setup`).deep.equal([]);
170
+ expect(result.isSetup, `${layer.name} is setup`).toBe(true);
171
+ return;
172
+ }
173
+
174
+ expect(result, `${layer.name} is setup`).toBe(true);
175
+ }
package/tsconfig.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "extends": "@ember/library-tsconfig",
3
3
  "compilerOptions": {
4
- "allowJs": true
4
+ "allowJs": true,
5
+ "checkJs": true
5
6
  },
6
- "types": ["ember-source/types", "@glint/ember-tsc/types"]
7
+ "types": ["ember-source/types", "@glint/ember-tsc/types", "vite/client", "vitest/importMeta"],
8
+ "exclude": [
9
+ "node_modules",
10
+ "dist",
11
+ "src/**/layers/*/files/**",
12
+ "src/**/bases/*/files/**",
13
+ "my-app"
14
+ ]
7
15
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ testTimeout: 60_000,
6
+ hookTimeout: 60_000,
7
+ include: ["test/**/*.test.ts", "src/**/*.test.ts"],
8
+ },
9
+ });
@@ -1,42 +0,0 @@
1
- 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));
6
-
7
- /**
8
- * ESLint Layer
9
- *
10
- * Adds ESLint support with modern configuration
11
- */
12
- export default {
13
- label: "ESLint",
14
- description: "Code linting with ESLint",
15
-
16
- async run({ targetDir }) {
17
- // Apply config files
18
- await files.applyFolder(join(__dirname, "files"), targetDir);
19
-
20
- // Add devDependencies
21
- await packageJson.addDevDependencies(
22
- {
23
- eslint: "^9.18.0",
24
- "@typescript-eslint/eslint-plugin": "^8.19.1",
25
- "@typescript-eslint/parser": "^8.19.1",
26
- "eslint-config-prettier": "^10.0.1",
27
- "eslint-plugin-ember": "^12.2.1",
28
- globals: "^15.14.0",
29
- },
30
- targetDir,
31
- );
32
-
33
- // Add scripts
34
- await packageJson.addScripts(
35
- {
36
- lint: "eslint . --cache",
37
- "lint:fix": "eslint . --fix",
38
- },
39
- targetDir,
40
- );
41
- },
42
- };
@@ -1,10 +0,0 @@
1
- import Application from "@ember/application";
2
- import compatModules from "@embroider/virtual/compat-modules";
3
- import Resolver from "ember-resolver";
4
- import config from "./config.ts";
5
-
6
- export default class App extends Application {
7
- modulePrefix = config.modulePrefix;
8
- podModulePrefix = config.podModulePrefix;
9
- Resolver = Resolver.withModules(compatModules);
10
- }
@@ -1,18 +0,0 @@
1
- const ENV = {
2
- modulePrefix: "__PROJECT_NAME__",
3
- environment: import.meta.env.DEV ? "development" : "production",
4
- rootURL: "/",
5
- locationType: "history",
6
- EmberENV: {},
7
- APP: {},
8
- } as {
9
- environment: string;
10
- modulePrefix: string;
11
- podModulePrefix?: string;
12
- rootURL: string;
13
- locationType: "history" | "hash";
14
- EmberENV: Record<string, unknown>;
15
- APP: Record<string, unknown>;
16
- };
17
-
18
- export default ENV;
@@ -1,9 +0,0 @@
1
- import EmberRouter from "@ember/routing/router";
2
- import config from "#app/config.ts";
3
-
4
- export default class Router extends EmberRouter {
5
- location = config.locationType;
6
- rootURL = config.rootURL;
7
- }
8
-
9
- Router.map(function () {});
@@ -1,7 +0,0 @@
1
- import { pageTitle } from 'ember-page-title';
2
-
3
- <template>
4
- {{pageTitle "__PROJECT_NAME__"}}
5
-
6
- {{outlet}}
7
- </template>
@@ -1,38 +0,0 @@
1
- import { dirname } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { buildMacros } from "@embroider/macros/babel";
4
-
5
- const macros = buildMacros();
6
-
7
- export default {
8
- plugins: [
9
- [
10
- "babel-plugin-ember-template-compilation",
11
- {
12
- compilerPath: "ember-source/dist/ember-template-compiler.js",
13
- transforms: [...macros.templateMacros],
14
- },
15
- ],
16
- [
17
- "module:decorator-transforms",
18
- {
19
- runtime: {
20
- import: import.meta.resolve("decorator-transforms/runtime-esm"),
21
- },
22
- },
23
- ],
24
- [
25
- "@babel/plugin-transform-runtime",
26
- {
27
- absoluteRuntime: dirname(fileURLToPath(import.meta.url)),
28
- useESModules: true,
29
- regenerator: false,
30
- },
31
- ],
32
- ...macros.babelMacros,
33
- ],
34
-
35
- generatorOpts: {
36
- compact: false,
37
- },
38
- };
@@ -1,89 +0,0 @@
1
- import { packageJson, files, js } from "ember-apply";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { readFile } from "node:fs/promises";
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
-
8
- /**
9
- * Minimal Layer
10
- *
11
- * Provides the absolute bare minimum for an Ember app:
12
- * - type: "module" in package.json
13
- * - No @embroider/compat (no-compat mode)
14
- * - No testing framework
15
- * - No linting or formatting
16
- * - No ember-welcome-page
17
- * - No warp-drive (opt-in if needed)
18
- */
19
- export default {
20
- label: "Minimal Base",
21
- description: "Bare minimum Ember app structure",
22
-
23
- async run({ targetDir, projectName }) {
24
- // Apply all files from the files directory
25
- await files.applyFolder(join(__dirname, "files"), targetDir);
26
-
27
- // Replace __PROJECT_NAME__ placeholders in specific files
28
- const filesToUpdate = [
29
- join(targetDir, "app", "config.ts"),
30
- join(targetDir, "app", "templates", "application.gts"),
31
- join(targetDir, "index.html"),
32
- ];
33
-
34
- for (const filePath of filesToUpdate) {
35
- const content = await readFile(filePath, "utf-8");
36
- const updated = content.replaceAll("__PROJECT_NAME__", projectName);
37
- await files.copyFileTo(filePath, { content: updated });
38
- }
39
-
40
- // Add dependencies
41
- await packageJson.addDependencies(
42
- {
43
- "@glimmer/component": "^1.1.2",
44
- "@glimmer/tracking": "^1.1.2",
45
- "@embroider/core": "^3.4.16",
46
- "@embroider/macros": "^1.16.7",
47
- "@embroider/router": "^2.1.8",
48
- "@embroider/vite": "^6.0.2",
49
- "@embroider/virtual": "^3.4.16",
50
- "ember-page-title": "^8.2.4",
51
- "ember-resolver": "^14.0.0",
52
- "ember-source": "^6.0.1",
53
- vite: "^6.0.3",
54
- },
55
- targetDir,
56
- );
57
-
58
- // Add devDependencies
59
- await packageJson.addDevDependencies(
60
- {
61
- "@babel/core": "^7.26.0",
62
- "@babel/plugin-transform-typescript": "^7.26.3",
63
- "@rollup/plugin-babel": "^6.0.4",
64
- "@tsconfig/ember": "^3.0.8",
65
- typescript: "^5.7.3",
66
- },
67
- targetDir,
68
- );
69
-
70
- // Add scripts
71
- await packageJson.addScripts(
72
- {
73
- start: "vite",
74
- build: "vite build",
75
- preview: "vite preview",
76
- },
77
- targetDir,
78
- );
79
-
80
- // Update package.json to set type: "module" and other metadata
81
- await packageJson.modify((json) => {
82
- json.name = projectName;
83
- json.version = "0.0.0";
84
- json.private = true;
85
- json.type = "module";
86
- return json;
87
- }, targetDir);
88
- },
89
- };
@@ -1,15 +0,0 @@
1
- module.exports = {
2
- singleQuote: true,
3
- trailingComma: 'es5',
4
- printWidth: 100,
5
- semi: true,
6
- plugins: ['prettier-plugin-ember-template-tag'],
7
- overrides: [
8
- {
9
- files: '*.{gjs,gts}',
10
- options: {
11
- parser: 'ember-template-tag',
12
- },
13
- },
14
- ],
15
- };