Background
We need to support different versions of our application. What constitues a unique version? Anytime we have an ember route that is mostly the same but with a some differences we customize any of the files that make up that screen including: Ember Data models, components, helpers, routes, controllers, route templates, i.e., anything.
Our current approach is to:
- Call a node script from
ember-cli-build.js
that creates an array of all the version files and writes them to an ignored file, e.g.,version-file-names.js
. It looks something like:
export default [
"./app/versions/version-1/components/component-1",
"./app/versions/version-1/components/component-2",
"./app/versions/version-1/controllers/controller-1",
"./app/versions/version-2/components/component-1",
"./app/versions/version-1/components/component-3",
"./app/versions/version-1/controllers/controller-100",
// ...more
];
- In an ember instance initializer:
- Grab the
version-file-names.js
file, filter it down based on the version the application is being built for. The version is set inconfig/environment.js
or on the process. - Grab the factory for each version file, e.g.,
const factory = appInstance.factoryFor(``auction-style:${fileName}``);
. - Register the file on the app instance, e.g.,
appInstance.register(``${singularize(type)}:${fileName}\\``, factory.class);
.
The Problem
This worked well until â in preparation for embroider and as a prerequisite to handling dynamic props passed to the template component
helper â we started switching from classic component structure to co-located, nested components i.e., what was:
app/components/component-1.ts
app/templates/components/component-1.hbs
Is now:
app/components/component-1/index.ts
app/components/component-1/index.hbs
Arguably a much better structure, and if I understand correctly, the future of ember. But now weâre running into a few issues with handling versions of our applications:
- A componentâs
index.hbs
file is compiled down toindex.js
. ember-cli-typescript throws a warning about this because the templateâsindex.js
file is now sibling to the componentâs TS file with the same nameindex.ts
. Hereâs the thread on it: Docs: add info about merged app directory ¡ Issue #780 ¡ typed-ember/ember-cli-typescript ¡ GitHub. Somehow the co-located, nested components directly inapp/components
donât throw this warning, but those in ourapp/versions/version-1/components
folder do. - Registering a co-located, nested component is non-trivial. Here is my non-working attempt. Basically I check to see if the component is template-only, if so, register it one way, if not, register it another way.
import ApplicationInstance from '@ember/application/instance';
import { singularize } from 'ember-inflector';
import VersionManagerService from '@auction-fe/base/services/version-manager';
// import { templateOnlyComponent } from '@glimmer/runtime';
// import { compile } from 'ember-template-compiler';
// @ts-expect-error
import { setComponentTemplate } from '@ember/component';
export function initialize(appInstance: ApplicationInstance): void {
const config = appInstance.lookup('main:config');
if (config.environment !== 'test') {
console.log(appInstance);
setStyleOverrides(appInstance);
}
}
export default { initialize };
function addComponent(
appInstance,
name,
{
ComponentClass = null,
template = null
}: { ComponentClass?: object | null, template?: object | null }
) {
if (ComponentClass) {
register(appInstance, 'components', name, { class: ComponentClass });
}
if (template) {
register(appInstance, 'templates', `components/${name}`, { class: template });
}
}
export function setStyleOverrides(appInstance: ApplicationInstance): void {
const { versionDirectory, modulePrefix } = appInstance.lookup('main:config');
const { version }: VersionManagerService = appInstance.lookup('service:version-manager');
const fileNames: Array<string> = appInstance.factoryFor('version:file-names').class;
let versionFileNames = fileNames.filter((fileName) => fileName.includes(`${versionDirectory}/${version}`));
versionFileNames = versionFileNames.map((fileName) => fileName.replace(`${versionDirectory}/`, ''));
return versionFileNames.forEach((fileName: string) => {
const fileNameWithoutExtension = fileName.replace(/\.(ts|js|hbs)/, '')
const componentRegEx = new RegExp('(.*)(components\/)(.*\/(template|index)$');
const isComponent = componentRegEx.test(fileNameWithoutExtension);
if (isComponent) {
const isTemplate = /(\.hbs)$/.test(fileName);
// NOTE
// Components can be template only or template and TS/JS. We handle
// the TS/JS registration at the same time as the template file, so we
// don't need to process them here.
if (!isTemplate) return;
const fileNameWithoutComponentIndex = fileNameWithoutExtension.replace(/(.*components\/.*)\/index$/, "$1"); // 2.
const templateString = appInstance.factoryFor(`version:${fileNameWithoutExtension}`).class;
const templateModuleName = `${modulePrefix}/${fileNameWithoutComponentIndex}`;
const isTemplateOnly = !versionFileNames.includes(fileName.replace('index.hbs', 'index.ts'));
if (isTemplateOnly) {
addComponent(
appInstance,
fileNameWithoutComponentIndex,
{
// ComponentClass: templateOnlyComponent(),
template: templateString,
},
);
} else {
const componentClass = appInstance.factoryFor(`version:${fileNameWithoutExtension}`).class;
addComponent(
appInstance,
fileNameWithoutComponentIndex,
{
ComponentClass: setComponentTemplate(templateString, componentClass),
}
);
}
} else {
const factory = appInstance.factoryFor(`version:${fileNameWithoutExtension}`);
const [type, ...filePath] = fileName.split('/');
register(appInstance, type, filePath.join('/'), factory);
}
});
}
function register(appInstance: ApplicationInstance, type: string, fileName: string, factory?: { class: unknown }) {
if (factory) {
appInstance.register(`${singularize(type)}:${fileName}`, factory.class);
}
}
Questions
- How do we maintain a set of co-located, nested components outside the
app
directory without getting the ember-cli-typescript warning about a JS/TS file collision? - What is the correct way to register a co-located, nested component on an app instance?
Alternative Solutions
- We have considered â and would very much like â to simply use broccoli and
ember-cli-build.js
file to merge our âversionâ file tree into our app tree (the trees can and should match exactly except their parent directory name) when the application is built. But there is a giant problem with that: testing. For tests we need to be able to dynamically set the version per test. This requires all version files to be registered at build time. - Something else better/simpler?