Skip to content
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

Add support for node ESM addons #10053

Merged
merged 2 commits into from Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 13 additions & 6 deletions lib/models/builder.js
Expand Up @@ -26,7 +26,6 @@ class Builder extends CoreObject {

// Use Broccoli 2.0 by default, if this fails due to .read/.rebuild API, fallback to broccoli-builder
this.broccoliBuilderFallback = false;
this.setupBroccoliBuilder();

this._instantiationStack = new Error().stack.replace(/[^\n]*\n/, '');
this._cleanup = this.cleanup.bind(this);
Expand All @@ -42,25 +41,31 @@ class Builder extends CoreObject {
* @method readBuildFile
* @param path The file path to read the build file from
*/
readBuildFile(path) {
async readBuildFile(path) {
// Load the build file
let buildFile = findBuildFile('ember-cli-build.js', path);
let buildFile = await findBuildFile(path);
if (buildFile) {
return buildFile({ project: this.project });
return await buildFile({ project: this.project });
}

throw new SilentError('No ember-cli-build.js found.');
}

async ensureBroccoliBuilder() {
if (this.builder === undefined) {
await this.setupBroccoliBuilder();
}
}

/**
* @private
* @method setupBroccoliBuilder
*/
setupBroccoliBuilder() {
async setupBroccoliBuilder() {
this.environment = this.environment || 'development';
process.env.EMBER_ENV = process.env.EMBER_ENV || this.environment;

this.tree = this.readBuildFile(this.project.root);
this.tree = await this.readBuildFile(this.project.root);

let broccoli = require('broccoli');

Expand Down Expand Up @@ -167,6 +172,8 @@ class Builder extends CoreObject {
* @return {Promise}
*/
async build(addWatchDirCallback, resultAnnotation) {
await this.ensureBroccoliBuilder();

let buildResults, uiProgressIntervalID;

try {
Expand Down
4 changes: 2 additions & 2 deletions lib/models/server-watcher.js
Expand Up @@ -3,8 +3,8 @@
const Watcher = require('./watcher');

module.exports = class ServerWatcher extends Watcher {
constructor(options) {
super(options);
constructor(options, build) {
super(options, build);

this.watcher.on('add', this.didAdd.bind(this));
this.watcher.on('delete', this.didDelete.bind(this));
Expand Down
29 changes: 23 additions & 6 deletions lib/models/watcher.js
Expand Up @@ -12,19 +12,35 @@ const eventTypeNormalization = {
change: 'changed',
};

const ConstructedFromBuilder = Symbol('Watcher.build');

module.exports = class Watcher extends CoreObject {
constructor(_options) {
constructor(_options, build) {
if (build !== ConstructedFromBuilder) {
throw new Error('instantiate Watcher with (await Watcher.build()).watcher, not new Watcher()');
}

super(_options);

this.verbose = true;
this.serving = _options.serving;
}

let options = this.buildOptions();
static async build(_options) {
let watcher = new this(_options, ConstructedFromBuilder);
await watcher.setupBroccoliWatcher(_options);

logger.info('initialize %o', options);
// This indirection is because Watcher instances are themselves spec
// noncompliant thennables (see the then() method) so returning watcher
// directly will interfere with `await Watcher.build()`
return { watcher };
}

this.serving = _options.serving;
async setupBroccoliWatcher() {
let options = this.buildOptions();

this.watcher = this.watcher || this.constructBroccoliWatcher(options);
logger.info('initialize %o', options);
this.watcher = this.watcher || (await this.constructBroccoliWatcher(options));

this.setupBroccoliChangeEvent();
this.watcher.on('buildStart', this._setupBroccoliWatcherBuild.bind(this));
Expand All @@ -38,8 +54,9 @@ module.exports = class Watcher extends CoreObject {
this.serveURL = serveURL;
}

constructBroccoliWatcher(options) {
async constructBroccoliWatcher(options) {
const { Watcher } = require('broccoli');
await this.builder.ensureBroccoliBuilder();
const { watchedSourceNodeWrappers } = this.builder.builder;

let watcher = new Watcher(this.builder, watchedSourceNodeWrappers, { saneOptions: options, ignored: this.ignored });
Expand Down
16 changes: 9 additions & 7 deletions lib/tasks/build-watch.js
Expand Up @@ -35,13 +35,15 @@ class BuildWatchTask extends Task {

let watcher =
options._watcher ||
new Watcher({
ui,
builder,
analytics: this.analytics,
options,
ignored: [path.resolve(this.project.root, options.outputPath)],
});
(
await Watcher.build({
ui,
builder,
analytics: this.analytics,
options,
ignored: [path.resolve(this.project.root, options.outputPath)],
})
).watcher;

await watcher;
// Run until failure or signal to exit
Expand Down
18 changes: 10 additions & 8 deletions lib/tasks/serve.js
Expand Up @@ -55,14 +55,16 @@ class ServeTask extends Task {

let watcher =
options._watcher ||
new Watcher({
ui: this.ui,
builder,
analytics: this.analytics,
options,
serving: true,
ignored: [path.resolve(this.project.root, options.outputPath)],
});
(
await Watcher.build({
ui: this.ui,
builder,
analytics: this.analytics,
options,
serving: true,
ignored: [path.resolve(this.project.root, options.outputPath)],
})
).watcher;

let serverRoot = './server';
let serverWatcher = null;
Expand Down
26 changes: 17 additions & 9 deletions lib/utilities/find-build-file.js
@@ -1,24 +1,32 @@
'use strict';
const findUp = require('find-up');
const path = require('path');
const url = require('url');

module.exports = function (file, dir) {
let buildFilePath = findUp.sync(file, { cwd: dir });
module.exports = async function (dir) {
let buildFilePath = null;

// Note: In the future this should throw
if (!buildFilePath) {
for (let ext of ['js', 'mjs', 'cjs']) {
let candidate = findUp.sync(`ember-cli-build.${ext}`, { cwd: dir });
if (candidate) {
buildFilePath = candidate;
break;
}
}

if (buildFilePath === null) {
return null;
}

process.chdir(path.dirname(buildFilePath));

let buildFile = null;
let buildFileUrl = url.pathToFileURL(buildFilePath);
try {
buildFile = require(buildFilePath);
// eslint-disable-next-line node/no-unsupported-features/es-syntax
let fn = (await import(buildFileUrl)).default;
return fn;
} catch (err) {
err.message = `Could not require '${file}': ${err.message}`;
err.message = `Could not \`import('${buildFileUrl}')\`: ${err.message}`;
throw err;
}

return buildFile;
};
28 changes: 28 additions & 0 deletions tests/acceptance/brocfile-smoke-test-slow.js
Expand Up @@ -61,6 +61,34 @@ describe('Acceptance: brocfile-smoke-test', function () {
await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test');
});

it('builds with an ES modules ember-cli-build.js', async function () {
await fs.writeFile(
'ember-cli-build.js',
`
import EmberApp from 'ember-cli/lib/broccoli/ember-app.js';

export default async function (defaults) {
const app = new EmberApp(defaults, { });

return app.toTree();
};
`
);

let appPackageJson = await fs.readJson('package.json');
appPackageJson.type = 'module';
await fs.writeJson('package.json', appPackageJson);

// lib/utilities/find-build-file.js uses await import and so can handle ES module ember-cli-build.js
//
// However, broccoli-config-loader uses require, so files like
// config/environment.js must be in commonjs format. The way to mix ES and
// commonjs formats in node is with multiple `package.json`s
await fs.writeJson('config/package.json', { type: 'commonjs' });
console.log(process.cwd());
await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build');
});

it('without app/templates', async function () {
await copyFixtureFiles('brocfile-tests/pods-templates');
await fs.remove(path.join(process.cwd(), 'app/templates'));
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/build/node-esm/app/hello.txt
@@ -0,0 +1 @@
Hello world
3 changes: 3 additions & 0 deletions tests/fixtures/build/node-esm/app/intro.md
@@ -0,0 +1,3 @@
# Introduction

This is the introduction markdown file
3 changes: 3 additions & 0 deletions tests/fixtures/build/node-esm/app/outro.md
@@ -0,0 +1,3 @@
# Outro

This is the outro
1 change: 1 addition & 0 deletions tests/fixtures/build/node-esm/app/test.txt
@@ -0,0 +1 @@
This is a test
3 changes: 3 additions & 0 deletions tests/fixtures/build/node-esm/dist/intro.md
@@ -0,0 +1,3 @@
# Introduction

This is the introduction markdown file
3 changes: 3 additions & 0 deletions tests/fixtures/build/node-esm/dist/outro.md
@@ -0,0 +1,3 @@
# Outro

This is the outro
3 changes: 3 additions & 0 deletions tests/fixtures/build/node-esm/dist/text.txt
@@ -0,0 +1,3 @@
Hello world

This is a test
17 changes: 17 additions & 0 deletions tests/fixtures/build/node-esm/ember-cli-build.cjs
@@ -0,0 +1,17 @@
const mergeTrees = require('broccoli-merge-trees');
const concat = require('broccoli-concat');
const funnel = require('broccoli-funnel');

module.exports = function() {
const txt = funnel('app', {
include: ['*.txt'],
});
const md = funnel('app', {
include: ['*.md'],
});
const concated = concat(txt, {
outputFile: 'text.txt',
});

return mergeTrees([concated, md], {annotation: 'The final merge'});
}