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;
|