ember.nvp 0.6.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.
Files changed (85) hide show
  1. package/.github/workflows/ci.yml +17 -2
  2. package/.release-plan.json +88 -4
  3. package/CHANGELOG.md +57 -0
  4. package/package.json +3 -2
  5. package/packages/build-tooling-utils/README.md +37 -0
  6. package/packages/build-tooling-utils/index.ts +2 -0
  7. package/packages/build-tooling-utils/package.json +42 -0
  8. package/packages/build-tooling-utils/rolldown.config.js +18 -0
  9. package/packages/build-tooling-utils/src/extensions.ts +18 -0
  10. package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
  11. package/packages/rolldown/README.md +115 -0
  12. package/packages/rolldown/index.ts +56 -0
  13. package/packages/rolldown/package.json +59 -0
  14. package/packages/rolldown/rolldown.config.js +21 -0
  15. package/packages/rolldown/src/babel.ts +136 -0
  16. package/packages/rolldown/src/define-config.ts +32 -0
  17. package/packages/rolldown/src/externals.ts +83 -0
  18. package/packages/rolldown/src/isolated-declarations.ts +65 -0
  19. package/packages/rolldown/src/transform.ts +162 -0
  20. package/packages/vite/README.md +7 -21
  21. package/packages/vite/index.ts +1 -1
  22. package/packages/vite/package.json +4 -6
  23. package/packages/vite/src/ember.ts +129 -15
  24. package/packages/vite/src/test-html.ts +86 -0
  25. package/src/bases/minimal-app/files/app/router.ts +1 -0
  26. package/src/bases/minimal-app/files/tsconfig.json +1 -0
  27. package/src/bases/minimal-app/index.js +2 -2
  28. package/src/bases/minimal-library/files/README.md +41 -0
  29. package/src/bases/minimal-library/files/package.json +49 -0
  30. package/src/bases/minimal-library/files/tsconfig.json +10 -0
  31. package/src/bases/minimal-library/files/tsdown.config.js +6 -0
  32. package/src/bases/minimal-library/index.js +158 -2
  33. package/src/cli/index.js +21 -2
  34. package/src/cli/questions/layers.js +5 -2
  35. package/src/cli/questions/project-type.js +0 -5
  36. package/src/layers/are-the-types-wrong/index.js +74 -0
  37. package/src/layers/git/files/gitignore +14 -1
  38. package/src/layers/git/index.js +6 -5
  39. package/src/layers/github-actions/index.js +12 -8
  40. package/src/layers/index.js +1 -1
  41. package/src/layers/inspector-support/index.js +101 -0
  42. package/src/layers/prettier/files/.prettierignore +4 -0
  43. package/src/layers/publint/index.js +68 -0
  44. package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
  45. package/src/layers/qunit/index.js +107 -7
  46. package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
  47. package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
  48. package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
  49. package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
  50. package/src/layers/qunit/library-files/vite.config.mjs +6 -0
  51. package/src/layers/typescript/index.js +43 -7
  52. package/src/layers/vitest/files/vitest.config.mjs +11 -11
  53. package/src/layers/vitest/index.js +70 -32
  54. package/src/utils/babel.js +9 -6
  55. package/src/utils/fs.js +44 -3
  56. package/src/utils/inspector-support.js +196 -0
  57. package/src/utils/npm.js +21 -7
  58. package/src/utils/remove-types.js +5 -0
  59. package/src/utils/rewrite-imports.js +157 -0
  60. package/src/utils/tsdown-config.js +85 -0
  61. package/src/utils/types.ts +5 -1
  62. package/test/ember.nvp/base-library.test.ts +232 -0
  63. package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
  64. package/test/ember.nvp/inspector-support.test.ts +179 -0
  65. package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
  66. package/test/ember.nvp/layer-publish-checks.test.ts +103 -0
  67. package/test/ember.nvp/layer-qunit.test.ts +376 -15
  68. package/test/ember.nvp/layer-vitest.test.ts +250 -0
  69. package/test/ember.nvp/layers.test.ts +16 -0
  70. package/test/ember.nvp/library-src-fixtures.ts +81 -0
  71. package/test/ember.nvp/permutations/app-1.test.ts +3 -0
  72. package/test/ember.nvp/permutations/app-2.test.ts +3 -0
  73. package/test/ember.nvp/permutations/library-1.test.ts +3 -0
  74. package/test/ember.nvp/permutations/library-2.test.ts +3 -0
  75. package/test/ember.nvp/permutations/shared.ts +239 -0
  76. package/test/ember.nvp/rewrite-imports.test.ts +235 -0
  77. package/test/test-helpers/permutate.test.ts +17 -0
  78. package/test/test-helpers.ts +20 -7
  79. package/tsconfig.json +1 -0
  80. package/packages/vite/src/maybe-babel.ts +0 -219
  81. package/src/layers/qunit/files/run-tests.mjs +0 -30
  82. package/src/layers/vitest/files/tests/test-helper.ts +0 -5
  83. package/test/ember.nvp/permutations.test.ts +0 -200
  84. /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
  85. /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
@@ -0,0 +1,235 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+
6
+ import { rewriteImportsToMatchFiles } from "#utils/rewrite-imports.js";
7
+
8
+ /**
9
+ * Generation renames files (e.g. type removal turns .ts into .js), so
10
+ * import specifiers are checked against the emitted project tree:
11
+ * whatever file actually exists on disk wins.
12
+ */
13
+ describe("rewriteImportsToMatchFiles", () => {
14
+ const dirs: string[] = [];
15
+
16
+ afterEach(async () => {
17
+ for (const dir of dirs.splice(0)) {
18
+ await rm(dir, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ /**
23
+ * Lays out an emitted-project fixture on disk (the way generation
24
+ * stages real files) and returns its root.
25
+ */
26
+ async function makeProject(files: Record<string, string>) {
27
+ const dir = await mkdtemp(join(tmpdir(), "rewrite-imports-test-"));
28
+ dirs.push(dir);
29
+
30
+ for (const [path, contents] of Object.entries(files)) {
31
+ await mkdir(join(dir, dirname(path)), { recursive: true });
32
+ await writeFile(join(dir, path), contents);
33
+ }
34
+
35
+ return dir;
36
+ }
37
+
38
+ const manifest = JSON.stringify({
39
+ name: "fixture",
40
+ imports: {
41
+ "#app/*": "./app/*",
42
+ "#config": "./app/config.js",
43
+ },
44
+ });
45
+
46
+ it("rewrites a specifier whose file was renamed", async () => {
47
+ const project = await makeProject({
48
+ "package.json": manifest,
49
+ "app/app.js": "",
50
+ "app/config.js": "",
51
+ });
52
+
53
+ const result = rewriteImportsToMatchFiles(
54
+ [`import Application from "#app/app.ts";`, `import config from "#config";`].join("\n"),
55
+ join(project, "tests/test-helper.js"),
56
+ );
57
+
58
+ expect(result).toMatchInlineSnapshot(`
59
+ "import Application from "#app/app.js";
60
+ import config from "#config";"
61
+ `);
62
+ });
63
+
64
+ it("leaves a specifier alone when its file exists as written", async () => {
65
+ const project = await makeProject({
66
+ "package.json": manifest,
67
+ "app/helpers/x.ts": "",
68
+ });
69
+
70
+ const result = rewriteImportsToMatchFiles(
71
+ `import x from "./helpers/x.ts";`,
72
+ join(project, "app/consumer.ts"),
73
+ );
74
+
75
+ expect(result).toMatchInlineSnapshot(`"import x from "./helpers/x.ts";"`);
76
+ });
77
+
78
+ it("adds the on-disk extension to extensionless local imports", async () => {
79
+ const project = await makeProject({
80
+ "package.json": manifest,
81
+ "app/helpers/x.js": "",
82
+ "app/app.ts": "",
83
+ });
84
+
85
+ const result = rewriteImportsToMatchFiles(
86
+ [`import x from "./helpers/x";`, `import Application from "#app/app";`].join("\n"),
87
+ join(project, "app/consumer.js"),
88
+ );
89
+
90
+ expect(result).toMatchInlineSnapshot(`
91
+ "import x from "./helpers/x.js";
92
+ import Application from "#app/app.ts";"
93
+ `);
94
+ });
95
+
96
+ it("keeps an extensionless specifier whose fully-specified form would not map", async () => {
97
+ const project = await makeProject({
98
+ "package.json": manifest,
99
+ "app/config.js": "",
100
+ });
101
+
102
+ // `#config` maps to "./app/config.js" -- the extension lives in the
103
+ // `imports` target, so "#config.js" would not resolve
104
+ const result = rewriteImportsToMatchFiles(
105
+ `import config from "#config";`,
106
+ join(project, "app/consumer.js"),
107
+ );
108
+
109
+ expect(result).toMatchInlineSnapshot(`"import config from "#config";"`);
110
+ });
111
+
112
+ it("rewrites relative, re-export, and dynamic import() specifiers", async () => {
113
+ const project = await makeProject({
114
+ "package.json": manifest,
115
+ "app/thing.js": "",
116
+ "shared/other.js": "",
117
+ "app/lazy.js": "",
118
+ });
119
+
120
+ const result = rewriteImportsToMatchFiles(
121
+ [
122
+ `export { thing } from "./thing.ts";`,
123
+ `export * from "../shared/other.ts";`,
124
+ `export async function load() {`,
125
+ ` return await import("./lazy.ts");`,
126
+ `}`,
127
+ ].join("\n"),
128
+ join(project, "app/consumer.js"),
129
+ );
130
+
131
+ expect(result).toMatchInlineSnapshot(`
132
+ "export { thing } from "./thing.js";
133
+ export * from "../shared/other.js";
134
+ export async function load() {
135
+ return await import("./lazy.js");
136
+ }"
137
+ `);
138
+ });
139
+
140
+ it("leaves a specifier alone when no file matches at all", async () => {
141
+ const project = await makeProject({
142
+ "package.json": manifest,
143
+ });
144
+
145
+ const result = rewriteImportsToMatchFiles(
146
+ `import missing from "./does-not-exist.ts";`,
147
+ join(project, "app/consumer.js"),
148
+ );
149
+
150
+ expect(result).toMatchInlineSnapshot(`"import missing from "./does-not-exist.ts";"`);
151
+ });
152
+
153
+ it("leaves package specifiers and non-import strings alone", async () => {
154
+ const project = await makeProject({
155
+ "package.json": manifest,
156
+ "app/looks-like-a-file.js": "",
157
+ });
158
+
159
+ const result = rewriteImportsToMatchFiles(
160
+ [`import lib from "some-lib/file.ts";`, `const notAnImport = "./looks-like-a-file.ts";`].join(
161
+ "\n",
162
+ ),
163
+ join(project, "app/consumer.js"),
164
+ );
165
+
166
+ expect(result).toMatchInlineSnapshot(`
167
+ "import lib from "some-lib/file.ts";
168
+ const notAnImport = "./looks-like-a-file.ts";"
169
+ `);
170
+ });
171
+
172
+ it("rewrites imports in gjs files while preserving the template", async () => {
173
+ const project = await makeProject({
174
+ "package.json": manifest,
175
+ "app/components/button.gjs": "",
176
+ });
177
+
178
+ const result = rewriteImportsToMatchFiles(
179
+ [
180
+ `import Component from "@glimmer/component";`,
181
+ `import Button from "./button.gts";`,
182
+ ``,
183
+ `export default class Foo extends Component {`,
184
+ ` <template>`,
185
+ ` <Button />`,
186
+ ` </template>`,
187
+ `}`,
188
+ ].join("\n"),
189
+ join(project, "app/components/foo.gjs"),
190
+ );
191
+
192
+ expect(result).toMatchInlineSnapshot(`
193
+ "import Component from "@glimmer/component";
194
+ import Button from "./button.gjs";
195
+ export default class Foo extends Component {
196
+ <template><Button /></template>
197
+ }"
198
+ `);
199
+ });
200
+
201
+ it("keeps comments in files whose imports are rewritten", async () => {
202
+ const project = await makeProject({
203
+ "package.json": manifest,
204
+ "app/app.js": "",
205
+ });
206
+
207
+ const result = rewriteImportsToMatchFiles(
208
+ [
209
+ `// top comment`,
210
+ `import Application from "#app/app.ts";`,
211
+ ``,
212
+ `/**`,
213
+ ` * doc comment for start`,
214
+ ` */`,
215
+ `export function start() {`,
216
+ ` // inner comment`,
217
+ ` Application.create();`,
218
+ `}`,
219
+ ].join("\n"),
220
+ join(project, "tests/test-helper.js"),
221
+ );
222
+
223
+ expect(result).toMatchInlineSnapshot(`
224
+ "// top comment
225
+ import Application from "#app/app.js";
226
+ /**
227
+ * doc comment for start
228
+ */
229
+ export function start() {
230
+ // inner comment
231
+ Application.create();
232
+ }"
233
+ `);
234
+ });
235
+ });
@@ -51,6 +51,23 @@ describe("permutate", () => {
51
51
  expect(hasOneEslint).toBe(true);
52
52
  });
53
53
 
54
+ it("enforces mutual exclusivity for testing layers", () => {
55
+ const result = permutate(["qunit", "vitest", "other"]);
56
+
57
+ for (const permutation of result) {
58
+ const testingCount = permutation.filter(
59
+ (item) => item === "qunit" || item === "vitest",
60
+ ).length;
61
+ expect(testingCount).toBeLessThanOrEqual(1);
62
+ }
63
+
64
+ const hasQunit = result.some((perm) => perm.includes("qunit"));
65
+ const hasVitest = result.some((perm) => perm.includes("vitest"));
66
+
67
+ expect(hasQunit).toBe(true);
68
+ expect(hasVitest).toBe(true);
69
+ });
70
+
54
71
  it("generates valid permutations with mixed items", () => {
55
72
  const result = permutate(["a", "eslint-foo", "eslint-bar", "b"]);
56
73
 
@@ -16,10 +16,22 @@ export const bases = [minimalApp, minimalAddon];
16
16
 
17
17
  export const layers = await discoverLayers();
18
18
 
19
+ /**
20
+ * Layers in the same group are alternatives to each other (one eslint
21
+ * config, one test framework): selecting two of them would produce a
22
+ * broken project, so permutations never combine them.
23
+ */
24
+ function exclusivityGroup(name: string): string | undefined {
25
+ if (name.startsWith("eslint-")) return "eslint";
26
+ if (name === "qunit" || name === "vitest") return "testing";
27
+
28
+ return undefined;
29
+ }
30
+
19
31
  export function permutate(toPermutate: string[]): string[][] {
20
32
  const out: string[][] = [];
21
33
 
22
- function backtrack(startIndex: number, prefix: string[], hasEslint: boolean) {
34
+ function backtrack(startIndex: number, prefix: string[], usedGroups: Set<string>) {
23
35
  if (prefix.length > 0) out.push(prefix.slice());
24
36
 
25
37
  for (let i = startIndex; i < toPermutate.length; i++) {
@@ -34,19 +46,20 @@ export function permutate(toPermutate: string[]): string[][] {
34
46
  // (silly TS not understanding loop bounds)
35
47
  if (!option) continue;
36
48
 
37
- const isEslint = option.startsWith("eslint-");
49
+ const group = exclusivityGroup(option);
38
50
 
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;
51
+ // Skip this option if we already have one from its exclusivity group
52
+ if (group && usedGroups.has(group)) continue;
42
53
 
54
+ if (group) usedGroups.add(group);
43
55
  prefix.push(option);
44
- backtrack(i + 1, prefix, hasEslint || isEslint);
56
+ backtrack(i + 1, prefix, usedGroups);
45
57
  prefix.pop();
58
+ if (group) usedGroups.delete(group);
46
59
  }
47
60
  }
48
61
 
49
- backtrack(0, [], false);
62
+ backtrack(0, [], new Set());
50
63
  return out;
51
64
  }
52
65
 
package/tsconfig.json CHANGED
@@ -10,6 +10,7 @@
10
10
  "dist",
11
11
  "packages",
12
12
  "src/**/layers/*/files/**",
13
+ "src/**/layers/*/library-files/**",
13
14
  "src/**/bases/*/files/**",
14
15
  "my-app"
15
16
  ]
@@ -1,219 +0,0 @@
1
- /**
2
- * Reasons we have to have babel
3
- * - decorators
4
- * - ember-concurrency
5
- * - scoped-css
6
- * - template compilation optimization
7
- */
8
- import { extensions } from "@embroider/vite";
9
- import { babel } from "@rollup/plugin-babel";
10
- import { join, resolve } from "node:path";
11
- import { parse as oxcParse } from "oxc-parser";
12
- import { transform } from "oxc-transform";
13
- import { walk } from "zimmerframe";
14
-
15
- /**
16
- * if a file imports any of these, it must be optimize
17
- */
18
- const babelTemplateImports = new Set([
19
- // Templates
20
- "@ember/template-compiler",
21
- "@ember/template-compilation",
22
- "ember-cli-htmlbars",
23
- "ember-cli-htmlbars-inline-precompile",
24
- "htmlbars-inline-precompile",
25
- ]);
26
-
27
- const babelMacroImports = new Set([
28
- // Macros
29
- "@embroider/macros",
30
- "@glimmer/env",
31
- "@ember/application/deprecations",
32
- // Macros only needed in prod
33
- "@ember/debug",
34
- ]); // Babel plugins required from libraries
35
- const babelOtherImports = new Set([
36
- "ember-concurrency",
37
- "ember-scoped-css",
38
- "ember-intl/helpers/format-message",
39
- ]);
40
-
41
- const babelRequiredImports = [...babelTemplateImports, ...babelMacroImports, ...babelOtherImports];
42
-
43
- const counts = {
44
- total: 0,
45
- babel: 0,
46
- why: {
47
- decorators: 0,
48
- formatMessage: 0,
49
- imports: {
50
- templates: 0,
51
- macros: 0,
52
- other: 0,
53
- },
54
- initializeRuntimeMacrosConfig: 0,
55
- },
56
- };
57
- export function maybeBabel(options: { parallel?: boolean; configFile: string; env: any }) {
58
- const { env, parallel, ...restOptions } = options;
59
- const original = (() => {
60
- const babelPath = resolve(join(process.cwd(), "./babel.config.js"));
61
-
62
- return babel({
63
- babelHelpers: "runtime",
64
- extensions,
65
- skipPreflightCheck: true,
66
- ...restOptions,
67
- configFile: babelPath,
68
- parallel: parallel ?? true,
69
- });
70
- })();
71
-
72
- /**
73
- * In @rollup/plugin-babel v7+, the `transform` hook is the object form
74
- * `{ filter, handler }` rather than a plain function. Normalize to the
75
- * underlying handler so we can invoke it directly.
76
- */
77
- const originalTransform = (
78
- typeof original.transform === "function" ? original.transform : original.transform?.handler
79
- )!;
80
-
81
- const babelMacros = new Set(babelRequiredImports);
82
- if (env.mode === "development") {
83
- babelMacros.delete("@ember/debug");
84
- }
85
-
86
- async function doTransform(this: any, code: string, id: string) {
87
- counts.total++;
88
-
89
- const ext = id.split(".").at(-1);
90
- const lang = (ext === "gjs" ? "js" : ext === "gts" ? "ts" : ext) as
91
- | "js"
92
- | "ts"
93
- | "jsx"
94
- | "tsx"
95
- | "dts"
96
- | undefined;
97
-
98
- let needsBabel = false;
99
-
100
- const estree = await oxcParse(id, code, { lang });
101
-
102
- walk(
103
- estree.program as any,
104
- /* state */ {},
105
- {
106
- Decorator(_node: any, { stop }: any) {
107
- needsBabel = true;
108
- counts.why.decorators++;
109
- stop();
110
- },
111
- MemberExpression(node: any, { stop }: any) {
112
- if (node.property?.name === "formatMessage" && node.object?.name === "intl") {
113
- needsBabel = true;
114
- counts.why.formatMessage++;
115
- stop();
116
- }
117
- },
118
- ImportDeclaration(node: any, { stop }: any) {
119
- const value = node.source.value;
120
- if (babelMacros.has(value)) {
121
- needsBabel = true;
122
-
123
- if (babelTemplateImports.has(value)) {
124
- counts.why.imports.templates++;
125
- } else if (babelMacroImports.has(value)) {
126
- counts.why.imports.macros++;
127
- } else {
128
- counts.why.imports.other++;
129
- }
130
-
131
- stop();
132
- }
133
- },
134
- },
135
- );
136
-
137
- if (!needsBabel && code.includes("initializeRuntimeMacrosConfig")) {
138
- counts.why.initializeRuntimeMacrosConfig++;
139
- needsBabel = true;
140
- }
141
-
142
- if (needsBabel) {
143
- counts.babel++;
144
-
145
- const result = await originalTransform.call(this, code, id);
146
-
147
- return result;
148
- }
149
-
150
- if (ext === "json") {
151
- return;
152
- }
153
-
154
- if (ext === "js" || ext === "gjs") {
155
- // We don't need to process JS
156
- return null;
157
- }
158
-
159
- const result = await transform(id, code, {
160
- lang,
161
- typescript: {
162
- onlyRemoveTypeImports: true,
163
- /**
164
- * We should work towards disabling this.
165
- */
166
- allowNamespaces: true,
167
- removeClassFieldsWithoutInitializer: false,
168
- rewriteImportExtensions: false,
169
- },
170
- });
171
-
172
- if (result.errors?.length) {
173
- console.error(`Errors during oxc-transform of ${id}:`);
174
- for (const err of result.errors) {
175
- if (err.labels) {
176
- console.error(err.labels);
177
- }
178
- console.error(err.codeframe || err.message);
179
- }
180
- throw result.errors;
181
- }
182
-
183
- return result;
184
- }
185
-
186
- return {
187
- enforce: "pre",
188
- buildEnd() {
189
- if (process.env.INSPECT) {
190
- console.debug(`Babel usage: ${counts.babel} / ${counts.total}`, counts.why);
191
- }
192
- },
193
- ...original,
194
- name: "nullvoxpopuli:babel",
195
- transform: {
196
- filter: {
197
- id: {
198
- include: [/\.js/, /\.gjs/, /\.ts/, /\.gts/],
199
- exclude: [/\.json/],
200
- },
201
- // Enabling this opts us out of template compilation
202
- // it's an AND instead of OR
203
- // code: {
204
- // include: [/initializeRuntimeMacrosConfig/, /precompileTemplate/],
205
- // },
206
- },
207
- handler(this: any, code: string, id: string) {
208
- try {
209
- return doTransform.call(this, code, id);
210
- } catch (e) {
211
- // Use VSCode JavaScript Debug Terminal
212
- // eslint-disable-next-line no-debugger
213
- debugger;
214
- throw e;
215
- }
216
- },
217
- },
218
- };
219
- }
@@ -1,30 +0,0 @@
1
- import { chromium } from "playwright";
2
-
3
- const browser = await chromium.launch();
4
- const page = await browser.newPage();
5
-
6
- page.on("console", (msg) => console.log(msg.text()));
7
-
8
- await page.goto("http://localhost:4173/tests");
9
-
10
- await page.waitForFunction(
11
- () => {
12
- const elem = document.getElementById("qunit-testresult");
13
- return elem?.classList.contains("complete");
14
- },
15
- { timeout: 30000 },
16
- );
17
-
18
- const failed = await page.evaluate(() => {
19
- const elem = document.getElementById("qunit-testresult");
20
- return elem?.querySelector(".failed")?.textContent;
21
- });
22
-
23
- await browser.close();
24
-
25
- if (failed && parseInt(failed) > 0) {
26
- console.error(`❌ ${failed} test(s) failed`);
27
- process.exit(1);
28
- }
29
-
30
- console.log("✅ All tests passed!");
@@ -1,5 +0,0 @@
1
- import Application from "#app/app.ts";
2
- import config from "#app/config.ts";
3
- import { setApplication } from "@ember/test-helpers";
4
-
5
- setApplication(Application.create(config));