Broccoli architecture question: Making new files available to node

I’m working on a feature for Mirage, that will make some Mirage code available to Node so that it can be used in an Express middleware.

I’d like some advice on how best to write the Broccoli code.

At a high level, here’s my current thinking on what needs to happen:

  1. Read code from the host app’s /mirage directory, turn it from ES6 into CJS
  2. Read code from Mirage’s source, turn it into CJS
  3. require() code from those places in the serverMiddleware() hook, instantiate a new Mirage server with config from the host app, and go from there

I’m trying to start out with just #2 – ignoring the host app for now, and just getting the Mirage server code written to CJS in a place where I can require() it from the serverMiddleware() hook.

What’s the right way to think about this?

In my experimenting so far, I followed some other addons and used Rollup via broccoli-rollup to read in ember-cli-mirage/addon/server.js (which is sort of the entry point for Mirage), and wrote it to CJS.

Where I’m getting confused is, where I can/should write this CJS file so that I can reliably “get” to it in serverMiddleware().

In general, since serverMiddleware doesn’t get a tree I feel confused about how that hook can interact with files created at build-time. I also sometimes reach for something like a .then method on my broccoli trees, but don’t know if that’s possible or a recommended pattern. (In other words, once I write my CJS file, if I could .then() and require() it, I could throw some instance data onto this and access it like that from serverMiddleware. Feels hacky, but it could get me unstuck.)

Any recommendations here? Really appreciate it!

There is an old PR to Mirage that does something similar, and uses broccoli-builder to make a new Builder. Then it uses to add some routes to Express.

Is that a good approach? I was sort of under the impression that index.js should be mainly concerned with trees and Broccoli plugins, and that making a new Builder was more low-level and wouldn’t fit nicely into the Ember CLI pipeline.

I think the proper place for the files would be the tmp directory if you want them cleaned up when the server is turned off. You can use broccoli-persistent-filter or broccoli-caching-writer if you want them cached between builds as well.

If you want them not cleaned up and visible to the user, then I don’t think there are any conventions. Maybe a dist/ or generated/ directory inside mirage/ so users can figure out that it can be deleted and get recreated?

Thanks. I don’t believe they need to be visible to the user.

Could you point me to any examples of using the tmp directory?

The package most commonly used in broccoli is quick-temp, and most broccoli plugins use it by simply inheriting from broccoli-plugin.

But since you need to manage the temporary directories yourself, you will likely make quick-temp a dependency and use it as explained in the README.

You ask for examples?

Does my use of quickTemp need to be inside of a Broccoli tree?

I think you should skip nearly all those steps and use @std/esm to directly require the user’s ES modules into node.

1 Like

Interesting, I wasn’t aware of this. Thanks for the pointer!

I’m playing around with it and it seems to be working with most of my imports. But I’m having trouble when importing from my addon’s top-level:

// relative path works
import Db from './db'; 

// absolute path starting with 'ember-cli-mirage' doesn't work
import { toCollectionName, toInternalCollectionName } from 'ember-cli-mirage/utils/normalize-name';

Any insight on why this might be happening? Is there some more magic happening by the time these import statements are run, that adds ember-cli-mirage to a lookup path or something?

In general NPM packages can’t resolve their own name. If you’re inside the same package you neee to use relative paths.

These work with Ember CLI’s normal build so any idea what they’re doing there?

In other words I’ve had imports like [this[( in Mirage as long as I can remember without any issue.

That is being resolved in the web browser, not in node.

I see. If it’s non-standard I’m happy to update my codebase, but do feel like the relative paths could get pretty crazy…

It’s really the tests that use it the most, for example this file. I do things like

import { Model, hasMany, belongsTo } from 'ember-cli-mirage';

which I like because it’s how how consumers of Mirage use it.

Is there any way to simplify relative path imports?

Looks like there’s no notion of an alias in node:

Too bad!

For an ember app in the browser it’s not non-standard, lots of things work that way. But yeah, it’s one of the semantic differences you encounter as soon as you try resolving your stuff in both node and the browser.

It surprised me the first time I noticed because it seems like node modules should be able to resolve themselves, but alas they cannot.

Gotcha, thanks. Feels good learning about this stuff, peeling back the curtain. Wonder if there’s some comprehensive material I could read/watch, or if I should just keep poking around?

Fwiw the actual changes needed in Mirage weren’t bad, the absolute path is most useful under /tests and I was able to keep those there (since I don’t need those atm in Node).

Have you heard of Maybe you should get those guys to make a video. :stuck_out_tongue: