summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/EleventyServe.js
diff options
context:
space:
mode:
authorShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
committerShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
commit7a52ddeba2a68388b544f529d2d92104420f77b0 (patch)
tree15ddd47457a2cb4a96060747437d36474e4f6b4e /node_modules/@11ty/eleventy/src/EleventyServe.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/EleventyServe.js')
-rw-r--r--node_modules/@11ty/eleventy/src/EleventyServe.js321
1 files changed, 321 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/EleventyServe.js b/node_modules/@11ty/eleventy/src/EleventyServe.js
new file mode 100644
index 0000000..65525ef
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/EleventyServe.js
@@ -0,0 +1,321 @@
+import assert from "node:assert";
+
+import debugUtil from "debug";
+import { Merge, DeepCopy, TemplatePath } from "@11ty/eleventy-utils";
+
+import EleventyBaseError from "./Errors/EleventyBaseError.js";
+import ConsoleLogger from "./Util/ConsoleLogger.js";
+import PathPrefixer from "./Util/PathPrefixer.js";
+import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js";
+import { getModulePackageJson } from "./Util/ImportJsonSync.js";
+import { EleventyImport } from "./Util/Require.js";
+import { isGlobMatch } from "./Util/GlobMatcher.js";
+
+const debug = debugUtil("Eleventy:EleventyServe");
+
+class EleventyServeConfigError extends EleventyBaseError {}
+
+const DEFAULT_SERVER_OPTIONS = {
+ module: "@11ty/eleventy-dev-server",
+ port: 8080,
+ // pathPrefix: "/",
+ // setup: function() {},
+ // ready: function(server) {},
+ // logger: { info: function() {}, error: function() {} }
+};
+
+class EleventyServe {
+ #eleventyConfig;
+
+ constructor() {
+ this.logger = new ConsoleLogger();
+ this._initOptionsFetched = false;
+ this._aliases = undefined;
+ this._watchedFiles = new Set();
+ }
+
+ get config() {
+ if (!this.eleventyConfig) {
+ throw new EleventyServeConfigError(
+ "You need to set the eleventyConfig property on EleventyServe.",
+ );
+ }
+
+ return this.eleventyConfig.getConfig();
+ }
+
+ set config(config) {
+ throw new Error("It’s not allowed to set config on EleventyServe. Set eleventyConfig instead.");
+ }
+
+ setAliases(aliases) {
+ this._aliases = aliases;
+
+ if (this._server && "setAliases" in this._server) {
+ this._server.setAliases(aliases);
+ }
+ }
+
+ get eleventyConfig() {
+ if (!this.#eleventyConfig) {
+ throw new EleventyServeConfigError(
+ "You need to set the eleventyConfig property on EleventyServe.",
+ );
+ }
+
+ return this.#eleventyConfig;
+ }
+
+ set eleventyConfig(config) {
+ this.#eleventyConfig = config;
+
+ if (checkPassthroughCopyBehavior(this.#eleventyConfig.userConfig, "serve")) {
+ this.#eleventyConfig.userConfig.events.on("eleventy.passthrough", ({ map }) => {
+ // for-free passthrough copy
+ this.setAliases(map);
+ });
+ }
+ }
+
+ // TODO directorynorm
+ setOutputDir(outputDir) {
+ // TODO check if this is different and if so, restart server (if already running)
+ // This applies if you change the output directory in your config file during watch/serve
+ this.outputDir = outputDir;
+ }
+
+ async getServerModule(name) {
+ try {
+ if (!name || name === DEFAULT_SERVER_OPTIONS.module) {
+ return import("@11ty/eleventy-dev-server").then((i) => i.default);
+ }
+
+ // Look for peer dep in local project
+ let projectNodeModulesPath = TemplatePath.absolutePath("./node_modules/");
+ let serverPath = TemplatePath.absolutePath(projectNodeModulesPath, name);
+ // No references outside of the project node_modules are allowed
+ if (!serverPath.startsWith(projectNodeModulesPath)) {
+ throw new Error("Invalid node_modules name for Eleventy server instance, received:" + name);
+ }
+
+ let serverPackageJson = getModulePackageJson(serverPath);
+ // Normalize with `main` entry from
+ if (TemplatePath.isDirectorySync(serverPath)) {
+ if (serverPackageJson.main) {
+ serverPath = TemplatePath.absolutePath(
+ projectNodeModulesPath,
+ name,
+ serverPackageJson.main,
+ );
+ } else {
+ throw new Error(
+ `Eleventy server ${name} is missing a \`main\` entry in its package.json file. Traversed up from ${serverPath}.`,
+ );
+ }
+ }
+
+ let module = await EleventyImport(serverPath);
+
+ if (!("getServer" in module)) {
+ throw new Error(
+ `Eleventy server module requires a \`getServer\` static method. Could not find one on module: \`${name}\``,
+ );
+ }
+
+ if (serverPackageJson["11ty"]?.compatibility) {
+ try {
+ this.eleventyConfig.userConfig.versionCheck(serverPackageJson["11ty"].compatibility);
+ } catch (e) {
+ this.logger.warn(`Warning: \`${name}\` Plugin Compatibility: ${e.message}`);
+ }
+ }
+
+ return module;
+ } catch (e) {
+ this.logger.error(
+ "There was an error with your custom Eleventy server. We’re using the default server instead.\n" +
+ e.message,
+ );
+ debug("Eleventy server error %o", e);
+ return import("@11ty/eleventy-dev-server").then((i) => i.default);
+ }
+ }
+
+ get options() {
+ if (this._options) {
+ return this._options;
+ }
+
+ this._options = Object.assign(
+ {
+ pathPrefix: PathPrefixer.normalizePathPrefix(this.config.pathPrefix),
+ logger: this.logger,
+ },
+ DEFAULT_SERVER_OPTIONS,
+ this.config.serverOptions,
+ );
+
+ this._savedConfigOptions = DeepCopy({}, this.config.serverOptions);
+
+ if (!this._initOptionsFetched && this.getSetupCallback()) {
+ throw new Error(
+ "Init options have not yet been fetched in the setup callback. This probably means that `init()` has not yet been called.",
+ );
+ }
+
+ return this._options;
+ }
+
+ get server() {
+ if (!this._server) {
+ throw new Error("Missing server instance. Did you call .initServerInstance?");
+ }
+
+ return this._server;
+ }
+
+ async initServerInstance() {
+ if (this._server) {
+ return;
+ }
+
+ let serverModule = await this.getServerModule(this.options.module);
+
+ // Static method `getServer` was already checked in `getServerModule`
+ this._server = serverModule.getServer("eleventy-server", this.outputDir, this.options);
+
+ this.setAliases(this._aliases);
+
+ if (this._globsNeedWatching) {
+ this._server.watchFiles(this._watchedFiles);
+ this._globsNeedWatching = false;
+ }
+ }
+
+ getSetupCallback() {
+ let setupCallback = this.config.serverOptions.setup;
+ if (setupCallback && typeof setupCallback === "function") {
+ return setupCallback;
+ }
+ }
+
+ async #init() {
+ let setupCallback = this.getSetupCallback();
+ if (setupCallback) {
+ let opts = await setupCallback();
+ this._initOptionsFetched = true;
+
+ if (opts) {
+ Merge(this.options, opts);
+ }
+ }
+ }
+
+ async init() {
+ if (!this._initPromise) {
+ this._initPromise = this.#init();
+ }
+
+ return this._initPromise;
+ }
+
+ // Port comes in here from --port on the command line
+ async serve(port) {
+ this._commandLinePort = port;
+
+ await this.init();
+ await this.initServerInstance();
+
+ this.server.serve(port || this.options.port);
+
+ if (typeof this.config.serverOptions?.ready === "function") {
+ if (typeof this.server.ready === "function") {
+ // Dev Server 2.0.7+
+ // wait for ready promise to resolve before triggering ready callback
+ await this.server.ready();
+ await this.config.serverOptions?.ready(this.server);
+ } else {
+ throw new Error(
+ "The `ready` option in Eleventy’s `setServerOptions` method requires a `ready` function on the Dev Server instance. If you’re using Eleventy Dev Server, you will need Dev Server 2.0.7+ or newer to use this feature.",
+ );
+ }
+ }
+ }
+
+ async close() {
+ if (this._server) {
+ await this._server.close();
+
+ this._server = undefined;
+ }
+ }
+
+ async sendError({ error }) {
+ if (this._server) {
+ await this.server.sendError({
+ error,
+ });
+ }
+ }
+
+ // Restart the server entirely
+ // We don’t want to use a native `restart` method (e.g. restart() in Vite) so that
+ // we can correctly handle a `module` property change (changing the server type)
+ async restart() {
+ // Blow away cached options
+ delete this._options;
+
+ await this.close();
+
+ // saved --port in `serve()`
+ await this.serve(this._commandLinePort);
+
+ // rewatch the saved watched files (passthrough copy)
+ if ("watchFiles" in this.server) {
+ this.server.watchFiles(this._watchedFiles);
+ }
+ }
+
+ // checkPassthroughCopyBehavior check is called upstream in Eleventy.js
+ // TODO globs are not removed from watcher
+ watchPassthroughCopy(globs) {
+ this._watchedFiles = globs;
+
+ if (this._server && "watchFiles" in this.server) {
+ this.server.watchFiles(globs);
+ this._globsNeedWatching = false;
+ } else {
+ this._globsNeedWatching = true;
+ }
+ }
+
+ isEmulatedPassthroughCopyMatch(filepath) {
+ return isGlobMatch(filepath, this._watchedFiles);
+ }
+
+ hasOptionsChanged() {
+ try {
+ assert.deepStrictEqual(this.config.serverOptions, this._savedConfigOptions);
+ return false;
+ } catch (e) {
+ return true;
+ }
+ }
+
+ // Live reload the server
+ async reload(reloadEvent = {}) {
+ if (!this._server) {
+ return;
+ }
+
+ // Restart the server if the options have changed
+ if (this.hasOptionsChanged()) {
+ debug("Server options changed, we’re restarting the server");
+ await this.restart();
+ } else {
+ await this.server.reload(reloadEvent);
+ }
+ }
+}
+
+export default EleventyServe;