253 lines
7.0 KiB
JavaScript
253 lines
7.0 KiB
JavaScript
|
const path = require("path");
|
|||
|
const fs = require("fs");
|
|||
|
const { match } = require("path-to-regexp");
|
|||
|
const { TemplatePath } = require("@11ty/eleventy-utils");
|
|||
|
|
|||
|
const Eleventy = require("./Eleventy");
|
|||
|
const normalizeServerlessUrl = require("./Util/NormalizeServerlessUrl");
|
|||
|
const deleteRequireCache = require("./Util/DeleteRequireCache");
|
|||
|
const debug = require("debug")("Eleventy:Serverless");
|
|||
|
|
|||
|
class Serverless {
|
|||
|
constructor(name, path, options = {}) {
|
|||
|
this.name = name;
|
|||
|
|
|||
|
// second argument is path
|
|||
|
if (typeof path === "string") {
|
|||
|
this.path = path;
|
|||
|
} else {
|
|||
|
// options is the second argument and path is inside options
|
|||
|
options = path;
|
|||
|
this.path = options.path;
|
|||
|
}
|
|||
|
|
|||
|
if (!this.path) {
|
|||
|
throw new Error("`path` must exist in the options argument in Eleventy Serverless.");
|
|||
|
}
|
|||
|
|
|||
|
// ServerlessBundlerPlugin hard-codes to this (even if you used a different file name)
|
|||
|
this.configFilename = "eleventy.config.js";
|
|||
|
|
|||
|
// Configuration Information
|
|||
|
this.configInfoFilename = "eleventy-serverless.json";
|
|||
|
|
|||
|
// Maps input files to eligible serverless URLs
|
|||
|
this.mapFilename = "eleventy-serverless-map.json";
|
|||
|
|
|||
|
this.options = Object.assign(
|
|||
|
{
|
|||
|
inputDir: null, // override only, we now inject this.
|
|||
|
functionsDir: "functions/",
|
|||
|
|
|||
|
matchUrlToPattern(path, urlToCompare) {
|
|||
|
urlToCompare = normalizeServerlessUrl(urlToCompare);
|
|||
|
|
|||
|
let fn = match(urlToCompare, { decode: decodeURIComponent });
|
|||
|
return fn(path);
|
|||
|
},
|
|||
|
|
|||
|
// Query String Parameters
|
|||
|
query: {},
|
|||
|
|
|||
|
// Configuration callback
|
|||
|
config: function (eleventyConfig) {},
|
|||
|
|
|||
|
// Is serverless build scoped to a single template?
|
|||
|
// Use `false` to make serverless more collections-friendly (but slower!)
|
|||
|
// With `false` you don’t need precompiledCollections.
|
|||
|
// Works great with on-demand builders
|
|||
|
singleTemplateScope: true,
|
|||
|
|
|||
|
// Inject shared collections
|
|||
|
precompiledCollections: {},
|
|||
|
},
|
|||
|
options
|
|||
|
);
|
|||
|
|
|||
|
this.dir = this.getProjectDir();
|
|||
|
}
|
|||
|
|
|||
|
initializeEnvironmentVariables() {
|
|||
|
this.serverlessEnvironmentVariableAlreadySet = !!process.env.ELEVENTY_SERVERLESS;
|
|||
|
}
|
|||
|
|
|||
|
deleteEnvironmentVariables() {
|
|||
|
if (!this.serverlessEnvironmentVariableAlreadySet) {
|
|||
|
delete process.env.ELEVENTY_SERVERLESS;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
getProjectDir() {
|
|||
|
// TODO? improve with process.env.LAMBDA_TASK_ROOT—was `/var/task/` on lambda (not local)
|
|||
|
let dir = path.join(this.options.functionsDir, this.name);
|
|||
|
let paths = [
|
|||
|
path.join(TemplatePath.getWorkingDir(), dir), // netlify dev
|
|||
|
path.join("/var/task/src/", dir), // AWS Lambda absolute path
|
|||
|
path.join(TemplatePath.getWorkingDir()), // after the chdir below
|
|||
|
];
|
|||
|
|
|||
|
for (let path of paths) {
|
|||
|
if (fs.existsSync(path)) {
|
|||
|
return path;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
throw new Error(`Couldn’t find the "${dir}" directory. Looked in: ${paths}`);
|
|||
|
}
|
|||
|
|
|||
|
getContentMap() {
|
|||
|
let fullPath = TemplatePath.absolutePath(this.dir, this.mapFilename);
|
|||
|
debug(`Including content map (maps output URLs to input files) from ${fullPath}`);
|
|||
|
|
|||
|
// TODO dedicated reset method, don’t delete this every time
|
|||
|
deleteRequireCache(fullPath);
|
|||
|
|
|||
|
return require(fullPath);
|
|||
|
}
|
|||
|
|
|||
|
getConfigInfo() {
|
|||
|
let fullPath = TemplatePath.absolutePath(this.dir, this.configInfoFilename);
|
|||
|
debug(`Including config info file from ${fullPath}`);
|
|||
|
|
|||
|
// TODO dedicated reset method, don’t delete this every time
|
|||
|
deleteRequireCache(fullPath);
|
|||
|
|
|||
|
return require(fullPath);
|
|||
|
}
|
|||
|
|
|||
|
isServerlessUrl(urlPath) {
|
|||
|
let contentMap = this.getContentMap();
|
|||
|
|
|||
|
for (let url in contentMap) {
|
|||
|
if (this.options.matchUrlToPattern(urlPath, url)) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
matchUrlPattern(urlPath) {
|
|||
|
let contentMap = this.getContentMap();
|
|||
|
let matches = [];
|
|||
|
|
|||
|
for (let url in contentMap) {
|
|||
|
let result = this.options.matchUrlToPattern(urlPath, url);
|
|||
|
if (result) {
|
|||
|
matches.push({
|
|||
|
compareTo: url,
|
|||
|
pathParams: result.params,
|
|||
|
inputPath: contentMap[url],
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (matches.length) {
|
|||
|
if (matches.length > 1) {
|
|||
|
console.log(
|
|||
|
`Eleventy Serverless conflict: there are multiple serverless paths that match the current URL (${urlPath}): ${JSON.stringify(
|
|||
|
matches,
|
|||
|
null,
|
|||
|
2
|
|||
|
)}`
|
|||
|
);
|
|||
|
}
|
|||
|
return matches[0];
|
|||
|
}
|
|||
|
|
|||
|
return {};
|
|||
|
}
|
|||
|
|
|||
|
async getOutput() {
|
|||
|
if (this.dir.startsWith("/var/task/")) {
|
|||
|
process.chdir(this.dir);
|
|||
|
}
|
|||
|
|
|||
|
let inputDir = this.options.input || this.options.inputDir || this.getConfigInfo().dir.input;
|
|||
|
let configPath = path.join(this.dir, this.configFilename);
|
|||
|
let { pathParams, inputPath } = this.matchUrlPattern(this.path);
|
|||
|
|
|||
|
if (!pathParams || !inputPath) {
|
|||
|
let err = new Error(
|
|||
|
`No matching URL found for ${this.path} in ${JSON.stringify(this.getContentMap())}`
|
|||
|
);
|
|||
|
err.httpStatusCode = 404;
|
|||
|
throw err;
|
|||
|
}
|
|||
|
|
|||
|
debug(`Current dir: ${process.cwd()}`);
|
|||
|
debug(`Project dir: ${this.dir}`);
|
|||
|
debug(`Config path: ${configPath}`);
|
|||
|
|
|||
|
debug(`Input dir: ${inputDir}`);
|
|||
|
debug(`Requested URL: ${this.path}`);
|
|||
|
debug("Path params: %o", pathParams);
|
|||
|
debug(`Input path: ${inputPath}`);
|
|||
|
|
|||
|
this.initializeEnvironmentVariables();
|
|||
|
|
|||
|
let isScoped = !!this.options.singleTemplateScope;
|
|||
|
let projectInput = isScoped ? this.options.input || inputPath : inputDir;
|
|||
|
|
|||
|
let elev = new Eleventy(projectInput, null, {
|
|||
|
// https://github.com/11ty/eleventy/issues/1957
|
|||
|
isServerless: true,
|
|||
|
configPath,
|
|||
|
inputDir,
|
|||
|
config: (eleventyConfig) => {
|
|||
|
if (Object.keys(this.options.precompiledCollections).length > 0) {
|
|||
|
eleventyConfig.setPrecompiledCollections(this.options.precompiledCollections);
|
|||
|
}
|
|||
|
|
|||
|
// Add the params to Global Data
|
|||
|
let globalData = {
|
|||
|
query: this.options.query,
|
|||
|
path: pathParams,
|
|||
|
};
|
|||
|
|
|||
|
eleventyConfig.addGlobalData("eleventy.serverless", globalData);
|
|||
|
|
|||
|
if (this.options.config && typeof this.options.config === "function") {
|
|||
|
this.options.config(eleventyConfig);
|
|||
|
}
|
|||
|
},
|
|||
|
});
|
|||
|
|
|||
|
if (!isScoped) {
|
|||
|
elev.setIncrementalFile(this.options.input || inputPath);
|
|||
|
}
|
|||
|
|
|||
|
let json = await elev.toJSON();
|
|||
|
|
|||
|
// https://github.com/11ty/eleventy/issues/1957
|
|||
|
this.deleteEnvironmentVariables();
|
|||
|
|
|||
|
let filtered = [];
|
|||
|
if (Array.isArray(json)) {
|
|||
|
filtered = json.filter((entry) => {
|
|||
|
return entry.inputPath === inputPath;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
if (!filtered.length) {
|
|||
|
let err = new Error(
|
|||
|
`Couldn’t find any generated output from Eleventy (Input path: ${inputPath}, URL path parameters: ${JSON.stringify(
|
|||
|
pathParams
|
|||
|
)}).`
|
|||
|
);
|
|||
|
err.httpStatusCode = 404;
|
|||
|
throw err;
|
|||
|
}
|
|||
|
|
|||
|
return filtered;
|
|||
|
}
|
|||
|
|
|||
|
/* Deprecated, use `getOutput` directly instead. */
|
|||
|
async render() {
|
|||
|
let json = await this.getOutput();
|
|||
|
|
|||
|
return json[0].content;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
module.exports = Serverless;
|