-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix importing of template-only components in V2 addons #1126
Fix importing of template-only components in V2 addons #1126
Conversation
Think of the In pseudocode, you need to do something like this where resolveId handles relative paths: resolveId(id) {
// ...
// for relative paths inside our own package, first try normal resolving.
// This is calling https://www.rollupjs.org/guide/en/#thisresolve
let result = this.resolve(source);
if (result) {
return result;
}
// only if that doesn't work, try to rewrite to .hbs
result = this.resolve(source.replace(/\.js$/, '.hbs'));
if (result) {
return result;
} |
59b64ba
to
e4f36d4
Compare
imported hbs files are now inlined -- which, imo, is a great thing to do, but it's not a safe thing to do as these components previously were still available to the globals resolver. adding hbs to publicEntrypoints' regex does the trick, but it also un-inlines the co-located templates for components that also have an associated class. 🤔 |
Co-located templates (meaning app/components/*.hbs) are never available to the resolver. Compare with a classic app where the behavior is the same. They always become an internal detail of the component JS module. |
I don't understand,
Or do you mean that, at the time the resolver becomes aware of anything, it's already been converted to JS? 🤔 maybe the approach is wrong here -- maybe we need addon.hbs() to instead convert hbs template-only to js and merge hbs+js co-located components. What I don't understand (at the moment): is the current behavior of the hbs plugin: let input = readFileSync(id, 'utf8');
let code =
`import { hbs } from 'ember-cli-htmlbars';\n` +
`export default hbs${backtick}${input}${backtick};`;
return {
code,
id: id + '.js',
}; would this not collide with the class half of a component? |
Yes, that. If you have: app/components/thing.js Those are two separate entries in the runtime resolver that ember puts together after finding both at runtime. But if you have app/components/thing.js Those become a single module from the perspective of the runtime resolver. And if you have app/components/thing.hbs It also becomes a single JS modules from ember's perspective.
This is misunderstanding how things work. We already let rollup inline lots of things, including templates. That has nothing to do with what will be accessible to the resolver. That's controlled entirely by the appReexports, which in turn depend on the publicEntrypoints. |
I forgot this option existed 😅
or (with v2 addons), will soon be (what I'm trying to figure out with this PR as well)
but it's all files based, yeah? like, you can't say "inline everything into one file, and then generate app re-exports for that", like, // dist/_app_/components/foo.js
export { Foo as default } from 'my-addon';
// dist/_app_/components/bar.js
export { Bar as default } from 'my-addon'; |
Speaking of, I think I just did it 🥳 https://github.com/NullVoxPopuli/ember-addon-v2-template-only-import
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does this emit the call to templateOnlyComponent()
? That is critical to generate correct template-only components.
The idea is that if somebody tries to load a nonexistent JS file beside an HBS file, we fill in the contents of the JS file with something like:
import templateOnlyComponent from "@ember/component/template-only";
export default templateOnlyComponent();
That is probably sufficient as long as the colocation plugin also has a chance to run and handle that. Otherwise we would also need to synthesize the import of the template and the setComponentTemplate
here.
The linked demo app seems to misunderstand the intended semantics of hbs imports in the v2 addon spec. This: should not produce a component, it should be just the template. We need this both for compatibility and because we use it internally to bind together a component's JS and HBS. That is, if you author both foo.js and foo.hbs, the colocation inserts an |
I will add that having the demo repo to go with this is a very good idea and once we get it to demonstrates all the features we want it can move directly into this repo as a test suite. |
558070d
to
bfdcdac
Compare
@ef4 I think I did it, can you verify? also, it'd be really handy to have automated tests in here -- have any thoughts on how that should be set up? maybe in the scenarios folder or something? |
bfdcdac
to
3c45615
Compare
There is a possible ways to test resolveId - https://github.com/rollup/plugins/blob/master/packages/virtual/test/test.js#L7 |
I was thinking testing a full build would be useful because the hbs plugin needs resolveId and resolve to work in concert with one another |
4801241
to
a886802
Compare
c4075fc
to
f52a8b9
Compare
3b426f8
to
2071415
Compare
2071415
to
9fa3edc
Compare
31dfe5c
to
ac54162
Compare
This is rebased, and my little demo repo works. example output: ❯ cat dist/components/demo/index.js
import { _ as _applyDecoratedDescriptor, a as _defineProperty, b as _initializerDefineProperty } from '../../_rollupPluginBabelHelpers-2eca8644.js';
import { setComponentTemplate } from '@ember/component';
import { hbs } from 'ember-cli-htmlbars';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import templateOnly from '@ember/component/template-only';
import Out from './out.js';
var TEMPLATE = hbs("Hello there!\n\n<this.Out>{{this.active}}</this.Out>\n\n<this.Button @onClick={{this.flip}} />\n<this.Button2 @onClick={{this.flip}} />\n");
var BlahButton = setComponentTemplate(hbs`<button {{on 'click' @onClick}}>
flip
</button>
`, templateOnly());
var _class, _descriptor;
let Demo = (_class = class Demo extends Component {
constructor(...args) {
super(...args);
_defineProperty(this, "Button", BlahButton);
_defineProperty(this, "Button2", BlahButton);
_defineProperty(this, "Out", Out);
_initializerDefineProperty(this, "active", _descriptor, this);
_defineProperty(this, "flip", () => this.active = !this.active);
}
}, (_descriptor = _applyDecoratedDescriptor(_class.prototype, "active", [tracked], {
configurable: true,
enumerable: true,
writable: true,
initializer: function () {
return false;
}
})), _class);
setComponentTemplate(TEMPLATE, Demo);
export { Demo as default }; The important thing I was trying to make sure happened:
So, success :D |
I've left the execa usage, because:
I don't think this sort of problem should be a blocker, because seems like we need to add behaviors to scenario-tester
Also, this PR has been open for a very long time (mostly my fault!) and the community needs the capabilities <3 Next up, I'll be working on seeing if this is still an issue: #1134 |
a37ec27
to
73591d1
Compare
Switch the tests to use the API I suggested in embroider-build#1126 (comment)
tests/scenarios/v2-addon-dev-test.ts
Outdated
import { tracked } from '@glimmer/tracking'; | ||
|
||
import FlipButton from './button'; | ||
import BlahButton from './button.hbs'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not correct. An import with an explicit hbs extension should not give you back a component. It gives back only the template. Which would not be invokable the way you're using this.
This is the same thing I pointed out in #1126 (comment)
let fileName = path.join(path.dirname(importer), source); | ||
let hbsExists = await pathExists(fileName + '.hbs'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is assuming that source
will always be a relative path, but that's not always true. For example, if somebody tries to import from an NPM package name that isn't installed, they will end up here, and we don't want to be appending package names to the importer path.
This really should be another call to this.resolve()
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is misleading. The globals resolver isn't really relevant to this. You control global resolving by deciding whether or not to put a module into appReexports. But this code runs for your modules whether or not they're in appReexports.
tests/scenarios/v2-addon-dev-test.ts
Outdated
plugins: [ | ||
addon.publicEntrypoints([ | ||
'**/*.js', | ||
'components/demo/out.hbs', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the meaning in imports, we need a way to talk about template files, so people need to be allowed to put explicit .hbs
here like this. But that can't mean the same thing as the template-only component. The template-only component is a JS file -- even if that JS file is synthesized by the build, rather than typed by the author.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NullVoxPopuli thanks for pushing this forward, and especially for getting good tests in place. I was able to sort out the implementation.
@@ -225,3 +207,8 @@ appScenarios | |||
}); | |||
}); | |||
}); | |||
|
|||
// https://github.com/ef4/scenario-tester/issues/5 | |||
function inDependency(app: PreparedApp, dependencyName: string): PreparedApp { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this makes sense, now that i see it 🤦
// 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'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is way simpler than what i was doing 🤔
@@ -56,13 +56,12 @@ appScenarios | |||
|
|||
plugins: [ | |||
addon.publicEntrypoints([ | |||
'**/*.js', | |||
'components/demo/out.hbs', | |||
'components/**/*.js', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we throw on non-js extensions in here and is app-re-exports?
) && | ||
!pathExistsSync(id) | ||
) { | ||
this.emitFile({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah,. This was probably what i was missing (and maybe all the changes in this file).
Because i was so focused on the other plugin, i didn't remember how in interacts with this plugin.
Accompanying repo for testing: https://github.com/NullVoxPopuli/ember-addon-v2-template-only-import
TODO
Resolves: #1121