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 netlify redirects support #57

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"mime": "^3.0.0",
"minimist": "^1.2.8",
"morphdom": "^2.7.0",
"netlify-redirect-parser": "^14.1.1",
"netlify-redirector": "^0.4.0",
"please-upgrade-node": "^3.2.0",
"ssri": "^8.0.1",
"ws": "^8.13.0"
Expand Down
106 changes: 95 additions & 11 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class EleventyDevServer {
debug("Creating new Dev Server instance.")
this.name = name;
this.normalizeOptions(options);

this.fileCache = {};
// Directory to serve
if(!dir) {
Expand Down Expand Up @@ -85,10 +85,10 @@ class EleventyDevServer {
// TODO if using Eleventy and `watch` option includes output folder (_site) this will trigger two update events!
this._watcher = chokidar.watch(this.options.watch, {
// TODO allow chokidar configuration extensions (or re-use the ones in Eleventy)

ignored: ["**/node_modules/**", ".git"],
ignoreInitial: true,

// same values as Eleventy
awaitWriteFinish: {
stabilityThreshold: 150,
Expand All @@ -100,7 +100,7 @@ class EleventyDevServer {
this.logger.log( `File changed: ${path} (skips build)` );
this.reloadFiles([path]);
});

this._watcher.on("add", (path) => {
this.logger.log( `File added: ${path} (skips build)` );
this.reloadFiles([path]);
Expand Down Expand Up @@ -430,6 +430,89 @@ class EleventyDevServer {
next();
}

async netlifyRedirectMiddleware(req, res, next) {
if (req.netlifyRedirectHandled) {
return next();
}
req.netlifyRedirectHandled = true;
const matcher = await this.getNetlifyRedirectMatcher();

// We need some valid URL here. Localhost is used as a placeholder.
const reqUrl = new URL(req.url, "http://localhost");
const match = matcher.match({
scheme: reqUrl.protocol.replace(/:.*$/, ""),
host: reqUrl.hostname,
path: decodeURIComponent(reqUrl.pathname),
query: reqUrl.search.slice(1),
});

// Avoid recursive matches
if (match && req.url !== match.to) {
// This is, why we can't extract this into a separate module.
// This just rewrites the url of the request and
// treats it as a new request in the request handler.
req.url = match.to;
this.onRequestHandler(req, res);
} else {
next();
}
}

// Inspired by https://github.com/netlify/cli/blob/0f7ac190f9e1c7ed056d2feb1a1834c8305a048a/src/utils/rules-proxy.mjs
async getNetlifyRedirectMatcher() {
// Importing dynamically, because these modules are ESM based.
// This also means that they are cached between requests.
const redirectParser = await import("netlify-redirect-parser");
const redirector = (await import("netlify-redirector")).default;
// Maybe some caching can be done here?
// Currently they are loaded per request, because redirects can be generated in the output folder
const { redirects, errors } = await redirectParser.parseAllRedirects({
redirectsFiles: [path.join(this.dir, "_redirects")],
netlifyConfigPath: path.join(".", "netlify.toml"),
});

if (errors.length) {
this.logger.error(errors);
}

// `netlify-redirector` does not handle the same shape as the `netlify-redirect-parser` provides:
// - `from` is called `origin`
// - `query` is called `params`
// - `conditions.role|country|language` are capitalized
const normalizeRedirect = function ({
conditions: { country, language, role, ...conditions },
from,
query,
signed,
...redirect
}) {
return {
...redirect,
origin: from,
params: query,
conditions: {
...conditions,
...(role && { Role: role }),
...(country && { Country: country }),
...(language && { Language: language }),
},
...(signed && {
sign: {
jwt_secret: signed,
},
}),
};
};

const normRedirects = redirects.map(normalizeRedirect);
const matcher = await redirector.parseJSON(
JSON.stringify(normRedirects),
{}
);

return matcher;
}

// This runs at the end of the middleware chain
eleventyProjectMiddleware(req, res) {
// Known issue with `finalhandler` and HTTP/2:
Expand Down Expand Up @@ -508,7 +591,7 @@ class EleventyDevServer {
res.setHeader("Content-Security-Policy", `script-src '${integrityHash}'`);
}
return this.augmentContentWithNotifier(content, res.statusCode !== 200, {
scriptContents,
scriptContents,
integrityHash
});
}
Expand All @@ -518,6 +601,7 @@ class EleventyDevServer {

let middlewares = this.options.middleware || [];
middlewares = middlewares.slice();
middlewares.push(this.netlifyRedirectMiddleware);

// TODO because this runs at the very end of the middleware chain,
// if we move the static stuff up in the order we could use middleware to modify
Expand Down Expand Up @@ -786,13 +870,13 @@ class EleventyDevServer {
build.templates = build.templates
.filter(entry => {
if(!this.options.domDiff) {
// Don’t include any files if the dom diffing option is disabled
return false;
}
// Don’t include any files if the dom diffing option is disabled
return false;
}

// Filter to only include watched templates that were updated
return (files || []).includes(entry.inputPath);
});
// Filter to only include watched templates that were updated
return (files || []).includes(entry.inputPath);
});
}

this.sendUpdateNotification({
Expand Down
5 changes: 5 additions & 0 deletions server/wrapResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ function getContentType(headers) {

// Inspired by `resp-modifier` https://github.com/shakyShane/resp-modifier/blob/4a000203c9db630bcfc3b6bb8ea2abc090ae0139/index.js
function wrapResponse(resp, transformHtml) {
// Response is already wrapped
if (resp._wrappedOriginalWrite) {
return resp;
}

resp._wrappedOriginalWrite = resp.write;
resp._wrappedOriginalWriteHead = resp.writeHead;
resp._wrappedOriginalEnd = resp.end;
Expand Down