diff --git a/PORTING-ADDONS-TO-V2.md b/PORTING-ADDONS-TO-V2.md index 82de5874e..c19239b28 100644 --- a/PORTING-ADDONS-TO-V2.md +++ b/PORTING-ADDONS-TO-V2.md @@ -61,10 +61,7 @@ The steps: ```json { "private": true, - "workspaces": [ - "addon", - "test-app" - ] + "workspaces": ["addon", "test-app"] } ``` @@ -219,6 +216,7 @@ Now that we've separated the test-app and docs app concerns from the addon, we c `yarn add --dev @embroider/addon-dev rollup @rollup/plugin-babel @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators` 6. Grab the [example babel config](https://github.com/embroider-build/embroider/blob/main/packages/addon-dev/sample-babel.config.json) and save it as `addon/babel.config.json` + - If you addon requires template transforms in order to publish to a shareable format. Apply transforms using the `@embroider/addon-dev/template-transform-plugin`. View how to use this in the [example babel.config.js](https://github.com/embroider-build/embroider/blob/main/packages/addon-dev/sample-babel.config.js) 7. Grab the [example rollup config](https://github.com/embroider-build/embroider/blob/main/packages/addon-dev/sample-rollup.config.js) and save it as `addon/rollup.config.js`. 8. Identify your **app reexports**. This is the list of modules from your addon that get reexported by files in the `addon/app` directory. 9. Delete the `addon/app` directory. You aren't going to need it anymore. @@ -230,10 +228,12 @@ Now that we've separated the test-app and docs app concerns from the addon, we c 11. Still editing `addon/rollup.config.js`, customize the `appReexports` to match all your **app reexports** as identified above. 12. Delete your `addon/index.js` file. 13. Create a new `addon/addon-main.js` file (this replaces `addon/index.js`) with this exact content: - ```js - const { addonV1Shim } = require('@embroider/addon-shim'); - module.exports = addonV1Shim(__dirname); - ``` + +```js +const { addonV1Shim } = require('@embroider/addon-shim'); +module.exports = addonV1Shim(__dirname); +``` + 14. In your `addon/.eslintrc.js`, replace "index.js" with "addon-main.js" so that our new file will lint correctly as Node code. 15. In your `addon/package.json`, add these things: ```js diff --git a/packages/addon-dev/.gitignore b/packages/addon-dev/.gitignore index 2ebf498ab..9b6211a0a 100644 --- a/packages/addon-dev/.gitignore +++ b/packages/addon-dev/.gitignore @@ -4,6 +4,9 @@ /src/**/*.js /src/**/*.d.ts /src/**/*.map +/tests/**/*.js +/tests/**/*.d.ts +/tests/**/*.map # dependencies /node_modules/ diff --git a/packages/addon-dev/jest.config.js b/packages/addon-dev/jest.config.js new file mode 100644 index 000000000..7f4f45dca --- /dev/null +++ b/packages/addon-dev/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + testEnvironment: 'node', + testMatch: [ + '/tests/**/*.test.js', + ], +}; diff --git a/packages/addon-dev/package.json b/packages/addon-dev/package.json index 6a94fd0e8..ce18863f6 100644 --- a/packages/addon-dev/package.json +++ b/packages/addon-dev/package.json @@ -14,6 +14,7 @@ }, "exports": { "./template-colocation-plugin": "./src/template-colocation-plugin.js", + "./template-transform-plugin": "./src/template-transform-plugin.js", "./rollup": "./src/rollup.js" }, "files": [ @@ -22,8 +23,12 @@ "src/**/*.d.ts", "src/**/*.js.map" ], + "scripts": { + "prepare": "tsc", + "test": "jest" + }, "dependencies": { - "@embroider/shared-internals": "^1.7.1", + "@embroider/core": "^1.7.1", "@rollup/pluginutils": "^4.1.1", "assert-never": "^1.2.1", "fs-extra": "^10.0.0", @@ -34,10 +39,21 @@ "yargs": "^17.0.1" }, "devDependencies": { + "@embroider/test-support": "0.36.0", + "@glimmer/syntax": "^0.84.2", "@types/fs-extra": "^9.0.12", "@types/minimatch": "^3.0.4", "@types/yargs": "^17.0.3", - "rollup": "^2.58.0" + "rollup": "^2.58.0", + "tmp": "^0.1.0" + }, + "peerDependencies": { + "ember-source": "*" + }, + "peerDependenciesMeta": { + "ember-source": { + "optional": true + } }, "engines": { "node": "12.* || 14.* || >= 16" diff --git a/packages/addon-dev/sample-babel.config.js b/packages/addon-dev/sample-babel.config.js new file mode 100644 index 000000000..91a6dc8ee --- /dev/null +++ b/packages/addon-dev/sample-babel.config.js @@ -0,0 +1,25 @@ +// Some addons need to transform their templates before they have a portable format. +// In "classic" builds this was done at the application. In embroider it should +// be done during the addon build. +const someAstTransformPlugin = require('./some-ast-transform-plugin'); + +// The `@embroider/addon-dev/template-transform-plugin` has the following options: +// `options.astTransforms` - an array of functions or paths to preprocess the GlimmerAST +// `options.compilerPath` - Optional: Defaults to `ember-source/dist/ember-template-compiler` + +module.exports = { + plugins: [ + '@embroider/addon-dev/template-colocation-plugin', + [ + '@embroider/addon-dev/template-transform-plugin', + { + astTransforms: [ + someAstTransformPlugin, + './path/to/another-template-transform-plugin', + ], + }, + ], + ['@babel/plugin-proposal-decorators', { legacy: true }], + '@babel/plugin-proposal-class-properties', + ], +}; diff --git a/packages/addon-dev/src/rollup-addon-dependencies.ts b/packages/addon-dev/src/rollup-addon-dependencies.ts index d7bfa5468..0f0172fdc 100644 --- a/packages/addon-dev/src/rollup-addon-dependencies.ts +++ b/packages/addon-dev/src/rollup-addon-dependencies.ts @@ -5,7 +5,7 @@ import { emberVirtualPeerDeps, packageName, templateCompilationModules, -} from '@embroider/shared-internals'; +} from '@embroider/core'; const compilationModules = new Set( templateCompilationModules.map((m) => m.module) diff --git a/packages/addon-dev/src/rollup-hbs-plugin.ts b/packages/addon-dev/src/rollup-hbs-plugin.ts index d8f180215..16529415c 100644 --- a/packages/addon-dev/src/rollup-hbs-plugin.ts +++ b/packages/addon-dev/src/rollup-hbs-plugin.ts @@ -6,7 +6,7 @@ import type { ResolvedId, } from 'rollup'; import { readFileSync } from 'fs'; -import { hbsToJS } from '@embroider/shared-internals'; +import { hbsToJS } from '@embroider/core'; import assertNever from 'assert-never'; import { parse as pathParse } from 'path'; diff --git a/packages/addon-dev/src/template-colocation-plugin.ts b/packages/addon-dev/src/template-colocation-plugin.ts index c8600be26..61c32b466 100644 --- a/packages/addon-dev/src/template-colocation-plugin.ts +++ b/packages/addon-dev/src/template-colocation-plugin.ts @@ -1,4 +1,7 @@ -export { - default, - Options, -} from '@embroider/shared-internals/src/template-colocation-plugin'; +import { + templateColocationPlugin, + Options as TemplateColocationPluginOptions, +} from '@embroider/core'; + +export { TemplateColocationPluginOptions as Options }; +export default templateColocationPlugin; diff --git a/packages/addon-dev/src/template-transform-plugin.ts b/packages/addon-dev/src/template-transform-plugin.ts new file mode 100644 index 000000000..000a84e53 --- /dev/null +++ b/packages/addon-dev/src/template-transform-plugin.ts @@ -0,0 +1,42 @@ +import make from '@embroider/core/src/babel-plugin-stage1-inline-hbs'; +import { TemplateCompiler, TemplateCompilerParams } from '@embroider/core'; +import { getEmberExports } from '@embroider/core/src/load-ember-template-compiler'; + +export type TemplateTransform = () => { name: string; visitor: {} }; +export type TemplateTransformPlugin = TemplateTransform | string; +export interface Options { + // An array of either Glimmer AST plugins or paths that can be resolved to a plugin. + astTransforms?: TemplateTransformPlugin[]; + // Defaults to 'ember-source/dist/ember-template-compiler' + compilerPath?: string; +} + +function resolvePlugins(plugins: TemplateTransformPlugin[]) { + return plugins.map((somePlugin: TemplateTransformPlugin) => { + // If it's a string attempt to resolve the path to a module. + return typeof somePlugin === 'string' + ? require(somePlugin) // eslint-disable-line @typescript-eslint/no-require-imports + : somePlugin; + }); +} + +export default make((options: Options) => { + let { + astTransforms: somePlugins = [], + compilerPath = 'ember-source/dist/ember-template-compiler', + } = options; + + compilerPath = require.resolve(compilerPath); + + const astTransforms: TemplateTransform[] = resolvePlugins(somePlugins); + + const params: TemplateCompilerParams = { + EmberENV: {}, + loadEmberTemplateCompiler: () => getEmberExports(compilerPath), + plugins: { + ast: astTransforms, + }, + }; + + return new TemplateCompiler(params); +}); diff --git a/packages/addon-dev/tests/template-transform-plugin.test.ts b/packages/addon-dev/tests/template-transform-plugin.test.ts new file mode 100644 index 000000000..55376f48f --- /dev/null +++ b/packages/addon-dev/tests/template-transform-plugin.test.ts @@ -0,0 +1,150 @@ +import { + allBabelVersions, + emberTemplateCompilerPath, +} from '@embroider/test-support'; +import { + TemplateTransformPlugin, + Options, +} from '../src/template-transform-plugin'; +import { hbsToJS } from '@embroider/core'; +import { AST } from '@glimmer/syntax'; +import { join } from 'path'; +import tmp from 'tmp'; +import { writeFileSync } from 'fs-extra'; + +describe('template-transform-plugin', () => { + jest.setTimeout(120000); + + const templateTransformBabelPlugin = join( + __dirname, + '../src/template-transform-plugin.js' + ); + + let plugins: any = []; + + function reverseTransform() { + return { + name: 'reverse-transform', + visitor: { + ElementNode(node: AST.ElementNode) { + node.tag = node.tag.split('').reverse().join(''); + }, + }, + }; + } + + function setupPlugins(options?: { + astTransforms: TemplateTransformPlugin[]; + }) { + const opts: Options = { + astTransforms: options?.astTransforms, + compilerPath: emberTemplateCompilerPath(), + }; + plugins = [[templateTransformBabelPlugin, opts]]; + } + + allBabelVersions({ + babelConfig() { + return { + plugins, + }; + }, + createTests(transform) { + afterEach(function () { + plugins = undefined; + }); + + test('no-op', () => { + setupPlugins(); + const code = hbsToJS('Hello {{@phrase}}'); + let output = transform(code); + expect(output).toMatch( + /import { hbs } from ['"]ember-cli-htmlbars['"];/ + ); + expect(output).toMatch( + /export default hbs\(['"]Hello {{@phrase}}['"]\);/ + ); + }); + + test('options.astTransforms empty array', () => { + setupPlugins({ + astTransforms: [], + }); + const code = hbsToJS('Hello {{@phrase}}'); + let output = transform(code); + expect(output).toMatch( + /import { hbs } from ['"]ember-cli-htmlbars['"];/ + ); + expect(output).toMatch( + /export default hbs\(['"]Hello {{@phrase}}['"]\);/ + ); + }); + test('options.astTransforms function', () => { + setupPlugins({ + astTransforms: [reverseTransform], + }); + + const code = hbsToJS('{{@phrase}}'); + let output = transform(code); + expect(output).toMatch( + /import { hbs } from ['"]ember-cli-htmlbars['"];/ + ); + expect(output).toMatch( + /export default hbs\(['"]\{{@phrase}}\<\/naps\>['"]\);/ + ); + }); + + test('options.astTransforms path', () => { + const someFile = tmp.fileSync(); + + const contents = `module.exports = function reverseTransform() { + return { + name: 'reverse-transform', + visitor: { + ElementNode(node) { + node.tag = node.tag.split('').reverse().join(''); + }, + }, + }; + }`; + + writeFileSync(someFile.name, contents, 'utf8'); + + setupPlugins({ + astTransforms: [someFile.name], + }); + + const code = hbsToJS('{{@phrase}}'); + + let output = transform(code); + + expect(output).toMatch( + /import { hbs } from ['"]ember-cli-htmlbars['"];/ + ); + expect(output).toMatch( + /export default hbs\(['"]\{{@phrase}}\<\/naps\>['"]\);/ + ); + + someFile.removeCallback(); + }); + + test('ember-cli-htmlbars alias import name', () => { + setupPlugins({ + astTransforms: [reverseTransform], + }); + + const code = `import { hbs as render } from 'ember-cli-htmlbars'; + export default render('{{@phrase}}');`; + + let output = transform(code); + + expect(output).toMatch( + /import { hbs as render } from ['"]ember-cli-htmlbars['"];/ + ); + expect(output).toMatch( + /export default render\(['"]\{{@phrase}}\<\/naps\>['"]\);/ + ); + }); + }, + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json index 45fa1c1fd..c26419b8a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,6 +18,7 @@ }, "./src/messages": "./src/messages.js", "./src/babel-plugin-inline-hbs": "./src/babel-plugin-inline-hbs.js", + "./src/babel-plugin-stage1-inline-hbs": "./src/babel-plugin-stage1-inline-hbs.js", "./src/mini-modules-polyfill": "./src/mini-modules-polyfill.js", "./src/load-ember-template-compiler": "./src/load-ember-template-compiler.js" }, diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index 245264dde..7e135dfc5 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -9,3 +9,7 @@ export { default as tmpdir } from './tmpdir'; export * from './ember-cli-models'; export * from './ember-standard-modules'; export { hbsToJS } from './hbs-to-js'; +export { + default as templateColocationPlugin, + Options as TemplateColocationPluginOptions, +} from './template-colocation-plugin'; diff --git a/yarn.lock b/yarn.lock index 5fc8e9738..d5bda6362 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1714,6 +1714,13 @@ dependencies: "@simple-dom/interface" "^1.4.0" +"@glimmer/interfaces@0.84.2": + version "0.84.2" + resolved "https://registry.yarnpkg.com/@glimmer/interfaces/-/interfaces-0.84.2.tgz#764cf92c954adcd1a851e5dc68ec1f6b654dc3bd" + integrity sha512-tMZxQpOddUVmHEOuripkNqVR7ba0K4doiYnFd4WyswqoHPlxqpBujbIamQ+bWCWEF0U4yxsXKa31ekS/JHkiBQ== + dependencies: + "@simple-dom/interface" "^1.4.0" + "@glimmer/interfaces@^0.42.2": version "0.42.2" resolved "https://registry.yarnpkg.com/@glimmer/interfaces/-/interfaces-0.42.2.tgz#9cf8d6f8f5eee6bfcfa36919ca68ae716e1f78db" @@ -1801,6 +1808,16 @@ "@handlebars/parser" "^1.1.0" simple-html-tokenizer "^0.5.10" +"@glimmer/syntax@^0.84.2": + version "0.84.2" + resolved "https://registry.yarnpkg.com/@glimmer/syntax/-/syntax-0.84.2.tgz#a3f65e51eec20f6adb79c6159d1ad1166fa5bccd" + integrity sha512-SPBd1tpIR9XeaXsXsMRCnKz63eLnIZ0d5G9QC4zIBFBC3pQdtG0F5kWeuRVCdfTIFuR+5WBMfk5jvg+3gbQhjg== + dependencies: + "@glimmer/interfaces" "0.84.2" + "@glimmer/util" "0.84.2" + "@handlebars/parser" "~2.0.0" + simple-html-tokenizer "^0.5.11" + "@glimmer/tracking@^1.0.0", "@glimmer/tracking@^1.0.3", "@glimmer/tracking@^1.0.4": version "1.1.2" resolved "https://registry.yarnpkg.com/@glimmer/tracking/-/tracking-1.1.2.tgz#74e71be07b0a7066518d24044d2665d0cf8281eb" @@ -1827,6 +1844,15 @@ "@glimmer/interfaces" "0.80.0" "@simple-dom/interface" "^1.4.0" +"@glimmer/util@0.84.2": + version "0.84.2" + resolved "https://registry.yarnpkg.com/@glimmer/util/-/util-0.84.2.tgz#2711ba40f25f44b2ea309cad49f5c2622c6211bc" + integrity sha512-VbhzE2s4rmU+qJF3gGBTL1IDjq+/G2Th51XErS8MQVMCmE4CU2pdwSzec8PyOowqCGUOrVIWuMzEI6VoPM4L4w== + dependencies: + "@glimmer/env" "0.1.7" + "@glimmer/interfaces" "0.84.2" + "@simple-dom/interface" "^1.4.0" + "@glimmer/util@^0.42.2": version "0.42.2" resolved "https://registry.yarnpkg.com/@glimmer/util/-/util-0.42.2.tgz#9ca1631e42766ea6059f4b49d0bdfb6095aad2c4" @@ -5562,7 +5588,15 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^3.2.6, browserslist@^4.0.0, browserslist@^4.14.0, browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^4.20.3: +browserslist@^3.2.6: + version "3.2.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" + integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ== + dependencies: + caniuse-lite "^1.0.30000844" + electron-to-chromium "^1.3.47" + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^4.20.3: version "4.20.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== @@ -5789,6 +5823,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001332: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001342.tgz#87152b1e3b950d1fbf0093e23f00b6c8e8f1da96" integrity sha512-bn6sOCu7L7jcbBbyNhLg0qzXdJ/PMbybZTH/BA6Roet9wxYRm6Tr9D0s0uhLkOZ6MSG+QU6txUgdpr3MXIVqjA== +caniuse-lite@^1.0.30000844: + version "1.0.30001344" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz#8a1e7fdc4db9c2ec79a05e9fd68eb93a761888bb" + integrity sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -7037,6 +7076,11 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +electron-to-chromium@^1.3.47: + version "1.4.143" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.143.tgz#10f1bb595ad6cd893c05097039c685dcf5c8e30c" + integrity sha512-2hIgvu0+pDfXIqmVmV5X6iwMjQ2KxDsWKwM+oI1fABEOy/Dqmll0QJRmIQ3rm+XaoUa/qKrmy5h7LSTFQ6Ldzg== + electron-to-chromium@^1.4.118: version "1.4.137" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" @@ -15071,7 +15115,7 @@ qunit-dom@^1.6.0: ember-cli-babel "^7.23.0" ember-cli-version-checker "^5.1.1" -qunit@^2.14.1, qunit@^2.16.0: +qunit@^2.16.0: version "2.19.1" resolved "https://registry.yarnpkg.com/qunit/-/qunit-2.19.1.tgz#eb1afd188da9e47f07c13aa70461a1d9c4505490" integrity sha512-gSGuw0vErE/rNjnlBW/JmE7NNubBlGrDPQvsug32ejYhcVFuZec9yoU0+C30+UgeCGwq6Ap89K65dMGo+kDGZQ== @@ -16180,7 +16224,7 @@ simple-dom@^1.4.0: "@simple-dom/serializer" "^1.4.0" "@simple-dom/void-map" "^1.4.0" -simple-html-tokenizer@^0.5.10, simple-html-tokenizer@^0.5.8: +simple-html-tokenizer@^0.5.10, simple-html-tokenizer@^0.5.11, simple-html-tokenizer@^0.5.8: version "0.5.11" resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.5.11.tgz#4c5186083c164ba22a7b477b7687ac056ad6b1d9" integrity sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==