Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ember-apply typescript #505

Merged
merged 5 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-glasses-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ember-apply": minor
---

Add ember.isV2Addon() to the ember family of methods/queries
5 changes: 5 additions & 0 deletions .changeset/pink-melons-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ember-apply/typescript": minor
---

Initial implementation of 'ember-apply typescript'
5 changes: 5 additions & 0 deletions .changeset/stupid-melons-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ember-apply": minor
---

New module, npm. Only one function so far -- pass an array of dependencies and get back an object with the latest version as the value for each dependency as the key
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ where `<feature-name>` is one of the options under [#Features](#features)

## Features

### `typescript`

```shell
npx ember-apply typescript
```

_Automates setting up TypeScript for your V1 Addon or App._
- correct dependencies
- correct types
- correct configs

### `volta`

```shell
Expand Down
37 changes: 37 additions & 0 deletions ember-apply/src/-private/ember/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,40 @@ export async function isAddon(dir) {
return false;
}
}

/**
* @example
* ```js
* import { ember } from 'ember-apply';
*
* await ember.isV2Addon();
* ```
*
* @param {string} [dir] optionally override the directory to read from -- defaults to the current working directory
* @returns {Promise<boolean>} true if the project is an ember project and is a v2 addon
*/
export async function isV2Addon(dir) {
let isEmberAddon = await isAddon(dir);

if (!isEmberAddon) {
return false;
}

try {
let json = await read(dir);

// what was implemented
let emberAddon = json['ember-addon'];
// what the RFC/design said to do
let ember = json['ember'];

return (
emberAddon?.version === 2 ||
ember?.version === 2 ||
emberAddon?.version === '2' ||
ember?.version === '2'
);
} catch {
return false;
}
}
24 changes: 24 additions & 0 deletions ember-apply/src/-private/npm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import latestVersion from 'latest-version';

/**
* For a list of dependencies, return an object of [depName] => version
*
* Uses [`latest-version`](https://www.npmjs.com/package/latest-version)
*
* @param {string[]} deps
* @param {'^' | '~' | '>=' | '' | string} [ range ] default range is ^
*/
export async function getLatest(deps, range = '^') {
/** @type {Record<string, string>} */
let depsWithVersions = {};

await Promise.all(
deps.map(async (dep) => {
let version = await latestVersion(dep);

depsWithVersions[dep] = `${range}${version}`;
}),
);

return depsWithVersions;
}
1 change: 1 addition & 0 deletions ember-apply/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * as ember from './-private/ember/index.js';
export * as files from './-private/files/index.js';
export * as html from './-private/html/index.js';
export * as js from './-private/js/index.js';
export * as npm from './-private/npm/index.js';
export * as packageJson from './-private/package-json/index.js';
export * as project from './-private/project/index.js';
2 changes: 1 addition & 1 deletion packages/ember/embroider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@
"volta": {
"node": "18.19.0"
}
}
}
3 changes: 3 additions & 0 deletions packages/ember/typescript/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declarations/
node_modules/
__snapshots__/
10 changes: 10 additions & 0 deletions packages/ember/typescript/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use strict";

const { configs } = require("@nullvoxpopuli/eslint-configs");

const config = configs.node();

module.exports = {
...config,
overrides: [...config.overrides],
};
3 changes: 3 additions & 0 deletions packages/ember/typescript/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declarations/
node_modules/
__snapshots__/
12 changes: 12 additions & 0 deletions packages/ember/typescript/.prettierrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use strict";

module.exports = {
overrides: [
{
files: "*.{js,ts}",
options: {
singleQuote: true,
},
},
],
};
127 changes: 127 additions & 0 deletions packages/ember/typescript/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// @ts-check
import assert from 'node:assert';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

import { ember, packageJson, project } from 'ember-apply';

import { createConfigDTS } from './src/config-directory.js';
import {
enableTSInAddonIndex,
enableTSInECBuild,
} from './src/ensure-ts-transpilation.js';
import { setupESLint } from './src/eslint.js';
import { printDetectedInfo } from './src/info.js';
import {
adjustAddonScripts,
adjustAppScripts,
configureDependencies,
} from './src/package-json.js';
import { createAddonTSConfig, createAppTSConfig } from './src/tsconfig.js';
import {
createAddonTypesDirectory,
createAppTypesDirectory,
} from './src/types-directory.js';

/**
* Codemod to transferm a JS ember project to a TS ember project.
* - Support
* - V1 App
* - V1 Addon
* - ember-data presence forcing the ejection of built in types
* (ember-data's types are not compatible with the built in types)
*
* - No Support
* - V2 Addon
*
*/
export default async function run() {
let isEmber = await ember.isEmberProject();

assert(
isEmber,
`Project is not an ember project, so typescript will not be applied`,
);

let isAddon = await ember.isAddon();
let isApp = !isAddon;
let isV2Addon = await ember.isV2Addon();

assert(
!isV2Addon,
`Project is a V2 addon and cannot be automatically updated to use TypeScript as V2 addons have too much variation. Confer with the V2 addon blueprint's --typescript output for details on using TypeScript: https://github.com/embroider-build/addon-blueprint/`,
);

assert(
await packageJson.hasDependency('ember-source', '>= 3.24.0'),
`ember-source version must be at least 3.24`,
);

if (isApp) {
return appToTypeScript();
}

return addonToTypeScript();
}

async function appToTypeScript() {
await printDetectedInfo();
await enableTSInECBuild();
await createConfigDTS();
await createAppTSConfig();
await createAppTypesDirectory();
await adjustAppScripts();
await setupESLint();
await configureDependencies();

// Grrr GlimmerVM
await fixPre1dot0GlimmerAndPnpm();

let manager = await project.getPackageManager();

console.info(`
✨✨✨

🥳 Your JavaScript app is now a TypeScript app 🥳

Tasks to manually do
- run '${manager} install'
- make sure '${manager} run lint:types' succeeds
- commit!

✨✨✨
`);
}

async function addonToTypeScript() {
await enableTSInECBuild(); // dummy app
await enableTSInAddonIndex();
await createAddonTSConfig();
await createAddonTypesDirectory();
await adjustAddonScripts();
await setupESLint();
await configureDependencies();
}

/**
* Without this we get a super old copy of manager/validator in the 0.4x range
*/
async function fixPre1dot0GlimmerAndPnpm() {
let manager = await project.getPackageManager();

if (manager !== 'pnpm') return;

let root = await project.workspaceRoot();

await packageJson.modify((json) => {
json.pnpm ||= {};
json.pnpm.overrides ||= {};
json.pnpm.overrides['@glimmer/manager'] ||= '>= 0.84.3';
json.pnpm.overrides['@glimmer/validator'] ||= '>= 0.84.3';
}, root);
}

// @ts-ignore
const __dirname = dirname(fileURLToPath(import.meta.url));

run.path = __dirname;
75 changes: 75 additions & 0 deletions packages/ember/typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@ember-apply/typescript",
"description": "ember-apply plugin for adding typescript to an existing app",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"preset": "index.js",
"author": "NullVoxPopuli",
"repository": {
"url": "https://github.com/NullVoxPopuli/ember-apply",
"type": "https",
"directory": "packages/ember/tailwind"
},
"keywords": [
"ember",
"typescript",
"ember-apply",
"blueprint"
],
"exports": {
"import": "./index.js"
},
"files": [
"index.js",
"src"
],
"scripts": {
"lint:types": "tsc --noEmit",
"build": "tsc --build",
"lint": "pnpm lint:js",
"lint:js": "eslint .",
"lint:fix": "pnpm lint:js --fix",
"test": "vitest --coverage --no-watch",
"test:watch": "vitest --watch"
},
"dependencies": {
"chalk": "^5.3.0",
"common-tags": "^1.8.2",
"ember-apply": "workspace:^",
"execa": "^8.0.1",
"fs-extra": "^11.1.1",
"latest-version": "^7.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.23.3",
"@nullvoxpopuli/eslint-configs": "^3.2.2",
"@types/common-tags": "^1.8.4",
"@types/fs-extra": "^11.0.4",
"@types/jscodeshift": "^0.11.10",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@vitest/coverage-c8": "^0.33.0",
"@vitest/coverage-v8": "^0.34.6",
"ast-types": "^0.14.2",
"autoprefixer": "^10.4.16",
"c8": "^8.0.1",
"eslint": "^8.54.0",
"eslint-plugin-decorator-position": "^5.0.2",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.1",
"jscodeshift": "^0.15.1",
"postcss": "^8.4.31",
"prettier": "^3.1.0",
"typescript": "5.2.2",
"vite": "^5.0.0",
"vitest": "^0.34.6"
},
"engines": {
"node": ">=16.0.0"
},
"volta": {
"node": "18.18.2"
}
}
32 changes: 32 additions & 0 deletions packages/ember/typescript/src/config-directory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fs from 'node:fs/promises';

import { stripIndent } from 'common-tags';
import fse from 'fs-extra';

import { getModuleName } from './utils.js';

export async function createConfigDTS() {
let name = await getModuleName();

await fse.ensureDir('app/config');

await fs.writeFile(
'app/config/environment.d.ts',
stripIndent`
/**
* Type declarations for
* import config from '${name}/config/environment'
*/
declare const config: {
environment: string;
modulePrefix: string;
podModulePrefix: string;
locationType: 'history' | 'hash' | 'none';
rootURL: string;
APP: Record<string, unknown>;
};

export default config;
`,
);
}
Empty file.
Loading