LitElement: import { LitElement, html }

I’m starting a new test project with Meteor 1.7+ and am using LitElement for a few components.

I installed Meteor like so:

meteor create meteor-lithtml --release 1.7.1-beta.29 --bare

Meteor node version:

meteor node --version v8.11.3

I installed like so:

meteor npm install --save @polymer/lit-element

My node_modules directory looks like so:

image

My package.json file:

{
  "name": "myapp",
  "private": true,
  "scripts": {
    "start": "meteor run"
  },
  "dependencies": {
    "@babel/runtime": "^7.0.0-beta.56",
    "@polymer/lit-element": "^0.5.2",
    "@vaadin/router": "^1.0.0",
    "meteor-node-stubs": "^0.4.1",
    "redux": "^4.0.0"
  },
  "meteor": {
    "mainModule": {
      "client": "client/index.js",
      "server": "server/index.js"
    }
  }
}

The typical way I see lit-element imported is not working. Just adding an index.js file and importing the lit-element module me errors. If I remove the import from the index.js file, the errors go away.

\\ client\index.js
import { LitElement, html } from '@polymer/lit-element';

Uncaught SyntaxError: Unexpected token { 

modules.js?hash=182125a3fa97eaa24f6d313584ca593c3aed2103:984 

tracker.js?hash=7255…82b90fd0455ebc45:17 Uncaught TypeError: Cannot read property 'meteorInstall' of undefined
    at tracker.js?hash=7255…82b90fd0455ebc45:17
    at tracker.js?hash=7255…2b90fd0455ebc45:672
minimongo.js?hash=8d…6d441ef38db014c1:17 Uncaught TypeError: Cannot read property 'DiffSequence' of undefined
    at minimongo.js?hash=8d…6d441ef38db014c1:17
    at minimongo.js?hash=8d…441ef38db014c1:4798
check.js?hash=401d16…dd6e014eda8f4123:17 Uncaught TypeError: Cannot read property 'EJSON' of undefined
    at check.js?hash=401d16…dd6e014eda8f4123:17
    at check.js?hash=401d16…d6e014eda8f4123:637
retry.js?hash=7ed3fc…3c1a05b724a47a0d:17 Uncaught TypeError: Cannot read property 'Random' of undefined
    at retry.js?hash=7ed3fc…3c1a05b724a47a0d:17
    at retry.js?hash=7ed3fc…c1a05b724a47a0d:106
callback-hook.js?has…9f154062677c5c94:17 Uncaught TypeError: Cannot read property 'meteorInstall' of undefined
    at callback-hook.js?has…9f154062677c5c94:17
    at callback-hook.js?has…f154062677c5c94:193
ddp-common.js?hash=4…f30ac562322abb24:17 Uncaught TypeError: Cannot read property 'check' of undefined
    at ddp-common.js?hash=4…f30ac562322abb24:17
    at ddp-common.js?hash=4…30ac562322abb24:518
reload.js?hash=ca0e0…8becb78af6861ca6:17 Uncaught TypeError: Cannot read property 'meteorInstall' of undefined
    at reload.js?hash=ca0e0…8becb78af6861ca6:17
    at reload.js?hash=ca0e0…becb78af6861ca6:256
socket-stream-client…f6f0548f9279a6f0:17 Uncaught TypeError: Cannot read property 'Retry' of undefined
    at socket-stream-client…f6f0548f9279a6f0:17
    at socket-stream-client…f0548f9279a6f0:3267
ddp-client.js?hash=8…f25cc5b8318191a3:17 Uncaught TypeError: Cannot read property 'check' of undefined
    at ddp-client.js?hash=8…f25cc5b8318191a3:17
    at ddp-client.js?hash=8…5cc5b8318191a3:2183
ddp.js?hash=fcd82059…a2e16c1a15e1480b:14 Uncaught TypeError: Cannot read property 'DDP' of undefined
    at ddp.js?hash=fcd82059…a2e16c1a15e1480b:14
    at ddp.js?hash=fcd82059…a2e16c1a15e1480b:23
allow-deny.js?hash=b…6af70d6446753d30:17 Uncaught TypeError: Cannot read property 'LocalCollection' of undefined
    at allow-deny.js?hash=b…6af70d6446753d30:17
    at allow-deny.js?hash=b…af70d6446753d30:556
mongo.js?hash=d31c20…2d572967865b2a09:17 Uncaught TypeError: Cannot read property 'AllowDeny' of undefined
    at mongo.js?hash=d31c20…2d572967865b2a09:17
    at mongo.js?hash=d31c20…d572967865b2a09:879
reactive-var.js?hash…0375579e8f4f84c3:17 Uncaught TypeError: Cannot read property 'Tracker' of undefined
    at reactive-var.js?hash…0375579e8f4f84c3:17
    at reactive-var.js?hash…375579e8f4f84c3:138
webapp.js?hash=3ff56…cfc31e5f30c8a5bf:17 Uncaught TypeError: Cannot read property 'meteorInstall' of undefined
    at webapp.js?hash=3ff56…cfc31e5f30c8a5bf:17
    at webapp.js?hash=3ff56…cfc31e5f30c8a5bf:68
livedata.js?hash=642…99913760aa3915cb:14 Uncaught TypeError: Cannot read property 'DDP' of undefined
    at livedata.js?hash=642…99913760aa3915cb:14
    at livedata.js?hash=642…99913760aa3915cb:27
autoupdate.js?hash=6…cc9742ecdc5a4f50:17 Uncaught TypeError: Cannot read property 'Tracker' of undefined
    at autoupdate.js?hash=6…cc9742ecdc5a4f50:17
    at autoupdate.js?hash=6…c9742ecdc5a4f50:249
global-imports.js?ha…9ebca19bdc4661d48:3 Uncaught TypeError: Cannot read property 'Mongo' of undefined
    at global-imports.js?ha…9ebca19bdc4661d48:3
app.js?hash=7f3d88fa…3881ea0db43a72eca:1 Uncaught ReferenceError: meteorInstall is not defined
    at app.js?hash=7f3d88fa…3881ea0db43a72eca:1


​
```

I've provided a reference repo here: https://github.com/aadamsx/meteor-lithtml

The very first error:

Uncaught SyntaxError: Unexpected token {

modules.js?hash=182125a3fa97eaa24f6d313584ca593c3aed2103:984 

Points to this location:

Expanding node_modules to look into this file:

This is the mixin file:

/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
import '../utils/boot.js';

import { dedupingMixin } from '../utils/mixin.js';
import { PropertiesChanged } from './properties-changed.js';

/**
 * Creates a copy of `props` with each property normalized such that
 * upgraded it is an object with at least a type property { type: Type}.
 *
 * @param {Object} props Properties to normalize
 * @return {Object} Copy of input `props` with normalized properties that
 * are in the form {type: Type}
 * @private
 */
function normalizeProperties(props) {
  const output = {};
  for (let p in props) {
    const o = props[p];
    output[p] = (typeof o === 'function') ? {type: o} : o;
  }
  return output;
}

/**
 * Mixin that provides a minimal starting point to using the PropertiesChanged
 * mixin by providing a mechanism to declare properties in a static
 * getter (e.g. static get properties() { return { foo: String } }). Changes
 * are reported via the `_propertiesChanged` method.
 *
 * This mixin provides no specific support for rendering. Users are expected
 * to create a ShadowRoot and put content into it and update it in whatever
 * way makes sense. This can be done in reaction to properties changing by
 * implementing `_propertiesChanged`.
 *
 * @mixinFunction
 * @polymer
 * @appliesMixin PropertiesChanged
 * @summary Mixin that provides a minimal starting point for using
 * the PropertiesChanged mixin by providing a declarative `properties` object.
 */
export const PropertiesMixin = dedupingMixin(superClass => {

 /**
  * @constructor
  * @implements {Polymer_PropertiesChanged}
  * @private
  */
 const base = PropertiesChanged(superClass);

 /**
  * Returns the super class constructor for the given class, if it is an
  * instance of the PropertiesMixin.
  *
  * @param {!PropertiesMixinConstructor} constructor PropertiesMixin constructor
  * @return {?PropertiesMixinConstructor} Super class constructor
  */
 function superPropertiesClass(constructor) {
   const superCtor = Object.getPrototypeOf(constructor);

   // Note, the `PropertiesMixin` class below only refers to the class
   // generated by this call to the mixin; the instanceof test only works
   // because the mixin is deduped and guaranteed only to apply once, hence
   // all constructors in a proto chain will see the same `PropertiesMixin`
   return (superCtor.prototype instanceof PropertiesMixin) ?
     /** @type {!PropertiesMixinConstructor} */ (superCtor) : null;
 }

 /**
  * Returns a memoized version of the `properties` object for the
  * given class. Properties not in object format are converted to at
  * least {type}.
  *
  * @param {PropertiesMixinConstructor} constructor PropertiesMixin constructor
  * @return {Object} Memoized properties object
  */
 function ownProperties(constructor) {
   if (!constructor.hasOwnProperty(JSCompiler_renameProperty('__ownProperties', constructor))) {
     let props = null;

     if (constructor.hasOwnProperty(JSCompiler_renameProperty('properties', constructor)) && constructor.properties) {
       props = normalizeProperties(constructor.properties);
     }

     constructor.__ownProperties = props;
   }
   return constructor.__ownProperties;
 }

 /**
  * @polymer
  * @mixinClass
  * @extends {base}
  * @implements {Polymer_PropertiesMixin}
  * @unrestricted
  */
 class PropertiesMixin extends base {

   /**
    * Implements standard custom elements getter to observes the attributes
    * listed in `properties`.
    * @suppress {missingProperties} Interfaces in closure do not inherit statics, but classes do
    */
   static get observedAttributes() {
     const props = this._properties;
     return props ? Object.keys(props).map(p => this.attributeNameForProperty(p)) : [];
   }

   /**
    * Finalizes an element definition, including ensuring any super classes
    * are also finalized. This includes ensuring property
    * accessors exist on the element prototype. This method calls
    * `_finalizeClass` to finalize each constructor in the prototype chain.
    * @return {void}
    */
   static finalize() {
     if (!this.hasOwnProperty(JSCompiler_renameProperty('__finalized', this))) {
       const superCtor = superPropertiesClass(/** @type {!PropertiesMixinConstructor} */(this));
       if (superCtor) {
         superCtor.finalize();
       }
       this.__finalized = true;
       this._finalizeClass();
     }
   }

   /**
    * Finalize an element class. This includes ensuring property
    * accessors exist on the element prototype. This method is called by
    * `finalize` and finalizes the class constructor.
    *
    * @protected
    */
   static _finalizeClass() {
     const props = ownProperties(/** @type {!PropertiesMixinConstructor} */(this));
     if (props) {
       this.createProperties(props);
     }
   }

   /**
    * Returns a memoized version of all properties, including those inherited
    * from super classes. Properties not in object format are converted to
    * at least {type}.
    *
    * @return {Object} Object containing properties for this class
    * @protected
    */
   static get _properties() {
     if (!this.hasOwnProperty(
       JSCompiler_renameProperty('__properties', this))) {
       const superCtor = superPropertiesClass(/** @type {!PropertiesMixinConstructor} */(this));
       this.__properties = Object.assign({},
         superCtor && superCtor._properties,
         ownProperties(/** @type {PropertiesMixinConstructor} */(this)));
     }
     return this.__properties;
   }

   /**
    * Overrides `PropertiesChanged` method to return type specified in the
    * static `properties` object for the given property.
    * @param {string} name Name of property
    * @return {*} Type to which to deserialize attribute
    *
    * @protected
    */
   static typeForProperty(name) {
     const info = this._properties[name];
     return info && info.type;
   }

   /**
    * Overrides `PropertiesChanged` method and adds a call to
    * `finalize` which lazily configures the element's property accessors.
    * @override
    * @return {void}
    */
   _initializeProperties() {
     this.constructor.finalize();
     super._initializeProperties();
   }

   /**
    * Called when the element is added to a document.
    * Calls `_enableProperties` to turn on property system from
    * `PropertiesChanged`.
    * @suppress {missingProperties} Super may or may not implement the callback
    * @return {void}
    * @override
    */
   connectedCallback() {
     if (super.connectedCallback) {
       super.connectedCallback();
     }
     this._enableProperties();
   }

   /**
    * Called when the element is removed from a document
    * @suppress {missingProperties} Super may or may not implement the callback
    * @return {void}
    * @override
    */
   disconnectedCallback() {
     if (super.disconnectedCallback) {
       super.disconnectedCallback();
     }
   }

 }

 return PropertiesMixin;

});

Where is the unexpected { token? Will someone help me track this down? :slight_smile:

FYI: I’ve provided a reference repo here just in case: https://github.com/aadamsx/meteor-lithtml

What browser/version are you using?

1 Like

The latest Chrome. :slight_smile:

The Polymer folks are among the only npm package authors who publish only ECMAScript module syntax (import and export declarations) directly to npm.

Since most code published to npm is precompiled (Polymer being a noteworthy exception) Meteor does not (re)compile the contents of node_modules by default.

However, as long as you’re using Meteor 1.7, you can get Meteor to compile this package using the following technique:

cd path/to/your/app
mkdir -p imports
cd imports
# Clone the source package locally, optionally using a git submodule.
git submodule add git@github.com:Polymer/lit-element.git
cd lit-element
# Now you need to build the package locally, according to whatever build
# steps the package authors use before they publish the package. This step
# may vary from package to package, though `npm install` often works.
# Fair warning: in this case, the full install can take a while.
meteor npm install
# Make sure lit-element.js exists, since it's the "main" in package.json.
# Good news: you won't need these 343MB of dependencies, unless you're
# planning to do local development of this package.
rm -rf node_modules
cd ../..
meteor npm install ./imports/lit-element

Since the imports/lit-element/* code is exposed as part of your Meteor application, Meteor will compile it like any application code, but the npm install step also links that code into node_modules/@polymer/lit-element, so you should be able to import it as if it was installed directly from npm.

Unfortunately, your work is not done yet, since you will need to repeat this process for any other Polymer packages, such as @polymer/polymer and lit-html, since they are also published with uncompiled ESM syntax.

The problem of recompiling packages installed in node_modules is tricky because doing it by default would destroy build performance (for any build tool), but the best API for selectively enabling compilation of specific npm packages is an unsolved problem in the JavaScript community. Meteor 1.7 has adopted an approach that gives you the greatest possible control over how the package is compiled (since you can do anything to the local version before you run npm install), though it is certainly not the simplest answer.

While I applaud the Polymer team for pushing the boundaries of JavaScript and web development, their npm publishing strategy sometimes feels like an academic exercise detached from the practical realities of application development. What I really mean is: by using Polymer, you’ve signed up for a little bit of extra build tool wrangling, in exchange for getting to use something very new and shiny. We’re happy to help you make it work, if we can!

10 Likes

I did not know that. Pesky Polymer People!

2 Likes

:sparkles: WAIT HOLD ON :sparkles:

Everything I said above still stands, but there’s a much easier way to get Polymer packages to be recompiled, as long as you don’t need to modify them locally. But first, let me explain how the technique above actually works.

Because node_modules/@polymer/lit-element is a symbolic link to imports/lit-element, any pair of paths like

node_modules/@polymer/lit-element/lit-element.js
imports/lit-element/lit-element.js

have the same “real path” (that is, the normalized path you get after you remove any symbolic links).

Meteor 1.7 notices this relationship, and makes sure behind the scenes that both paths can be used to refer to the same module. In other words, it doesn’t matter whether you import @polymer/lit-element (preferred) or ./imports/lit-element (also ok), since there’s only one logical package, with two (or more!) different ways of referring to it.

The really cool thing about this behavior is that the relationship of “having the same real path” is technically an equivalence relation, which is a fancy way of saying it doesn’t make a difference which way the symbolic link points. In fact, both paths can contain symbolic links, so that neither of them is the real path, as long as they both share a common real path.

In other words, instead of cloning the source package locally and linking it into node_modules, you can just install the package from npm, and then create a symbolic link to the installed package somewhere outside of node_modules:

meteor npm install @polymer/lit-element
mkdir -p imports/links
cd imports/links
ln -s ../../node_modules/@polymer . # handles everything under @polymer/...
ln -s ../../node_modules/lit-html .
# ... keep going until you've "exposed" everything that needs to be recompiled
cd ../..
meteor run

And that’s it!

When you create the symbolic link in this direction, you’re selectively enabling recompilation for certain subdirectories of node_modules, without having to worry about building the packages from source.

As before,

node_modules/@polymer/lit-element/lit-element.js
imports/links/@polymer/lit-element/lit-element.js

both have the same real path, even though we’re now linking into node_modules instead of linking out of node_modules. Note: this variation of the technique can be a little tricky for Windows developers, since creating symbolic links is hard without admin privileges, but it’s totally doable. I’m happy to provide additional advice if this is relevant to you.

Best of all, Meteor will recompile the code in question in different ways for modern browsers, legacy browsers, and Node, which is something that just isn’t possible when you’re publishing a package to npm the normal way. In a sense, it’s a good thing that Polymer isn’t trying to make compilation decisions for you, since that would rob you of this amazing opportunity to optimize your JS bundles for different JS environments. More on this modern/legacy bundling system in the Meteor 1.7 blog post:

14 Likes

Quick PR against your reproduction repo showing that the second technique works:

3 Likes

THANK YOU for the PR and the detailed explaination on this!

I think what the Meteor and Polymer team is doing in the latest releases is cutting edge and really cool stuff. It seems like our JS quality of life is getting better ever so surely!

I really like how this came together! :smiley:

The MDG community is SUPER LUCKY to have you at the helm! :slight_smile:

6 Likes

Well I just learned a bunch. Wow. Thank you, Ben.

3 Likes

Keeping up with Meteor’s pace is an achievement in itself :joy:

1 Like

Hey!

I am trying to use ionic (specifically the ionic react beta) with meteor. the problem is that ionic also only uses esm exports, so i followed your instructions and added a symlink for

node_modules/@ionic

this solved the first error but ionic is depending on the ionicons package which uses import for svg icon files. this produces the following error:

Unexpected identifier
import iosAddCircleOutline from './ios-add-circle-outline.svg';

but when i add a symlink for

node_modules/ionicons

it rewrites the import statement to:

let iosAddCircleOutline;
module.link("./ios-add-circle-outline.svg", {
  default(v) {
    iosAddCircleOutline = v;
  }

}, 0);

and produces the following error

Cannot find module './ios-add-circle-outline.svg'

this probably happens because the paths are messed up or something?
so, i am not expecting a solution, but rather wanted to share my findings and maybe someone has an idea what could be done. and in the longrun i think it would be awsome if meteor could support import/export used in node_modules. also ionic + react + meteor would be a really sweet stack.

thanks in advance,
lukas