Skip to content

Commit

Permalink
[FEATURE] Ember/Glimmer interop
Browse files Browse the repository at this point in the history
This PR adds basic Ember/Glimmer interop for GlimmerX. This allows users
to write libraries and components using GlimmerX in both frameworks. It
works by providing alternative implementations for each package, one for
standard Glimmer/TS/Webpack builds, one for Ember builds. Eventually the
goal is to unify these as much as possible, but for now this unlocks
experimentation and cross-compatiblity.

Changes include:

- Babel plugin has been updated to include an `ember` option and the
  ability to pass a custom `precompileTemplate` function, for the Ember
  side to use.
- Most Glimmer imports have been replaced with no-ops (like `on` and
  `fn`). Ember templates and components still rely on resolution, so
  these "just work" anyways.
- User "imports" will continue to work in Ember, as long as the files
  are exported in the correct place, since Ember still relies on
  resolution and doesn't have true imports.
- Services work, and an empty base `Service` class has been added so
  that users can import and extend it, like they would in Ember. In the
  Ember side, services need to be exported from the `/services` folder
  like normal. In the Glimmer side, they need to be instantiated and
  passed to `renderComponent`.

The PR also adds smoke tests for the new functionality, to make sure we
don't accidentally break it.
  • Loading branch information
Chris Garrett committed Apr 7, 2020
1 parent e333dba commit 084ef34
Show file tree
Hide file tree
Showing 45 changed files with 359 additions and 171 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,17 @@ jobs:
- uses: volta-cli/action@v1
- run: yarn install --frozen-lockfile --non-interactive
- run: yarn test

ember-smoke-tests:
name: "Ember Tests: ${{ matrix.ember-version }}"
runs-on: ubuntu-latest

strategy:
matrix:
ember-version: [default, release, beta, canary, lts-3.16]

steps:
- uses: actions/checkout@v2
- uses: volta-cli/action@v1
- run: yarn install --frozen-lockfile --non-interactive
- run: yarn test:ember ember-${{ matrix.ember-version }} --skip-cleanup
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"problems": "tsc -p tsconfig.json --noEmit",
"start": "webpack-dev-server",
"test": "yarn lint && testem ci && yarn test:babel-plugins",
"test:ember": "yarn workspace basic-addon ember try:one",
"test:watch": "testem",
"lint": "eslint . --cache --ext .js,.ts",
"test:babel-plugins": "mocha -r esm --timeout 5000 packages/@glimmerx/babel-plugin-component-templates/test packages/@glimmerx/eslint-plugin/test/lib/rules",
Expand Down
77 changes: 46 additions & 31 deletions packages/@glimmerx/babel-plugin-component-templates/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { precompileTemplate } = require('@glimmer/babel-plugin-strict-template-precompile');
const {
precompileTemplate: glimmerPrecompileTemplate,
} = require('@glimmer/babel-plugin-strict-template-precompile');
const { addNamed } = require('@babel/helper-module-imports');
const { traverse, preprocess } = require('@glimmer/syntax');

Expand Down Expand Up @@ -107,54 +109,67 @@ function getTemplateTokens(html, nativeTokens) {
module.exports = function(babel, options) {
const { types: t, parse } = babel;
const {
// Whether or not we'ret targeting Ember. If so, we use the Ember global
// instead of adding imports, and we use different default import paths.
ember = false,

setTemplatePath = '@glimmer/core',
setTemplateName = 'setComponentTemplate',
setTemplateName = ember ? '_setComponentTemplate' : 'setComponentTemplate',

createTemplatePath = '@glimmer/core',
createTemplateName = 'createTemplate',

templateOnlyComponentPath = '@glimmer/core',
templateOnlyComponentName = 'templateOnlyComponent',
precompile: precompileOptions,
templateOnlyComponentName = ember ? '_templateOnlyComponent' : 'templateOnlyComponent',

precompile: precompileOptions = {},

// Users can pass a custom precompile function. This is used by Ember.
precompileTemplate = glimmerPrecompileTemplate,
} = options || {};

const shouldPrecompile = !(precompileOptions && precompileOptions.disabled);
const shouldPrecompile = precompileOptions.disabled !== false;

function maybeAddSetTemplateImport(state, programPath) {
if (!state.setTemplateId) {
state.setTemplateId = addNamed(programPath, setTemplateName, setTemplatePath, {
importedType: 'es6',
}).name;
function maybeAddImport(state, programPath, name, importPath) {
let stateKey = `__import__${name}`;

if (!state[stateKey]) {
if (ember) {
// In Ember, we destructure the value from the Ember global
const globalImport = t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern([t.objectProperty(t.identifier(name), t.identifier(name))]),
t.identifier('Ember')
),
]);

programPath.get('body.0').insertBefore(globalImport);

state[stateKey] = name;
} else {
state[stateKey] = addNamed(programPath, name, importPath, {
importedType: 'es6',
}).name;
}
}

return state.setTemplateId;
return state[stateKey];
}

function maybeAddCreateTemplateImport(state, programPath) {
if (!state.createTemplateId) {
state.createTemplateId = addNamed(programPath, createTemplateName, createTemplatePath, {
importedType: 'es6',
}).name;
}
function maybeAddSetTemplateImport(state, programPath) {
return maybeAddImport(state, programPath, setTemplateName, setTemplatePath);
}

return state.createTemplateId;
function maybeAddCreateTemplateImport(state, programPath) {
return maybeAddImport(state, programPath, createTemplateName, createTemplatePath);
}

function maybeAddTemplateOnlyComponentImport(state, programPath) {
if (!state.templateOnlyComponentImportId) {
state.templateOnlyComponentImportId = addNamed(
programPath,
templateOnlyComponentName,
templateOnlyComponentPath,
{
importedType: 'es6',
}
).name;
}

return state.templateOnlyComponentImportId;
return maybeAddImport(state, programPath, templateOnlyComponentName, templateOnlyComponentPath);
}

return {
name: '@glimmerx/babel-plugin-glimmer-components',
name: '@glimmerx/babel-plugin-component-templates',
manipulateOptions({ parserOpts }) {
parserOpts.plugins.push(['classProperties']);
},
Expand Down
6 changes: 6 additions & 0 deletions packages/@glimmerx/component/addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default } from '@glimmer/component';
export { tracked } from '@glimmer/tracking';

export function hbs() {
throw new Error('hbs template should have been compiled at build time');
}
37 changes: 37 additions & 0 deletions packages/@glimmerx/component/ember-addon-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

function makePrecompileTemplate(templateCompiler) {
const precompileTemplate = (templateString, templateTokens, options) => {
let compiled = templateCompiler.precompile(templateString, options);

return `Ember.HTMLBars.template(${compiled})`;
};

precompileTemplate.baseDir = () => __dirname;

return precompileTemplate;
}

module.exports = {
name: require('./package').name,

included(parent) {
this._super.included.apply(this, arguments);

let { hasPlugin, addPlugin } = require('ember-cli-babel-plugin-helpers');

if (!hasPlugin(parent, '@glimmerx/babel-plugin-component-templates')) {
const ember = this.project.findAddonByName('ember-source');
const templateCompiler = require(ember.absolutePaths.templateCompiler);

addPlugin(parent, [
require.resolve('@glimmerx/babel-plugin-component-templates'),
{
ember: true,

precompileTemplate: makePrecompileTemplate(templateCompiler),
},
]);
}
},
};
5 changes: 4 additions & 1 deletion packages/@glimmerx/component/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export { default } from '@glimmer/component';
export { tracked } from '@glimmer/tracking';
export { hbs } from './src/hbs';

export function hbs(_strings: TemplateStringsArray) {
throw new Error('hbs template should have been compiled at build time');
}
11 changes: 10 additions & 1 deletion packages/@glimmerx/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@glimmerx/component",
"version": "0.1.1",
"description": "Base component class",
"keywords": [
"ember-addon"
],
"main": "dist/commonjs/index.js",
"module": "dist/modules/index.js",
"repository": "https://github.com/tomdale/glimmer-lite",
Expand All @@ -13,10 +16,16 @@
},
"dependencies": {
"@glimmer/component": "^2.0.0-beta.5",
"@glimmer/tracking": "^2.0.0-beta.5"
"@glimmer/tracking": "^2.0.0-beta.5",
"@glimmerx/babel-plugin-component-templates": "^0.1.14",
"ember-cli-babel": "^7.17.2",
"ember-cli-babel-plugin-helpers": "^1.1.0"
},
"volta": {
"node": "12.10.0",
"yarn": "1.22.4"
},
"ember-addon": {
"main": "ember-addon-main.js"
}
}
3 changes: 0 additions & 3 deletions packages/@glimmerx/component/src/hbs.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/@glimmerx/helper/addon/-private/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function helper() {
throw new Error('helper() has not been implemented in Ember yet.');
}
7 changes: 7 additions & 0 deletions packages/@glimmerx/helper/addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { helper, Helper } from './-private/helper';

export function fn() {
throw new Error(
'Attempted to call {{fn}} directly. {{fn}} is built in in Ember, so it should automatically resolve. This function is for interop with Glimmer.js only.'
);
}
5 changes: 5 additions & 0 deletions packages/@glimmerx/helper/ember-addon-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = {
name: require('./package').name,
};
9 changes: 8 additions & 1 deletion packages/@glimmerx/helper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@glimmerx/helper",
"version": "0.1.10",
"description": "Helper Functionality",
"keywords": [
"ember-addon"
],
"main": "dist/commonjs/index.js",
"module": "dist/modules/index.js",
"repository": "https://github.com/tomdale/glimmer-lite",
Expand All @@ -14,10 +17,14 @@
"dependencies": {
"@glimmerx/core": "0.1.10",
"@glimmer/helper": "^2.0.0-beta.5",
"@glimmer/interfaces": "^0.50.0"
"@glimmer/interfaces": "^0.50.0",
"ember-cli-babel": "^7.18.0"
},
"volta": {
"node": "12.10.0",
"yarn": "1.22.4"
},
"ember-addon": {
"main": "ember-addon-main.js"
}
}
7 changes: 7 additions & 0 deletions packages/@glimmerx/modifier/addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { action } from '@ember/object';

export function on() {
throw new Error(
'Attempted to call {{on}} directly. {{on}} is built in in Ember, so it should automatically resolve. This function is for interop with Glimmer.js only.'
);
}
5 changes: 5 additions & 0 deletions packages/@glimmerx/modifier/ember-addon-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = {
name: require('./package').name,
};
9 changes: 8 additions & 1 deletion packages/@glimmerx/modifier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@glimmerx/modifier",
"version": "0.1.10",
"description": "Modifier Functionality",
"keywords": [
"ember-addon"
],
"main": "dist/commonjs/index.js",
"module": "dist/modules/index.js",
"repository": "https://github.com/tomdale/glimmer-lite",
Expand All @@ -13,10 +16,14 @@
},
"dependencies": {
"@glimmer/core": "^2.0.0-beta.5",
"@glimmer/modifier": "^2.0.0-beta.5"
"@glimmer/modifier": "^2.0.0-beta.5",
"ember-cli-babel": "^7.18.0"
},
"volta": {
"node": "12.10.0",
"yarn": "1.22.4"
},
"ember-addon": {
"main": "ember-addon-main.js"
}
}
1 change: 1 addition & 0 deletions packages/@glimmerx/service/addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, inject as service } from '@ember/service';
5 changes: 5 additions & 0 deletions packages/@glimmerx/service/ember-addon-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = {
name: require('./package').name,
};
2 changes: 2 additions & 0 deletions packages/@glimmerx/service/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { service } from './src/decorator';

export default class Service {}
9 changes: 8 additions & 1 deletion packages/@glimmerx/service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@glimmerx/service",
"version": "0.1.10",
"description": "Services Functionality",
"keywords": [
"ember-addon"
],
"main": "dist/commonjs/index.js",
"module": "dist/modules/index.js",
"repository": "https://github.com/tomdale/glimmer-lite",
Expand All @@ -13,10 +16,14 @@
},
"dependencies": {
"@glimmerx/core": "0.1.10",
"@glimmer/env": "0.1.7"
"@glimmer/env": "0.1.7",
"ember-cli-babel": "^7.18.0"
},
"volta": {
"node": "12.10.0",
"yarn": "1.22.4"
},
"ember-addon": {
"main": "ember-addon-main.js"
}
}
8 changes: 5 additions & 3 deletions packages/examples/basic-addon/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ module.exports = {
}
},
plugins: [
'ember'
'ember',
'@glimmerx'
],
extends: [
'eslint:recommended',
Expand All @@ -21,7 +22,8 @@ module.exports = {
browser: true
},
rules: {
'ember/no-jquery': 'error'
'ember/no-jquery': 'error',
'@glimmerx/template-vars': 'error'
},
overrides: [
// node files
Expand All @@ -30,7 +32,7 @@ module.exports = {
'.eslintrc.js',
'.template-lintrc.js',
'ember-cli-build.js',
'index.js',
'ember-addon-main.js',
'testem.js',
'blueprints/*/index.js',
'config/**/*.js',
Expand Down
Loading

0 comments on commit 084ef34

Please sign in to comment.