diff --git a/text/0938-strict-es-module-support.md b/text/0938-strict-es-module-support.md new file mode 100644 index 0000000000..3a009c0902 --- /dev/null +++ b/text/0938-strict-es-module-support.md @@ -0,0 +1,471 @@ +--- +stage: accepted +start-date: 2023-07-19T00:00:00.000Z +release-date: +release-versions: +teams: # delete teams that aren't relevant + - cli + - framework + - learning +prs: + accepted: https://github.com/emberjs/rfcs/pull/938 +project-link: +--- + + + +# Strict ES Module Support + +## Summary + +Deprecate `require()` and `define()` and enforce the ES module spec. + +## Motivation + +Ember has been using ES modules as the official authoring format for many years. +But we don't actually follow the ES modules spec. We diverge from the spec in +several important ways: + + - our "modules" can be accessed via the synchronous, dynamic `require()` + function. They evaluate the first time someone `require`s them. This behavior + is impossible for true ES modules, which support synchronous-and-static + inclusion via `import` or asynchronous-and-dynamic inclusion via `import()`, + but never synchronous-and-dynamic inclusion. + + - in the spec, trying to import a nonexistent name from another module is an + early error -- your module **will never even execute** if it does this. In + the current implementation, your code happily runs and receives an undefined + local binding. + + - in the spec, these two lines mean very different things: + ```js + import * as TheModule from "the-module"; + import TheModule from "the-module"; + ``` + In our current implementation, you can sometimes use them interchangeably. + + - In the spec, modules are always read-only. You cannot mutate them from the outside. In our implementation, you can. + +This is very bad, because + - our code breaks when you try to run it in environments that actually follow the spec. + - refactors of Ember and its build pipeline that would otherwise be 100% +invisible to apps are instead breaking changes, because apps and addons are +relying on leaked details (including timing) of the AMD implementation. + - we cannot implement other standard ECMA features like top-level await until these incompatible APIs are gone (consider: how is `require()` supposed to deal with discovering a transitive dependency that contains top-level await?) + - the reliance on global `require` and `define` means Ember apps have [interoperability problems](https://github.com/emberjs/rfcs/pull/784) if you want to embed more than one into a page, or have an Ember app share a page with some other app that uses AMD. + +## Detailed Design + +### Architectural Intro + +There are four salient layers to consider in Ember's architecture: + + - Container holds state. When Container is asked for something it doesn't have yet, it gets the factory from the Registry. + - Registry holds factories. You can manually register factories with the registry. When someone asks for a factory that is not already registered, the Registry delegates to the Resolver. + - Resolver maps between requests like "service:translations" and module names like "my-app/services/translations". How it does this mapping can be customized. After mapping the name, it retrieves a module from loader.js. + - loader.js holds modules. Modules get added via `define` and retrieved via `require`. In addition to being used by the Resolver, loader.js is used directly whenever a module imports another module. + +This RFC replaces loader.js with ES Modules. + - instead of using `define` to get modules into the system, you must create an actual module in your app or addon. Nothing about "how do I create a module" is changed by this RFC. It has long been possible to just create a javascript file and the default build system would `define` it for you. As long as you stick to that normal happy path, none of your code is changing. It will just go through some other mechanism than `define` to get to the browser. (Whether that mechanism is literally browser-native ES modules or some other transpilation target is immaterial -- the point is that if you follow the ES module spec it doesn't matter.) + + - instead of using `require`, modules can only access each other via `import` or `import()` (or `import.meta.glob()`, see [RFC 939](https://github.com/emberjs/rfcs/pull/939)). + +This RFC does not immediately propose changing Container or Registry or the Registry-facing Resolver interface. It *does* imply that Resolver's implementation must change, because today Resolver relies on `require` to synchronously retrieve things from loader.js, which will not be possible. + +Instead, anything that needs to be synchronously resolvable by the Registry will need to be either: + - explicitly preloaded into the Registry (using the existing `register` public API) + ```js + import Button from './components/button'; + registry.register('component:button', Button); + ``` + - or passed into a new Resolver implementation that has some access to **preloaded** ES modules: + ```diff + // app/app.js + -import Resolver from 'ember-resolver'; + +import Resolver from '@ember/resolver'; + export default class App extends Application { + - Resolver = Resolver; + + // For illustration purposes only! Not real API. + + Resolver = Resolver(import.meta.glob('./**/*.js', { eager: true })) + } + ``` + This would continue to support existing custom resolver rules, but people would need to extend their custom resolvers from this new Resolver that serves requests out of preloaded modules, rather than extend from the current Resolver that serves them out of loader.js. + +### New Feature: strict-es-modules optional feature + +We will introduce a new Ember optional feature: + +```json +// optional-features.json +{ + "strict-es-modules": true +} +``` + +And we will emit a deprecation warning in any app that has not enabled this new optional feature. + +When `strict-es-modules` is set: + - the global names `require`, `requirejs`, `requireModule`, `define`, and `loader` become undefined. + - importing anything from `"require"` becomes an early error. This outlaws usage like: + ```js + import require from "require"; + import { has } from "require"; + ``` + - Default module exports will no longer get automatically converted to namespace exports. + - Importing (including re-exporting) a non-existent name becomes an early error. + - Attempting to mutate a module from the outside throws an exception. + - `importSync` from `'@embroider/macros'` continues to work but it no longer guarantees lazy-evaluation (since lazy-and-synchronous is impossible under strict ES modules). + - the new special imports `#ember-compat-modules` and `#ember-compat-test-modules` become available (see below). + - loading component templates via `