322 lines
8.3 KiB
JavaScript
322 lines
8.3 KiB
JavaScript
|
|
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;
|