>;
+```
+
+How that works:
+
+- [`Awaited`][awaited] says "if this is a promise, the type here is whatever the promise resolves to; otherwise, it's just the value"
+- [`ReturnType`][return-type] gets the return value of a given function
+- `R['model']` (where `R` has to be `Route` itself or a subclass) says "the property named `model` on Route `R`"
+
+`ModelFrom` ends up giving you the resolved value returned from the `model` hook for a given route. We can use this functionality to guarantee that the `model` on a `Controller` is always exactly the type returned by `Route::model` by writing something like this:
+
+```typescript {data-filename="app/controllers/controller-with-model.ts"}
+import Controller from '@ember/controller';
+import MyRoute from 'my-app/routes/my-route';
+import { ModelFrom } from 'my-app/lib/type-utils';
+
+export default class ControllerWithModel extends Controller {
+ declare model: ModelFrom;
+}
+```
+
+Now, our controller's `model` property will _always_ stay in sync with the corresponding route's model hook.
+
+
+
+
+
Zoey says...
+
+
+ The ModelFrom
type utility only works if you do not mutate the model
in either the afterModel
or setupController
hooks on the route! That's generally considered to be a bad practice anyway.
+
+
+
+
+
+
+
+## Controller Injections and Lookups
+
+If you are using controller injections via the `@inject` decorator from `@ember/controller`, see the ["Decorators"][decorators] documentation.
+
+If you need to lookup a controller with `Owner.lookup`, you'll need to first register your controller in Ember's TypeScript Controller registry as described in ["Registries"][registries]:
+
+```typescript {data-filename="app/controllers/my.ts"}
+import Controller from '@ember/controller';
+
+export default class MyController extends Controller {
+ //...
+}
+
+declare module '@ember/controller' {
+ interface Registry {
+ my: MyController;
+ }
+}
+```
+
+
+
+[controllers]: ../../../routing/controllers/
+[decorators]: ../../additional-resources/gotchas/#toc_decorators
+[registries]: ../../additional-resources/gotchas/#toc_registries
+[routes]: ../../../routing/defining-your-routes/
+
+
+
+[awaited]: https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype
+[const-assertions]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
+[return-type]: https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype
diff --git a/guides/typescript/core-concepts/services.md b/guides/typescript/core-concepts/services.md
new file mode 100644
index 000000000..257f81a99
--- /dev/null
+++ b/guides/typescript/core-concepts/services.md
@@ -0,0 +1,115 @@
+Ember [Services][] are global singleton classes that can be made available to different parts of an Ember application via dependency injection. Due to their global, shared nature, writing services in TypeScript gives you a build-time-enforceable API for some of the most central parts of your application.
+
+## A Basic Service
+
+Let's take this example from elsewhere in the [Ember Guides][example-location]:
+
+```typescript {data-filename="app/services/shopping-cart.ts"}
+import Service from '@ember/service';
+import { TrackedSet } from 'tracked-built-ins';
+
+export default class ShoppingCartService extends Service {
+ items = new TrackedSet();
+
+ add(item) {
+ this.items.add(item);
+ }
+
+ remove(item) {
+ this.items.remove(item);
+ }
+
+ empty() {
+ this.items.clear();
+ }
+}
+```
+
+Just making this a TypeScript file gives us some type safety without having to add any additional type information. We'll see this when we use the service elsewhere in the application.
+
+## Using Services
+
+Ember looks up services with the `@service` decorator at runtime, using the name of the service being injected as the default value—a clever bit of metaprogramming that makes for a nice developer experience. TypeScript cannot do this, because the name of the service to inject isn't available at compile time in the same way.
+
+Since legacy decorators do not have access to enough information to produce an appropriate type by themselves, we need to import and add the type explicitly. Also, we must use the [`declare`][declare] property modifier to tell the TypeScript compiler to trust that this property will be set up by something outside this component—namely, the decorator. (Learn more about using Ember's decorators with TypeScript [here][decorators].) Here's an example using the `ShoppingCartService` we defined above in a component:
+
+```typescript {data-filename="app/components/cart-contents.ts"}
+import Component from '@glimmer/component';
+import { service } from '@ember/service';
+import { action } from '@ember/object';
+
+import ShoppingCartService from 'my-app/services/shopping-cart';
+
+export default class CartContentsComponent extends Component {
+ @service declare shoppingCart: ShoppingCartService;
+
+ @action
+ remove(item) {
+ this.shoppingCart.remove(item);
+ }
+}
+```
+
+Any attempt to access a property or method not defined on the service will fail type-checking:
+
+```typescript {data-filename="app/components/cart-contents.ts"}
+import Component from '@glimmer/component';
+import { service } from '@ember/service';
+import { action } from '@ember/object';
+
+import ShoppingCartService from 'my-app/services/shopping-cart';
+
+export default class CartContentsComponent extends Component {
+ @service declare shoppingCart: ShoppingCartService;
+
+ @action
+ remove(item) {
+ // Error: Property 'saveForLater' does not exist on type 'ShoppingCartService'.
+ this.shoppingCart.saveForLater(item);
+ }
+}
+```
+
+Services can also be loaded from the dependency injection container manually:
+
+```typescript {data-filename="app/components/cart-contents.ts"}
+import Component from '@glimmer/component';
+import { getOwner } from '@ember/owner';
+import { action } from '@ember/object';
+
+export default class CartContentsComponent extends Component {
+ get cart() {
+ return getOwner(this)?.lookup('service:shopping-cart');
+ }
+
+ @action
+ remove(item) {
+ this.cart.remove(item);
+ }
+}
+```
+
+In order for TypeScript to infer the correct type for the `ShoppingCartService` from the call to `Owner.lookup`, we must first [register][registries] the `ShoppingCartService` type with `declare module`:
+
+```typescript {data-filename="app/services/shopping-cart.ts"}
+export default class ShoppingCartService extends Service {
+ //...
+}
+
+declare module '@ember/service' {
+ interface Registry {
+ 'shopping-cart': ShoppingCartService;
+ }
+}
+```
+
+
+
+[example-location]: ../../../services/#toc_defining-services
+[decorators]: ../../additional-resources/gotchas/#toc_decorators
+[registries]: ../../additional-resources/gotchas/#toc_registries
+[services]: ../../../services/
+
+
+
+[declare]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier
diff --git a/guides/typescript/getting-started.md b/guides/typescript/getting-started.md
new file mode 100644
index 000000000..88f045e84
--- /dev/null
+++ b/guides/typescript/getting-started.md
@@ -0,0 +1,72 @@
+## Create a New TypeScript Application
+
+To start a new Ember project with TypeScript, add the `--typescript` flag when you run [`ember new`][ember-new]:
+
+```shell
+ember new my-typescript-app --typescript
+```
+
+Using the `--typescript` flag changes the output of `ember new` in a few ways:
+
+### TypeScript Project Files
+
+Project files will be generated with `.ts` extensions instead of `.js`.
+
+### Packages to Support TypeScript
+
+In addition to the usual packages added with `ember new`, the following packages will be added at their current "latest" value:
+
+- `typescript`
+- `@tsconfig/ember`
+- `@typescript-eslint/*`
+- `@types/ember`
+- `@types/ember-data`
+- `@types/ember__*` – `@types/ember__object` for `@ember/object`, etc.
+- `@types/ember-data__*` – `@types/ember-data__model` for `@ember-data/model`, etc.
+- `@types/qunit`
+- `@types/rsvp`
+
+The `typescript` package provides tooling to support TypeScript type checking and compilation. The `@types` packages from [DefinitelyTyped][] provide TypeScript type definitions for all of the Ember and EmberData modules.
+
+
+
+
+
Zoey says...
+
+ Ember also publishes its own native types compiled directly from its source code, as described
in this blog post. For now, we continue to use the
@types
packages by default for the sake of compatibility with EmberData, because EmberData is not yet compatible with Ember's native official types. However, if you do not use EmberData, we
highly recommend following the instructions in that blog post to switch to the native types, which are guaranteed to always be 100% correct and 100% up to date!
+
+
+
+
+
+
+### Files and Config to Support TypeScript
+
+In addition to the usual files added with `ember new`, we also add:
+
+- [`tsconfig.json`][tsconfig] – configuration to set up TypeScript for your project
+- [`types/global.d.ts`][global-types] – the location for any global type declarations you need to write
+- [`app/config/environment.d.ts`][environment-types] – a basic set of types defined for the contents of your `config/environment.js` file
+
+Additionally:
+
+- `package.json` will have a `lint:types` script to check types with the command line.
+- `ember-cli-build.js` will be configured to transform TypeScript at build-time.
+- `.ember-cli` has `isTypeScriptProject` set to true, which will force the blueprint generators to generate TypeScript rather than JavaScript by default.
+- `.eslintrc.js` will be configured for TypeScript.
+
+## Convert an Existing App to TypeScript
+
+To convert an existing app to TypeScript, you'll need to make the changes described above manually (for now). To facilitate this, we've included a guide [here][converting].
+
+
+
+[converting]: ../application-development/converting-an-app/
+[ember-new]: ../../getting-started/quick-start/
+[environment-types]: ../additional-resources/faq/#toc_environment-configuration-typings
+[global-types]: ../additional-resources/faq/#toc_global-types-for-your-project
+[tsconfig]: ../application-development/configuration/#toc_tsconfigjson
+
+
+
+[DefinitelyTyped]: https://github.com/DefinitelyTyped/DefinitelyTyped
diff --git a/guides/typescript/index.md b/guides/typescript/index.md
new file mode 100644
index 000000000..956e9d151
--- /dev/null
+++ b/guides/typescript/index.md
@@ -0,0 +1,43 @@
+This guide is designed to help you get up and running with TypeScript in an Ember app.
+
+This is _not_ an introduction to TypeScript _or_ Ember. Throughout this guide, we'll link back to [the TypeScript docs][typescript-docs] and to other sections of [the Ember Guides][ember-guides] when there are specific concepts that we will not explain here but which are important for understanding what we're covering!
+
+Not sure where to get started? Here's an overview of the content within:
+
+- If you're totally new to using TypeScript with Ember, start with [Core Concepts: TypeScript and Ember][core-concepts].
+- To create a new Ember app or addon with TypeScript, check out [Getting Started with TypeScript][getting-started] and [Building Addons in TypeScript][addons].
+- If you're looking to convert an existing Ember app to TypeScript, check out [Converting an Existing Ember App to TypeScript][converting-an-app].
+- If you're working with legacy (pre-Octane) Ember and TypeScript together, you should read [TypeScript and Ember Classic][legacy].
+- Not ready to switch to TypeScript? You can get many of TypeScript's benefits by [adding types with JSDoc comments][types-with-jsdoc]. We'll talk a bit about this over in the [Signatures][] section.
+- Looking for type-checking in Ember templates? Check out [Glint][].
+
+## Why TypeScript?
+
+What is TypeScript, and why should you adopt it?
+
+> TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
+>
+> — [typescriptlang.org][typescript]
+
+TypeScript lets you build _ambitious web applications_ with confidence—so it's a perfect fit for Ember apps!
+
+- Get rid of `undefined is not a function` and `null is not an object` once and for all.
+- Enjoy API docs… that are always up-to-date.
+- Experience better developer productivity through top-notch editor support, including incredible autocomplete, guided refactoring, automatic imports, and more.
+
+
+
+[addons]: ./application-development/addons/
+[converting-an-app]: ./application-development/converting-an-app
+[core-concepts]: ./core-concepts
+[ember-guides]: ..
+[getting-started]: ./getting-started
+[legacy]: ./additional-resources/legacy
+[signatures]: ./core-concepts/invokables/#toc_signature-basics
+
+
+
+[glint]: https://typed-ember.gitbook.io/glint/
+[types-with-jsdoc]: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
+[typescript]: http://www.typescriptlang.org
+[typescript-docs]: https://www.typescriptlang.org/docs/
diff --git a/guides/upgrading/current-edition/glimmer-components.md b/guides/upgrading/current-edition/glimmer-components.md
index c58b94010..b6781b926 100644
--- a/guides/upgrading/current-edition/glimmer-components.md
+++ b/guides/upgrading/current-edition/glimmer-components.md
@@ -784,15 +784,17 @@ functionality that lifecycle hooks contained.
#### Writing your own modifiers
-There are also community APIs available for writing your own modifiers, such as
-[ember-modifier](https://github.com/ember-modifier/ember-modifier).
-Ember itself has low level APIs known as _modifier managers_ which can be used
-to write these higher level APIs. In general, it's recommended to use a
-community addon to write modifiers, and _not_ to write your own modifier
-manager.
+New Ember apps ship with a dependency on
+[ember-modifier](https://github.com/ember-modifier/ember-modifier), which
+provides a friendly API for writing your own element modifiers. This library is
+in turn based on a low level API named _modifier managers_. Managers are a
+framework-development level feature, and not something most developers need to
+interact with.
-Let's see what our first example would look like if we were to write it as a
-modifier using `ember-modifier`:
+Custom modifiers based on the `ember-modifier` API can be a more expressive
+interface for your logic, and can better encapsulate an implementation.
+
+Let's write a modifier that implements adding an event listener.
```js {data-filename=app/modifiers/add-event-listener.js}
import { modifier } from 'ember-modifier';
@@ -825,12 +827,16 @@ export default class ScrollComponent extends Component {
```
-This modifier generalizes the functionality that the component implemented using
-lifecycle hooks before, so we can use this modifier whenever we need to in _any_
-component. This is a much better solution than manually managing event listeners
-every time we need one! At this point, the modifier is effectively the same as
-the `{{on}}` modifier as well, so we could get rid of it altogether and replace
-it with `on`:
+The new `add-event-listener` modifier presents a more expressive interface to
+the `hbs` template: There is only a single modifier to apply instead of two, the
+implementation always tears down after itself upon teardown of the target element,
+and the only JavaScript you have to write during re-user is the implementation
+of the business logic.
+
+At this point, it is worth noting that the custom `{{add-event-listener}}`
+modifier is effectively a re-implementation of the Ember built-in `{{on}}`
+modifier (See the
+[documentation](https://api.emberjs.com/ember/5.1/classes/Ember.Templates.helpers/methods/on?anchor=on)). Using that built-in looks like:
```handlebars {data-filename=app/components/scroll-component.hbs}
diff --git a/guides/upgrading/current-edition/native-classes.md b/guides/upgrading/current-edition/native-classes.md
index 83f0db308..ab53879a6 100644
--- a/guides/upgrading/current-edition/native-classes.md
+++ b/guides/upgrading/current-edition/native-classes.md
@@ -432,10 +432,11 @@ support Ember mixins at all. In the future, mixins will be removed from the
framework, and will not be replaced directly. For apps that use mixins, the
recommended path is to refactor the mixins to other patterns, including:
-* Pure native classes, sharing functionality via class inheritance.
-* Utility functions which can be imported and used in multiple classes.
-* Services which can be injected into multiple classes, sharing functionality
- and state between them.
+1. For functionality which encapsulates DOM modification, rewrite as a custom modifier using [ember-modifier](https://github.com/ember-modifier/ember-modifier).
+1. If the mixin is a way of supplying shared behavior (not data), extract the behavior to utility functions, usually just living in module scope and imported and exported as needed.
+1. If the mixin is a way of supplying long-lived, shared state, replace it with a service and inject it where it was used before. This pattern is uncommon, but sometimes appears when mixing functionality into multiple controllers or services.
+1. If the mixin is a way of supplying non-shared state which follows the lifecycle of a given object, replace it with a utility class instantiated in the owning class's `constructor` (or `init` for legacy classes).
+1. If none of the above, extract to pure native classes, sharing functionality via class inheritance.
## Cheatsheet
diff --git a/package-lock.json b/package-lock.json
index 89f617ca5..0a8de3608 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"broccoli-asset-rev": "^3.0.0",
"chai": "^4.3.4",
"dictionary-fr": "^2.8.0",
+ "dotenv": "^16.3.1",
"ember-auto-import": "^2.6.1",
"ember-cli": "~3.28.6",
"ember-cli-app-version": "^5.0.0",
@@ -58,8 +59,9 @@
"loader.js": "^4.7.0",
"lodash": "^4.17.21",
"markdown-link-extractor": "^1.2.2",
+ "minimist-lite": "^2.2.1",
"mocha": "^8.3.2",
- "node-fetch": "^2.6.7",
+ "node-fetch": "^2.7.0",
"npm-run-all": "^4.1.5",
"prember": "^1.0.3",
"prettier": "^2.5.1",
@@ -77,6 +79,7 @@
"retext-spell": "^5.3.0",
"retext-syntax-urls": "^3.1.2",
"sass": "^1.62.1",
+ "shelljs": "^0.8.5",
"unified": "^10.1.2",
"walk-sync": "^2.0.2"
},
@@ -11939,6 +11942,18 @@
"node": ">=8"
}
},
+ "node_modules/dotenv": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
+ "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/motdotla/dotenv?sponsor=1"
+ }
+ },
"node_modules/duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@@ -30459,6 +30474,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/minimist-lite": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/minimist-lite/-/minimist-lite-2.2.1.tgz",
+ "integrity": "sha512-RSrWIRWGYoM2TDe102s7aIyeSipXMIXKb1fSHYx1tAbxAV0z4g2xR6ra3oPzkTqFb0EIUz1H3A/qvYYeDd+/qQ==",
+ "dev": true
+ },
"node_modules/minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
@@ -31142,9 +31163,9 @@
"dev": true
},
"node_modules/node-fetch": {
- "version": "2.6.8",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
- "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
diff --git a/package.json b/package.json
index 4bfcd8d50..e6a4ba3f3 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,8 @@
"test:ember": "ember test",
"test:node": "mocha node-tests --exclude node-tests/local/**",
"test:node-local": "mocha node-tests/local",
- "test:node-local-exclude-api-urls": "FOLLOW_API_URLS=false npm run test:node-local"
+ "test:node-local-exclude-api-urls": "FOLLOW_API_URLS=false npm run test:node-local",
+ "catchup": "node scripts/catchup.mjs"
},
"devDependencies": {
"@ember/optional-features": "^2.0.0",
@@ -39,6 +40,7 @@
"broccoli-asset-rev": "^3.0.0",
"chai": "^4.3.4",
"dictionary-fr": "^2.8.0",
+ "dotenv": "^16.3.1",
"ember-auto-import": "^2.6.1",
"ember-cli": "~3.28.6",
"ember-cli-app-version": "^5.0.0",
@@ -80,8 +82,9 @@
"loader.js": "^4.7.0",
"lodash": "^4.17.21",
"markdown-link-extractor": "^1.2.2",
+ "minimist-lite": "^2.2.1",
"mocha": "^8.3.2",
- "node-fetch": "^2.6.7",
+ "node-fetch": "^2.7.0",
"npm-run-all": "^4.1.5",
"prember": "^1.0.3",
"prettier": "^2.5.1",
@@ -99,6 +102,7 @@
"retext-spell": "^5.3.0",
"retext-syntax-urls": "^3.1.2",
"sass": "^1.62.1",
+ "shelljs": "^0.8.5",
"unified": "^10.1.2",
"walk-sync": "^2.0.2"
},
diff --git a/scripts/catchup.mjs b/scripts/catchup.mjs
new file mode 100644
index 000000000..fc9a137bb
--- /dev/null
+++ b/scripts/catchup.mjs
@@ -0,0 +1,236 @@
+import 'dotenv/config';
+import fetch from 'node-fetch';
+import fs from 'fs';
+import minimist from 'minimist-lite';
+import shell from 'shelljs';
+
+// Declare repository
+const repo = 'dazzlingfugu/ember-fr-guides-source';
+
+// Read Github token
+const token = process.env.GITHUB_TOKEN;
+
+// Read script arguments
+const argv = minimist(process.argv.slice(2));
+
+const currentEmberVersion = `${argv.from}`;
+if (currentEmberVersion.match(/\d+[.]\d+/g)?.[0] !== currentEmberVersion) {
+ console.log('Error: please provide the current Ember version under translation to option --from (e.g. --from=5.1)');
+ process.exit(2);
+}
+console.log(`Ember version under translation: ${currentEmberVersion}`);
+
+const newEmberVersion = `${argv.to}`;
+if (newEmberVersion.match(/\d+[.]\d+/g)?.[0] !== newEmberVersion) {
+ console.log('Error: please provide the new Ember version documented on upstream to option --to (e.g. --to=5.4)');
+ process.exit(2);
+}
+console.log(`New Ember version documented on upstream: ${newEmberVersion}`);
+
+// Create a catchup branch out of the current branch (should be up to date master)
+const catchupBranch = `catchup-${newEmberVersion}`;
+
+if (shell.exec(`git switch --create ${catchupBranch}`).code !== 0) {
+ console.log(`shelljs: "git switch --create ${catchupBranch}" command failed`);
+ process.exit(1);
+}
+console.log(`shelljs: "git switch --create ${catchupBranch}" executed`);
+
+// Fetch the latest updates in the official Ember Guides
+if (shell.exec('git fetch upstream').code !== 0) {
+ console.log('shelljs: "git fetch upstream" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git fetch upstream" executed');
+
+// Output the list of markdown files impacted by latest changes on upstream
+if (shell.exec('git diff --name-only ref-upstream upstream/master -- guides/release > list.diff').code !== 0) {
+ console.log('shelljs: "git diff --name-only ref-upstream upstream/master -- guides/release > list.diff" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git diff --name-only ref-upstream upstream/master -- guides/release > list.diff" executed');
+
+// Read list.diff to extract the list of path to markdown files
+let data = fs.readFileSync('list.diff', 'utf8');
+let files = data.split(/[\n\r]/).filter(name => name.length);
+fs.unlink('list.diff', function(err) {
+ if (err) throw err;
+ console.log('list.diff did its job, deleted');
+});
+
+// Create a directory to put the children diff
+fs.mkdirSync('scripts/patches');
+console.log('scripts/patches folder created to store the patch files');
+
+// Compare filename in both branches and output a [index].diff
+const createDiff = (filename, index) => {
+ const diffName = `scripts/patches/${index}.diff`
+ if (shell.exec(`git diff ref-upstream upstream/master -- ${filename} > ${diffName}`).code !== 0) {
+ console.log(`shelljs: "git diff ref-upstream upstream/master -- ${filename} > ${diffName}" command failed`);
+ process.exit(1);
+ }
+ console.log(`shelljs: "git diff ref-upstream upstream/master -- ${filename} > ${diffName}" executed`);
+ return diffName;
+}
+
+// Execute all the read/write/unlink operations on diff files
+const writeDiffFiles = async (filesToPost) => {
+ let writePromises = files.map((filename, index) => {
+ const diffName = createDiff(filename, index);
+ return new Promise((resolve, reject) => {
+ // Rewrite the path to adjust it to our Guidemaker scaffolding
+ fs.readFile(diffName, 'utf8', function(err, data) {
+ if (err) reject(err);
+ const replacement = data.replace(/guides\/release/g, 'guides');
+ fs.writeFile(diffName, replacement, 'utf8', function(err) {
+ if (err) reject(err);
+ console.log(`path in ${diffName} updated`);
+ // Try to apply automatically
+ if (shell.exec(`git apply ${diffName}`).code !== 0) {
+ shell.echo(`shelljs: "git apply" command failed for ${diffName}`);
+ filesToPost.push({filename, diffName})
+ } else {
+ // Remove the file if the apply was successfull
+ fs.unlink(diffName, function(err) {
+ if (err) throw err;
+ console.log(`${diffName} handled and deleted`);
+ });
+ }
+ resolve();
+ });
+ });
+ });
+ });
+ console.log('Ready to create the patch files');
+ await Promise.all(writePromises).then(() => {
+ console.log('All writing operations are done, patch files are applied or stored in scripts/patches/');
+ });
+}
+
+// Post a GitHub issue
+const postIssue = (file) => {
+ const { filename, diffName } = file;
+ let diffblock = fs.readFileSync(diffName, 'utf8');
+ diffblock = diffblock.replaceAll('```', '');
+ let shorterName = filename.substring(14);
+
+ return fetch(`https://api.github.com/repos/${repo}/issues`, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/vnd.github+json',
+ 'Authorization': `token ${token}`,
+ 'X-GitHub-Api-Version': '2022-11-28'
+ },
+ body: JSON.stringify({
+ title: `Translate \`${shorterName}\`, Ember ${newEmberVersion}`,
+ body: `
+Please assign yourself to the issue or let a comment at the very moment you start the translation.
+
+File: \`${filename}\`
+From Ember: **${currentEmberVersion}**
+To Ember: **${newEmberVersion}**
+
+\`\`\`diff
+${diffblock}
+\`\`\`
+ `,
+ labels: ['Guides FR trad']
+ })
+ });
+}
+
+// Try to apply the diff files automatically and keep track of the failed ones
+let filesToPost = [];
+await writeDiffFiles(filesToPost);
+console.log('Files to post on GitHub', filesToPost);
+
+let issuePostingError = false;
+
+// Post the diff files content that couldn't be handled automatically to Github
+filesToPost.forEach(async (file) => {
+ try {
+ console.log(`Attempting to open an issue for ${file.filename}`);
+ const response = await postIssue(file);
+ const jsonResponse = await response.json();
+ console.log('Server responded with:', jsonResponse);
+ } catch (error) {
+ console.log('Issue posting has failed:', error);
+ issuePostingError = true;
+ }
+ await new Promise(resolve => setTimeout(resolve, 1000));
+});
+
+if (!issuePostingError) {
+ // Once the issues are posted, delete patches folder and files
+ fs.rmSync('scripts/patches', { recursive: true, force: true });
+ console.log('scripts/patches folder and files did their job, deleted');
+}
+// If one of the post failed, the files are still here so we can easily open the issue manually
+
+// Add, commit, push the modifications done automatically so far
+if (shell.exec('git add guides').code !== 0) {
+ console.log('shelljs: "git add guides" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git add guides" executed');
+
+if (shell.exec(`git commit -m "feat: automatic catch up from ${currentEmberVersion} to ${newEmberVersion}"`).code !== 0) {
+ console.log('shelljs: "git commit -m" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git commit -m" executed');
+
+if (shell.exec(`git push origin ${catchupBranch}`).code !== 0) {
+ console.log(`shelljs: "git push origin ${catchupBranch}" command failed`);
+ process.exit(1);
+}
+console.log(`shelljs: "git push origin ${catchupBranch}" executed`);
+
+// Post the catchup PR
+try{
+ console.log('Attempting to post the catch up PR');
+ const prResponse = await fetch(`https://api.github.com/repos/${repo}/pulls`, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/vnd.github+json',
+ 'Authorization': `token ${token}`,
+ 'X-GitHub-Api-Version': '2022-11-28'
+ },
+ body: JSON.stringify({
+ title: `Catch up latest docs: from ${currentEmberVersion} to ${newEmberVersion}`,
+ body: 'This is an automatic catch up PR to align our non-translated documentation with the latest official documentation.',
+ head: catchupBranch,
+ base: 'master',
+ labels: ['Guides FR trad']
+ })
+ });
+ const jsonPrResponse = await prResponse.json();
+ console.log('Server responded with:', jsonPrResponse);
+} catch (error) {
+ console.log('Catch up PR posting has failed:', error);
+}
+
+// Replace ref-upstream with current upstream then go back to master
+if (shell.exec('git switch ref-upstream').code !== 0) {
+ console.log('shelljs: "git switch ref-upstream" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git switch ref-upstream" executed');
+
+if (shell.exec('git reset --hard upstream/master').code !== 0) {
+ console.log('shelljs: "git reset --hard upstream/master" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git reset --hard upstream/master" executed');
+
+if (shell.exec('git push origin -f ref-upstream').code !== 0) {
+ console.log('shelljs: "git push origin -f ref-upstream" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git push origin -f ref-upstream" executed');
+
+if (shell.exec('git switch master').code !== 0) {
+ console.log('shelljs: "git switch master" command failed');
+ process.exit(1);
+}
+console.log('shelljs: "git switch master" executed');
\ No newline at end of file