From 73591d1975d7bdc4cc807db9ff353debe6dcce64 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Thu, 10 Feb 2022 22:31:57 -0500 Subject: [PATCH 1/4] Support template-only imports --- packages/addon-dev/src/rollup-hbs-plugin.ts | 90 ++++++- .../src/rollup-public-entrypoints.ts | 15 +- tests/scenarios/package.json | 23 +- tests/scenarios/v2-addon-dev-test.ts | 227 ++++++++++++++++++ yarn.lock | 129 +++++++++- 5 files changed, 463 insertions(+), 21 deletions(-) create mode 100644 tests/scenarios/v2-addon-dev-test.ts diff --git a/packages/addon-dev/src/rollup-hbs-plugin.ts b/packages/addon-dev/src/rollup-hbs-plugin.ts index 048a811db..671770d69 100644 --- a/packages/addon-dev/src/rollup-hbs-plugin.ts +++ b/packages/addon-dev/src/rollup-hbs-plugin.ts @@ -1,7 +1,10 @@ import { createFilter } from '@rollup/pluginutils'; -import type { Plugin } from 'rollup'; import { readFileSync } from 'fs'; import { hbsToJS } from '@embroider/shared-internals'; +import path from 'path'; +import { pathExists } from 'fs-extra'; + +import type { Plugin } from 'rollup'; export default function rollupHbsPlugin(): Plugin { const filter = createFilter('**/*.hbs'); @@ -9,14 +12,53 @@ export default function rollupHbsPlugin(): Plugin { return { name: 'rollup-hbs-plugin', async resolveId(source: string, importer: string | undefined, options) { - const resolution = await this.resolve(source, importer, { + let resolution = await this.resolve(source, importer, { skipSelf: true, ...options, }); - const id = resolution?.id; + let id = resolution?.id; - if (!filter(id)) return null; + // if id is undefined, it's possible we're importing a file that that rollup + // doesn't natively support such as a template-only component that the author + // doesn't want to be available on the globals resolver + // + // e.g.: + // import { default as Button } from './button'; + // + // where button.hbs is the sole "button" file. + // + // if someone where to specify the `.hbs` extension themselves as in: + // + // import { default as Button } from './button'; + // + // then this whole block will be skipped + if (importer && !id) { + // We can't just emit the js side of the template-only component (export default templateOnly()) + // because we can't tell rollup where to put the file -- all emitted files are + // not-on-disk-area-used-at-build-time -- emitted files are for the build output + // + // https://github.com/rollup/rollup/blob/master/docs/05-plugin-development.md + // + // So, to deal with this, we need to ensure there _is no corresponding js/ts file_ + // for the imported hbs, and then, add in some meta so that the load hook can + // generate the setComponentTemplate + templateOnly() combo + let fileName = path.join(path.dirname(importer), source); + let hbsExists = await pathExists(fileName + '.hbs'); + + if (!hbsExists) return null; + + resolution = await this.resolve(source + '.hbs', importer, { + skipSelf: true, + ...options, + }); + + id = resolution?.id; + } + + if (!filter(id) || !id) return null; + + let isTO = await isTemplateOnly(id); // This creates an `*.hbs.js` that we will populate in `load()` hook. return { @@ -25,18 +67,28 @@ export default function rollupHbsPlugin(): Plugin { meta: { 'rollup-hbs-plugin': { originalId: id, + isTemplateOnly: isTO, }, }, }; }, load(id: string) { const meta = this.getModuleInfo(id)?.meta; - const originalId = meta?.['rollup-hbs-plugin']?.originalId; + const pluginMeta = meta?.['rollup-hbs-plugin']; + const originalId = pluginMeta?.originalId; + const isTemplateOnly = pluginMeta?.isTemplateOnly; if (!originalId) { return; } + if (isTemplateOnly) { + let code = getTemplateOnly(originalId); + + return { code }; + } + + // Co-located js + hbs let input = readFileSync(originalId, 'utf8'); let code = hbsToJS(input); return { @@ -45,3 +97,31 @@ export default function rollupHbsPlugin(): Plugin { }, }; } + +const backtick = '`'; + +async function isTemplateOnly(hbsPath: string) { + let jsPath = hbsPath.replace(/\.hbs$/, '.js'); + let tsPath = hbsPath.replace(/\.hbs$/, '.ts'); + + let [hasJs, hasTs] = await Promise.all([ + pathExists(jsPath), + pathExists(tsPath), + ]); + + let hasClass = hasJs || hasTs; + + return !hasClass; +} + +function getTemplateOnly(hbsPath: string) { + let input = readFileSync(hbsPath, 'utf8'); + let code = + `import { hbs } from 'ember-cli-htmlbars';\n` + + `import templateOnly from '@ember/component/template-only';\n` + + `import { setComponentTemplate } from '@ember/component';\n` + + `export default setComponentTemplate(\n` + + `hbs${backtick}${input}${backtick}, templateOnly());`; + + return code; +} diff --git a/packages/addon-dev/src/rollup-public-entrypoints.ts b/packages/addon-dev/src/rollup-public-entrypoints.ts index 2e788334c..a85c8d379 100644 --- a/packages/addon-dev/src/rollup-public-entrypoints.ts +++ b/packages/addon-dev/src/rollup-public-entrypoints.ts @@ -1,9 +1,10 @@ import walkSync from 'walk-sync'; -import type { Plugin } from 'rollup'; import { join } from 'path'; +import type { Plugin } from 'rollup'; + function normalizeFileExt(fileName: string) { - return fileName.replace(/\.ts|\.gts|\.gjs$/, '.js'); + return fileName.replace(/\.ts|\.hbs|\.gts|\.gjs$/, '.js'); } export default function publicEntrypoints(args: { @@ -12,10 +13,12 @@ export default function publicEntrypoints(args: { }): Plugin { return { name: 'addon-modules', - buildStart() { - for (let name of walkSync(args.srcDir, { - globs: args.include, - })) { + async buildStart() { + let matches = walkSync(args.srcDir, { + globs: [...args.include], + }); + + for (let name of matches) { this.emitFile({ type: 'chunk', id: join(args.srcDir, name), diff --git a/tests/scenarios/package.json b/tests/scenarios/package.json index 5c9640f93..b380df962 100644 --- a/tests/scenarios/package.json +++ b/tests/scenarios/package.json @@ -4,6 +4,7 @@ "dependencies": { "@embroider/shared-internals": "1.6.0", "@types/qunit": "^2.11.1", + "execa": "^5.0.0", "fastboot": "^3.1.0", "fs-extra": "^10.0.0", "globby": "^11.0.3", @@ -21,37 +22,43 @@ }, "license": "MIT", "devDependencies": { + "@babel/core": "^7.17.5", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-decorators": "^7.17.2", + "@babel/preset-env": "^7.16.11", "@ember/string": "^1.0.0", "@embroider/macros": "1.6.0", "@embroider/addon-shim": "1.6.0", "@embroider/router": "1.6.0", "@embroider/util": "1.6.0", + "@rollup/plugin-babel": "^5.3.1", "bootstrap": "^4.3.1", "broccoli-funnel": "^3.0.5", "broccoli-merge-trees": "^3.0.2", "broccoli-persistent-filter": "^3.1.2", "ember-bootstrap": "^5.0.0", + "ember-cli": "~3.28.0", "ember-cli-3.16": "npm:ember-cli@~3.16.0", "ember-cli-3.24": "npm:ember-cli@~3.24.0", - "ember-cli": "~3.28.0", "ember-cli-beta": "npm:ember-cli@beta", - "ember-cli-htmlbars-inline-precompile": "^2.1.0", + "ember-cli-fastboot": "^3.2.0", "ember-cli-htmlbars-3": "npm:ember-cli-htmlbars@3", + "ember-cli-htmlbars-inline-precompile": "^2.1.0", "ember-cli-latest": "npm:ember-cli@latest", - "ember-cli-fastboot": "^3.2.0", "ember-composable-helpers": "^4.4.1", + "ember-data": "~3.28.0", "ember-data-3.16": "npm:ember-data@~3.16.0", "ember-data-3.24": "npm:ember-data@~3.24.0", - "ember-data": "~3.28.0", "ember-data-latest": "npm:ember-data@latest", "ember-engines": "^0.8.17", "ember-inline-svg": "^0.2.1", - "ember-source-latest": "npm:ember-source@latest", - "ember-source-beta": "npm:ember-source@beta", + "ember-source": "~3.28.0", "ember-source-3.16": "npm:ember-source@~3.16.0", "ember-source-3.24": "npm:ember-source@~3.24.0", - "ember-source": "~3.28.0", - "ember-truth-helpers": "^3.0.0" + "ember-source-beta": "npm:ember-source@beta", + "ember-source-latest": "npm:ember-source@latest", + "ember-truth-helpers": "^3.0.0", + "rollup": "^2.69.1" }, "volta": { "node": "14.16.1", diff --git a/tests/scenarios/v2-addon-dev-test.ts b/tests/scenarios/v2-addon-dev-test.ts new file mode 100644 index 000000000..7fdb7dc26 --- /dev/null +++ b/tests/scenarios/v2-addon-dev-test.ts @@ -0,0 +1,227 @@ +import path from 'path'; +import { appScenarios, baseV2Addon } from './scenarios'; +import { PreparedApp } from 'scenario-tester'; +import QUnit from 'qunit'; +import merge from 'lodash/merge'; +import execa from 'execa'; +import { pathExists } from 'fs-extra'; + +const { module: Qmodule, test } = QUnit; + +/** + * The type of addon this is testing with only works in + * ember-source@3.25+ + */ +appScenarios + .skip('lts_3_16') + .skip('lts_3_24') + .map('v2 addon can have imports of template-only components', async project => { + let addon = baseV2Addon(); + addon.pkg.name = 'v2-addon'; + addon.pkg.files = ['dist']; + addon.pkg.exports = { + './*': './dist/*', + './addon-main.js': './addon-main.js', + }; + + merge(addon.files, { + 'babel.config.json': ` + { + "presets": [ + ["@babel/preset-env", { + "targets": ["last 1 firefox versions"] + }] + ], + "plugins": [ + "@embroider/addon-dev/template-colocation-plugin", + ["@babel/plugin-proposal-decorators", { "legacy": true }], + [ "@babel/plugin-proposal-class-properties" ] + ] + } + `, + 'rollup.config.mjs': ` + import { babel } from '@rollup/plugin-babel'; + import { Addon } from '@embroider/addon-dev/rollup'; + + const addon = new Addon({ + srcDir: 'src', + destDir: 'dist', + }); + + export default { + output: addon.output(), + + plugins: [ + addon.publicEntrypoints([ + '**/*.js', + 'components/demo/out.hbs', + ]), + + addon.appReexports([ + 'components/**/*.js', + 'components/demo/out.hbs', + ]), + + addon.hbs(), + addon.dependencies(), + + babel({ babelHelpers: 'bundled' }), + + addon.clean(), + ], + }; + `, + src: { + components: { + demo: { + 'button.hbs': ` + + `, + 'out.hbs': ` + {{yield}} + `, + 'index.js': ` + import Component from '@glimmer/component'; + import { tracked } from '@glimmer/tracking'; + + import FlipButton from './button'; + import BlahButton from './button.hbs'; + import Out from './out'; + + export default class ExampleComponent extends Component { + Button = FlipButton; + Button2 = BlahButton; + Out = Out; + + @tracked active = false; + + flip = () => (this.active = !this.active); + } + `, + 'index.hbs': ` + Hello there! + + {{this.active}} + + + + `, + }, + }, + }, + }); + + addon.linkDependency('@embroider/addon-shim', { baseDir: __dirname }); + addon.linkDependency('@embroider/addon-dev', { baseDir: __dirname }); + addon.linkDevDependency('@babel/core', { baseDir: __dirname }); + addon.linkDevDependency('@babel/plugin-proposal-class-properties', { baseDir: __dirname }); + addon.linkDevDependency('@babel/plugin-proposal-decorators', { baseDir: __dirname }); + addon.linkDevDependency('@babel/preset-env', { baseDir: __dirname }); + addon.linkDevDependency('@rollup/plugin-babel', { baseDir: __dirname }); + addon.linkDevDependency('rollup', { baseDir: __dirname }); + + project.addDevDependency(addon); + + merge(project.files, { + tests: { + 'the-test.js': ` + import { click, render } from '@ember/test-helpers'; + import { hbs } from 'ember-cli-htmlbars'; + import { module, test } from 'qunit'; + import { setupRenderingTest } from 'ember-qunit'; + + module('v2 addon tests', function (hooks) { + setupRenderingTest(hooks); + + test('', async function (assert) { + await render(hbs\`\`); + + assert.dom('out').containsText('false'); + + await click('button'); + + assert.dom('out').containsText('true'); + }); + + test('', async function (assert) { + await render(hbs\`hi\`); + + assert.dom('out').containsText('hi'); + }); + }); + `, + }, + }); + }) + .forEachScenario(scenario => { + Qmodule(scenario.name, function (hooks) { + let app: PreparedApp; + + async function getAddonInfo() { + let pkgPath = path.resolve(path.join(app.dir, 'node_modules/v2-addon/package.json')); + let dir = path.dirname(pkgPath); + + return { + dir, + distDir: path.join(dir, 'dist'), + build: async () => { + let rollupBin = 'node_modules/rollup/dist/bin/rollup'; + + await execa(path.join(dir, rollupBin), ['-c', './rollup.config.mjs'], { + cwd: dir, + }); + }, + reExports: async () => { + let pkgInfo = await import(pkgPath); + return pkgInfo['ember-addon']['app-js'] as Record; + }, + }; + } + + hooks.before(async () => { + app = await scenario.prepare(); + + let { build } = await getAddonInfo(); + + await build(); + }); + + Qmodule('The addon', function () { + test('output directories exist', async function (assert) { + let { distDir } = await getAddonInfo(); + + assert.strictEqual(await pathExists(distDir), true, 'dist/'); + assert.strictEqual(await pathExists(path.join(distDir, '_app_')), true, 'dist/_app_'); + }); + + test('package.json is modified appropriately', async function (assert) { + let { reExports } = await getAddonInfo(); + + assert.deepEqual(await reExports(), { + './components/demo/index.js': './dist/_app_/components/demo/index.js', + './components/demo/out.js': './dist/_app_/components/demo/out.js', + }); + }); + + test('the addon was built successfully', async function (assert) { + let { reExports, dir } = await getAddonInfo(); + let files = Object.values(await reExports()); + + assert.expect(files.length); + + for (let pathName of files) { + assert.deepEqual(await pathExists(path.join(dir, pathName)), true, `pathExists: ${pathName}`); + } + }); + }); + + Qmodule('Consuming app', function () { + test(`yarn test`, async function (assert) { + let result = await app.execute('yarn test'); + assert.equal(result.exitCode, 0, result.output); + }); + }); + }); + }); diff --git a/yarn.lock b/yarn.lock index cdf4ff4ac..4daef3667 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,13 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" +"@ampproject/remapping@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" + integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -49,6 +56,27 @@ json5 "^2.1.2" semver "^6.3.0" +"@babel/core@^7.17.5": + version "7.17.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.5.tgz#6cd2e836058c28f06a4ca8ee7ed955bbf37c8225" + integrity sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.3" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.17.2" + "@babel/parser" "^7.17.3" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + "@babel/generator@^7.17.0", "@babel/generator@^7.4.0": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.0.tgz#7bd890ba706cd86d3e2f727322346ffdbf98f65e" @@ -58,6 +86,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.17.3": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.3.tgz#a2c30b0c4f89858cb87050c3ffdfd36bdf443200" + integrity sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg== + dependencies: + "@babel/types" "^7.17.0" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" @@ -96,6 +133,19 @@ "@babel/helper-replace-supers" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" +"@babel/helper-create-class-features-plugin@^7.17.1": + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" + integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-create-regexp-features-plugin@^7.16.7": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" @@ -162,7 +212,7 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.8.3": +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.8.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== @@ -265,6 +315,15 @@ "@babel/traverse" "^7.17.0" "@babel/types" "^7.17.0" +"@babel/helpers@^7.17.2": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417" + integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.0" + "@babel/types" "^7.17.0" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7": version "7.16.10" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" @@ -279,6 +338,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c" integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw== +"@babel/parser@^7.17.3": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0" + integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" @@ -332,6 +396,17 @@ "@babel/plugin-syntax-decorators" "^7.17.0" charcodes "^0.2.0" +"@babel/plugin-proposal-decorators@^7.17.2": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.2.tgz#c36372ddfe0360cac1ee331a238310bddca11493" + integrity sha512-WH8Z95CwTq/W8rFbMqb9p3hicpt4RX4f0K659ax2VHxgOyT6qQmUaEVEjIh4WR9Eh9NymkVn5vwsrE68fAQNUw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.17.1" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/plugin-syntax-decorators" "^7.17.0" + charcodes "^0.2.0" + "@babel/plugin-proposal-dynamic-import@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" @@ -858,7 +933,7 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/preset-env@^7.10.2", "@babel/preset-env@^7.14.5", "@babel/preset-env@^7.16.5", "@babel/preset-env@^7.16.7", "@babel/preset-env@^7.9.0": +"@babel/preset-env@^7.10.2", "@babel/preset-env@^7.14.5", "@babel/preset-env@^7.16.11", "@babel/preset-env@^7.16.5", "@babel/preset-env@^7.16.7", "@babel/preset-env@^7.9.0": version "7.16.11" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982" integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g== @@ -988,6 +1063,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.17.3": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" + integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.3" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.3" + "@babel/types" "^7.17.0" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.7.2": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" @@ -2227,6 +2318,23 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== +"@rollup/plugin-babel@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + "@rollup/pluginutils@^4.1.1": version "4.1.2" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.2.tgz#ed5821c15e5e05e32816f5fb9ec607cdf5a75751" @@ -2514,6 +2622,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + "@types/estree@^0.0.50": version "0.0.50" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" @@ -9325,6 +9438,11 @@ estree-walker@^0.6.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + estree-walker@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" @@ -15600,6 +15718,13 @@ rollup@^2.50.0, rollup@^2.58.0: optionalDependencies: fsevents "~2.3.2" +rollup@^2.69.1: + version "2.69.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.69.1.tgz#d37f8bf9c9d60018df58c5c9ec36705a7b90dc6e" + integrity sha512-xaQKTomUVZBopk38EIshM/kOoPFkKWisgBV7Emy80coP9MOSLUDrba1jKZhqH0iS5DoGcRbbcuyl/BzblV8w5w== + optionalDependencies: + fsevents "~2.3.2" + rsvp@^3.0.14, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" From 12ddb98ba69445b25326aab9599b26a2cdc4830e Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Tue, 12 Apr 2022 23:16:53 -0400 Subject: [PATCH 2/4] use PreparedApp.execute Switch the tests to use the API I suggested in https://github.com/embroider-build/embroider/pull/1126#discussion_r824028017 --- tests/scenarios/package.json | 1 - tests/scenarios/v2-addon-dev-test.ts | 61 +++++++++++----------------- 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/tests/scenarios/package.json b/tests/scenarios/package.json index b380df962..0d49059a0 100644 --- a/tests/scenarios/package.json +++ b/tests/scenarios/package.json @@ -4,7 +4,6 @@ "dependencies": { "@embroider/shared-internals": "1.6.0", "@types/qunit": "^2.11.1", - "execa": "^5.0.0", "fastboot": "^3.1.0", "fs-extra": "^10.0.0", "globby": "^11.0.3", diff --git a/tests/scenarios/v2-addon-dev-test.ts b/tests/scenarios/v2-addon-dev-test.ts index 7fdb7dc26..861125319 100644 --- a/tests/scenarios/v2-addon-dev-test.ts +++ b/tests/scenarios/v2-addon-dev-test.ts @@ -3,8 +3,7 @@ import { appScenarios, baseV2Addon } from './scenarios'; import { PreparedApp } from 'scenario-tester'; import QUnit from 'qunit'; import merge from 'lodash/merge'; -import execa from 'execa'; -import { pathExists } from 'fs-extra'; +import { pathExistsSync, readJsonSync } from 'fs-extra'; const { module: Qmodule, test } = QUnit; @@ -15,13 +14,17 @@ const { module: Qmodule, test } = QUnit; appScenarios .skip('lts_3_16') .skip('lts_3_24') - .map('v2 addon can have imports of template-only components', async project => { + .map('v2-addon-dev', async project => { let addon = baseV2Addon(); addon.pkg.name = 'v2-addon'; addon.pkg.files = ['dist']; addon.pkg.exports = { './*': './dist/*', './addon-main.js': './addon-main.js', + './package.json': './package.json', + }; + addon.pkg.scripts = { + build: './node_modules/rollup/dist/bin/rollup -c ./rollup.config.mjs', }; merge(addon.files, { @@ -159,60 +162,39 @@ appScenarios Qmodule(scenario.name, function (hooks) { let app: PreparedApp; - async function getAddonInfo() { - let pkgPath = path.resolve(path.join(app.dir, 'node_modules/v2-addon/package.json')); - let dir = path.dirname(pkgPath); - - return { - dir, - distDir: path.join(dir, 'dist'), - build: async () => { - let rollupBin = 'node_modules/rollup/dist/bin/rollup'; - - await execa(path.join(dir, rollupBin), ['-c', './rollup.config.mjs'], { - cwd: dir, - }); - }, - reExports: async () => { - let pkgInfo = await import(pkgPath); - return pkgInfo['ember-addon']['app-js'] as Record; - }, - }; - } - hooks.before(async () => { app = await scenario.prepare(); - - let { build } = await getAddonInfo(); - - await build(); + let result = await inDependency(app, 'v2-addon').execute('yarn build'); + if (result.exitCode !== 0) { + throw new Error(result.output); + } }); Qmodule('The addon', function () { test('output directories exist', async function (assert) { - let { distDir } = await getAddonInfo(); - - assert.strictEqual(await pathExists(distDir), true, 'dist/'); - assert.strictEqual(await pathExists(path.join(distDir, '_app_')), true, 'dist/_app_'); + let { dir } = inDependency(app, 'v2-addon'); + assert.strictEqual(pathExistsSync(path.join(dir, 'dist')), true, 'dist/'); + assert.strictEqual(pathExistsSync(path.join(dir, 'dist', '_app_')), true, 'dist/_app_'); }); test('package.json is modified appropriately', async function (assert) { - let { reExports } = await getAddonInfo(); + let { dir } = inDependency(app, 'v2-addon'); + let reExports = readJsonSync(path.join(dir, 'package.json'))['ember-addon']['app-js']; - assert.deepEqual(await reExports(), { + assert.deepEqual(reExports, { './components/demo/index.js': './dist/_app_/components/demo/index.js', './components/demo/out.js': './dist/_app_/components/demo/out.js', }); }); test('the addon was built successfully', async function (assert) { - let { reExports, dir } = await getAddonInfo(); - let files = Object.values(await reExports()); + let { dir } = inDependency(app, 'v2-addon'); + let files: string[] = Object.values(readJsonSync(path.join(dir, 'package.json'))['ember-addon']['app-js']); assert.expect(files.length); for (let pathName of files) { - assert.deepEqual(await pathExists(path.join(dir, pathName)), true, `pathExists: ${pathName}`); + assert.deepEqual(pathExistsSync(path.join(dir, pathName)), true, `pathExists: ${pathName}`); } }); }); @@ -225,3 +207,8 @@ appScenarios }); }); }); + +// https://github.com/ef4/scenario-tester/issues/5 +function inDependency(app: PreparedApp, dependencyName: string): PreparedApp { + return new PreparedApp(path.dirname(require.resolve(`${dependencyName}/package.json`, { paths: [app.dir] }))); +} From f036ca60484ac52e5f7673f5e9b5d8dd2caec2f3 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 13 Apr 2022 01:21:56 -0400 Subject: [PATCH 3/4] refactor to compose component JS and HBS layers correctly --- packages/addon-dev/package.json | 1 + packages/addon-dev/src/rollup-hbs-plugin.ts | 194 +++++++++--------- .../src/rollup-public-entrypoints.ts | 38 +++- tests/scenarios/v2-addon-dev-test.ts | 10 +- 4 files changed, 133 insertions(+), 110 deletions(-) diff --git a/packages/addon-dev/package.json b/packages/addon-dev/package.json index 91ed5cfad..5dc4057db 100644 --- a/packages/addon-dev/package.json +++ b/packages/addon-dev/package.json @@ -25,6 +25,7 @@ "dependencies": { "@embroider/shared-internals": "^1.6.0", "@rollup/pluginutils": "^4.1.1", + "assert-never": "^1.2.1", "fs-extra": "^10.0.0", "minimatch": "^3.0.4", "rollup-plugin-copy-assets": "^2.0.3", diff --git a/packages/addon-dev/src/rollup-hbs-plugin.ts b/packages/addon-dev/src/rollup-hbs-plugin.ts index 671770d69..d8f180215 100644 --- a/packages/addon-dev/src/rollup-hbs-plugin.ts +++ b/packages/addon-dev/src/rollup-hbs-plugin.ts @@ -1,14 +1,16 @@ import { createFilter } from '@rollup/pluginutils'; +import type { + Plugin, + PluginContext, + CustomPluginOptions, + ResolvedId, +} from 'rollup'; import { readFileSync } from 'fs'; import { hbsToJS } from '@embroider/shared-internals'; -import path from 'path'; -import { pathExists } from 'fs-extra'; - -import type { Plugin } from 'rollup'; +import assertNever from 'assert-never'; +import { parse as pathParse } from 'path'; export default function rollupHbsPlugin(): Plugin { - const filter = createFilter('**/*.hbs'); - return { name: 'rollup-hbs-plugin', async resolveId(source: string, importer: string | undefined, options) { @@ -17,111 +19,109 @@ export default function rollupHbsPlugin(): Plugin { ...options, }); - let id = resolution?.id; - - // if id is undefined, it's possible we're importing a file that that rollup - // doesn't natively support such as a template-only component that the author - // doesn't want to be available on the globals resolver - // - // e.g.: - // import { default as Button } from './button'; - // - // where button.hbs is the sole "button" file. - // - // if someone where to specify the `.hbs` extension themselves as in: - // - // import { default as Button } from './button'; - // - // then this whole block will be skipped - if (importer && !id) { - // We can't just emit the js side of the template-only component (export default templateOnly()) - // because we can't tell rollup where to put the file -- all emitted files are - // not-on-disk-area-used-at-build-time -- emitted files are for the build output - // - // https://github.com/rollup/rollup/blob/master/docs/05-plugin-development.md - // - // So, to deal with this, we need to ensure there _is no corresponding js/ts file_ - // for the imported hbs, and then, add in some meta so that the load hook can - // generate the setComponentTemplate + templateOnly() combo - let fileName = path.join(path.dirname(importer), source); - let hbsExists = await pathExists(fileName + '.hbs'); - - if (!hbsExists) return null; - - resolution = await this.resolve(source + '.hbs', importer, { - skipSelf: true, - ...options, - }); - - id = resolution?.id; + if (!resolution) { + return maybeSynthesizeComponentJS(this, source, importer, options); + } else { + return maybeRewriteHBS(resolution); } - - if (!filter(id) || !id) return null; - - let isTO = await isTemplateOnly(id); - - // This creates an `*.hbs.js` that we will populate in `load()` hook. - return { - ...resolution, - id: id + '.js', - meta: { - 'rollup-hbs-plugin': { - originalId: id, - isTemplateOnly: isTO, - }, - }, - }; }, - load(id: string) { - const meta = this.getModuleInfo(id)?.meta; - const pluginMeta = meta?.['rollup-hbs-plugin']; - const originalId = pluginMeta?.originalId; - const isTemplateOnly = pluginMeta?.isTemplateOnly; - if (!originalId) { + load(id: string) { + const meta = getMeta(this, id); + if (!meta) { return; } - if (isTemplateOnly) { - let code = getTemplateOnly(originalId); - - return { code }; + switch (meta.type) { + case 'template': + let input = readFileSync(meta.originalId, 'utf8'); + let code = hbsToJS(input); + return { + code, + }; + case 'template-only-component-js': + return { + code: templateOnlyComponent, + }; + default: + assertNever(meta); } - - // Co-located js + hbs - let input = readFileSync(originalId, 'utf8'); - let code = hbsToJS(input); - return { - code, - }; }, }; } -const backtick = '`'; - -async function isTemplateOnly(hbsPath: string) { - let jsPath = hbsPath.replace(/\.hbs$/, '.js'); - let tsPath = hbsPath.replace(/\.hbs$/, '.ts'); - - let [hasJs, hasTs] = await Promise.all([ - pathExists(jsPath), - pathExists(tsPath), - ]); +const templateOnlyComponent = + `import templateOnly from '@ember/component/template-only';\n` + + `export default templateOnly();\n`; + +type Meta = + | { + type: 'template'; + originalId: string; + } + | { + type: 'template-only-component-js'; + }; + +function getMeta(context: PluginContext, id: string): Meta | null { + const meta = context.getModuleInfo(id)?.meta?.['rollup-hbs-plugin']; + if (meta) { + return meta as Meta; + } else { + return null; + } +} - let hasClass = hasJs || hasTs; +function correspondingTemplate(filename: string): string { + let { ext } = pathParse(filename); + return filename.slice(0, filename.length - ext.length) + '.hbs'; +} - return !hasClass; +async function maybeSynthesizeComponentJS( + context: PluginContext, + source: string, + importer: string | undefined, + options: { custom?: CustomPluginOptions; isEntry: boolean } +) { + let templateResolution = await context.resolve( + correspondingTemplate(source), + importer, + { + skipSelf: true, + ...options, + } + ); + if (!templateResolution) { + return null; + } + // we're trying to resolve a JS module but only the corresponding HBS + // file exists. Synthesize the template-only component JS. + return { + id: templateResolution.id.replace(/\.hbs$/, '.js'), + meta: { + 'rollup-hbs-plugin': { + type: 'template-only-component-js', + }, + }, + }; } -function getTemplateOnly(hbsPath: string) { - let input = readFileSync(hbsPath, 'utf8'); - let code = - `import { hbs } from 'ember-cli-htmlbars';\n` + - `import templateOnly from '@ember/component/template-only';\n` + - `import { setComponentTemplate } from '@ember/component';\n` + - `export default setComponentTemplate(\n` + - `hbs${backtick}${input}${backtick}, templateOnly());`; +const hbsFilter = createFilter('**/*.hbs'); + +function maybeRewriteHBS(resolution: ResolvedId) { + if (!hbsFilter(resolution.id)) { + return null; + } - return code; + // This creates an `*.hbs.js` that we will populate in `load()` hook. + return { + ...resolution, + id: resolution.id + '.js', + meta: { + 'rollup-hbs-plugin': { + type: 'template', + originalId: resolution.id, + }, + }, + }; } diff --git a/packages/addon-dev/src/rollup-public-entrypoints.ts b/packages/addon-dev/src/rollup-public-entrypoints.ts index a85c8d379..c854222a7 100644 --- a/packages/addon-dev/src/rollup-public-entrypoints.ts +++ b/packages/addon-dev/src/rollup-public-entrypoints.ts @@ -1,12 +1,16 @@ import walkSync from 'walk-sync'; import { join } from 'path'; +import minimatch from 'minimatch'; import type { Plugin } from 'rollup'; +import { pathExistsSync } from 'fs-extra'; function normalizeFileExt(fileName: string) { return fileName.replace(/\.ts|\.hbs|\.gts|\.gjs$/, '.js'); } +const hbsPattern = '**/*.hbs'; + export default function publicEntrypoints(args: { srcDir: string; include: string[]; @@ -15,15 +19,37 @@ export default function publicEntrypoints(args: { name: 'addon-modules', async buildStart() { let matches = walkSync(args.srcDir, { - globs: [...args.include], + globs: [...args.include, hbsPattern], }); for (let name of matches) { - this.emitFile({ - type: 'chunk', - id: join(args.srcDir, name), - fileName: normalizeFileExt(name), - }); + if (args.include.some((pattern) => minimatch(name, pattern))) { + // anything that matches one of the user's patterns is definitely emitted + this.emitFile({ + type: 'chunk', + id: join(args.srcDir, name), + fileName: normalizeFileExt(name), + }); + } else { + // this file didn't match one of the user's patterns, so it must match + // our hbsPattern. Infer the possible existence of a synthesized + // template-only component JS file and test whether that file would + // match the user's patterns. + let normalizedName = normalizeFileExt(name); + let id = join(args.srcDir, normalizedName); + if ( + args.include.some((pattern) => + minimatch(normalizedName, pattern) + ) && + !pathExistsSync(id) + ) { + this.emitFile({ + type: 'chunk', + id, + fileName: normalizedName, + }); + } + } } }, }; diff --git a/tests/scenarios/v2-addon-dev-test.ts b/tests/scenarios/v2-addon-dev-test.ts index 861125319..a3047acd8 100644 --- a/tests/scenarios/v2-addon-dev-test.ts +++ b/tests/scenarios/v2-addon-dev-test.ts @@ -56,13 +56,12 @@ appScenarios plugins: [ addon.publicEntrypoints([ - '**/*.js', - 'components/demo/out.hbs', + 'components/**/*.js', ]), addon.appReexports([ - 'components/**/*.js', - 'components/demo/out.hbs', + 'components/demo/index.js', + 'components/demo/out.js', ]), addon.hbs(), @@ -90,12 +89,10 @@ appScenarios import { tracked } from '@glimmer/tracking'; import FlipButton from './button'; - import BlahButton from './button.hbs'; import Out from './out'; export default class ExampleComponent extends Component { Button = FlipButton; - Button2 = BlahButton; Out = Out; @tracked active = false; @@ -109,7 +106,6 @@ appScenarios {{this.active}} - `, }, }, From c684edf85a5ac65a312937f7b16d51e5c4d93e5a Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 13 Apr 2022 01:54:34 -0400 Subject: [PATCH 4/4] attempt to tweak CI for windows --- tests/scenarios/v2-addon-dev-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scenarios/v2-addon-dev-test.ts b/tests/scenarios/v2-addon-dev-test.ts index a3047acd8..85a05ff30 100644 --- a/tests/scenarios/v2-addon-dev-test.ts +++ b/tests/scenarios/v2-addon-dev-test.ts @@ -24,7 +24,7 @@ appScenarios './package.json': './package.json', }; addon.pkg.scripts = { - build: './node_modules/rollup/dist/bin/rollup -c ./rollup.config.mjs', + build: 'node ./node_modules/rollup/dist/bin/rollup -c ./rollup.config.mjs', }; merge(addon.files, {