Repository: ember-cli/ember-cli (js)

Issue:
Creating a new app or addon with the `--typescript` flag overrules the use of the `--skip-npm` flag
I.e. all node modules are installed regardless of if the user passed in the `--skip-npm` flag.
Probably because we use `addAddonToProject` to add the `ember-cli-typescript` addon to the generated app or addon.

Pull request:
Refactor `--typescript` support in blueprints
This is refactoring the `--typescript` support in app and addon blueprints according to the RFC update of https://github.com/emberjs/rfcs/pull/902. More specifically it will
* not use `ember-cli-typescript` for the scaffolding - which fixes #10045 and helps with a bunch of issues where the blueprint is used downstream: v2 addon blueprint, migration tools etc.
* not use `ember-cli-typescript` in the actual app/addon
* ~introduce Glint~
  actually I reverted my change to not do this (yet), because I had [some concerns about it](https://github.com/emberjs/rfcs/pull/902#discussion_r1213469949). We do add some Glint setup to make the (dummy) app what I was calling "Glint-ready" in the sense that it is able to consume Glint-enabled addons without running into type errors, as discussed [here on Discord](https://discord.com/channels/480462759797063690/484421406659182603/1115926217149915206), but it is not enabling full Glint type checking for the app itself. Omitting this potentially controversial choice felt like increasing the chances of getting this merged faster. If we still think full Glint-enablement should be part of the [revised RFC0800](https://github.com/emberjs/rfcs/pull/902), then we can add that later as a separate PR easily. 
* replace the `prepack`/`postpack` scripts for addon publishing from the commands provided by `ember-cli-typescript` to simple commands referring to `tsc` (`rimraf`) directly. For this I had to introduce a separate `tsconfig.declarations.json` (that replicates the [previous `tsc` options](https://github.com/typed-ember/ember-cli-typescript/blob/master/ts/lib/commands/precompile.ts#L29-L36)), as we need to change `includes` to not produce type declarations for tests, and there is no `--includes` we could pass to the `tsc` CLI. Also the declarations are now written to a separate `declarations` folder (`dist` is already taken), and mapped to by `typesVersions`. This was needed so we can easily clean up those files as part of `postpack` (actually, we could also just not clean up, and just gitignore the folder!? 🤔 ). Previously the `.d.ts` were written into the root (so `typesVersions` was not needed), but there was some special housekeeping in place to selectively remove those files again as part of the e-c-ts commands, which we cannot replicate easily without introducing custom commands again...

To Do:
- [x] figure out how to do the `prepack`/`postpack` scripts for addons _without_ e-c-ts
- [x] finalize discussion and get https://github.com/emberjs/rfcs/pull/902 merged? - are we blocked on this?
- [x] battle-test the produced output (especially TS addons consumed by TS apps)

---
I cannot request reviewers here, so pinging a few (active and former) CLI + TS folks for some of you to review please! 😀 
@kategengler @kellyselden @bertdeblock @ef4 @NullVoxPopuli @mansona @chriskrycho @gitKrystan @jamescdavis @wagenet @dfreeman 

Patch:
diff --git a/blueprints/addon/additional-package.json b/blueprints/addon/additional-package.json
index 9fa5afc1a..10e4e5803 100644
--- a/blueprints/addon/additional-package.json
+++ b/blueprints/addon/additional-package.json
@@ -1,7 +1,5 @@
 {
-  "keywords": [
-    "ember-addon"
-  ],
+  "keywords": ["ember-addon"],
   "scripts": {
     "test:ember-compatibility": "ember try:each"
   },
diff --git a/blueprints/addon/files/ember-cli-build.js b/blueprints/addon/files/ember-cli-build.js
index 366cbe507..6ca7d2794 100644
--- a/blueprints/addon/files/ember-cli-build.js
+++ b/blueprints/addon/files/ember-cli-build.js
@@ -4,7 +4,9 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
 
 module.exports = function (defaults) {
   const app = new EmberAddon(defaults, {
-    // Add options here
+    <% if (typescript) {%>'ember-cli-babel': { enableTypeScriptTransform: true },
+
+    <% } %>// Add options here
   });
 
   /*
diff --git a/blueprints/addon/files/index.js b/blueprints/addon/files/index.js
index 0ca063d42..a64c45f74 100644
--- a/blueprints/addon/files/index.js
+++ b/blueprints/addon/files/index.js
@@ -1,5 +1,9 @@
 'use strict';
 
 module.exports = {
-  name: require('./package').name,
+  name: require('./package').name,<% if (typescript) {%>
+
+  options: {
+    'ember-cli-babel': { enableTypeScriptTransform: true },
+  },<% } %>
 };
diff --git a/blueprints/addon/files/tsconfig.declarations.json b/blueprints/addon/files/tsconfig.declarations.json
new file mode 100644
index 000000000..5a21df72e
--- /dev/null
+++ b/blueprints/addon/files/tsconfig.declarations.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "declarationDir": "declarations",
+    "emitDeclarationOnly": true,
+    "noEmit": false,
+    "rootDir": "."
+  },
+  "include": ["addon", "addon-test-support"]
+}
diff --git a/blueprints/addon/files/tsconfig.json b/blueprints/addon/files/tsconfig.json
new file mode 100644
index 000000000..b46423069
--- /dev/null
+++ b/blueprints/addon/files/tsconfig.json
@@ -0,0 +1,18 @@
+{
+  "extends": "@tsconfig/ember/tsconfig.json",
+  "compilerOptions": {
+    // The combination of `baseUrl` with `paths` allows Ember's classic package
+    // layout, which is not resolvable with the Node resolution algorithm, to
+    // work with TypeScript.
+    "baseUrl": ".",
+    "paths": {
+      "dummy/tests/*": ["tests/*"],
+      "dummy/*": ["tests/dummy/app/*", "app/*"],
+      "<%= addonName %>": ["addon"],
+      "<%= addonName %>/*": ["addon/*"],
+      "<%= addonName %>/test-support": ["addon-test-support"],
+      "<%= addonName %>/test-support/*": ["addon-test-support/*"],
+      "*": ["types/*"]
+    }
+  }
+}
diff --git a/blueprints/addon/index.js b/blueprints/addon/index.js
index 6b36ee288..af51c4d79 100644
--- a/blueprints/addon/index.js
+++ b/blueprints/addon/index.js
@@ -64,6 +64,11 @@ module.exports = {
 
     // 95% of addons don't need ember-data or ember-fetch, make them opt-in instead
     delete contents.devDependencies['ember-data'];
+    delete contents.devDependencies['@types/ember-data'];
+    delete contents.devDependencies['@types/ember-data__adapter'];
+    delete contents.devDependencies['@types/ember-data__model'];
+    delete contents.devDependencies['@types/ember-data__serializer'];
+    delete contents.devDependencies['@types/ember-data__store'];
     delete contents.devDependencies['ember-fetch'];
 
     // Per RFC #811, addons should not have this dependency.
@@ -77,6 +82,22 @@ module.exports = {
     // 100% of addons don't need ember-cli-app-version, make it opt-in instead
     delete contents.devDependencies['ember-cli-app-version'];
 
+    // add scripts to build type declarations for TypeScript addons
+    if (this.options.typescript) {
+      contents.devDependencies.rimraf = '^5.0.1';
+
+      contents.scripts.prepack = 'tsc --project tsconfig.declarations.json';
+      contents.scripts.postpack = 'rimraf declarations';
+
+      contents.typesVersions = {
+        '*': {
+          'test-support': ['declarations/addon-test-support/index.d.ts'],
+          'test-support/*': ['declarations/addon-test-support/*', 'declarations/addon-test-support/*/index.d.ts'],
+          '*': ['declarations/addon/*', 'declarations/addon/*/index.d.ts'],
+        },
+      };
+    }
+
     merge(contents, ADDITIONAL_PACKAGE);
 
     return stringifyAndNormalize(sortPackageJson(contents));
@@ -110,15 +131,6 @@ module.exports = {
     this.ui.writeLine(prependEmoji('✨', `Creating a new Ember addon in ${chalk.yellow(process.cwd())}:`));
   },
 
-  async afterInstall(options) {
-    if (options.typescript) {
-      await this.addAddonToProject({
-        name: 'ember-cli-typescript',
-        blueprintOptions: { ...options, save: true },
-      });
-    }
-  },
-
   locals(options) {
     let entity = { name: 'dummy' };
     let rawName = entity.name;
@@ -172,7 +184,9 @@ module.exports = {
   },
 
   files(options) {
-    let appFiles = this.lookupBlueprint(this.appBlueprintName).files(options);
+    let appFiles = this.lookupBlueprint(this.appBlueprintName)
+      .files(options)
+      .filter((file) => !['types/ember-data/types/registries/model.d.ts'].includes(file));
     let addonFilesPath = this.filesPath(this.options);
     let ignoredCITemplate = this.options.ciProvider !== 'travis' ? '.travis.yml' : '.github';
 
@@ -182,6 +196,10 @@ module.exports = {
       addonFiles = addonFiles.filter((file) => !file.endsWith('.npmrc'));
     }
 
+    if (!options.typescript) {
+      addonFiles = addonFiles.filter((file) => file !== 'tsconfig.json' && !file.endsWith('.d.ts'));
+    }
+
     return uniq(appFiles.concat(addonFiles));
   },
 
diff --git a/blueprints/app/files/app/config/environment.d.ts b/blueprints/app/files/app/config/environment.d.ts
new file mode 100644
index 000000000..0c5780482
--- /dev/null
+++ b/blueprints/app/files/app/config/environment.d.ts
@@ -0,0 +1,14 @@
+/**
+ * Type declarations for
+ *    import config from '<%= name %>/config/environment'
+ */
+declare const config: {
+  environment: string;
+  modulePrefix: string;
+  podModulePrefix: string;
+  locationType: 'history' | 'hash' | 'none' | 'auto';
+  rootURL: string;
+  APP: Record<string, unknown>;
+};
+
+export default config;
diff --git a/blueprints/app/files/ember-cli-build.js b/blueprints/app/files/ember-cli-build.js
index 64c714e88..c69fbbce6 100644
--- a/blueprints/app/files/ember-cli-build.js
+++ b/blueprints/app/files/ember-cli-build.js
@@ -4,7 +4,9 @@ const EmberApp = require('ember-cli/lib/broccoli/ember-app');
 
 module.exports = function (defaults) {
   const app = new EmberApp(defaults, {
-    // Add options here
+    <% if (typescript) {%>'ember-cli-babel': { enableTypeScriptTransform: true },
+
+    <% } %>// Add options here
   });
 
   <% if (embroider) { %>const { Webpack } = require('@embroider/webpack');
diff --git a/blueprints/app/files/gitignore b/blueprints/app/files/gitignore
index 2f6fd0493..71ad79d02 100644
--- a/blueprints/app/files/gitignore
+++ b/blueprints/app/files/gitignore
@@ -1,5 +1,6 @@
 # compiled output
 /dist/
+/declarations/
 
 # dependencies
 /node_modules/
diff --git a/blueprints/app/files/package.json b/blueprints/app/files/package.json
index a817e9684..3c863bffb 100644
--- a/blueprints/app/files/package.json
+++ b/blueprints/app/files/package.json
@@ -36,6 +36,37 @@
     "@embroider/webpack": "^3.0.0<% } %>",
     "@glimmer/component": "^1.1.2",
     "@glimmer/tracking": "^1.1.2<% if (typescript) { %>",
+    "@glint/environment-ember-loose": "^1.0.2",
+    "@glint/template": "^1.0.2",
+    "@tsconfig/ember": "^2.0.0",
+    "@types/ember": "^4.0.3",
+    "@types/ember-data": "^4.4.10",
+    "@types/ember-data__adapter": "^4.0.1",
+    "@types/ember-data__model": "^4.0.0",
+    "@types/ember-data__serializer": "^4.0.1",
+    "@types/ember-data__store": "^4.0.2",
+    "@types/ember__application": "^4.0.5",
+    "@types/ember__array": "^4.0.3",
+    "@types/ember__component": "^4.0.13",
+    "@types/ember__controller": "^4.0.4",
+    "@types/ember__debug": "^4.0.3",
+    "@types/ember__destroyable": "^4.0.1",
+    "@types/ember__engine": "^4.0.4",
+    "@types/ember__error": "^4.0.2",
+    "@types/ember__helper": "^4.0.1",
+    "@types/ember__modifier": "^4.0.3",
+    "@types/ember__object": "^4.0.5",
+    "@types/ember__owner": "^4.0.3",
+    "@types/ember__polyfills": "^4.0.1",
+    "@types/ember__routing": "^4.0.12",
+    "@types/ember__runloop": "^4.0.2",
+    "@types/ember__service": "^4.0.2",
+    "@types/ember__string": "^3.0.10",
+    "@types/ember__template": "^4.0.1",
+    "@types/ember__test": "^4.0.1",
+    "@types/ember__utils": "^4.0.2",
+    "@types/qunit": "^2.19.5",
+    "@types/rsvp": "^4.0.4",
     "@typescript-eslint/eslint-plugin": "^5.60.0",
     "@typescript-eslint/parser": "^5.60.0<% } %>",
     "broccoli-asset-rev": "^3.0.0",
@@ -73,7 +104,8 @@
     "stylelint": "^15.8.0",
     "stylelint-config-standard": "^33.0.0",
     "stylelint-prettier": "^3.0.0",
-    "tracked-built-ins": "^3.1.1",
+    "tracked-built-ins": "^3.1.1<% if (typescript) { %>",
+    "typescript": "^5.0.4<% } %>",
     "webpack": "^5.87.0"
   },
   "engines": {
diff --git a/blueprints/app/files/tsconfig.json b/blueprints/app/files/tsconfig.json
new file mode 100644
index 000000000..f53dc6e61
--- /dev/null
+++ b/blueprints/app/files/tsconfig.json
@@ -0,0 +1,14 @@
+{
+  "extends": "@tsconfig/ember/tsconfig.json",
+  "compilerOptions": {
+    // The combination of `baseUrl` with `paths` allows Ember's classic package
+    // layout, which is not resolvable with the Node resolution algorithm, to
+    // work with TypeScript.
+    "baseUrl": ".",
+    "paths": {
+      "<%= name %>/tests/*": ["tests/*"],
+      "<%= name %>/*": ["app/*"],
+      "*": ["types/*"]
+    }
+  }
+}
diff --git a/blueprints/app/files/types/ember-data/types/registries/model.d.ts b/blueprints/app/files/types/ember-data/types/registries/model.d.ts
new file mode 100644
index 000000000..e7a68fcd0
--- /dev/null
+++ b/blueprints/app/files/types/ember-data/types/registries/model.d.ts
@@ -0,0 +1,6 @@
+/**
+ * Catch-all for ember-data.
+ */
+export default interface ModelRegistry {
+  [key: string]: any;
+}
diff --git a/blueprints/app/files/types/global.d.ts b/blueprints/app/files/types/global.d.ts
new file mode 100644
index 000000000..2c531e29a
--- /dev/null
+++ b/blueprints/app/files/types/global.d.ts
@@ -0,0 +1,1 @@
+import '@glint/environment-ember-loose';
diff --git a/blueprints/app/index.js b/blueprints/app/index.js
index ed281287b..59117f1d5 100644
--- a/blueprints/app/index.js
+++ b/blueprints/app/index.js
@@ -72,11 +72,19 @@ module.exports = {
 
     let files = this._super();
     if (options.ciProvider !== 'travis') {
-      this._files = files.filter((file) => file !== '.travis.yml');
+      files = files.filter((file) => file !== '.travis.yml');
     } else {
-      this._files = files.filter((file) => file.indexOf('.github') < 0);
+      files = files.filter((file) => file.indexOf('.github') < 0);
     }
 
+    if (!options.typescript) {
+      files = files.filter(
+        (file) => !['tsconfig.json', 'app/config/', 'types/'].includes(file) && !file.endsWith('.d.ts')
+      );
+    }
+
+    this._files = files;
+
     return this._files;
   },
 
@@ -88,10 +96,4 @@ module.exports = {
     this.ui.writeLine('');
     this.ui.writeLine(prependEmoji('✨', `Creating a new Ember app in ${chalk.yellow(process.cwd())}:`));
   },
-
-  async afterInstall(options) {
-    if (options.typescript) {
-      await this.addAddonToProject({ name: 'ember-cli-typescript', blueprintOptions: options });
-    }
-  },
 };


Test patch:
diff --git a/tests/acceptance/init-test.js b/tests/acceptance/init-test.js
index 26d81e173..5b8a1ace7 100644
--- a/tests/acceptance/init-test.js
+++ b/tests/acceptance/init-test.js
@@ -47,8 +47,10 @@ describe('Acceptance: ember init', function () {
 
   function confirmBlueprinted(typescript = false) {
     let blueprintPath = path.join(root, 'blueprints', 'app', 'files');
-    // ignore .travis.yml
-    let expected = walkSync(blueprintPath, { ignore: ['.travis.yml'] })
+    // ignore .travis.yml and TypeScript files
+    let expected = walkSync(blueprintPath, {
+      ignore: ['.travis.yml', 'tsconfig.json', 'types', 'app/config'],
+    })
       .map((name) => (typescript ? name : name.replace(/\.ts$/, '.js')))
       .sort();
     let actual = walkSync('.').sort();
diff --git a/tests/acceptance/new-test.js b/tests/acceptance/new-test.js
index fb8448087..3bcf0b971 100644
--- a/tests/acceptance/new-test.js
+++ b/tests/acceptance/new-test.js
@@ -41,10 +41,10 @@ describe('Acceptance: ember new', function () {
 
   function confirmBlueprintedForDir(blueprintDir, expectedAppDir = 'foo', typescript = false) {
     let blueprintPath = path.join(root, blueprintDir, 'files');
-    // ignore .travis.yml
-    let expected = walkSync(blueprintPath, { ignore: ['.travis.yml'] }).map((name) =>
-      typescript ? name : name.replace(/\.ts$/, '.js')
-    );
+    // ignore .travis.yml and TypeScript files
+    let expected = walkSync(blueprintPath, {
+      ignore: ['.travis.yml', 'tsconfig.json', 'types', 'app/config'],
+    }).map((name) => (typescript ? name : name.replace(/\.ts$/, '.js')));
 
     let actual = walkSync('.').sort();
     let directory = path.basename(process.cwd());
@@ -469,6 +469,17 @@ describe('Acceptance: ember new', function () {
 
       // ember new without --lang flag (default) has no lang attribute in index.html
       expect(file('app/index.html')).to.contain('<html>');
+
+      // no TypeScript files
+      [
+        'tsconfig.json',
+        'app/config/environment.d.ts',
+        'types/global.d.ts',
+        'types/foo/index.d.ts',
+        'types/ember-data/types/registries/model.d.ts',
+      ].forEach((filePath) => {
+        expect(file(filePath)).to.not.exist;
+      });
     });
 
     it('addon defaults', async function () {
@@ -498,6 +509,11 @@ describe('Acceptance: ember new', function () {
 
       // ember addon without --lang flag (default) has no lang attribute in dummy index.html
       expect(file('tests/dummy/app/index.html')).to.contain('<html>');
+
+      // no TypeScript files
+      ['tsconfig.json', 'tests/dummy/app/config/environment.d.ts', 'types/global.d.ts'].forEach((filePath) => {
+        expect(file(filePath)).to.not.exist;
+      });
     });
 
     it('app + npm + !welcome', async function () {
@@ -651,10 +667,6 @@ describe('Acceptance: ember new', function () {
     });
 
     it('app + typescript', async function () {
-      // This is a very slow test, as the blueprint installs ember-cli-typescript, which requires installing all dependencies,
-      // regardless of --skip-npm
-      this.timeout(600000);
-
       // we have to use yarn here, as npm fails on unresolvable peer dependencies, see https://github.com/emberjs/ember-test-helpers/issues/1236
       await ember(['new', 'foo', '--typescript', '--skip-npm', '--skip-git', '--yarn']);
 
@@ -666,47 +678,47 @@ describe('Acceptance: ember new', function () {
       }
 
       // check fixtures
-      ['.ember-cli', 'tests/helpers/index.ts'].forEach((filePath) => {
+      [
+        '.ember-cli',
+        'tests/helpers/index.ts',
+        'tsconfig.json',
+        'app/config/environment.d.ts',
+        'types/global.d.ts',
+        'types/ember-data/types/registries/model.d.ts',
+      ].forEach((filePath) => {
         checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath));
       });
       checkFileWithEmberCLIVersionReplacement(fixturePath, 'config/ember-cli-update.json');
+      checkFileWithEmberCLIVersionReplacement(fixturePath, 'package.json');
+      checkEmberCLIBuild(fixturePath, 'ember-cli-build.js');
       checkEslintConfig(fixturePath);
 
-      // smoke test for the existence of essential TypeScript features...
-      // we are deliberately *not* comparing the package.json against a fixture here, as we have delegated essential
-      // TS setup to ember-cli-typescript's own blueprint. Instead, we are relying on its own test coverage, otherwise
-      // we would get very brittle tests
-      let pkgJson = fs.readJsonSync('package.json');
-      expect(pkgJson.scripts['lint:types']).to.equal('tsc --noEmit');
-      expect(pkgJson.devDependencies['ember-cli-typescript']).to.exist;
-      expect(pkgJson.devDependencies['typescript']).to.exist;
-      expect(Object.keys(pkgJson.devDependencies).some((pkgName) => pkgName.match(/^@types/))).to.be.true;
-
-      expect(file('tsconfig.json')).to.exist;
+      expect(file('tsconfig.declarations.json')).to.not.exist;
     });
 
     it('addon + typescript', async function () {
-      this.timeout(600000);
-
       await ember(['addon', 'foo', '--typescript', '--skip-npm', '--skip-git', '--yarn']);
 
       let fixturePath = 'addon/typescript';
 
       // check fixtures
-      ['.ember-cli'].forEach((filePath) => {
+      [
+        '.ember-cli',
+        'index.js',
+        'tests/helpers/index.ts',
+        'tsconfig.json',
+        'tsconfig.declarations.json',
+        'tests/dummy/app/config/environment.d.ts',
+        'types/global.d.ts',
+      ].forEach((filePath) => {
         checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath));
       });
       checkFileWithEmberCLIVersionReplacement(fixturePath, 'tests/dummy/config/ember-cli-update.json');
+      checkFileWithEmberCLIVersionReplacement(fixturePath, 'package.json');
+      checkEmberCLIBuild(fixturePath, 'ember-cli-build.js');
       checkEslintConfig(fixturePath);
 
-      // smoke test for the existence of essential TypeScript features... (see app test)
-      let pkgJson = fs.readJsonSync('package.json');
-      expect(pkgJson.scripts['lint:types']).to.equal('tsc --noEmit');
-      expect(pkgJson.dependencies['ember-cli-typescript']).to.exist;
-      expect(pkgJson.devDependencies['typescript']).to.exist;
-      expect(Object.keys(pkgJson.devDependencies).some((pkgName) => pkgName.match(/^@types/))).to.be.true;
-
-      expect(file('tsconfig.json')).to.exist;
+      expect(file('types/ember-data/types/registries/model.d.ts')).to.not.exist;
     });
   });
 
diff --git a/tests/fixtures/addon/typescript/ember-cli-build.js b/tests/fixtures/addon/typescript/ember-cli-build.js
new file mode 100644
index 000000000..d522ae640
--- /dev/null
+++ b/tests/fixtures/addon/typescript/ember-cli-build.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
+
+module.exports = function (defaults) {
+  const app = new EmberAddon(defaults, {
+    'ember-cli-babel': { enableTypeScriptTransform: true },
+
+    // Add options here
+  });
+
+  /*
+    This build file specifies the options for the dummy test app of this
+    addon, located in `/tests/dummy`
+    This build file does *not* influence how the addon or the app using it
+    behave. You most likely want to be modifying `./index.js` or app's build file
+  */
+
+  const { maybeEmbroider } = require('@embroider/test-setup');
+  return maybeEmbroider(app, {
+    skipBabel: [
+      {
+        package: 'qunit',
+      },
+    ],
+  });
+};
diff --git a/tests/fixtures/addon/typescript/index.js b/tests/fixtures/addon/typescript/index.js
new file mode 100644
index 000000000..602e58b5e
--- /dev/null
+++ b/tests/fixtures/addon/typescript/index.js
@@ -0,0 +1,9 @@
+'use strict';
+
+module.exports = {
+  name: require('./package').name,
+
+  options: {
+    'ember-cli-babel': { enableTypeScriptTransform: true },
+  },
+};
diff --git a/tests/fixtures/addon/typescript/package.json b/tests/fixtures/addon/typescript/package.json
new file mode 100644
index 000000000..e48afeff6
--- /dev/null
+++ b/tests/fixtures/addon/typescript/package.json
@@ -0,0 +1,133 @@
+{
+  "name": "foo",
+  "version": "0.0.0",
+  "description": "The default blueprint for ember-cli addons.",
+  "keywords": [
+    "ember-addon"
+  ],
+  "repository": "",
+  "license": "MIT",
+  "author": "",
+  "typesVersions": {
+    "*": {
+      "test-support": [
+        "declarations/addon-test-support/index.d.ts"
+      ],
+      "test-support/*": [
+        "declarations/addon-test-support/*",
+        "declarations/addon-test-support/*/index.d.ts"
+      ],
+      "*": [
+        "declarations/addon/*",
+        "declarations/addon/*/index.d.ts"
+      ]
+    }
+  },
+  "directories": {
+    "doc": "doc",
+    "test": "tests"
+  },
+  "scripts": {
+    "build": "ember build --environment=production",
+    "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
+    "lint:css": "stylelint \"**/*.css\"",
+    "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
+    "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
+    "lint:hbs": "ember-template-lint .",
+    "lint:hbs:fix": "ember-template-lint . --fix",
+    "lint:js": "eslint . --cache",
+    "lint:js:fix": "eslint . --fix",
+    "lint:types": "tsc --noEmit",
+    "prepack": "tsc --project tsconfig.declarations.json",
+    "postpack": "rimraf declarations",
+    "start": "ember serve",
+    "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
+    "test:ember": "ember test",
+    "test:ember-compatibility": "ember try:each"
+  },
+  "dependencies": {
+    "ember-cli-babel": "^7.26.11",
+    "ember-cli-htmlbars": "^6.2.0"
+  },
+  "devDependencies": {
+    "@ember/optional-features": "^2.0.0",
+    "@ember/string": "^3.1.1",
+    "@ember/test-helpers": "^3.1.0",
+    "@embroider/test-setup": "^3.0.1",
+    "@glimmer/component": "^1.1.2",
+    "@glimmer/tracking": "^1.1.2",
+    "@glint/environment-ember-loose": "^1.0.2",
+    "@glint/template": "^1.0.2",
+    "@tsconfig/ember": "^2.0.0",
+    "@types/ember": "^4.0.3",
+    "@types/ember__application": "^4.0.5",
+    "@types/ember__array": "^4.0.3",
+    "@types/ember__component": "^4.0.13",
+    "@types/ember__controller": "^4.0.4",
+    "@types/ember__debug": "^4.0.3",
+    "@types/ember__destroyable": "^4.0.1",
+    "@types/ember__engine": "^4.0.4",
+    "@types/ember__error": "^4.0.2",
+    "@types/ember__helper": "^4.0.1",
+    "@types/ember__modifier": "^4.0.3",
+    "@types/ember__object": "^4.0.5",
+    "@types/ember__owner": "^4.0.3",
+    "@types/ember__polyfills": "^4.0.1",
+    "@types/ember__routing": "^4.0.12",
+    "@types/ember__runloop": "^4.0.2",
+    "@types/ember__service": "^4.0.2",
+    "@types/ember__string": "^3.0.10",
+    "@types/ember__template": "^4.0.1",
+    "@types/ember__test": "^4.0.1",
+    "@types/ember__utils": "^4.0.2",
+    "@types/qunit": "^2.19.5",
+    "@types/rsvp": "^4.0.4",
+    "@typescript-eslint/eslint-plugin": "^5.60.0",
+    "@typescript-eslint/parser": "^5.60.0",
+    "broccoli-asset-rev": "^3.0.0",
+    "concurrently": "^8.2.0",
+    "ember-auto-import": "^2.6.3",
+    "ember-cli": "~<%= emberCLIVersion %>",
+    "ember-cli-clean-css": "^2.0.0",
+    "ember-cli-dependency-checker": "^3.3.2",
+    "ember-cli-inject-live-reload": "^2.1.0",
+    "ember-cli-sri": "^2.1.1",
+    "ember-cli-terser": "^4.0.2",
+    "ember-load-initializers": "^2.1.2",
+    "ember-page-title": "^7.0.0",
+    "ember-qunit": "^7.0.0",
+    "ember-resolver": "^10.1.1",
+    "ember-source": "~5.1.0-beta.1",
+    "ember-source-channel-url": "^3.0.0",
+    "ember-template-lint": "^5.10.3",
+    "ember-try": "^2.0.0",
+    "eslint": "^8.43.0",
+    "eslint-config-prettier": "^8.8.0",
+    "eslint-plugin-ember": "^11.8.0",
+    "eslint-plugin-n": "^16.0.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-qunit": "^7.3.4",
+    "loader.js": "^4.7.0",
+    "prettier": "^2.8.8",
+    "qunit": "^2.19.4",
+    "qunit-dom": "^2.0.0",
+    "rimraf": "^5.0.1",
+    "stylelint": "^15.8.0",
+    "stylelint-config-standard": "^33.0.0",
+    "stylelint-prettier": "^3.0.0",
+    "typescript": "^5.0.4",
+    "webpack": "^5.87.0"
+  },
+  "peerDependencies": {
+    "ember-source": ">= 4.0.0"
+  },
+  "engines": {
+    "node": "16.* || >= 18"
+  },
+  "ember": {
+    "edition": "octane"
+  },
+  "ember-addon": {
+    "configPath": "tests/dummy/config"
+  }
+}
diff --git a/tests/fixtures/addon/typescript/tests/dummy/app/config/environment.d.ts b/tests/fixtures/addon/typescript/tests/dummy/app/config/environment.d.ts
new file mode 100644
index 000000000..1725b438f
--- /dev/null
+++ b/tests/fixtures/addon/typescript/tests/dummy/app/config/environment.d.ts
@@ -0,0 +1,14 @@
+/**
+ * Type declarations for
+ *    import config from 'dummy/config/environment'
+ */
+declare const config: {
+  environment: string;
+  modulePrefix: string;
+  podModulePrefix: string;
+  locationType: 'history' | 'hash' | 'none' | 'auto';
+  rootURL: string;
+  APP: Record<string, unknown>;
+};
+
+export default config;
diff --git a/tests/fixtures/addon/typescript/tests/helpers/index.ts b/tests/fixtures/addon/typescript/tests/helpers/index.ts
new file mode 100644
index 000000000..670bc6435
--- /dev/null
+++ b/tests/fixtures/addon/typescript/tests/helpers/index.ts
@@ -0,0 +1,43 @@
+import {
+  setupApplicationTest as upstreamSetupApplicationTest,
+  setupRenderingTest as upstreamSetupRenderingTest,
+  setupTest as upstreamSetupTest,
+  SetupTestOptions,
+} from 'ember-qunit';
+
+// This file exists to provide wrappers around ember-qunit's / ember-mocha's
+// test setup functions. This way, you can easily extend the setup that is
+// needed per test type.
+
+function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) {
+  upstreamSetupApplicationTest(hooks, options);
+
+  // Additional setup for application tests can be done here.
+  //
+  // For example, if you need an authenticated session for each
+  // application test, you could do:
+  //
+  // hooks.beforeEach(async function () {
+  //   await authenticateSession(); // ember-simple-auth
+  // });
+  //
+  // This is also a good place to call test setup functions coming
+  // from other addons:
+  //
+  // setupIntl(hooks); // ember-intl
+  // setupMirage(hooks); // ember-cli-mirage
+}
+
+function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) {
+  upstreamSetupRenderingTest(hooks, options);
+
+  // Additional setup for rendering tests can be done here.
+}
+
+function setupTest(hooks: NestedHooks, options?: SetupTestOptions) {
+  upstreamSetupTest(hooks, options);
+
+  // Additional setup for unit tests can be done here.
+}
+
+export { setupApplicationTest, setupRenderingTest, setupTest };
diff --git a/tests/fixtures/addon/typescript/tsconfig.declarations.json b/tests/fixtures/addon/typescript/tsconfig.declarations.json
new file mode 100644
index 000000000..5a21df72e
--- /dev/null
+++ b/tests/fixtures/addon/typescript/tsconfig.declarations.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "declarationDir": "declarations",
+    "emitDeclarationOnly": true,
+    "noEmit": false,
+    "rootDir": "."
+  },
+  "include": ["addon", "addon-test-support"]
+}
diff --git a/tests/fixtures/addon/typescript/tsconfig.json b/tests/fixtures/addon/typescript/tsconfig.json
new file mode 100644
index 000000000..a292f6acc
--- /dev/null
+++ b/tests/fixtures/addon/typescript/tsconfig.json
@@ -0,0 +1,18 @@
+{
+  "extends": "@tsconfig/ember/tsconfig.json",
+  "compilerOptions": {
+    // The combination of `baseUrl` with `paths` allows Ember's classic package
+    // layout, which is not resolvable with the Node resolution algorithm, to
+    // work with TypeScript.
+    "baseUrl": ".",
+    "paths": {
+      "dummy/tests/*": ["tests/*"],
+      "dummy/*": ["tests/dummy/app/*", "app/*"],
+      "foo": ["addon"],
+      "foo/*": ["addon/*"],
+      "foo/test-support": ["addon-test-support"],
+      "foo/test-support/*": ["addon-test-support/*"],
+      "*": ["types/*"]
+    }
+  }
+}
diff --git a/tests/fixtures/addon/typescript/types/global.d.ts b/tests/fixtures/addon/typescript/types/global.d.ts
new file mode 100644
index 000000000..2c531e29a
--- /dev/null
+++ b/tests/fixtures/addon/typescript/types/global.d.ts
@@ -0,0 +1,1 @@
+import '@glint/environment-ember-loose';
diff --git a/tests/fixtures/app/typescript-embroider/app/config/environment.d.ts b/tests/fixtures/app/typescript-embroider/app/config/environment.d.ts
new file mode 100644
index 000000000..f0cad8b31
--- /dev/null
+++ b/tests/fixtures/app/typescript-embroider/app/config/environment.d.ts
@@ -0,0 +1,14 @@
+/**
+ * Type declarations for
+ *    import config from 'foo/config/environment'
+ */
+declare const config: {
+  environment: string;
+  modulePrefix: string;
+  podModulePrefix: string;
+  locationType: 'history' | 'hash' | 'none' | 'auto';
+  rootURL: string;
+  APP: Record<string, unknown>;
+};
+
+export default config;
diff --git a/tests/fixtures/app/typescript-embroider/ember-cli-build.js b/tests/fixtures/app/typescript-embroider/ember-cli-build.js
new file mode 100644
index 000000000..ca9e8b2df
--- /dev/null
+++ b/tests/fixtures/app/typescript-embroider/ember-cli-build.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+
+module.exports = function (defaults) {
+  const app = new EmberApp(defaults, {
+    'ember-cli-babel': { enableTypeScriptTransform: true },
+
+    // Add options here
+  });
+
+  const { Webpack } = require('@embroider/webpack');
+  return require('@embroider/compat').compatBuild(app, Webpack, {
+    skipBabel: [
+      {
+        package: 'qunit',
+      },
+    ],
+  });
+};
diff --git a/tests/fixtures/app/typescript-embroider/package.json b/tests/fixtures/app/typescript-embroider/package.json
new file mode 100644
index 000000000..2ccca4cff
--- /dev/null
+++ b/tests/fixtures/app/typescript-embroider/package.json
@@ -0,0 +1,113 @@
+{
+  "name": "foo",
+  "version": "0.0.0",
+  "private": true,
+  "description": "Small description for foo goes here",
+  "repository": "",
+  "license": "MIT",
+  "author": "",
+  "directories": {
+    "doc": "doc",
+    "test": "tests"
+  },
+  "scripts": {
+    "build": "ember build --environment=production",
+    "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
+    "lint:css": "stylelint \"**/*.css\"",
+    "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
+    "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
+    "lint:hbs": "ember-template-lint .",
+    "lint:hbs:fix": "ember-template-lint . --fix",
+    "lint:js": "eslint . --cache",
+    "lint:js:fix": "eslint . --fix",
+    "lint:types": "tsc --noEmit",
+    "start": "ember serve",
+    "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
+    "test:ember": "ember test"
+  },
+  "devDependencies": {
+    "@ember/optional-features": "^2.0.0",
+    "@ember/string": "^3.1.1",
+    "@ember/test-helpers": "^3.1.0",
+    "@embroider/compat": "^3.0.2",
+    "@embroider/core": "^3.0.2",
+    "@embroider/webpack": "^3.0.0",
+    "@glimmer/component": "^1.1.2",
+    "@glimmer/tracking": "^1.1.2",
+    "@glint/environment-ember-loose": "^1.0.2",
+    "@glint/template": "^1.0.2",
+    "@tsconfig/ember": "^2.0.0",
+    "@types/ember": "^4.0.3",
+    "@types/ember-data": "^4.4.10",
+    "@types/ember-data__adapter": "^4.0.1",
+    "@types/ember-data__model": "^4.0.0",
+    "@types/ember-data__serializer": "^4.0.1",
+    "@types/ember-data__store": "^4.0.2",
+    "@types/ember__application": "^4.0.5",
+    "@types/ember__array": "^4.0.3",
+    "@types/ember__component": "^4.0.13",
+    "@types/ember__controller": "^4.0.4",
+    "@types/ember__debug": "^4.0.3",
+    "@types/ember__destroyable": "^4.0.1",
+    "@types/ember__engine": "^4.0.4",
+    "@types/ember__error": "^4.0.2",
+    "@types/ember__helper": "^4.0.1",
+    "@types/ember__modifier": "^4.0.3",
+    "@types/ember__object": "^4.0.5",
+    "@types/ember__owner": "^4.0.3",
+    "@types/ember__polyfills": "^4.0.1",
+    "@types/ember__routing": "^4.0.12",
+    "@types/ember__runloop": "^4.0.2",
+    "@types/ember__service": "^4.0.2",
+    "@types/ember__string": "^3.0.10",
+    "@types/ember__template": "^4.0.1",
+    "@types/ember__test": "^4.0.1",
+    "@types/ember__utils": "^4.0.2",
+    "@types/qunit": "^2.19.5",
+    "@types/rsvp": "^4.0.4",
+    "@typescript-eslint/eslint-plugin": "^5.60.0",
+    "@typescript-eslint/parser": "^5.60.0",
+    "broccoli-asset-rev": "^3.0.0",
+    "concurrently": "^8.2.0",
+    "ember-auto-import": "^2.6.3",
+    "ember-cli": "~<%= emberCLIVersion %>",
+    "ember-cli-app-version": "^6.0.1",
+    "ember-cli-babel": "^7.26.11",
+    "ember-cli-clean-css": "^2.0.0",
+    "ember-cli-dependency-checker": "^3.3.2",
+    "ember-cli-htmlbars": "^6.2.0",
+    "ember-cli-inject-live-reload": "^2.1.0",
+    "ember-data": "~5.1.0-beta.0",
+    "ember-fetch": "^8.1.2",
+    "ember-load-initializers": "^2.1.2",
+    "ember-modifier": "^4.1.0",
+    "ember-page-title": "^7.0.0",
+    "ember-qunit": "^7.0.0",
+    "ember-resolver": "^10.1.1",
+    "ember-source": "~5.1.0-beta.1",
+    "ember-template-lint": "^5.10.3",
+    "ember-welcome-page": "^7.0.2",
+    "eslint": "^8.43.0",
+    "eslint-config-prettier": "^8.8.0",
+    "eslint-plugin-ember": "^11.8.0",
+    "eslint-plugin-n": "^16.0.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-qunit": "^7.3.4",
+    "loader.js": "^4.7.0",
+    "prettier": "^2.8.8",
+    "qunit": "^2.19.4",
+    "qunit-dom": "^2.0.0",
+    "stylelint": "^15.8.0",
+    "stylelint-config-standard": "^33.0.0",
+    "stylelint-prettier": "^3.0.0",
+    "tracked-built-ins": "^3.1.1",
+    "typescript": "^5.0.4",
+    "webpack": "^5.87.0"
+  },
+  "engines": {
+    "node": "16.* || >= 18"
+  },
+  "ember": {
+    "edition": "octane"
+  }
+}
diff --git a/tests/fixtures/app/typescript-embroider/tsconfig.json b/tests/fixtures/app/typescript-embroider/tsconfig.json
new file mode 100644
index 000000000..1bafd423d
--- /dev/null
+++ b/tests/fixtures/app/typescript-embroider/tsconfig.json
@@ -0,0 +1,14 @@
+{
+  "extends": "@tsconfig/ember/tsconfig.json",
+  "compilerOptions": {
+    // The combination of `baseUrl` with `paths` allows Ember's classic package
+    // layout, which is not resolvable with the Node resolution algorithm, to
+    // work with TypeScript.
+    "baseUrl": ".",
+    "paths": {
+      "foo/tests/*": ["tests/*"],
+      "foo/*": ["app/*"],
+      "*": ["types/*"]
+    }
+  }
+}
diff --git a/tests/fixtures/app/typescript-embroider/types/ember-data/types/registries/model.d.ts b/tests/fixtures/app/typescript-embroider/types/ember-data/types/registries/model.d.ts
new file mode 100644
index 000000000..e7a68fcd0
--- /dev/null
+++ b/tests/fixtures/app/typescript-embroider/types/ember-data/types/registries/model.d.ts
@@ -0,0 +1,6 @@
+/**
+ * Catch-all for ember-data.
+ */
+export default interface ModelRegistry {
+  [key: string]: any;
+}
diff --git a/tests/fixtures/app/typescript-embroider/types/global.d.ts b/tests/fixtures/app/typescript-embroider/types/global.d.ts
new file mode 100644
index 000000000..2c531e29a
--- /dev/null
+++ b/tests/fixtures/app/typescript-embroider/types/global.d.ts
@@ -0,0 +1,1 @@
+import '@glint/environment-ember-loose';
diff --git a/tests/fixtures/app/typescript/app/config/environment.d.ts b/tests/fixtures/app/typescript/app/config/environment.d.ts
new file mode 100644
index 000000000..f0cad8b31
--- /dev/null
+++ b/tests/fixtures/app/typescript/app/config/environment.d.ts
@@ -0,0 +1,14 @@
+/**
+ * Type declarations for
+ *    import config from 'foo/config/environment'
+ */
+declare const config: {
+  environment: string;
+  modulePrefix: string;
+  podModulePrefix: string;
+  locationType: 'history' | 'hash' | 'none' | 'auto';
+  rootURL: string;
+  APP: Record<string, unknown>;
+};
+
+export default config;
diff --git a/tests/fixtures/app/typescript/ember-cli-build.js b/tests/fixtures/app/typescript/ember-cli-build.js
new file mode 100644
index 000000000..12f8dea58
--- /dev/null
+++ b/tests/fixtures/app/typescript/ember-cli-build.js
@@ -0,0 +1,13 @@
+'use strict';
+
+const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+
+module.exports = function (defaults) {
+  const app = new EmberApp(defaults, {
+    'ember-cli-babel': { enableTypeScriptTransform: true },
+
+    // Add options here
+  });
+
+  return app.toTree();
+};
diff --git a/tests/fixtures/app/typescript/package.json b/tests/fixtures/app/typescript/package.json
new file mode 100644
index 000000000..112935289
--- /dev/null
+++ b/tests/fixtures/app/typescript/package.json
@@ -0,0 +1,112 @@
+{
+  "name": "foo",
+  "version": "0.0.0",
+  "private": true,
+  "description": "Small description for foo goes here",
+  "repository": "",
+  "license": "MIT",
+  "author": "",
+  "directories": {
+    "doc": "doc",
+    "test": "tests"
+  },
+  "scripts": {
+    "build": "ember build --environment=production",
+    "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
+    "lint:css": "stylelint \"**/*.css\"",
+    "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
+    "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
+    "lint:hbs": "ember-template-lint .",
+    "lint:hbs:fix": "ember-template-lint . --fix",
+    "lint:js": "eslint . --cache",
+    "lint:js:fix": "eslint . --fix",
+    "lint:types": "tsc --noEmit",
+    "start": "ember serve",
+    "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
+    "test:ember": "ember test"
+  },
+  "devDependencies": {
+    "@ember/optional-features": "^2.0.0",
+    "@ember/string": "^3.1.1",
+    "@ember/test-helpers": "^3.1.0",
+    "@glimmer/component": "^1.1.2",
+    "@glimmer/tracking": "^1.1.2",
+    "@glint/environment-ember-loose": "^1.0.2",
+    "@glint/template": "^1.0.2",
+    "@tsconfig/ember": "^2.0.0",
+    "@types/ember": "^4.0.3",
+    "@types/ember-data": "^4.4.10",
+    "@types/ember-data__adapter": "^4.0.1",
+    "@types/ember-data__model": "^4.0.0",
+    "@types/ember-data__serializer": "^4.0.1",
+    "@types/ember-data__store": "^4.0.2",
+    "@types/ember__application": "^4.0.5",
+    "@types/ember__array": "^4.0.3",
+    "@types/ember__component": "^4.0.13",
+    "@types/ember__controller": "^4.0.4",
+    "@types/ember__debug": "^4.0.3",
+    "@types/ember__destroyable": "^4.0.1",
+    "@types/ember__engine": "^4.0.4",
+    "@types/ember__error": "^4.0.2",
+    "@types/ember__helper": "^4.0.1",
+    "@types/ember__modifier": "^4.0.3",
+    "@types/ember__object": "^4.0.5",
+    "@types/ember__owner": "^4.0.3",
+    "@types/ember__polyfills": "^4.0.1",
+    "@types/ember__routing": "^4.0.12",
+    "@types/ember__runloop": "^4.0.2",
+    "@types/ember__service": "^4.0.2",
+    "@types/ember__string": "^3.0.10",
+    "@types/ember__template": "^4.0.1",
+    "@types/ember__test": "^4.0.1",
+    "@types/ember__utils": "^4.0.2",
+    "@types/qunit": "^2.19.5",
+    "@types/rsvp": "^4.0.4",
+    "@typescript-eslint/eslint-plugin": "^5.60.0",
+    "@typescript-eslint/parser": "^5.60.0",
+    "broccoli-asset-rev": "^3.0.0",
+    "concurrently": "^8.2.0",
+    "ember-auto-import": "^2.6.3",
+    "ember-cli": "~<%= emberCLIVersion %>",
+    "ember-cli-app-version": "^6.0.1",
+    "ember-cli-babel": "^7.26.11",
+    "ember-cli-clean-css": "^2.0.0",
+    "ember-cli-dependency-checker": "^3.3.2",
+    "ember-cli-htmlbars": "^6.2.0",
+    "ember-cli-inject-live-reload": "^2.1.0",
+    "ember-cli-sri": "^2.1.1",
+    "ember-cli-terser": "^4.0.2",
+    "ember-data": "~5.1.0-beta.0",
+    "ember-fetch": "^8.1.2",
+    "ember-load-initializers": "^2.1.2",
+    "ember-modifier": "^4.1.0",
+    "ember-page-title": "^7.0.0",
+    "ember-qunit": "^7.0.0",
+    "ember-resolver": "^10.1.1",
+    "ember-source": "~5.1.0-beta.1",
+    "ember-template-lint": "^5.10.3",
+    "ember-welcome-page": "^7.0.2",
+    "eslint": "^8.43.0",
+    "eslint-config-prettier": "^8.8.0",
+    "eslint-plugin-ember": "^11.8.0",
+    "eslint-plugin-n": "^16.0.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-qunit": "^7.3.4",
+    "loader.js": "^4.7.0",
+    "prettier": "^2.8.8",
+    "qunit": "^2.19.4",
+    "qunit-dom": "^2.0.0",
+    "stylelint": "^15.8.0",
+    "stylelint-config-standard": "^33.0.0",
+    "stylelint-prettier": "^3.0.0",
+    "tracked-built-ins": "^3.1.1",
+    "typescript": "^5.0.4",
+    "webpack": "^5.87.0"
+  },
+  "engines": {
+    "node": "16.* || >= 18"
+  },
+  "ember": {
+    "edition": "octane"
+  }
+}
diff --git a/tests/fixtures/app/typescript/tsconfig.json b/tests/fixtures/app/typescript/tsconfig.json
new file mode 100644
index 000000000..1bafd423d
--- /dev/null
+++ b/tests/fixtures/app/typescript/tsconfig.json
@@ -0,0 +1,14 @@
+{
+  "extends": "@tsconfig/ember/tsconfig.json",
+  "compilerOptions": {
+    // The combination of `baseUrl` with `paths` allows Ember's classic package
+    // layout, which is not resolvable with the Node resolution algorithm, to
+    // work with TypeScript.
+    "baseUrl": ".",
+    "paths": {
+      "foo/tests/*": ["tests/*"],
+      "foo/*": ["app/*"],
+      "*": ["types/*"]
+    }
+  }
+}
diff --git a/tests/fixtures/app/typescript/types/ember-data/types/registries/model.d.ts b/tests/fixtures/app/typescript/types/ember-data/types/registries/model.d.ts
new file mode 100644
index 000000000..e7a68fcd0
--- /dev/null
+++ b/tests/fixtures/app/typescript/types/ember-data/types/registries/model.d.ts
@@ -0,0 +1,6 @@
+/**
+ * Catch-all for ember-data.
+ */
+export default interface ModelRegistry {
+  [key: string]: any;
+}
diff --git a/tests/fixtures/app/typescript/types/global.d.ts b/tests/fixtures/app/typescript/types/global.d.ts
new file mode 100644
index 000000000..2c531e29a
--- /dev/null
+++ b/tests/fixtures/app/typescript/types/global.d.ts
@@ -0,0 +1,1 @@
+import '@glint/environment-ember-loose';