├── app
├── .gitkeep
└── initializers
│ └── ember-cli-conditional-compile-features.js
├── addon
└── .gitkeep
├── vendor
└── .gitkeep
├── tests
├── unit
│ ├── .gitkeep
│ └── controllers
│ │ └── application-test.js
├── dummy
│ ├── app
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── models
│ │ │ └── .gitkeep
│ │ ├── routes
│ │ │ └── .gitkeep
│ │ ├── styles
│ │ │ └── app.css
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ ├── .gitkeep
│ │ │ └── application.js
│ │ ├── templates
│ │ │ ├── components
│ │ │ │ └── .gitkeep
│ │ │ └── application.hbs
│ │ ├── router.js
│ │ ├── app.js
│ │ └── index.html
│ ├── public
│ │ ├── robots.txt
│ │ └── crossdomain.xml
│ └── config
│ │ ├── optional-features.json
│ │ ├── feature-flags.js
│ │ ├── ember-cli-update.json
│ │ └── environment.js
├── helpers
│ ├── resolver.js
│ └── index.js
├── test-helper.js
├── .jshintrc
├── index.html
├── acceptance
│ └── application-test.js
└── integration
│ └── components
│ └── compiler-test.js
├── .watchmanconfig
├── config
├── environment.js
├── feature-flags.js
└── ember-try.js
├── ember-cli-build.js
├── .travis.yml
├── .npmignore
├── node-tests
├── with-extra-config-file
│ └── config
│ │ └── feature-flags.js
└── addon-test.js
├── .gitignore
├── .ember-cli
├── .editorconfig
├── CHANGELOG.md
├── Makefile
├── CONTRIBUTING.md
├── testem.js
├── LICENSE.md
├── .eslintrc.js
├── .github
└── workflows
│ └── ci.yml
├── lib
└── template-compiler.js
├── package.json
├── index.js
└── README.md
/app/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": ["tmp"]
3 | }
4 |
--------------------------------------------------------------------------------
/tests/dummy/public/robots.txt:
--------------------------------------------------------------------------------
1 | # http://www.robotstxt.org
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/config/environment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(/* environment, appConfig */) {
4 | return { };
5 | };
6 |
--------------------------------------------------------------------------------
/tests/dummy/config/optional-features.json:
--------------------------------------------------------------------------------
1 | {
2 | "application-template-wrapper": false,
3 | "default-async-observers": true,
4 | "jquery-integration": false,
5 | "template-only-glimmer-components": true
6 | }
7 |
--------------------------------------------------------------------------------
/ember-cli-build.js:
--------------------------------------------------------------------------------
1 | /* global require, module */
2 | const EmberApp = require('ember-cli/lib/broccoli/ember-addon');
3 |
4 | module.exports = function(defaults) {
5 | const app = new EmberApp(defaults, {});
6 |
7 | return app.toTree();
8 | };
9 |
--------------------------------------------------------------------------------
/config/feature-flags.js:
--------------------------------------------------------------------------------
1 | module.exports = function() {
2 |
3 | const GLOBAL_FLAGS = {
4 | featureFlags: {
5 | ENABLE_FOO: true,
6 | ENABLE_BAR: false
7 | },
8 | includeDirByFlag: {
9 | ENABLE_FOO: []
10 | }
11 | }
12 | return GLOBAL_FLAGS
13 | }
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/application.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Controller.extend({
4 | foo: Ember.computed(() => {
5 | return ENABLE_FOO;
6 | }),
7 |
8 | bar: Ember.computed(() => {
9 | return ENABLE_BAR;
10 | })
11 | });
12 |
--------------------------------------------------------------------------------
/tests/dummy/app/router.js:
--------------------------------------------------------------------------------
1 | import EmberRouter from '@ember/routing/router';
2 | import config from './config/environment';
3 |
4 | let Router = EmberRouter.extend({
5 | location: config.locationType
6 | });
7 |
8 | Router.map(function() {
9 | });
10 |
11 | export default Router;
12 |
--------------------------------------------------------------------------------
/tests/dummy/config/feature-flags.js:
--------------------------------------------------------------------------------
1 | module.exports = function() {
2 |
3 | const GLOBAL_FLAGS = {
4 | featureFlags: {
5 | ENABLE_FOO: true,
6 | ENABLE_BAR: false
7 | },
8 | includeDirByFlag: {
9 | ENABLE_FOO: []
10 | }
11 | }
12 | return GLOBAL_FLAGS
13 | }
--------------------------------------------------------------------------------
/tests/helpers/resolver.js:
--------------------------------------------------------------------------------
1 | import Resolver from 'ember/resolver';
2 | import config from '../../config/environment';
3 |
4 | const resolver = Resolver.create();
5 |
6 | resolver.namespace = {
7 | modulePrefix: config.modulePrefix,
8 | podModulePrefix: config.podModulePrefix
9 | };
10 |
11 | export default resolver;
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 | node_js:
4 | - "10"
5 |
6 | sudo: false
7 | cache: yarn
8 |
9 | matrix:
10 | fast_finish: true
11 |
12 | before_install:
13 | - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH
14 |
15 | install:
16 | - yarn install
17 |
18 | script:
19 | - yarn test:node && ember try:each
20 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | bower_components/
2 | tests/
3 | node-tests/
4 | tmp/
5 | dist/
6 | config/ember-try.js
7 |
8 | Makefile
9 | .bowerrc
10 | .editorconfig
11 | .eslintrc.js
12 | .watchmanconfig
13 | .ember-cli
14 | .travis.yml
15 | .npmignore
16 | **/.gitkeep
17 | bower.json
18 | ember-cli-build.js
19 | Brocfile.js
20 | testem.js
21 | README.md
22 |
--------------------------------------------------------------------------------
/tests/test-helper.js:
--------------------------------------------------------------------------------
1 | import Application from 'dummy/app';
2 | import config from 'dummy/config/environment';
3 | import { setApplication } from '@ember/test-helpers';
4 | import * as QUnit from 'qunit';
5 | import { setup } from 'qunit-dom';
6 | import { start } from 'ember-qunit';
7 |
8 | setup(QUnit.assert);
9 | setApplication(Application.create(config.APP));
10 | start();
--------------------------------------------------------------------------------
/app/initializers/ember-cli-conditional-compile-features.js:
--------------------------------------------------------------------------------
1 |
2 | const initializer = {
3 | name: 'ember-cli-conditional-compile-features',
4 | initialize: function() {}
5 | };
6 |
7 | const feature_flags = EMBER_CLI_CONDITIONAL_COMPILE_INJECTIONS;
8 | Object.keys(feature_flags).map(function(flag) {
9 | window[flag] = feature_flags[flag];
10 | })
11 |
12 | export default initializer;
13 |
--------------------------------------------------------------------------------
/node-tests/with-extra-config-file/config/feature-flags.js:
--------------------------------------------------------------------------------
1 | module.exports = function(environment) {
2 |
3 | const GLOBAL_FLAGS = {
4 | featureFlags: {
5 | ENABLE_FROM_FILE: true,
6 | ENABLE_FROM_FILE_CLOWNSTAGE: false,
7 | },
8 | includeDirByFlag: {
9 | ENABLE_FROM_FILE: [],
10 | }
11 | }
12 | if (environment === 'clownstage') {
13 | GLOBAL_FLAGS.featureFlags.ENABLE_FROM_FILE_CLOWNSTAGE = true;
14 | }
15 |
16 | return GLOBAL_FLAGS
17 | }
--------------------------------------------------------------------------------
/tests/dummy/app/app.js:
--------------------------------------------------------------------------------
1 | import config from './config/environment';
2 | import Application from '@ember/application';
3 | import loadInitializers from 'ember-load-initializers';
4 | import Resolver from 'ember-resolver';
5 |
6 | let App;
7 |
8 | App = Application.extend({
9 | modulePrefix: config.modulePrefix,
10 | podModulePrefix: config.podModulePrefix,
11 | Resolver: Resolver
12 | });
13 |
14 | loadInitializers(App, config.modulePrefix);
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 |
10 | # misc
11 | /.sass-cache
12 | /connect.lock
13 | /coverage/*
14 | /libpeerconnection.log
15 | /npm-debug.log*
16 | /testem.log
17 | /yarn-error.log
18 | .tool-versions
19 |
20 | # ember-try
21 | /.node_modules.ember-try/
22 | /bower.json.ember-try
23 | /npm-shrinkwrap.json.ember-try
24 | /package.json.ember-try
25 | /package-lock.json.ember-try
26 | /yarn.lock.ember-try
27 |
28 |
--------------------------------------------------------------------------------
/tests/unit/controllers/application-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setupTest } from '../../helpers/index';
3 |
4 | module('Unit | Controller | application', function(hooks) {
5 | setupTest(hooks);
6 |
7 | // Replace this with your real tests.
8 | test('it can access set properties', function(assert) {
9 | let controller = this.owner.lookup('controller:application');
10 |
11 | assert.ok(controller);
12 | assert.equal(true, controller.get('foo'));
13 | assert.equal(false, controller.get('bar'));
14 | });
15 | });
--------------------------------------------------------------------------------
/tests/dummy/config/ember-cli-update.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": "1.0.0",
3 | "packages": [
4 | {
5 | "name": "ember-cli",
6 | "version": "4.4.0",
7 | "blueprints": [
8 | {
9 | "name": "addon",
10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output",
11 | "codemodsSource": "ember-addon-codemods-manifest@1",
12 | "isBaseBlueprint": true,
13 | "options": [
14 | "--yarn",
15 | "--no-welcome"
16 | ]
17 | }
18 | ]
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Ember CLI sends analytics information by default. The data is completely
4 | anonymous, but there are times when you might want to disable this behavior.
5 |
6 | Setting `disableAnalytics` to true will prevent any data from being sent.
7 | */
8 | "disableAnalytics": false,
9 |
10 | /**
11 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
12 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
13 | */
14 | "isTypeScriptProject": false
15 | }
16 |
--------------------------------------------------------------------------------
/tests/dummy/public/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dummy
6 |
7 |
8 |
9 | {{content-for 'head'}}
10 |
11 |
12 |
13 |
14 | {{content-for 'head-footer'}}
15 |
16 |
17 | {{content-for 'body'}}
18 |
19 |
20 |
21 |
22 | {{content-for 'body-footer'}}
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [*.js]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [*.hbs]
21 | insert_final_newline = false
22 | indent_style = space
23 | indent_size = 2
24 |
25 | [*.css]
26 | indent_style = space
27 | indent_size = 2
28 |
29 | [*.html]
30 | indent_style = space
31 | indent_size = 2
32 |
33 | [*.{diff,md}]
34 | trim_trailing_whitespace = false
35 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 2.0.0
4 |
5 | ### Breaking changes
6 |
7 | The semantics of `includeDirByFlag` have changed and now take a file glob pattern instead of a RegExp. This is because the mechanism for removing files from the build pipeline has changed to work around a bug in broccoli. (#111)
8 |
9 | ### New features
10 |
11 | - Support for external config file to prevent leaking of feature flag names (#110)
12 | - Support for multiple environments (For example to roll out features on a staging environment) (#110)
13 |
14 | ### Updates/Changes
15 |
16 | - Modernised the code and all dependencies to be compatible with Ember 3.x and up (#107)
17 | - Added @ember/string to be compatible with Ember 5.x (#117)
18 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REMOTE=origin
2 |
3 | # Utility target for checking required parameters
4 | guard-%:
5 | @if [ "$($*)" = '' ]; then \
6 | echo "Missing required $* variable."; \
7 | exit 1; \
8 | fi;
9 |
10 | release: guard-VERSION
11 | @if [ "$$(git rev-parse --abbrev-ref HEAD)" != "master" ]; then \
12 | echo "You must be on master to update the version"; \
13 | exit 1; \
14 | fi;
15 | sed -i "" -e 's/"version": ".*/"version": "$(VERSION)",/' package.json
16 |
17 | git add ./package.json
18 | git commit ./package.json -m 'Bump version to $(VERSION)'
19 | git tag release/$(VERSION) -m 'ember-cli-conditional-compile $(VERSION) - $(DATE)'
20 | git push $(REMOTE) --tags
21 | git push $(REMOTE) master
22 | npm publish
23 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How To Contribute
2 |
3 | ## Installation
4 |
5 | * `git clone `
6 | * `cd ember-cli-conditional-compile`
7 | * `yarn install`
8 |
9 | ## Linting
10 |
11 | * `yarn lint`
12 | * `yarn lint:fix`
13 |
14 | ## Running tests
15 |
16 | * `ember test` – Runs the test suite on the current Ember version
17 | * `ember test --server` – Runs the test suite in "watch mode"
18 | * `ember try:each` – Runs the test suite against multiple Ember versions
19 |
20 | ## Running the dummy application
21 |
22 | * `ember serve`
23 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200).
24 |
25 | For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/).
26 |
--------------------------------------------------------------------------------
/testem.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | test_page: 'tests/index.html?hidepassed',
4 | disable_watching: true,
5 | launch_in_ci: [
6 | 'Chrome'
7 | ],
8 | launch_in_dev: [
9 | 'Chrome'
10 | ],
11 | browser_start_timeout: 60,
12 | browser_disconnect_timeout: 1000,
13 | parallel: -1,
14 | browser_args: {
15 | Chrome: [
16 | '--disable-gpu',
17 | '--disable-dev-shm-usage',
18 | '--disable-software-rasterizer',
19 | '--disable-web-security',
20 | '--headless',
21 | '--incognito',
22 | '--mute-audio',
23 | '--no-sandbox',
24 | '--remote-debugging-address=0.0.0.0',
25 | '--remote-debugging-port=9222',
26 | '--window-size=1440,900'
27 | ]
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/tests/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "document",
4 | "window",
5 | "location",
6 | "setTimeout",
7 | "$",
8 | "-Promise",
9 | "define",
10 | "console",
11 | "visit",
12 | "exists",
13 | "fillIn",
14 | "click",
15 | "keyEvent",
16 | "triggerEvent",
17 | "find",
18 | "findWithAssert",
19 | "wait",
20 | "DS",
21 | "andThen",
22 | "currentURL",
23 | "currentPath",
24 | "currentRouteName",
25 | "ENABLE_FOO",
26 | "ENABLE_BAR"
27 | ],
28 | "node": false,
29 | "browser": false,
30 | "boss": true,
31 | "curly": true,
32 | "debug": false,
33 | "devel": false,
34 | "eqeqeq": true,
35 | "evil": true,
36 | "forin": false,
37 | "immed": false,
38 | "laxbreak": false,
39 | "newcap": true,
40 | "noarg": true,
41 | "noempty": false,
42 | "nonew": false,
43 | "nomen": false,
44 | "onevar": false,
45 | "plusplus": false,
46 | "regexp": false,
47 | "undef": true,
48 | "sub": true,
49 | "strict": false,
50 | "white": false,
51 | "eqnull": true,
52 | "esnext": true,
53 | "unused": true
54 | }
55 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/application.hbs:
--------------------------------------------------------------------------------
1 | Welcome to Ember
2 |
3 | {{outlet}}
4 |
5 | {{#if-flag-ENABLE_FOO}}
6 | ENABLED_FOO!! \o/
7 | {{else}}
8 | DISABLED_FOO!! \o/
9 | {{/if-flag-ENABLE_FOO}}
10 |
11 | {{#if-flag-ENABLE_BAR}}
12 | ENABLED_BAR!! \o/
13 | {{else}}
14 | DISABLED_BAR!! \o/
15 | {{/if-flag-ENABLE_BAR}}
16 |
17 | {{#unless-flag-ENABLE_FOO}}
18 | ENABLED_FOO!! \o/
19 | {{else}}
20 | DISABLED_FOO!! \o/
21 | {{/unless-flag-ENABLE_FOO}}
22 |
23 | {{#unless-flag-ENABLE_BAR}}
24 | ENABLED_BAR!! \o/
25 | {{else}}
26 | DISABLED_BAR!! \o/
27 | {{/unless-flag-ENABLE_BAR}}
28 |
29 | {{#if-flag ENABLE_FOO}}
30 | ENABLED_FOO!! \o/
31 | {{else}}
32 | DISABLED_FOO!! \o/
33 | {{/if-flag}}
34 |
35 | {{#unless-flag ENABLE_BAR}}
36 | ENABLE_BAR!! \o/
37 | {{else}}
38 | DISABLED_BAR!! \o/
39 | {{/unless-flag}}
40 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dummy Tests
6 |
7 |
8 |
9 | {{content-for "head"}}
10 | {{content-for "test-head"}}
11 |
12 |
13 |
14 |
15 |
16 | {{content-for "head-footer"}}
17 | {{content-for "test-head-footer"}}
18 |
19 |
20 | {{content-for "body"}}
21 | {{content-for "test-body"}}
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{content-for "body-footer"}}
37 | {{content-for "test-body-footer"}}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/tests/helpers/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | setupApplicationTest as upstreamSetupApplicationTest,
3 | setupRenderingTest as upstreamSetupRenderingTest,
4 | setupTest as upstreamSetupTest
5 | } from 'ember-qunit';
6 |
7 | // This file exists to provide wrappers around ember-qunit's / ember-mocha's
8 | // test setup functions. This way, you can easily extend the setup that is
9 | // needed per test type.
10 |
11 | function setupApplicationTest(hooks, options) {
12 | upstreamSetupApplicationTest(hooks, options);
13 |
14 | // Additional setup for application tests can be done here.
15 | //
16 | // For example, if you need an authenticated session for each
17 | // application test, you could do:
18 | //
19 | // hooks.beforeEach(async function () {
20 | // await authenticateSession(); // ember-simple-auth
21 | // });
22 | //
23 | // This is also a good place to call test setup functions coming
24 | // from other addons:
25 | //
26 | // setupIntl(hooks); // ember-intl
27 | // setupMirage(hooks); // ember-cli-mirage
28 | }
29 |
30 | function setupRenderingTest(hooks, options) {
31 | upstreamSetupRenderingTest(hooks, options);
32 |
33 | // Additional setup for rendering tests can be done here.
34 | }
35 |
36 | function setupTest(hooks, options) {
37 | upstreamSetupTest(hooks, options);
38 |
39 | // Additional setup for unit tests can be done here.
40 | }
41 |
42 | export { setupApplicationTest, setupRenderingTest, setupTest };
43 |
--------------------------------------------------------------------------------
/tests/dummy/config/environment.js:
--------------------------------------------------------------------------------
1 | module.exports = function(environment) {
2 | let ENV = {
3 | modulePrefix: 'dummy',
4 | environment: environment,
5 | rootURL: '/',
6 | locationType: 'history',
7 | contentSecurityPolicy: {
8 | 'default-src': "'none'",
9 | 'script-src': "'self' 'unsafe-inline' 'unsafe-eval'",
10 | 'font-src': "'self' data:",
11 | 'connect-src': "'self'",
12 | 'img-src': "'self'",
13 | 'style-src': "'self' 'unsafe-inline'",
14 | 'frame-src': ""
15 | },
16 | EmberENV: {
17 | FEATURES: {
18 | // Here you can enable experimental features on an ember canary build
19 | // e.g. 'with-controller': true
20 | }
21 | },
22 |
23 | APP: {
24 | // Here you can pass flags/options to your application instance
25 | // when it is created
26 | }
27 | };
28 |
29 | if (environment === 'development') {
30 | // ENV.APP.LOG_RESOLVER = true;
31 | // ENV.APP.LOG_ACTIVE_GENERATION = true;
32 | // ENV.APP.LOG_TRANSITIONS = true;
33 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
34 | // ENV.APP.LOG_VIEW_LOOKUPS = true;
35 | }
36 |
37 | if (environment === 'test') {
38 | // Testem prefers this...
39 | ENV.rootURL = '/';
40 | ENV.locationType = 'none';
41 | ENV.APP.autoboot = false;
42 | // keep test console output quieter
43 | ENV.APP.LOG_ACTIVE_GENERATION = false;
44 | ENV.APP.LOG_VIEW_LOOKUPS = false;
45 |
46 | ENV.APP.rootElement = '#ember-testing';
47 | }
48 |
49 | return ENV;
50 | };
51 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | ecmaVersion: 2018,
5 | sourceType: 'module'
6 | },
7 | extends: [
8 | 'eslint:recommended'
9 | ],
10 | env: {
11 | 'browser': false,
12 | 'es6': true,
13 | 'node': true
14 | },
15 | globals: {
16 | 'window': true,
17 | 'EMBER_CLI_CONDITIONAL_COMPILE_INJECTIONS': true,
18 | 'ENABLE_FOO': true,
19 | 'ENABLE_BAR': true,
20 | 'visit': true,
21 | 'andThen': true,
22 | 'find': true
23 | },
24 | rules: {
25 | 'array-bracket-spacing': ['error', 'never'],
26 | 'arrow-spacing': ['error'],
27 | 'block-spacing': ['error'],
28 | 'brace-style': ['error', '1tbs'],
29 | 'comma-dangle': ['error', 'never'],
30 | 'comma-style': ['error'],
31 | 'eqeqeq': ['error'],
32 | 'func-call-spacing': ['error', 'never'],
33 | 'indent': ['error', 2, {
34 | 'ArrayExpression': 1,
35 | 'CallExpression': { 'arguments': 1 },
36 | 'ObjectExpression': 1,
37 | 'SwitchCase': 1,
38 | 'VariableDeclarator': 1
39 | }],
40 | 'key-spacing': ['error'],
41 | 'keyword-spacing': ['error'],
42 | 'linebreak-style': ['error'],
43 | 'no-confusing-arrow': ['error'],
44 | 'no-trailing-spaces': ['error'],
45 | 'no-var': ['error'],
46 | 'object-curly-spacing': ['error', 'always'],
47 | 'one-var-declaration-per-line': ['error'],
48 | 'semi-spacing': ['error'],
49 | 'space-before-blocks': ['error', 'always'],
50 | 'space-before-function-paren': ['error', 'never'],
51 | 'space-in-parens': ['error', 'never'],
52 | 'spaced-comment': ['error', 'always', {
53 | 'block': { 'balanced': true }
54 | }]
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/tests/acceptance/application-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { visit } from '@ember/test-helpers';
3 | import { setupApplicationTest } from '../helpers/index';
4 |
5 | module('Acceptance | application', function(hooks) {
6 | setupApplicationTest(hooks);
7 |
8 | test('enabled flags are shown', async function(assert) {
9 | await visit('/');
10 | assert.dom('.enabled_foo').exists().hasText('ENABLED_FOO!! \\o/');
11 | });
12 |
13 | test('enabled flags are shown for unless helper', async function(assert) {
14 | await visit('/');
15 |
16 | assert.dom('.unless_disabled_foo').exists().hasText('DISABLED_FOO!! \\o/')
17 | });
18 |
19 | test('disabled flags are not shown', async function(assert) {
20 | await visit('/');
21 |
22 | assert.dom('enabled_bar').doesNotExist();
23 | });
24 |
25 | test('disabled else blocks are shown', async function(assert) {
26 | await visit('/');
27 |
28 | assert.dom('.disabled_bar').exists().hasText('DISABLED_BAR!! \\o/')
29 | });
30 |
31 | test('enabled else blocks are not shown', async function(assert) {
32 | await visit('/');
33 |
34 | assert.dom('.disabled_foo').doesNotExist();
35 | });
36 |
37 | test('new style flag enabled blocks are shown', async function(assert) {
38 | await visit('/');
39 |
40 | assert.dom('.new_flag_enabled_foo').exists()
41 | assert.dom('.new_flag_disabled_foo').doesNotExist()
42 | });
43 |
44 | test('new style unless flag enabled blocks are shown', async function(assert) {
45 | await visit('/');
46 |
47 | assert.dom('.new_flag_unless_enabled_bar').exists()
48 | assert.dom('.new_flag_unless_disabled_bar').doesNotExist()
49 | });
50 | });
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - master
8 | pull_request: {}
9 |
10 | concurrency:
11 | group: ci-${{ github.head_ref || github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | test:
16 | name: "Tests"
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Install Node
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: 14.x
25 | cache: 'yarn'
26 | - name: Install Dependencies
27 | run: yarn install --frozen-lockfile
28 | - name: Lint
29 | run: yarn lint
30 | - name: Run Tests
31 | run: yarn test
32 |
33 | floating:
34 | name: "Floating Dependencies"
35 | runs-on: ubuntu-latest
36 |
37 | steps:
38 | - uses: actions/checkout@v3
39 | - uses: actions/setup-node@v3
40 | with:
41 | node-version: 14.x
42 | cache: yarn
43 | - name: Install Dependencies
44 | run: yarn install --no-lockfile
45 | - name: Run Tests
46 | run: yarn test
47 |
48 | try-scenarios:
49 | name: ${{ matrix.try-scenario }}
50 | runs-on: ubuntu-latest
51 | needs: "test"
52 |
53 | strategy:
54 | fail-fast: false
55 | matrix:
56 | try-scenario:
57 | - ember-lts-3.16
58 | - ember-lts-3.20
59 | - ember-lts-3.24
60 | - ember-lts-3.28
61 | - ember-release
62 | - ember-beta
63 | - ember-canary
64 | - ember-classic
65 | # - embroider-safe
66 | # - embroider-optimized
67 |
68 | steps:
69 | - uses: actions/checkout@v3
70 | - name: Install Node
71 | uses: actions/setup-node@v3
72 | with:
73 | node-version: 14.x
74 | cache: yarn
75 | - name: Install Dependencies
76 | run: yarn install --frozen-lockfile
77 | - name: Run Tests
78 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }}
79 |
--------------------------------------------------------------------------------
/config/ember-try.js:
--------------------------------------------------------------------------------
1 | const getChannelURL = require('ember-source-channel-url');
2 |
3 | module.exports = async function() {
4 | return {
5 | useYarn: true,
6 | scenarios: [
7 | {
8 | name: 'ember-lts-3.16',
9 | npm: {
10 | devDependencies: {
11 | 'ember-source': '~3.16.10'
12 | }
13 | }
14 | },
15 | {
16 | name: 'ember-lts-3.20',
17 | npm: {
18 | devDependencies: {
19 | 'ember-source': '~3.20.7'
20 | }
21 | }
22 | },
23 | {
24 | name: 'ember-lts-3.24',
25 | npm: {
26 | devDependencies: {
27 | 'ember-source': '~3.24.3'
28 | }
29 | }
30 | },
31 | {
32 | name: 'ember-lts-3.28',
33 | npm: {
34 | devDependencies: {
35 | 'ember-source': '~3.28.8'
36 | }
37 | }
38 | },
39 | {
40 | name: 'ember-release',
41 | npm: {
42 | devDependencies: {
43 | 'ember-source': await getChannelURL('release')
44 | }
45 | }
46 | },
47 | {
48 | name: 'ember-beta',
49 | npm: {
50 | devDependencies: {
51 | 'ember-source': await getChannelURL('beta')
52 | }
53 | }
54 | },
55 | {
56 | name: 'ember-canary',
57 | npm: {
58 | devDependencies: {
59 | 'ember-source': await getChannelURL('canary')
60 | }
61 | }
62 | },
63 | {
64 | name: 'ember-classic',
65 | env: {
66 | EMBER_OPTIONAL_FEATURES: JSON.stringify({
67 | 'application-template-wrapper': true,
68 | 'default-async-observers': false,
69 | 'template-only-glimmer-components': false
70 | })
71 | },
72 | npm: {
73 | devDependencies: {
74 | 'ember-source': '~3.28.8'
75 | },
76 | ember: {
77 | edition: 'classic'
78 | }
79 | }
80 | }
81 | ]
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/tests/integration/components/compiler-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setupRenderingTest } from '../../helpers/index';
3 | import { render } from '@ember/test-helpers';
4 | import hbs from 'htmlbars-inline-precompile';
5 |
6 | module('Integration | Component | compiler', function(hooks) {
7 | setupRenderingTest(hooks);
8 |
9 | test('precompile enabled flags', async function(assert) {
10 | await render(hbs`
11 | {{#if-flag-ENABLE_FOO}}Foo{{/if-flag-ENABLE_FOO}}
12 | `);
13 |
14 | assert.dom(this.element).hasText('Foo');
15 | });
16 |
17 | test('precompile {{unless-flag}} enabled flags', async function(assert) {
18 | render(hbs`
19 | {{#unless-flag-ENABLE_FOO}}Foo{{/unless-flag-ENABLE_FOO}}
20 | `);
21 |
22 | assert.dom(this.element).hasText('');
23 | });
24 |
25 | test('precompile enabled flags', async function(assert) {
26 | await render(hbs`
27 | {{#if-flag-ENABLE_FOO}}Foo{{/if-flag-ENABLE_FOO}}
28 | `);
29 |
30 | assert.dom(this.element).hasText('Foo');
31 | });
32 |
33 | test('precompile {{unless-flag}} enabled flags', async function(assert) {
34 | await render(hbs`
35 | {{#unless-flag-ENABLE_FOO}}Foo{{/unless-flag-ENABLE_FOO}}
36 | `);
37 |
38 | assert.dom(this.element).hasText('');
39 | });
40 |
41 | test('precompile disabled flags', async function(assert) {
42 | await render(hbs`
43 | {{#if-flag-ENABLE_BAR}}Bar{{/if-flag-ENABLE_BAR}}
44 | `);
45 |
46 | assert.dom(this.element).hasText('');
47 | });
48 |
49 | test('precompile {{unless-flag}} disabled flags', async function(assert) {
50 | await render(hbs`
51 | {{#unless-flag-ENABLE_BAR}}Bar{{/unless-flag-ENABLE_BAR}}
52 | `);
53 |
54 | assert.dom(this.element).hasText('Bar');
55 | });
56 |
57 | test('precompile else block', async function(assert) {
58 | await render(hbs`
59 | {{#if-flag-ENABLE_BAR}}Bar{{else}}Baz{{/if-flag-ENABLE_BAR}}
60 | `);
61 |
62 | assert.dom(this.element).hasText('Baz');
63 | });
64 |
65 | test('precompile {{unless-flag}} else block', async function(assert) {
66 | await render(hbs`
67 | {{#unless-flag-ENABLE_BAR}}Bar{{else}}Baz{{/unless-flag-ENABLE_BAR}}
68 | `);
69 |
70 | assert.dom(this.element).hasText('Bar');
71 | });
72 | });
73 |
74 |
75 |
--------------------------------------------------------------------------------
/lib/template-compiler.js:
--------------------------------------------------------------------------------
1 | let compileFlags = null;
2 | let tEnv = null
3 |
4 | function transform(ast) {
5 | let walker = new tEnv.syntax.Walker();
6 |
7 | walker.visit(ast, function(node) {
8 | if (!validate(node)) {
9 | return;
10 | }
11 |
12 | processParams(node);
13 | });
14 |
15 | // return ast;
16 | }
17 |
18 |
19 | function ConditionalTemplateCompiler(env) {
20 | tEnv = env;
21 | return {
22 | name: 'condititional-compiler',
23 | visitor: {
24 | Program(ast) {
25 | return transform(ast);
26 | }
27 | }
28 | }
29 | }
30 |
31 | function mungeNode(node, flag, unless) {
32 | let b = tEnv.syntax.builders;
33 |
34 | if (!unless) {
35 | node.path = b.path('if');
36 | } else {
37 | node.path = b.path('unless');
38 | }
39 |
40 | let flagEnabled = compileFlags[flag];
41 |
42 | node.params = [b.boolean(flagEnabled)];
43 |
44 | if (unless) {
45 | flagEnabled = !flagEnabled;
46 | }
47 |
48 | if (flagEnabled && node.inverse) {
49 | node.inverse = b.program(false, false, node.inverse.loc);
50 | } else if (!flagEnabled && node.program) {
51 | node.program = b.program(false, false, node.program.loc);
52 | }
53 | }
54 |
55 | function mungePlainHelperNode(node, unless) {
56 | let compileFlag = node.params[0]['original'];
57 |
58 | if (!(compileFlag in compileFlags)) {
59 | throw 'No compile flag found for ' + compileFlag;
60 | }
61 |
62 | mungeNode(node, compileFlag, unless);
63 | }
64 |
65 | function processParams(node) {
66 | for (let flag in compileFlags) {
67 | if (node.path.original === 'if-flag-' + flag) {
68 | mungeNode(node, flag, false);
69 | }
70 |
71 | if (node.path.original === 'unless-flag-' + flag) {
72 | mungeNode(node, flag, true);
73 | }
74 | }
75 |
76 | if (node.path.original === 'if-flag') {
77 | mungePlainHelperNode(node, false);
78 | }
79 |
80 | if (node.path.original === 'unless-flag') {
81 | mungePlainHelperNode(node, true);
82 | }
83 | }
84 |
85 | function validate(node) {
86 | let nodeType = (node.type === 'BlockStatement' || node.type === 'MustacheStatement');
87 |
88 | if (!nodeType) {
89 | return false;
90 | }
91 |
92 | return (
93 | node.path && (
94 | node.path.original.startsWith('if-flag') ||
95 | node.path.original.startsWith('unless-flag')
96 | )
97 | );
98 | }
99 |
100 | module.exports = function(flags) {
101 | compileFlags = flags;
102 | return ConditionalTemplateCompiler;
103 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-cli-conditional-compile",
3 | "version": "2.0.0",
4 | "description": "Conditional compilation (feature-flags) for Ember apps",
5 | "keywords": [
6 | "ember-addon",
7 | "ember-cli",
8 | "feature-flags",
9 | "ember-compile"
10 | ],
11 | "repository": "https://github.com/minichate/ember-cli-conditional-compile.git",
12 | "license": "MIT",
13 | "author": "",
14 | "directories": {
15 | "doc": "doc",
16 | "test": "tests"
17 | },
18 | "scripts": {
19 | "build": "ember build",
20 | "lint": "eslint app config lib tests",
21 | "start": "ember server",
22 | "test": "yarn test:node && yarn test:ember",
23 | "test:ember": "ember test",
24 | "test:node": "mocha node-tests"
25 | },
26 | "dependencies": {
27 | "babel-plugin-inline-replace-variables": "1.3.1",
28 | "broccoli-babel-transpiler": "^7.1.1",
29 | "broccoli-funnel": "~2.0.1",
30 | "broccoli-replace": "0.12.0",
31 | "chalk": "~2.4.2",
32 | "ember-cli-babel": "^7.26.11",
33 | "ember-cli-version-checker": "~3.0.1",
34 | "lodash.merge": "~4.6.1",
35 | "object-hash": "^1.3.1"
36 | },
37 | "devDependencies": {
38 | "@babel/core": "^7.18.2",
39 | "@ember/optional-features": "^1.3.0",
40 | "@ember/string": "^3.1.1",
41 | "@ember/test-helpers": "^2.7.0",
42 | "@glimmer/component": "^1.0.0",
43 | "@glimmer/tracking": "^1.0.0",
44 | "babel-eslint": "~10.0.1",
45 | "broccoli-asset-rev": "~3.0.0",
46 | "broccoli-test-helper": "^2.0.0",
47 | "chai": "^4.2.0",
48 | "co": "^4.6.0",
49 | "common-tags": "^1.8.0",
50 | "ember-auto-import": "^2.4.2",
51 | "ember-cli": "^4.4.0",
52 | "ember-cli-dependency-checker": "~3.3.1",
53 | "ember-cli-eslint": "~5.1.0",
54 | "ember-cli-htmlbars": "6.0.1",
55 | "ember-cli-inject-live-reload": "~2.0.1",
56 | "ember-cli-release": "~1.0.0-beta.2",
57 | "ember-load-initializers": "2.0.0",
58 | "ember-maybe-import-regenerator": "^1.0.0",
59 | "ember-qunit": "^5.1.5",
60 | "ember-resolver": "^8.0.3",
61 | "ember-source": "^4.4.0",
62 | "ember-try": "^1.1.0",
63 | "eslint": "~5.12.1",
64 | "eslint-plugin-ember": "^10.6.1",
65 | "glob": "~7.1.3",
66 | "loader.js": "4.7.0",
67 | "mocha": "^5.2.0",
68 | "qunit": "^2.19.1",
69 | "qunit-dom": "^2.0.0",
70 | "webpack": "^5.72.1"
71 | },
72 | "engines": {
73 | "node": "12.* || 14.* || >= 16"
74 | },
75 | "ember": {
76 | "edition": "octane"
77 | },
78 | "ember-addon": {
79 | "configPath": "tests/dummy/config",
80 | "versionCompatibility": {
81 | "ember": ">= 3.15.0"
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/node-tests/addon-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const co = require('co');
4 | const expect = require('chai').expect;
5 | const CoreObject = require('core-object');
6 | const path = require('path');
7 | const AddonMixin = require('../index');
8 | const BroccoliTestHelper = require('broccoli-test-helper');
9 | const CommonTags = require('common-tags');
10 | const stripIndent = CommonTags.stripIndent;
11 | const createBuilder = BroccoliTestHelper.createBuilder;
12 | const createTempDir = BroccoliTestHelper.createTempDir;
13 |
14 | let Addon = CoreObject.extend(AddonMixin);
15 |
16 | describe('ember-cli-conditional-compile', function() {
17 |
18 | describe('transpileTree', function() {
19 | this.timeout(0);
20 |
21 | let input;
22 | let output;
23 | let subject;
24 |
25 | beforeEach(co.wrap(function* () {
26 | input = yield createTempDir();
27 |
28 | let project = {
29 | root: __dirname,
30 | config: function() {
31 | return {
32 | featureFlags: {
33 | ENABLE_FOO: true
34 | }
35 | }
36 | }
37 | };
38 | this.addon = new Addon({
39 | project,
40 | parent: project
41 | });
42 | this.addon.readConfig();
43 | }));
44 |
45 | afterEach(co.wrap(function* () {
46 | yield input.dispose();
47 | yield output.dispose();
48 | }));
49 |
50 | describe('in developemt', function() {
51 | it("keeps the flag in development", co.wrap(function* () {
52 | process.env.EMBER_ENV = 'development';
53 |
54 | let contents = stripIndent`
55 | if (ENABLE_FOO) {
56 | console.log('Feature Mode!');
57 | }
58 | `;
59 |
60 | input.write({
61 | "foo.js": contents
62 | });
63 |
64 | subject = this.addon.transpileTree(input.path());
65 |
66 | output = createBuilder(subject);
67 |
68 | yield output.build();
69 |
70 | expect(
71 | output.read()
72 | ).to.deep.equal({
73 | "foo.js": `if (ENABLE_FOO) {\n console.log('Feature Mode!');\n}`
74 | });
75 | }));
76 | });
77 |
78 | describe('when minification is enabled', function() {
79 | it("inlines the feature flags value", co.wrap(function* () {
80 | process.env.EMBER_ENV = 'development';
81 |
82 | let contents = stripIndent`
83 | if (ENABLE_FOO) {
84 | console.log('Feature Mode!');
85 | }
86 | `;
87 |
88 | input.write({
89 | "foo.js": contents
90 | });
91 | this.addon.enableCompile = true;
92 | subject = this.addon.transpileTree(input.path());
93 |
94 | output = createBuilder(subject);
95 |
96 | yield output.build();
97 |
98 | expect(
99 | output.read()
100 | ).to.deep.equal({
101 | "foo.js": `if (true) {\n console.log('Feature Mode!');\n}`
102 | });
103 | }));
104 | });
105 | });
106 |
107 | describe('with extra config file', function() {
108 |
109 | this.timeout(0);
110 |
111 | let input;
112 | let output;
113 | let subject;
114 |
115 |
116 | beforeEach(co.wrap(function* () {
117 | input = yield createTempDir();
118 |
119 | let project = {
120 | root: path.join(__dirname, 'with-extra-config-file'),
121 | config: function() {
122 | return {}
123 | }
124 | };
125 | this.addon = new Addon({
126 | project,
127 | parent: project
128 | });
129 | this.addon.readConfig();
130 | }));
131 |
132 | afterEach(co.wrap(function* () {
133 | yield input.dispose();
134 | yield output.dispose();
135 | }));
136 |
137 | describe('when minification is enabled', function() {
138 | it("inlines the feature flags value", co.wrap(function* () {
139 | process.env.EMBER_ENV = 'development';
140 |
141 | let contents = stripIndent`
142 | if (ENABLE_FROM_FILE) {
143 | console.log('Feature Mode!');
144 | }
145 | `;
146 | let clownContents = stripIndent`
147 | if (ENABLE_FROM_FILE_CLOWNSTAGE) {
148 | console.log('Feature Mode!');
149 | }
150 | `;
151 |
152 | input.write({
153 | "foo.js": contents,
154 | 'clown.js': clownContents,
155 | });
156 |
157 | this.addon.enableCompile = true;
158 | subject = this.addon.transpileTree(input.path());
159 |
160 | output = createBuilder(subject);
161 |
162 | yield output.build();
163 |
164 | expect(
165 | output.read()
166 | ).to.deep.equal({
167 | "foo.js": `if (true) {\n console.log('Feature Mode!');\n}`,
168 | "clown.js": `if (false) {\n console.log('Feature Mode!');\n}`
169 | });
170 | }));
171 |
172 | it("respects additional environments", co.wrap(function* () {
173 | process.env.EMBER_ENV = 'development';
174 |
175 | let project = {
176 | root: path.join(__dirname, 'with-extra-config-file'),
177 | config: function() {
178 | return {
179 | featureFlagsEnvironment: 'clownstage'
180 | }
181 | }
182 | };
183 | this.addon = new Addon({
184 | project,
185 | parent: project
186 | });
187 | this.addon.readConfig();
188 |
189 | let contents = stripIndent`
190 | if (ENABLE_FROM_FILE) {
191 | console.log('Feature Mode!');
192 | }
193 | `;
194 | let clownContents = stripIndent`
195 | if (ENABLE_FROM_FILE_CLOWNSTAGE) {
196 | console.log('Feature Mode!');
197 | }
198 | `;
199 |
200 | input.write({
201 | "foo.js": contents,
202 | 'clown.js': clownContents,
203 | });
204 |
205 | this.addon.enableCompile = true;
206 | subject = this.addon.transpileTree(input.path());
207 |
208 | output = createBuilder(subject);
209 |
210 | yield output.build();
211 |
212 | expect(
213 | output.read()
214 | ).to.deep.equal({
215 | "foo.js": `if (true) {\n console.log('Feature Mode!');\n}`,
216 | "clown.js": `if (true) {\n console.log('Feature Mode!');\n}`
217 | });
218 | }));
219 | });
220 | })
221 | });
222 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const EmberApp = require("ember-cli/lib/broccoli/ember-app");
2 | const merge = require("lodash.merge");
3 | const replace = require("broccoli-replace");
4 | const chalk = require("chalk");
5 | const VersionChecker = require("ember-cli-version-checker");
6 | const TemplateCompiler = require("./lib/template-compiler");
7 | const hash = require("object-hash");
8 | const fs = require("fs");
9 | const path = require("path");
10 |
11 | module.exports = {
12 | name: "ember-cli-conditional-compile",
13 | enableCompile: false,
14 |
15 | init: function () {
16 | this._super.init && this._super.init.apply(this, arguments);
17 |
18 | const checker = new VersionChecker(this);
19 | checker.for("ember-source").assertAbove("2.9.0");
20 |
21 | this.htmlbarsVersion = checker.for("ember-cli-htmlbars", "npm");
22 | this.uglifyVersion = checker.for("ember-cli-uglify", "npm");
23 | this.terserVersion = checker.for("ember-cli-terser", "npm");
24 | },
25 |
26 | included: function (app, parentAddon) {
27 | this.readConfig();
28 |
29 | const target = parentAddon || app;
30 |
31 | let options = {
32 | options: {
33 | compress: {
34 | global_defs: this._config.featureFlags,
35 | },
36 | },
37 | };
38 |
39 | if (this.terserVersion.exists()) {
40 | target.options = merge(target.options, {
41 | "ember-cli-terser": { terser: options.options },
42 | });
43 | this.enableCompile = target.options["ember-cli-terser"].enabled;
44 | } else if (this.uglifyVersion.satisfies(">= 2.0.0")) {
45 | target.options = merge(target.options, {
46 | "ember-cli-uglify": { uglify: options.options },
47 | });
48 | this.enableCompile = target.options["ember-cli-uglify"].enabled;
49 | } else {
50 | target.options.minifyJS = merge(target.options.minifyJS, options);
51 | this.enableCompile = target.options.minifyJS.enabled;
52 | }
53 |
54 | const templateCompilerInstance = {
55 | name: "conditional-compile-template",
56 | plugin: TemplateCompiler(this._config.featureFlags),
57 | };
58 |
59 | if (this.htmlbarsVersion.satisfies(">= 1.3.0")) {
60 | templateCompilerInstance["baseDir"] = function () {
61 | return __dirname;
62 | };
63 |
64 | const featureFlags = this._config.featureFlags;
65 |
66 | templateCompilerInstance["cacheKey"] = function () {
67 | return hash(featureFlags);
68 | };
69 | } else {
70 | console.log(
71 | chalk.yellow(
72 | "Upgrade to ember-cli-htmlbars >= 1.3.0 to get build caching"
73 | )
74 | );
75 | }
76 |
77 | target.registry.add("htmlbars-ast-plugin", templateCompilerInstance);
78 | },
79 |
80 | readConfig() {
81 | const root = this.project.root;
82 | const config = this.project.config(EmberApp.env());
83 | const flagsEnv = config.featureFlagsEnvironment || EmberApp.env();
84 |
85 | let configFactory = path.join(root, "config", "feature-flags.js");
86 |
87 | if (fs.existsSync(configFactory)) {
88 | this._config = Object.assign({}, require(configFactory)(flagsEnv));
89 | } else {
90 | // try the app environment as a fallback
91 | const envFeatureFlags = config["featureFlags"] || {};
92 | const envIncludeDirByFlag = config["includeDirByFlag"] || {};
93 | this._config = {
94 | featureFlags: envFeatureFlags,
95 | includeDirByFlag: envIncludeDirByFlag,
96 | };
97 | }
98 | },
99 |
100 | setupPreprocessorRegistry: function (type, registry) {
101 | registry.add("js", {
102 | name: "ember-cli-conditional-compile",
103 | ext: "js",
104 | toTree: (tree) => this.transpileTree(tree),
105 | });
106 | },
107 |
108 | /**
109 | * Inline feature flags value so that babili's dead code elimintation plugin
110 | * removes the code non reachable.
111 | */
112 | transpileTree(tree) {
113 | const esTranspiler = require("broccoli-babel-transpiler");
114 | const inlineFeatureFlags = require("babel-plugin-inline-replace-variables");
115 | if (!this.enableCompile) {
116 | return tree;
117 | }
118 | return esTranspiler(tree, {
119 | plugins: [[inlineFeatureFlags, this._config.featureFlags]],
120 | });
121 | },
122 |
123 | postprocessTree: function (type, tree) {
124 | if (type !== "js") return tree;
125 |
126 | let config = this.project.config(EmberApp.env());
127 |
128 | if (!this._config.featureFlags) {
129 | console.log(
130 | chalk.red(
131 | "Could not find any feature flags." +
132 | "You may need to add them in your config/environment.js"
133 | )
134 | );
135 | return tree;
136 | }
137 |
138 | let excludes = [];
139 |
140 | if (this._config.featureFlags) {
141 | Object.keys(this._config.featureFlags).map(function (flag) {
142 | if (
143 | this._config.includeDirByFlag &&
144 | !this._config.featureFlags[flag] &&
145 | this._config.includeDirByFlag[flag]
146 | ) {
147 | const flaggedExcludes = this._config.includeDirByFlag[flag].map(
148 | function (glob) {
149 | return config.modulePrefix + "/" + glob;
150 | }
151 | );
152 | excludes = excludes.concat(flaggedExcludes);
153 | }
154 | }, this);
155 | }
156 |
157 | if (this.enableCompile) {
158 | tree = replace(tree, {
159 | files: [
160 | config.modulePrefix +
161 | "/initializers/ember-cli-conditional-compile-features.js",
162 | ],
163 | patterns: [
164 | {
165 | match: /EMBER_CLI_CONDITIONAL_COMPILE_INJECTIONS/g,
166 | replacement: "{}",
167 | },
168 | ],
169 | });
170 | } else {
171 | tree = replace(tree, {
172 | files: [
173 | config.modulePrefix +
174 | "/initializers/ember-cli-conditional-compile-features.js",
175 | ],
176 | patterns: [
177 | {
178 | match: /EMBER_CLI_CONDITIONAL_COMPILE_INJECTIONS/g,
179 | replacement: JSON.stringify(this._config.featureFlags || {}),
180 | },
181 | ],
182 | });
183 | }
184 |
185 | return replace(tree, {
186 | files: excludes,
187 | patterns: [
188 | {
189 | match: /.*/g,
190 | replacement: "/**/",
191 | },
192 | ],
193 | });
194 | },
195 | };
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ember-cli-conditional-compile
2 |
3 | 
4 |
5 | The goal of ember-cli-conditional-compile is to provide easy to use feature switches to Ember applications such that code that is hidden behind a disabled feature switch is not in the compiled code.
6 |
7 | # Getting Started
8 |
9 | This is an ember-cli addon, so all you need to do is
10 |
11 | ```bash
12 | ember install ember-cli-conditional-compile
13 | ```
14 |
15 | # Compatibility
16 |
17 | Version 2.x targets Ember 3.x and beyond. It is tested against the latest ember release and the latest beta release (See [config/ember-try.js](config/ember-try.js) for the testing matrix)
18 |
19 | # Upgrading to 2.x
20 |
21 | Our workaround around a broccoli pipeline bug in newer versions of ember-cli slightly changes the semantics of `includeDirByFlag`. Instead of a RegExp, you now need to specify a Glob-style pattern.
22 |
23 | # Usage
24 |
25 | To actually use the feature switches you'll need to add some configuration in your `environment.js` file. For example, lets pretend you want to have two feature switches; `ENABLE_FOO` and `ENABLE_BAR`:
26 |
27 | ```javascript
28 | var ENV = {
29 | // other settings ...
30 |
31 | featureFlags: {
32 | ENABLE_FOO: true,
33 | ENABLE_BAR: true,
34 | },
35 | includeDirByFlag: {
36 | ENABLE_FOO: ["pods/foos/**", "pods/foo/**"],
37 | ENABLE_BAR: [],
38 | },
39 | };
40 |
41 | // other environments ...
42 |
43 | if (environment === "production") {
44 | ENV.featureFlags.ENABLE_FOO = false;
45 | }
46 | ```
47 |
48 | Alternatively, you can define your feature flags in `config/feature-flags.js` looking like this:
49 |
50 | ```javascript
51 | module.exports = function (environment) {
52 | const GLOBAL_FLAGS = {
53 | featureFlags: {
54 | ENABLE_FOO: true,
55 | ENABLE_BAR: true,
56 | },
57 | includeDirByFlag: {
58 | ENABLE_FOO: [/pods\/foos/, /pods\/foo/],
59 | ENABLE_BAR: [],
60 | },
61 | };
62 |
63 | if (environment === "production") {
64 | GLOBAL_FLAGS.featureFlags.ENABLE_FOO = false;
65 | }
66 |
67 | return GLOBAL_FLAGS;
68 | };
69 | ```
70 |
71 | This has two advantages: It declutters `environment.js` a bit, especially if you have many flags, but also prevents your flag names from leaking into the application code under certain circumstances.
72 |
73 | We'll look at the two new options in more detail below, but for now we can see that by default both features are enabled, but in the `production` environment `ENABLE_FOO` is disabled, and related code under the `pods/foos` and `pods/foo` directories are excluded from compilation.
74 |
75 | ## ENV.featureFlags
76 |
77 | This setting sets up which flags will be available to actually switch use. A value of `true` means that the flag will be enabled, `false` means that it will not be.
78 |
79 | ## ENV.includeDirByFlag
80 |
81 | Given a key which has been defined above, the value is an array of regexes of files/paths which will _only_ be included in the compiled product if the related feature flag is enabled. In the example above, in the development environment `ENABLE_FOO` is `true`, so the `pods/foo` and `pods/foos` paths will be included.
82 |
83 | However, since the flag is `false` in production, any code in those directories will not be compiled in.
84 |
85 | # How it works
86 |
87 | _ember-cli-conditional-compile_ adds itself to the Broccoli compile pipeline for your Ember application. Depending on which environment you're building it acts in two different ways:
88 |
89 | ## Development and test environments
90 |
91 | Global variables are injected into the page which have the current state of the feature flags. For example:
92 |
93 | ```javascript
94 | if (ENABLE_FOO) {
95 | this.route("foo");
96 | console.log("The feature ENABLE_FOO is enabled in this environment");
97 | }
98 | ```
99 |
100 | will be represented in development and test environments as:
101 |
102 | ```javascript
103 | window.ENABLE_FOO = true;
104 |
105 | if (ENABLE_FOO) {
106 | this.route("foo");
107 | console.log("The feature ENABLE_FOO is enabled in this environment");
108 | }
109 | ```
110 |
111 | In Handlebars/HTMLBars templates, you can also make use of the flags using the `if-flag` block helper:
112 |
113 | ```hbs
114 | {{#if-flag ENABLE_FOO}}
115 | Foo is enabled! \o/
116 | {{else}}
117 | Foo is disabled
118 | {{/if-flag}}
119 | ```
120 |
121 | You can also use the `unless-flag` style block helper:
122 |
123 | ```hbs
124 | {{#unless-flag ENABLE_FOO}}
125 | Foo is disabled
126 | {{else}}
127 | Foo is enabled! \o/
128 | {{/unless-flag}}
129 | ```
130 |
131 | ## Production environment
132 |
133 | We use UglifyJS's `global_defs` feature to replace the value of feature flags with their constant values. UglifyJS's dead code implementation then cleans up unreachable code and performs inlining, such that:
134 |
135 | ```javascript
136 | if (ENABLE_FOO) {
137 | this.route("foo");
138 | console.log("The feature ENABLE_FOO is enabled in this environment");
139 | }
140 | ```
141 |
142 | will be represented in the production environment as the following if `ENABLE_FOO` is configured to be `true`:
143 |
144 | ```javascript
145 | this.route("foo");
146 | console.log("The feature ENABLE_FOO is enabled in this environment");
147 | ```
148 |
149 | or the following if `ENABLE_FOO` is configured to be `false`;
150 |
151 | ```javascript
152 | // empty since the condition can never be satisfied!
153 | ```
154 |
155 | Furthermore, if you use the HTMLBars helpers the AST transformations will shake
156 | out and remove impossible-to-reach sides of the condition:
157 |
158 | ```hbs
159 | {{#if-flag ENABLE_FOO}}
160 | Foo is enabled
161 | {{else}}
162 | This won't be reached, because ENABLE_FOO is true
163 | {{/if-flag}}
164 | ```
165 |
166 | will get transformed into:
167 |
168 | ```hbs
169 | Foo is enabled
170 | ```
171 |
172 | This is really handy, since it vastly cuts down on the amount of precompiled
173 | template code that your users need to download even though it'll never be
174 | executed!
175 |
176 | ## Defining additional environments
177 |
178 | By defining `ENV.featureFlagsEnvironment` you can separate your feature flags by more than just test/development/production, for example to have a beta environment that is identical to production but has a couple more flags activated. This only works if you have your flags in `config.featureFlags` - The `environment` passed in into the wrapper function will be `ENV.featureFlagsEnvironment` if set.
179 |
180 | # Licence
181 |
182 | This library is lovingly brought to you by the FreshBooks developers. We've released it under the MIT license.
183 |
--------------------------------------------------------------------------------