Stencil: Sudden build error without message after adding `#Method` to component - stenciljs

I cannot give too much information here (because there really isn't), but only this:
All of the sudden, after adding a #Method function to a stencil component:
#Method()
async setMenuItems(items: Element[]): Promise<void> {
// code here
}
the component stopped compiling with the following - really unhelpful - error:
[ ERROR ] ./src/components/menu-content/menu-content.tsx:63:44
build error
L62: #Method()
L63: async setMenuItems(elements: Element[]): Promise<void> {
L64: const unsupportedChildren = elements.filter(e => !this.isSupportedChild(e)).map(e => e.tagName);
[12:37.1] build failed in 7.02 s
Things to notice
the return type Promise<void> inside the error-message is highlighted red
there are other #Methods that do work within this component (even with the same return type).
the "broken" #Method is structurally equal to those that do work.
TypeScript compiler does not complain about anything
Only stencil compiler fails
I already tried...
to google for this issue - did not find any hints to this problem
to remove the async and add return Promise.resolve()
to rename the method (I mean.. why not)
to move the method to another place in class
to remove the whole method (compiles fine x( )
to remove the #Method decorator (compiled, but of course my method is removed from API)
to delete node_modules folder and reinstall
I remember that I already had this error once, and apparently I somehow fixed it (or not, idk). But if I did, I cannot remember how.
Does anyone have an idea how to debug this - or even better fix?

I figured it out. A more complete version of my component is:
import { Element, ... } from '#stencil/core';
class MenuContent {
#Element() element: MenuContentElement;
#Method()
setMenuItems(elements: Element[]): Promise<void> {
// ------------------^^^^^^^
// Element is meant to be the built-in browser interface for Element
// but stencil thinks that this is the imported Element from '#stencil/core'!
}
}
The exact problem here is, that the stencil-compiler seems to assume that the elements parameter is of type Element that is imported from #stencil/core which is obviously wrong and leads to this strange unhelpful error.
Possible Solutions
1. Use an alias type for the built-in Element type
// types.ts
export type DomElement = Element;
// menu-content.tsx
import { DomElement } from './types';
...
async setMenuItems(elements: DomElement[]): Promise<void> { ... }
2. Switch to HTMLElement
Note: This is only legit, when you don't need to support other Element-types such as SVGElements for example!
async setMenuItems(elements: HTMLElement[]): Promise<void> { ... }
3. Use alias in import statement
Please note: When using #stencil eslint rules, they will complain about your renamed import and say that "own class members are not allowed to be public".
I created a ticket for it here: https://github.com/ionic-team/stencil-eslint/issues/28
import { Element as ElementDecorator} from '#stencil/core';
class MenuContent {
// eslint will complain about that:
// "Own class properties cannot be public."
#ElementDecorator() element: HTMLMenuContentElement;
}

I experienced this same issue not with the Element type but with the Event type, so it appears to be rooted deeper - also about a year after you reported this issue it seems to still be a problem with Stencil

Related

TypeScript safe route names?

Consider the following code:
const router = useRouter()
await router.push({
name: 'NonExistingRoute', // no typescript error ):
})
A non existing route name was given, but no TypeScript error happens. Instead the issue will only be noticed on runtime. Any way to get a compile-time error on this?
Perhaps you could wrap this in a utility function that only accepts typed route strings
const router = useRouter()
export type NamedRoute = "login" | "logout" | "user-profile";
export async function goToNamedRoute(name: NamedRoute): Promise<void> {
return router.push({name});
}
In short no.
For a compile error to exist there would need to be something explicitly wrong with the code, referencing an non-existent file, syntax error, etc.
It does sound like you are trying to solve some other issue here...i.e. why do you have the names of non-existing routes in your app?
In any case, perhaps you can avoid your errors programmatically, e.g.
let r = router.resolve({name: 'NonExistingRoute'});
if (r.resolved.matched.length > 0){
// exists
} else {
// doesn't exist
}
If you want to rely on Typescript for detecting wrong routes you might just use enums or closed types maybe?, although that will surely require some composition. Probably one way to go could be:
enum Cities {
NY,
London
}
function routeCreator(city: Cities, restOfPath?: string){
//combine paths somehow, e.g.
if(!restOfPath) return `/${Cities[city]}/`;
return `/${Cities[city]}/${restOfPath}`
}

trace() error "can only be used inside a tracked computed value or a Reaction" when used inside computed

I've used MobX for a few years now, and love it, but sometimes my trace calls are not functioning, and I don't understand why not. There must be some fundamental thing that I've completely misunderstood, but most likely have been lucky enough to get through anyway. Here's an example of using trace() where I'm getting an error:
import { computed, observable, trace } from "mobx";
class Stat {
#observable baseValue = 1;
#computed get value() {
trace();
return this.baseValue;
}
}
const strength = new Stat();
strength.baseValue = strength.baseValue + 1;
The expected output, in my mind, is that trace reacts to the change in "baseValue" and logs the change. Instead, I'm getting the following error:
Error: [MobX] 'trace(break?)' can only be used inside a tracked computed value or a Reaction. Consider passing in the computed value or reaction explicitly
"Inside a tracked computed value" is, to my understanding, exactly what I'm doing. Or..?
Full sandbox: https://codesandbox.io/s/mobx-trace-trouble-ki2qj?file=/index.ts:0-312
As far as I understand this phrase
inside a tracked computed value or a Reaction.
you need to access computed value inside reactive context, like inside observer or reaction or autorun. Otherwise trace just don't have information about what is going on because your computed value is untracked at that moment by any observer.
So this will work:
const MyComponent = observer(() => {
return <div>{strength.value}</name>
})
or this
autorun(() => {
console.log(strength.value);
});

Jest spyOn vs mock - both defined and undefined errors

Just upgraded to jsdom-fourteen in my jest configuration. It's working wonderfully, but a single test is failing.
test('Do the thing', () => {
window.location.assign = jest.fn();
});
I inherited this code. It looks like a simple enough jest mock. It complains that it cannot assign the read-only property assign and that makes sense, I assume this is jsdom functionality that was added.
However... I can't do a jest.spyOn either, which seems to be what is suggested. I've not used spyOn before.
jest.spyOn(window.location.assign);
But this gives me an undefined property error:
Cannot spy the undefined property because it is not a function; undefined given instead
The line before this, I added a log just to check. It is definitely a function:
console.log(window.location.assign);
=> [Function: assign]
I'm not sure how these two errors can even coexist - both defined and undefined?
Due to how JavaScript works, it would be impossible to write spyOn function the way that allowed it to work like spyOn(window.location.assign). Inside spyOn, it's possible to retrieve window.location.assign function that was provided as an argument but not window.location object and assign method name to do window.location.assign = jest.fn().
The signature of spyOn is:
jest.spyOn(object, methodName)
It should be:
jest.spyOn(window.location, 'assign');
This may be unworkable as well because window.location.assign is read-only in later JSDOM versions, which is used by Jest to emulate DOM in Node.js. The error confirms that this is the issue.
It may be possible to mock read-only property manually:
const origAssign = Object.getOwnPropertyDescriptor(window.location, 'assign');
beforeEach(() => {
Object.defineProperty(window.location, 'assign', { value: jest.fn() })
});
afterEach(() => {
Object.defineProperty(window.location, 'assign', origAssign)
});
This wouldn't work with real DOM because built-ins may be read-only and non-configurable. This is the issue in Chrome. For testability reasons it may be beneficial to use location.href instead of location.assign.
Eventually worked through some things and found this:
delete global.window.location;
window.location = { assign : jest.fn()};
As it appears later iterations of jsdom lock the location object down further and further until it's completely not modifiable, #Estus' answer will only work in lower versions of jsdom/jest.

How to override dojo's domReady

I want to override dijit._CssStateMixin's domReady() method.
Is there any way to override that instead of changing the listener mechanism in Dojo.
I tried overriding _cssMouseEvent() method in simple javascript, but it still does invoke dijit's _cssMouseEvent() from domReady().
I have tried following approach:
dojoConfig = {
map: {
'dijit/_CssStateMixin': {
'dojo/domReady': 'app/noop'
}
}
};
I have added 'app' folder and then 'noop.js' inside that.
noop.js has nothing in it:
define([], function () {
return function () {};
});
Even after this I can see that dijit.js's _CssStateMaxin domReady() getting called from listener.apply (code snippet pasted below)
var addStopImmediate = function(listener){
return function(event){
if(!event.immediatelyStopped){// check to make sure it hasn't been stopped immediately
event.stopImmediatePropagation = stopImmediatePropagation;
return listener.apply(this, arguments);
}
};
}
If your ultimate goal is to prevent the domReady callback in dijit/_CssStateMixin from running, your simplest bet is likely to re-map dojo/domReady to a different module that doesn't call the callback at all, when loaded via dijit/_CssStateMixin.
NOTE: Stripping out these handlers might have adverse visual effects on Dijit widgets which inherit _CssStateMixin, since it may hinder the application of Dijit CSS classes related to hover and focus. But if your concern is that _CssStateMixin is hampering performance, it may at least be worth a try to confirm or deny your suspicion.
First we have to create a simple module that returns a function that does nothing, which we will later substitute for dojo/domReady when loaded by dijit/_CssStateMixin, so that it can still call domReady but it won't execute the callback it passes.
For simplicity's sake I'll assume you already have a custom package that you can easily add a module to; for this example I'll assume it's called app. Let's create app/noop:
define([], function () {
return function () {};
});
Now let's configure the loader to map app/noop in place of dojo/domReady specifically when loaded by dijit/_CssStateMixin:
var dojoConfig = {
...,
map: {
'dijit/_CssStateMixin': {
'dojo/domReady': 'app/noop'
}
},
...
};
Now the offending domReady callback should no longer be run.
If you're curious about map, you can read more about it in this SitePen FAQ.

How to register component interface in wxwebconnect?

I'm doing an experiment with wxWebConnect test application, incorporating the xpcom tutorial at "http://nerdlife.net/building-a-c-xpcom-component-in-windows/"
I adapt MyComponent class as necessary to compile together with testapp.exe (not as separate dll), and on MyApp::OnInit I have the following lines:
ns_smartptr<nsIComponentRegistrar> comp_reg;
res = NS_GetComponentRegistrar(&comp_reg.p);
if (NS_FAILED(res))
return false;
ns_smartptr<nsIFactory> prompt_factory;
CreateMyComponentFactory(&prompt_factory.p);
nsCID prompt_cid = MYCOMPONENT_CID;
res = comp_reg->RegisterFactory(prompt_cid,
"MyComponent",
"#mozilla.org/mycomp;1",
prompt_factory);
Those lines are copied from GeckoEngine::Init(), using the same mechanism to register PromptService, etc. The code compiles well and testapp.exe is running as expected.
I put javascript test as below :
try {
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
const cid = "#mozilla.org/mycomp;1";
obj = Components.classes[cid].createInstance();
alert(typeof obj);
// bind the instance we just created to our interface
alert(Components.interfaces.nsIMyComponent);
obj = obj.QueryInterface(Components.interfaces.nsIMyComponent);
} catch (err) {
alert(err);
return;
}
and get the following exception:
Could not convert JavaScript argument arg 0 [nsISupport.QueryInterface]
The first alert says "object", so the line
Components.classes[cid].createInstance()
is returning the created instance.
The second alert says "undefined", so the interface nsIMyComponent is not recognized by XULRunner.
How to dynamically registering nsIMyComponent interface in wxWebConnect environment ?
Thx
I'm not sure what is happening here. The first thing I would check is that your component is scriptable (I assume it is, since the demo you copy from is). The next thing I would check is whether you can instantiate other, standard XULRunner components and get their interface (try something like "alert('Components.interfaces.nsIFile');" - at least in my version of wxWebConnect this shows an alert box with string "nsIFile".
Also, I think it would be worth checking the Error Console to make sure there are no errors or warnings reported. A magic string to do that (in Javascript) is:
window.open('chrome://global/content/console.xul', '', 'chrome,dialog=no,toolbar,resizable');