diff --git a/README.md b/README.md index 5d10de90..0cfe56ca 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,12 @@ [![CI](https://github.com/DazzlingFugu/ember-cli-embedded/actions/workflows/ci.yml/badge.svg)](https://github.com/DazzlingFugu/ember-cli-embedded/actions/workflows/ci.yml) [![Ember Observer Score](https://emberobserver.com/badges/ember-cli-embedded.svg)](https://emberobserver.com/addons/ember-cli-embedded) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -⚠️ This addon depends on [ember-export-application-global](https://github.com/ember-cli/ember-export-application-global) -to get your application globally exposed, but it's deprecated. - Makes it easier to embed your Ember application in another (non-Ember) app. This addon gives you more control over how and when your Ember app will boot and also allows how to add/override some configuration so that the Ember app can boot with some context-dependent config. We found it especially useful, for example, when migrating an existing app to Ember part by part. - ## Compatibility * Ember.js v3.28 or above @@ -37,15 +32,23 @@ In your `config/environment.js`, add the following config to the `ENV`: ```js let ENV = { - ... + ..., + modulePrefix: 'my-app-name', + embedded: { delegateStart: true, config: { // optional // Default values for the config passed at boot }, }, - ... - }; + + /* + * 1. If you leave this flag undefined, you will have to start your app with `MyAppName.start(...)` + * 2. If you set this flag to `SomeOtherAppName` (String), you will have to start your app with `SomeOtherAppName.start(...)` + * 3. If you set this flag to `false` (Boolean), you will NOT be able to start your app with `.start(...)` at all + */ + exportApplicationGlobal: 'SomeOtherAppName' + } ``` Doing so will make your application hold until you manually start it. (read on to learn more) @@ -57,10 +60,11 @@ Doing so will make your application hold until you manually start it. (read on t ### Start your app -In your JS code, execute `MyApp.start(/* optionalConfig */)` to resume the boot of your application. As per the example, it takes an optional configuration as its first argument. +In your JS code, execute `MyAppName.start(/* optionalConfig */)` to resume the boot of your application. As per the example, it takes an optional configuration as its first argument. -Remember: -Your app __will not start__ unless you call `MyApp.start(/* optionalConfig */)` method. +### Attention :warning: +1. Your app __will not start__ unless you call `MyAppName.start(/* optionalConfig */)` method. +2. Calling `MyAppName.start(...)` will __not work__ if you've set `exportApplicationGlobal: false` in `your config/environment.js` ### Access the config from your application @@ -71,14 +75,15 @@ Consider the following `config/environment.js` file: ```js let ENV = { - ... + ..., + modulePrefix: 'my-app', embedded: { config: { option1: 'value-1', }, }, ... - }; + } ``` And the application is started that way: @@ -137,6 +142,8 @@ Consider the following `config/environment.js` file: rootElement: `#some-element`, }, + modulePrefix: 'my-app', + embedded: { config: { option1: 'value-1', diff --git a/addon/initializers/embedded.ts b/addon/initializers/embedded.ts index eb9350f4..1f674cb7 100644 --- a/addon/initializers/embedded.ts +++ b/addon/initializers/embedded.ts @@ -1,27 +1,11 @@ import Application from '@ember/application' import { deprecate } from '@ember/debug' - -interface ObjectConfig { - delegateStart?: - | undefined - | boolean - - config?: - | undefined - | Record // empty object `{}` - | Record -} - -type NullishConfig = - | null - | undefined - -type DeprecatedBooleanConfig = boolean - -type GivenConfig = - | NullishConfig - | DeprecatedBooleanConfig - | ObjectConfig +import { + ObjectConfig, + NullishConfig, + DeprecatedBooleanConfig, + GivenConfig +} from '../../types' function configIsNullish(config: GivenConfig): config is NullishConfig { return config === null || config === undefined @@ -97,6 +81,7 @@ function normalizeConfig(userConfig: GivenConfig): ObjectConfig { } export function initialize(application: Application): void { + const env = application.resolveRegistration('config:environment') as { embedded?: GivenConfig } const embeddedConfig: ObjectConfig = normalizeConfig(env.embedded) @@ -126,5 +111,5 @@ export function initialize(application: Application): void { export default { name: 'ember-cli-embedded', after: 'export-application-global', - initialize, + initialize } diff --git a/addon/initializers/export-application-global.ts b/addon/initializers/export-application-global.ts new file mode 100644 index 00000000..71817648 --- /dev/null +++ b/addon/initializers/export-application-global.ts @@ -0,0 +1,49 @@ +import Application from '@ember/application' +import { classify } from '@ember/string' + +export function initialize(application: Application): void { + const env = application.resolveRegistration('config:environment') as { + embedded?: { + delegateStart: boolean + }, + exportApplicationGlobal: boolean | string, + modulePrefix: string + } + + const mustExportApplicationGlobal = env.embedded?.delegateStart === true && env.exportApplicationGlobal !== false + + if (mustExportApplicationGlobal) { + let theGlobal + + if (typeof window !== 'undefined') { + theGlobal = window + } else if (typeof global !== 'undefined') { + theGlobal = global + } else if (typeof self !== 'undefined') { + theGlobal = self + } else { + return + } + + const value = env.exportApplicationGlobal + + let globalName + + if (typeof value === 'string') { + globalName = value + } else { + globalName = classify(env.modulePrefix) + } + + // @ts-ignore: until there's a way to access a dynamic propertyName of window in TS ? + if (!theGlobal[globalName]) { + // @ts-ignore: until there's a way to set a dynamic propertyName on the window in TS ? + theGlobal[globalName] = application + } + } +} + +export default { + name: 'export-application-global', + initialize +} \ No newline at end of file diff --git a/app/initializers/export-application-global.js b/app/initializers/export-application-global.js new file mode 100644 index 00000000..93552564 --- /dev/null +++ b/app/initializers/export-application-global.js @@ -0,0 +1 @@ +export { default, initialize } from 'ember-cli-embedded/initializers/export-application-global' diff --git a/package.json b/package.json index a6ac3445..0daaedc8 100644 --- a/package.json +++ b/package.json @@ -47,11 +47,11 @@ "dependencies": { "ember-cli-babel": "^7.26.11", "ember-cli-htmlbars": "^6.1.1", - "ember-cli-typescript": "^5.2.1", - "ember-export-application-global": "^2.0.1" + "ember-cli-typescript": "^5.2.1" }, "devDependencies": { "@ember/optional-features": "^2.0.0", + "@ember/string": "^3.1.1", "@ember/test-helpers": "^2.9.3", "@embroider/test-setup": "^1.8.3", "@tsconfig/ember": "^1.1.0", diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/initializers/export-application-global-test.ts b/tests/unit/initializers/export-application-global-test.ts new file mode 100644 index 00000000..be8e0dab --- /dev/null +++ b/tests/unit/initializers/export-application-global-test.ts @@ -0,0 +1,121 @@ +import Application from '@ember/application' +import { initialize } from 'dummy/initializers/export-application-global' +import { module, test } from 'qunit' +import Resolver from 'ember-resolver' +import { classify } from '@ember/string' +import { run } from '@ember/runloop' + +type TestApplication = Application & { + // Public types are currently incomplete, these 2 properties exist: + // https://github.com/emberjs/ember.js/blob/v3.26.1/packages/@ember/application/lib/application.js#L376-L377 + _booted: boolean + _readinessDeferrals: number +} + +// How an app would look like with our Initializer `embedded` +interface EmbeddedApp extends TestApplication { + start?: (config?: Record) => void +} + +interface Context { + TestApplication: typeof Application + application: EmbeddedApp +} + +module('Unit | Initializer | export-application-global', function (hooks) { + hooks.beforeEach(function (this: Context) { + this.TestApplication = class TestApplication extends Application { + modulePrefix = 'whatever' + } + + this.TestApplication.initializer({ + name: 'export application global initializer', + initialize, + }) + + // @ts-ignore: temporarily required as public types are incomplete + this.application = this.TestApplication.create({ + autoboot: false, + Resolver + }) + + this.application.register('config:environment', {}) + }) + + hooks.afterEach(function (this: Context) { + const config:any = this.application.resolveRegistration('config:environment') + const exportedApplicationGlobal:string = classify(config.modulePrefix) + // @ts-ignore: because TS doesn't like window[dynamicPropertyName] + delete window[exportedApplicationGlobal] + run(this.application, 'destroy') + }) + + // @ts-ignore: because QUnit is not set up with TS propertly and does not like .each() + test.each('it adds expected application global to window if config.embedded.delegateStart is true', [ + ['something-random', 'SomethingRandom'], + ['something_more-random', 'SomethingMoreRandom'], + ['something-', 'Something'], + ['something', 'Something'] + ], async function (this: Context, assert: Record, testData: Array>) { + const [modulePrefix, exportedApplicationGlobal] = testData + + this.application.register('config:environment', { + modulePrefix, + embedded: { + delegateStart: true + } + }) + + await this.application.boot() + + // @ts-ignore: because TS doesn't like modulePrefix + assert.strictEqual(classify(modulePrefix), exportedApplicationGlobal, 'it "classifies" module prefix') + + // @ts-ignore: because TS doesn't like window[dynamicPropertyName] + assert.deepEqual(window[exportedApplicationGlobal], this.application, 'it creates expected application global on window') + }) + + test('it does not add application global to window if config.embedded.delegateStart is not true', async function (this: Context, assert) { + this.application.register('config:environment', { + modulePrefix: 'something-random' + }) + + await this.application.boot() + + // @ts-ignore: because TS doesn't like window[dynamicPropertyName] + assert.notOk(window.SomethingRandom) + }) + + test('it does not create application global on window if config.exportApplicationGlobal is false', async function (this: Context, assert) { + this.application.register('config:environment', { + modulePrefix: 'something-random', + embedded: { + delegateStart: true + }, + exportApplicationGlobal: false + }) + + await this.application.boot() + + // @ts-ignore: because TS doesn't like window[dynamicPropertyName] + assert.notOk(window.SomethingRandom) + }) + + test('it adds application global to window using value of config.exportApplicationGlobal, if it is a String', async function (this: Context, assert) { + this.application.register('config:environment', { + modulePrefix: 'something-random', + embedded: { + delegateStart: true + }, + exportApplicationGlobal: 'SomethingElse' + }) + + await this.application.boot() + + // @ts-ignore: because TS doesn't like window.PropertyName ? + assert.deepEqual(window.SomethingElse, this.application, 'name set in config is used for exported application global, instead of original module prefix') + + // @ts-ignore: because TS doesn't like window.PropertyName ? + assert.notOk(window.SomethingRandom, 'original module prefix is not used in exported application global') + }) +}) diff --git a/types/dummy/index.d.ts b/types/dummy/index.d.ts index 85e0b910..bef63c37 100644 --- a/types/dummy/index.d.ts +++ b/types/dummy/index.d.ts @@ -1,3 +1,4 @@ declare module 'dummy/app' declare module 'dummy/initializers/embedded' +declare module 'dummy/initializers/export-application-global' declare module 'dummy/instance-initializers/embedded' diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 00000000..b323e350 --- /dev/null +++ b/types/index.ts @@ -0,0 +1,21 @@ +export interface ObjectConfig { + delegateStart?: + | undefined + | boolean + + config?: + | undefined + | Record // empty object `{}` + | Record +} + +export type NullishConfig = + | null + | undefined + +export type DeprecatedBooleanConfig = boolean + +export type GivenConfig = + | NullishConfig + | DeprecatedBooleanConfig + | ObjectConfig \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0a4ed2b3..be4cb928 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1022,6 +1022,13 @@ mkdirp "^1.0.4" silent-error "^1.1.1" +"@ember/string@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@ember/string/-/string-3.1.1.tgz#0a5ac0d1e4925259e41d5c8d55ef616117d47ff0" + integrity sha512-UbXJ+k3QOrYN4SRPHgXCqYIJ+yWWUg1+vr0H4DhdQPTy8LJfyqwZ2tc5uqpSSnEXE+/1KopHBE5J8GDagAg5cg== + dependencies: + ember-cli-babel "^7.26.6" + "@ember/test-helpers@^2.9.3": version "2.9.3" resolved "https://registry.yarnpkg.com/@ember/test-helpers/-/test-helpers-2.9.3.tgz#c2a9d6ab1c367af92cf1a334f97eb19b8e06e6e1" @@ -4098,11 +4105,6 @@ ember-disable-prototype-extensions@^1.1.3: resolved "https://registry.yarnpkg.com/ember-disable-prototype-extensions/-/ember-disable-prototype-extensions-1.1.3.tgz#1969135217654b5e278f9fe2d9d4e49b5720329e" integrity sha512-SB9NcZ27OtoUk+gfalsc3QU17+54OoqR668qHcuvHByk4KAhGxCKlkm9EBlKJcGr7yceOOAJqohTcCEBqfRw9g== -ember-export-application-global@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ember-export-application-global/-/ember-export-application-global-2.0.1.tgz#b120a70e322ab208defc9e2daebe8d0dfc2dcd46" - integrity sha512-B7wiurPgsxsSGzJuPFkpBWnaeuCu2PGpG2BjyrfA1VcL7//o+5RSnZqiCEY326y7qmxb2GoCgo0ft03KBU0rRw== - ember-load-initializers@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-2.1.2.tgz#8a47a656c1f64f9b10cecdb4e22a9d52ad9c7efa"