Using ECMAScript to export from packages


#1

I’m trying to build a new package using import and export in ECMAScript 6, but I’m not sure how to do it. In the end, I want to use my package something like this in a Meteor app:

// main-meteor-app.js
import {TheMainClass} from 'meteor/my-name:my-package'

var myMainClass = new TheMainClass()

So in my package.js file I have the following:

// package.js
Package.onUse(function(api) {
	
	api.versionsFrom('1.3.2.4')
	api.use('ecmascript')
	
	api.mainModule('client/main.js', 'client')
	
})

But I don’t know what to put in client/main.js:

// client/main.js
// Here I should export TheMainClass coming from client/TheMainClass.js.

I guess my question is: should I use import and export internally in my package, or should I simply use package variables? I know how to write my code using package variables, but I guess it’s better to use import/export now when we have that available, right? So if I do that, should I in client/TheMainClass.js export TheMainClass, then import it into client/main.js where I simply export it again, so I finally can import it from main-meteor-app.js? Or what’s the best way to do it?


#2

You can stick with a single file until your package expands (in client/main.js):

class TheMainClass {
  ...
}

export { TheMainClass }

You can import and re-export from another file if you like. You can also import directly from that other file in your app, e.g.

import { TheMainClass } from 'meteor/my-name:my-package/client/TheMainClass';

#3

Thanks for your answer. My package is expanding, so I’m just looking for the best way to structure it.

I thought packages only would reveal values exported from mainModule, but if applications can import from any file in a package I guess it makes most sense to do it that way, so I’ll give that a try. Thanks!


#4

Be careful. I’m surprised that approach is supported. It makes your internal package structure part of your interface. Changing your internal structure would be a breaking change for anyone using your package because they would have to change their import statements.


#5

Right? In an attempt to fix the 2% of the time that load order was a problem in isobuild, we’ve adopted a system where an order of magnitude more errors are going to be caused by broken import paths. Boggles the mind sometimes.

That being said, we can use both api.export() and api.mainModule() in a package to support both isobuild and modules, correct?

Package.describe({
  name: 'clinical:foolist'
});
Package.onUse(function(api) {
  api.versionsFrom('1.3.2.4')
  api.use('ecmascript')
  api.use('templating')
  api.use('meteor-platform')
	
  api.mainModule('client/react/FooList.jsx', 'client')
  api.export('Foo')	
})

I’m particularly interested in the use case of migrating packages from Blaze to React. Say we have a package that looks like the following:

package.js
client/react/FooList.jsx
client/blaze/FooList/FooList.html
client/blaze/FooList/FooList.js
client/blaze/FooList/FooList.less
lib/Foo.js

Is the idea that we would load up the React modules like this?

import { FooList } from 'meteor/clinical:foolist/FooList';

// or maybe this?  
import { FooList } from 'meteor/clinical:foolist/client/react/FooList';

#6

But if I have one “public interface file” in the package in which I only import and export the values parts of the API, will lazy loading still apply? If an application imports one value from the “public interface file”, won’t the “public interface file” import all the values?


#7

First, I need to give the disclaimer that I am no Meteor expert - just cutting my teeth here like most others. I am, however, opinionated :slight_smile:

To clarify a little, my initial reaction was to the statement [quote=“Peppe_LG, post:3, topic:24066”]
if applications can import from any file in a package I guess it makes most sense to do it that way
[/quote]

The idea that users of a package could fully bypass the api and import any file rings alarm bells all over.

I haven’t tested gaddicc’s direct import statement that seems to reach into the package instead of just accepting what the declared mainModule exports. Nor can I find any documentation that says we can bypass the interface as specified by package.js of the package in this manner. The closest thing seems to be the {package-name} syntax allowed for importing styles, but it requires the curly-braces according to the documentation and there is no indication that it will work to import js files. I also grep’d the meteor source and every other source on my system for examples of this form of import (or require) being used and found none.

The only documented flexibility I see for defining interfaces from multiple files is the client and server specifications that can be provided in the second parameter of the api.mainModule call. I haven’t tried it, but can’t imagine that multiple api.mainModule calls for client or server would either work or make sense.

So, the problems I have with this (assuming it works) are that it is undocumented and therefore can change at any time and that it bypasses the defined api.

I’d say, yes, you’re going to be loading everything for client or everything for server. I’ve seen no indication in either the documentation or the guide that the ability to use modules within packages was intended to allow us to pull in pieces of packages. It appears to be for allowing us to better organize complex packages not complex interfaces.

The pattern I’ve seen for organizing packages that have grown to have core and optional functionality (which seems to be your case) is to break them into core and optional packages. In this way, you can be absolutely sure of what is being pulled into your application.

Yes, there is an example of them being used together in the modules document.

Hmmm. It appears you’re considering supporting both react and blaze from the same package. Frankly, I’ve never even thought of the possibility. My first thought on a change of that extent is to create clinical:foolist-react or just give it a major version change and never look back. Wouldn’t there be dead code hanging around and being sent over the wire otherwise?


#8

That is one solution, but if files were lazy loaded from packages one could just stick with one package, which I would find more convenient.

I hear a bell ringing too, but just because they can bypass the API doesn’t mean they should. For example, in Meteor we have access to a lot of stuff that we aren’t allowed to touch, so if that’s the philosophy of the framework, why should packages be different? And is it really that bad if one sees the files being part of the API? They are nothing more than names, which is what we use in ordinary API:s anyway (note: this is a question, not a statement :slight_smile:) .


#9

It’s up to the developer how they structure their package. Just because you can do something doesn’t mean you should, but there are a lot of advantages for allowing this type of syntax.

  1. Optional imports. Let’s say package foo has optional components for React. Obviously these components require React and will break your app if you import them and your project doesn’t use React. If I re-exported the React components from foo, then foo would break my app. But if foo contains just the JS stuff, and I can import all the components from foo/react, without publishing a separate package for every integration, that’s a win.

  2. Optional shipping (which is only going to improve with tree shaking, HTTP/2, etc). I only use a certain part of a package? Only those parts will be shipped. If I have a default export that re-exports the entire library, and I import that… I get the entire library. That’s bad. Think about libraries like Material-UI, etc… you only want to ship the components you’re actually using, e.g. import AppBar from 'material-ui/AppBar'.

Re private methods and data, there’s nothing about this that prevents someone from carefully structuring their package. Usually modules (files) will export classes, which are useless on their own. Other modules will import these classes, instantiate new instances, and only export their public API. Nothing has really changed here.

If you have a package/secrets, and think that’s offering you any privacy, especially on the client, it’s a big mistake. All required modules are shipped to the client.


#10

I’ve got a couple of issues with this thought. First, I haven’t checked that the lazy loading will actually pick pieces out of a package and leave others behind. That issue could be resolved with testing. The other though is that it would be easy in all but the most nicely separated cases to accidentally be loading more than what you need. That one will always require some consciousness when writing the code to carefully keep common routines in one place, to never put two different optional things in the same file, and to never have one optional thing reference something from another when it doesn’t have to. This same thought process has to go on when separating into different packages, but it is very nicely enforced so that accidents can’t happen.

Of course, this whole discussion can only be happening if you’re planning on reusing the package between applications. You’d never have “optional” stuff within a single application. You’d simply delete code you’re not using. That thought leads to the rest of this discussion.

I feel that the difference in packages is that their primary purpose is to provide software for redistribution. As such, we have to be more aware of the API. This is an external API.

I can actually see in this last statement from you that you do in fact hear the bell ringing. You’ve stated both sides. On one side, you’ve indicated we have access to a lot of stuff that we aren’t allowed to touch. And, if you’re like me, you don’t touch them except in the most dire cases of need. On the other side, you’ve said “is it really that bad if one sees the files being part of the API”. I think this falls into the category of something we shouldn’t be allowed to touch and I wouldn’t do it unless a package writer gave me no choice and documented it as their intentional design (along with their intent to not change it without a major version tick).

A lot of these thoughts go away if your package is completely local and will never be reused. But then, I wonder if you’ve fully thought through the implications of modules if you’re still using packages in that fashion. I now find a good directory plan combined with the namespace control the modules give me to be a much richer way of organizing my app than packages.

Perhaps the biggest reason that the file names being part of the external API bugs me is that Meteorland is still rapidly evolving and I’ve already found myself reorganizing my files multiple times. I like the idea of being able to refactor my applications and packages and reorganize the files within them at will without worrying about breaking my interfaces. I’ve just finished that with the adoption of modules and already have thoughts of more such change happening around moves to NPM 4 modules instead of packages, to React, and to Apollo. And, my observations of several different large scale packages, applications, and demo apps out there is that I’m not at all alone. There is a lot of debate about how to organize an app’s files and I see app reorganizations occurring with nearly every minor release of meteor. So, I’m very wary of tying my external API to one of the most volatile aspects of my application, directory organization and file naming.


#11

To me it sounds like your saying “If it’s easy to import stuff people will start to import unnecessary stuff (by accident), but if it’s hard to import stuff people will think twice and only import what they actually need”. Correct? I see the point, but I don’t like it. I don’t want to make things harder just to prevent poor programmers from doing mistakes. The beauty with Meteor (in my opinion) is that it’s simple, and I would like to keep it that way.

In the package I’m working on now I’m using the “favor composition over inheritance” pattern. When creating a new instance of class A, you need to pass it an instance of class B1, B2, B3 or B4. Apps will probably just use one of the B* classes, so lazy loading would really be appreciated. Creating four different packages instead does not seem like a good solution, since if class A in the future also should receive an instance of the class C1, C2 or C3, we now need 12 packages!

Yea, that’s something I’ve experienced too, and I haven’t taken that into account in this discussion. But I’m working on a package now, and I’ll test to make files part of the public API and see how it goes.

Thanks for your thoughts!


#12

Close. It’s more like it would be easy to import something from one optional portion of your package into another instead of factoring it out into the core part of your package and, more importantly, the detrimental effects would almost definitely go unnoticed as you’d only see them if you’re carefully watching what is being sent over the wire. I like it when my tools and methodology help me catch problems that aren’t very visible.

In any case, I can clearly tell you’re not going in blind, and your use case does sound tailor-made for this.

You’re welcome and good luck!