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

Make config run async #1429

Closed
wants to merge 13 commits into from
Closed

Conversation

MadeByMike
Copy link
Contributor

@MadeByMike MadeByMike commented Sep 27, 2020

This allows for acync config. Such as:

module.exports = async (eleventyConfig) => {
   const data = await something();
   eleventyConfig.doSomethingWith(data)
}

I know this looks like a HEFTY PR please ignore the number of files changed and look at the types of changes. Significan't changes to the way tests are structure and how templateConfig is handled but the changes to the core core are relatively small.

The main changes are to cmd.js and the main Eleventy class. A number of calls in main 11ty runner are now delayed until after the config has been initialised. Other significant changes include the way plugins are added. These are now also async so they are resisted and resolved when config is run.

Finally the way the global template config is managed has changed. Previously this could be imported from src/Config.js into anywhere and this would work since it is global. Now, since config is async we need to ensure that it is passed explicitly to each classes that uses it after it has been initialised.

Because of this there were also significant changes to tests to ensure that the config has been initialised before tests that depend on it.

There are a few todos including:

  • More tests of async config functionality
  • Tests to ensure the merging of plugins always occurs in the order added

Throwing this up here for feedback and to test the CI. Since 11ty doesn't ship a lock file I sometimes get differences with the way prism does highlighting.

I think the passing around of templateConfig could be improved by adding a base class that sets and manages these global properties. Tests could then override this property for individual classes. Happy to consider these type of suggestions or for future improvement to 11ty in general.

@MadeByMike
Copy link
Contributor Author

Fixes: #614

@MadeByMike
Copy link
Contributor Author

I've been testing this in a real setting today and I hit this:

(node:8341) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 afterBuild listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit

I'm not sure if it is related to this change or not but it looks like then the config changes the beforeBuild and afterBuild listeners is added each time.

We should test this... might be happening with the existing prod build.

@MadeByMike
Copy link
Contributor Author

I found a bug where changes to the .md files correctly trigger a rebuild but changes to .njk files trigger the watcher (and rebuild the file) but without the updated data.

  • I might need help tracking this one down... probably in mergeConfig ?

@MadeByMike
Copy link
Contributor Author

MadeByMike commented Oct 6, 2020

Got it! The data is correct. I'd moved the initialization of the watcher until after the config was initialized because it needed the config in the constructor. However init is run on change so I ended up with a new browser-sync server instance every time files changed and thus... no reload.

Fixed by changing the watcher constructor.

@MadeByMike
Copy link
Contributor Author

I've been testing this in a real setting today and I hit this:

(node:8341) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 afterBuild listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit

I think this was solved by ensuring the config init() was only called once

Copy link
Member

@zachleat zachleat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really sorry to do this but adding config arguments to constructors isn’t likely ideal for future maintenance—can you rework those? 😭

cmd.js Show resolved Hide resolved
src/Eleventy.js Outdated Show resolved Hide resolved
this.config.inputDir = this.inputDir;

let formats = this.formatsOverride || this.config.templateFormats;
this.extensionMap = new EleventyExtensionMap(formats);
this.extensionMap = new EleventyExtensionMap(formats, config);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d super rather just use the setter style here rather than changing the constructor signature. this.extensionMap.config =

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can attempt this, but in some cases config is already used in the constructor and I'm not sure I know how to handle that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could really use an example I think.


this.templateData = new TemplateData(this.inputDir);
this.templateData = new TemplateData(this.inputDir, config);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the comment above but it looks like this one will be a little more annoying because there isn’t a setter in place already

let localConfig = {};
let path = TemplatePath.join(
TemplatePath.getWorkingDir(),
localProjectConfigPath
);
debug(`Merging config with ${path}`);

// Note for Mike: I'm delaying the processing of plugins until here.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you have your think here 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I did if by think you mean run enough scenarios in a production scale project until you hit the bugs.

What I did was move the merging of the config part to a new method: mergeEleventyConfig now I run this before and after processing the plugins. This means that the plugins get the initial merged data and that any data changes\added by plugins is remerged.

    let merged = this.mergeEleventyConfig(localConfig);
    await eleventyConfig.applyPlugins(merged);
    merged = this.mergeEleventyConfig(localConfig);

For performance reasons I chose not to merge after each individual plugin, meaning plugins can't read data added or changed by other plugins, and order of plugins doesn't matter.

Will delete comment.

src/Eleventy.js Outdated Show resolved Hide resolved
this.config.inputDir = this.inputDir;

let formats = this.formatsOverride || this.config.templateFormats;
this.extensionMap = new EleventyExtensionMap(formats);
this.extensionMap = new EleventyExtensionMap(formats, config);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can attempt this, but in some cases config is already used in the constructor and I'm not sure I know how to handle that.

Comment on lines -26 to +27
return this.configOverride || require("./Config").getConfig();
return this.configOverride || this.config;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another reason I am confused about using a getter\setter is that config is not the same thing as templateConfig need multiple getters\setters?

}
set config(cfg) {
this.configOverride = cfg;
}

get engineManager() {
if (!this._engineManager) {
this._engineManager = new TemplateEngineManager();
this._engineManager = new TemplateEngineManager(this.templateConfig);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getter/Setter here too?

@@ -2,8 +2,8 @@ const HandlebarsLib = require("handlebars");
const TemplateEngine = require("./TemplateEngine");

class Handlebars extends TemplateEngine {
constructor(name, includesDir) {
super(name, includesDir);
constructor(name, includesDir, templateConfig) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getter/Setter here too?

let localConfig = {};
let path = TemplatePath.join(
TemplatePath.getWorkingDir(),
localProjectConfigPath
);
debug(`Merging config with ${path}`);

// Note for Mike: I'm delaying the processing of plugins until here.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I did if by think you mean run enough scenarios in a production scale project until you hit the bugs.

What I did was move the merging of the config part to a new method: mergeEleventyConfig now I run this before and after processing the plugins. This means that the plugins get the initial merged data and that any data changes\added by plugins is remerged.

    let merged = this.mergeEleventyConfig(localConfig);
    await eleventyConfig.applyPlugins(merged);
    merged = this.mergeEleventyConfig(localConfig);

For performance reasons I chose not to merge after each individual plugin, meaning plugins can't read data added or changed by other plugins, and order of plugins doesn't matter.

Will delete comment.

this.config.inputDir = this.inputDir;

let formats = this.formatsOverride || this.config.templateFormats;
this.extensionMap = new EleventyExtensionMap(formats);
this.extensionMap = new EleventyExtensionMap(formats, config);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could really use an example I think.

cmd.js Show resolved Hide resolved
@mariusschulz
Copy link
Contributor

@zachleat @MadeByMike I love this change! It'll be super useful for all kinds of async tasks done during the config setup, e.g. using Shiki (which requires async initialization) as a syntax highlighter.

Thanks for working on this! :)

@kruncher
Copy link

kruncher commented Jun 9, 2021

This feature would be extremely useful for me too.

@AleksandrHovhannisyan
Copy link
Contributor

AleksandrHovhannisyan commented Jun 21, 2021

Agreed, this would be amazing! In particular, it would let you do really cool things like generating different favicon sizes at build time so that you don't have to do that with external tools. I don't think you can do this with the 11ty Image plugin unless you make it a shortcode since it's async.

(Edit: This is possible, disregard my old post.)

@zachleat
Copy link
Member

zachleat commented Jan 3, 2022

Hey y’all, I feel bad about this but unfortunately (and this is entirely my fault) I need to close this one as stale. The timing ended up being really bad and the scope of this PR ended up being a huge contributor to its downfall, as I did a pretty sizable refactor of the configuration stuff shortly after this was opened.

Really sorry.

(Also cross linking to #2103)

@zachleat
Copy link
Member

zachleat commented Jan 3, 2022

Another cross link to home base at #614

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants