# V2 Package Spec # Motivation One of the good things about Ember is that apps and addons have a powerful set of build-time capabilities that allow lots of shared code with zero-to-no manual integration steps for the typical user. We have been doing “zero config” since before it was a cool buzzword (it was just called “convention over configuration”). And we’ve been broadly successful at maintaining very wide backward- and forward-compatibility for a large body of highly-rated community-maintained addons. But one of the challenging things about Ember is that our ecosystem’s build-time capabilities are more implementation-defined than spec-defined, and the implementation has accumulated capabilities organically while only rarely phasing out older patterns. I believe the lack of a clear, foundational, build-time public API specification is the fundamental underlying issue that efforts like the various packaging / packager RFCs have tried to work around. The benefits to users for this RFC are: - faster builds and faster NPM installs - “zero-config import from NPM — both static and dynamic” as a first-class feature all apps and addons can rely on. - immediate tree-shaking of app- and addon-provided modules that are consumed directly via ECMA imports (for example, any ember-animated transition you don’t use in your app won’t get included in the build), with a smooth improvement path for steadily increasing the level of static analysis as other efforts like templates imports land. - a more approachable build system that enables more people to contribute and better integration with other JS toolchains. # Key Ideas ## Fully Embrace ES Modules Ember was one of the earliest adopters of ECMAScript modules, and Ember core team members were directly involved in helping move that features through TC39. Ember’s early experiences with modules influenced the spec itself. _Yet we have lagged in truly embracing modules._ For example, how do Ember apps express that they depend on a third-party library? The [app.import](https://ember-cli.com/user-guide/#javascript-assets) API. This should be ECMA standard `import`. Another way to state the problem is that apps and addons all _push_ whatever code they want into the final built app. Whereas ES modules can _pull_ each other into the build as needed. ## Play nice with NPM Conventions The ECMA module spec by itself doesn’t try to define a module resolution algorithm. But the overwhelmingly most popular convention is the [node_modules resolution algorithm](https://nodejs.org/api/all.html#modules_all_together). Ember addons do respect node_module resolution for build-time code, but they do not respect it for runtime code. There’s no reason not to. ## Verbose, Static Javascript as a Compiler Target Ember’s strong conventions mean that many kinds of dependencies can be inferred (including _statically_ inferred) without requiring the developer to laboriously manage them. This is a good thing and I believe the current fad in the wider Javascript ecosystem for making developers hand-write verbose static imports for everything confuses the benefits of having static analysis (which is good) with the benefits of hand-managing those static imports (which is unnecessary cognitive load when you have clear conventions and a compiler). This design is about compiling today’s idiomatic Ember code into more “vanilla” patterns that leverage ES modules, node_modules resolution, and spec-compliant static and dynamic `import` to express the structure of an Ember application in a much more “vanilla Javascript” way. This compile step lets us separate the authoring format (which isn’t changing in any significant way in this RFC) from the packaging format (which can be more verbose and static than we would want in an authoring format). # Detailed Design ## Definitions **package**: every addon and app is a package. Usually synonymous with “NPM package”, but we also include in-repo packages. The most important fact about a package is that it’s often the boundary around code that comes from a particular author, team, or organization, so coordination across packages is a more sensitive design problem than coordination within apps. **app**: a package used at the root of a project. **addon**: a package not used at the root of a project. Will be an **allowed dependency** of either an **app** or an **addon**. **allowed dependency**: For **addons**, the **allowed dependencies** are the `dependencies` and `peerDependencies` in `package.json` plus any in-repo addons. For **apps**, the **allowed dependencies** are the `dependencies`, `peerDependencies`, and `devDependencies` in `package.json` plus any in-repo addons. **Ember package metadata**: the `ember-addon` section inside `package.json`. This already exists in v1, we’re going to extend it. **v2 package**: a package with `package.json` like: "keywords": [ "ember-addon" ], "ember-addon": { "version": 2 } **v1 package**: a package with `package.json` like: "keywords": [ "ember-addon" ] and no `version` key (or version key less than 2) in **Ember package metadata**. **non-Ember package**: a package without `keywords: ["ember-addon"]` ## Package Public API Overview The structure we are about to describe _is a publication format_. Not necessarily an authoring format. By separating the two, we make it easier to evolve the authoring formats without breaking ecosystem-wide compatibility. The publication format is deliberately more explicit and less dynamic that what we would want for an authoring format. First, here’s the list of things a v2 package can provide. More detail on each of these will follow: - **Own Javascript**: javascript and templates under the package’s own namespace (the v1 equivalent is `/addon/**/*.js/`) - **App Javascript**: javascript and templates that must be merged with the consuming app’s namespace (the v1 equivalent is `/app/**/*.js`). This likely stops being needed when use with a template imports feature, but v2 package format is not dependent on that. - **CSS:** available for `@import` by other CSS files (both in the same package and across packages) and by ECMA `import` directives in Javascript modules (both in the same package and across packages). - **Assets**: any files that should be available in the final built application directory (typical examples are images and fonts). - **Middleware**: express middleware that will mount automatically during development, unchanged from v1. - **Preprocessors**: for producing JS, CSS, or HBS. - **Commands**: commands that can be invoked from the command line. Unchanged from v1. - **Blueprints**: blueprints for generating new files from the command line. Unchanged from v1. - **ContentFor**: the ability to insert snippets into key places, like the document header. - **Active Dependencies**: the subset of a given package’s **allowed dependencies** that are Ember packages and that the given package considers active. ## Own Javascript The public `main` (as defined in `package.json`) of a v2 package points to its **Own Javascript**. The code is formatted as ES modules using ES latest features. Templates are in place in hbs format, and any custom AST transforms have already been applied. (Remember, we’re describing the _publication_ format, not the _authoring_ format. Authors can still do what they do today, using preprocessors provided by other addons. But that will all run before publishing.) The benefit of this design is that it makes our packages understandable by a broader set of tooling. Editors and build tools can follow `import` statements across packages and end up in the right place. In v1 packages, `main` usually points to a build-time configuration file. That file is moving and will be described in the **Addon Hooks** section below. Modules in **Own Javascript** are allowed to use ECMA static `import` to resolve any **allowed dependency**, causing it to be included in the build whenever the importing module is included. This replaces `app.import`. Notice that a package’s **allowed dependencies** do not include the package itself. This is consistent with how node module resolution works. This is different from how run-time AMD module resolution has historically worked in Ember Apps, so the build step that produces the v2 publication format will need to adjust import paths appropriately. For example, if `your-package/a.js` tries to import from `"your-package/b"`, that needs to get rewritten to “`./b`". Modules in **Own Javascript** are also allowed to use the (currently stage 3) ECMA dynamic `import()`, and the specifiers have the same meanings as in static import. We impose one caveat: only string-literal specifiers are supported. So `import('./lang-en')` is OK but `import("./lang-"+language)` is not. We retain the option to relax this restriction in the future. The restriction allows us to do better analysis of possible inter-module dependencies (see **Build-time Conditionals** below for an example). Modules in **Own Javascript** are allowed to import template files. This is common in today’s addons (they import their own layout to set it explicitly). Modules in **Own Javascript** are allowed to use `hbs` tagged template strings as provided by `ember-cli-htmlbars-inline-precompile`, and we promise to compile the templates at app build time. You’re allowed to `import` from both other v2 Ember packages and non-Ember packages. The only difference is that v2 Ember packages necessarily agree to provide ES modules with ES latest features, and so we will always apply the application’s browser-specific Babel transpilation to them. Non-Ember packages can be authored in lots of ways, and we will use best-effort to consume them, including conversion of ESM or CJS to whatever format we’re using in the browser (currently AMD), but we won’t apply the app’s Babel transpilation to them, because it’s usually just unnecessary expense — the most common way to ship NPM packages outside of well-known build systems like ember-cli is to transpile before publication. _A recent lesson from ember-auto-import is that we’re going to want to allow people to opt-in to babel transpilation of specific foreign packages, as the wider ecosystem’s norms evolve and more projects ship modern JS untranspiled. Unfortunately there is no simple correct universal answer here. Double transpilation is not safe in general, since choices get made about how to map between modules, AMD, UMD, etc._ ## App Javascript To provide **App Javascript**, a package includes the `app-js` key in **Ember package metadata**. For example, to duplicate the behavior of v1 packages, you could say: "ember-addon": { "version": 2, "app-js": "./app" } Like the **Own Javascript**, templates are in place in hbs format with any AST transforms already applied. Javascript is in ES modules, using only ES latest features. ECMA static and dynamic imports from any **allowed dependency** are supported. By making this an explicit key in **Ember package metadata**, our publication format is more durable (you can rearrange the conventional directory structure in the future without breaking the format) and more performant (less filesystem traversal is required to decide which features the package is using). ## CSS To provide **CSS**, a package can include any number of CSS files. These files can `@import` each other via relative paths, which will result in build-time inclusion (as already works in v1 packages). If any of the **Own Javascript** or **App Javascript** modules depend on the presence of a CSS file in the same package, it should say so explicitly via an ECMA relative import, like: import '../css/some-component.css'; This is interpreted as a build-time directive that ensures that before the Javascript module is evaluated, the CSS file's contents will be present in the DOM. > Q: Does this interfere with the ability to do CSS-in-JS style for people who like that? > A: No, because that would be a preprocessing step before publication. It’s a choice of authoring format, just like TypeScript or SCSS. It is also possible for other packages (including the consuming application) to depend on a CSS file in any of its **allowed dependencies**, from either Javascript or CSS. From Javascript it looks like: // This will resolve the `your-addon` package and find // './some-component.css' relative to the package root. // The .css file extension is mandatory import 'your-addon/some-component.css'; And from CSS it looks like: @import 'your-addon/some-component'; What about SCSS _et al_? You’re still free to use them as your authoring format, and they should be transpiled to CSS in your publication format. If you want to offer the original SCSS to consuming packages, you’re free to include it in the publication format too. Since we’re making all packages resolvable via normal node rules, it’s now dramatically easier to implement a preprocessor that supports inter-package dependencies. (The same logic applies to TypeScript.) ## Assets To provide **Assets**, a package includes the `public-assets` key in **Ember package metadata**. It's a mapping from local paths to app-relative URLs that should be available in the final app. "name": "my-addon", "ember-addon": { "version": 2, "public-assets": { "./public/image.png": "/my-addon/image.png" } } with: my-addon └── public └── image.png will result in final build output: dist └── my-addon └── image.png Notice that we’re _not_ choosing to include assets via explicit ECMA `import`. The reason is that fine-grained inclusion of asset files is not critical to runtime performance. Any assets that your app doesn’t actually need, it should never fetch. ## ContentFor The following targets are supported the same as in v1 packages: - head - head-footer - body - body-footer - test-head - test-head-footer - test-body - test-body-footer - config-module The following targets are deprecated because they tie us permanently to the idea of fixed app/test/vendor Javascript bundles, and because they are not widely used according to the EmberObserver code search: - app-boot - app-prefix - app-suffix - test-support-prefix - test-support-suffix - vendor-prefix - vendor-suffix ## What about Tests? v1 packages can provide `treeForTestSupport`, `treeForAddonTestSupport`, and `app.import` with `type="test"`. All of these features are dropped. To provide test-support code, make a separate module within your package and tell people to `import` it from their tests. As long as it is only imported from tests, it will not be present in non-test bundles. ​​ ## Package Hooks In today’s v1 addon packages, the `index.js` file is the main entrypoint that allows an addon to integrate itself with the overall ember-cli build pipeline. The same idea carries forward to v2, with some changes. It is no longer the `main` entrypoint of the package (see **Own Javascript**). Instead, it’s located via the `build` key in **Ember package metadata**, which should point at a Javascript file. `build` is optional — if you don’t have anything to say, you don’t need the file. It is now an ECMA module, not a CJS file. The default export is a class that implements your addon hooks (no base class is required). One area that is under-documented and under-designed in the existing hooks is: which ones cascade into active grandchild addons? Do they cascade via `super` so you can (accidentally or on purpose) block the cascade? Section **Active Dependencies** makes these rules consistent and clear. List of existing v1 public methods and properties on addons, and their disposition in v2: - blueprintsPath: unchanged in v2 - buildError: Kept. This is an event hook that makes it possible to implement things like ember-cli-build-notifications. - cacheKeyForTree: Dropped. This is a build-time feature, it doesn’t belong in the publication format. - config: TODO. - contentFor: Some of the possible destinations for content are removed. See **ContentFor** section. - dependencies: Dropped. Can’t find any usages in the wild. - description: Dropped. This is redundant with the description in package.json. - import: Dropped. This is replaced with actual ECMA `import` for both Javascript and CSS. - importTransforms: Dropped, because this goes with `this.import()` above. All examples in the wild that I could find are handled better by other alternatives. - the CJS and AMD transforms aren’t needed because better packagers can automate the transformation of both, as demonstrated by ember-auto-import - the fastboot transform is used to neuter whole dependencies in fastboot. This can be handled by ECMA dynamic `import()` instead. - most other occurrences in the EmberObserver code search are actually addons re-exporting the fastboot transform (because apparently `importTransforms` doesn’t cascade properly). - included: Unchanged, but it should be needed much more rarely. Today it is mostly used to `this.import()` things, which is not a thing anymore. - includedCommands: Unchanged. - init: Dropped in favor of `constructor`, since we’re now talking about a native class. - isDevelopingAddon: Dropped. This doesn’t belong in each addon’s code, it’s a runtime decision and control over it belongs in ember-cli proper. Under embroider developers can set a comma-separated list of addon package names in the EMBROIDER_REBUILD_ADDONS environment variable instead. - isEnabled: Dropped. Rarely used. This decision doesn’t belong inside an addon, it belongs in the addon’s parent which will decide to activate it or not. Putting it here means every addon needs to invent its own API for how to tell it to activate or not. - lintTree: Kept. This is a legit runtime thing to do. - moduleName: Dropped. Using a moduleName that doesn’t match your NPM package name is a megatroll, and it won’t work with build tools that know how to follow the Node package resolution algorithm. - name: Dropped. Setting a name that doesn’t match your NPM package name is a megatroll. - outputReady: Kept. - postBuild: Kept. - postprocessTree: TODO. need to confirm existing pre/postprocessTree behaviors. I think most of the trees (js, styles, templates, all) only apply to your immediate parent, meaning they can run at publication time when they’re being applied to an addon. - preBuild: Kept - preprocessTree: TODO. Same boat as postprocessTree. - serverMiddleware: Kept. - setupPreprocessorRegistry: Kept. But remember, it will have an effect whenever the consuming package is built, which for apps will be the same as today, but for addons will be publication time. - shouldIncludeChildAddon: Dropped in favor of `activeDependencies` because we’re changing the semantics. This method receives a complete instance representing each child addon, which unintentionally exposes way too much API. And the meaning of being an active dependency has been rationalized. See section **Active Dependencies**. - testemMiddleware: Kept. - treeFor, treeForAddon, treeForAddonTemplates, treeForAddonTestSupport, treeForApp, treeForPublic, treeForStyles, treeForTemplates, treeForTestSupport, treeForVendor: Dropped. Dynamically generating broccoli trees at app build time is no longer supported. Your trees are built at publication time. If you need to produce different output at build time based on dynamic configuration, see **Build-time Conditionals**. New addon hooks: - `activeDependencies`: defined in its own section below Finally, your `build` module may export named constants that will be made available to your runtime Javascript. See **Build-time Conditionals** for details. ## Build-time Conditionals The v2 format deliberately moves a lot of dynamic behavior to publication time. So how do we deal with remaining cases where different code needs to be included based on dynamic information? You may export named `const` values from your `build` module (as defined in the **Addon Hooks** section). These constants will be available to your Javascript via `import { someConstant } from` `'@ember/build-time-config/your-package-name'`, and we guarantee that a dead-code elimination step can see any boolean constant branch predicates (this is how feature flags already work inside Ember itself). For example: import { needsLegacySupport } from '@ember/build-time-config/my-package'; let MyComponent = Component.extend({ //.... }); if (needsLegacySupport) { MyComponent.reopen({ // add some extra code here. It will be stripped from builds that don't need it. }); } export default MyComponent; This is also a motivating example for our support of dynamic `imports()`: it allows you to conditionally depend on other JS modules or CSS: import { provideDefaultStyles } from '@ember/build-time-config/my-package'; if (provideDefaultStyles) { import("../css/default-styles.css"); } Your `build` module is evaluated in Node, not the browser. We just promise that any JSON-serializable constants it exports will get packaged up into the special Ember-provided `@ember/build-time-config` package. **Template Build-time conditionals** TODO: this section is a rough first pass. Once clarified, it should also get reflected in the other places where we talk about the template publication format. We also need build-time conditional capability in templates, because (for example) many of the AST transforms we will be asking addons to pre-apply are supposed to behave differently depending on the Ember version. The input data is exactly the same as used for Javascript build-time conditionals (any JSON-serializable constants exported from your build module are available via the `@ember/build-config-config` package). We add a helper for accessing those values: {{#if (ember-build-time-config "my-package" "needsOldFeature")}} ... {{else}} ... {{/if}} And we implement a transform in the template compiler that does branch elimination based off the values. Note that only Boolean predicates are handled by the dead-code elimination. You can produce Booleans from arbitrary logic in your `build` module (including things like semver tests or feature probing). ## Active Dependencies The `activeDependencies` hook receives the list of names of your **allowed dependencies** that are Ember packages as input and returns either the same list or a subset of the list: activeDependencies(childPackageNames) { if (someThingIsDisabled) { return childPackageNames.filter(name => name !== 'the-one-we-dont-need'); } else { return childPackageNames; } } If you don’t implement the `activeDependencies` hook, all your `dependencies` are considered active. When and only when a package is active: - all standard Ember module types (`your-package/components/*.js`, `your-package/services/*.js`, etc) from its **Own Javascript** _that cannot be statically ruled out as unnecessary_ are included in the build as if some application code has `import`ed them. (What counts as “cannot be statically ruled out” is free to change as apps adopt increasingly static practices. This doesn’t break any already published packages, it just makes builds that consume them more efficient.) - if your **Ember package metadata** contains `"implicit-scripts"` or `"implicit-test-scripts"`, the listed scripts will be included in the consuming app or its tests, respectively. Each of these keys can contain a list of specifier strings that will be resolved relative to the package. This is a backward-compatibility feature for capturing the behavior of v1 packages. New features are encouraged to use direct `import` where possible. Example: "ember-addon": { "version": 2, "implicit-scripts": ["./vendor/my-package/some-shim", "lodash/sortBy"] } Scripts included this way are _not_ interpreted as ES modules. They are evaluated in script context (think `