Compare commits
2 Commits
143258a86d
...
36a3deddc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36a3deddc7 | ||
|
|
e532fe52d2 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,4 +47,5 @@ next-env.d.ts
|
||||
|
||||
# cloudflare
|
||||
.open-next/
|
||||
.open-next 2/
|
||||
.wrangler/
|
||||
|
||||
@ -1,539 +0,0 @@
|
||||
globalThis.disableIncrementalCache = false;globalThis.disableDynamoDBCache = false;globalThis.isNextAfter15 = true;globalThis.openNextDebug = false;globalThis.openNextVersion = "3.9.12";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/cache.js
|
||||
var cache_exports = {};
|
||||
__export(cache_exports, {
|
||||
SOFT_TAG_PREFIX: () => SOFT_TAG_PREFIX,
|
||||
default: () => Cache
|
||||
});
|
||||
module.exports = __toCommonJS(cache_exports);
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/error.js
|
||||
function isOpenNextError(e) {
|
||||
try {
|
||||
return "__openNextInternal" in e;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/logger.js
|
||||
function debug(...args) {
|
||||
if (globalThis.openNextDebug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
function warn(...args) {
|
||||
console.warn(...args);
|
||||
}
|
||||
var DOWNPLAYED_ERROR_LOGS = [
|
||||
{
|
||||
clientName: "S3Client",
|
||||
commandName: "GetObjectCommand",
|
||||
errorName: "NoSuchKey"
|
||||
}
|
||||
];
|
||||
var isDownplayedErrorLog = (errorLog) => DOWNPLAYED_ERROR_LOGS.some((downplayedInput) => downplayedInput.clientName === errorLog?.clientName && downplayedInput.commandName === errorLog?.commandName && (downplayedInput.errorName === errorLog?.error?.name || downplayedInput.errorName === errorLog?.error?.Code));
|
||||
function error(...args) {
|
||||
if (args.some((arg) => isDownplayedErrorLog(arg))) {
|
||||
return debug(...args);
|
||||
}
|
||||
if (args.some((arg) => isOpenNextError(arg))) {
|
||||
const error2 = args.find((arg) => isOpenNextError(arg));
|
||||
if (error2.logLevel < getOpenNextErrorLogLevel()) {
|
||||
return;
|
||||
}
|
||||
if (error2.logLevel === 0) {
|
||||
return console.log(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
if (error2.logLevel === 1) {
|
||||
return warn(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
return console.error(...args);
|
||||
}
|
||||
console.error(...args);
|
||||
}
|
||||
function getOpenNextErrorLogLevel() {
|
||||
const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1";
|
||||
switch (strLevel.toLowerCase()) {
|
||||
case "debug":
|
||||
case "0":
|
||||
return 0;
|
||||
case "error":
|
||||
case "2":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/cache.js
|
||||
async function hasBeenRevalidated(key, tags, cacheEntry) {
|
||||
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
||||
return false;
|
||||
}
|
||||
const value = cacheEntry.value;
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if ("type" in cacheEntry && cacheEntry.type === "page") {
|
||||
return false;
|
||||
}
|
||||
const lastModified = cacheEntry.lastModified ?? Date.now();
|
||||
if (globalThis.tagCache.mode === "nextMode") {
|
||||
return tags.length === 0 ? false : await globalThis.tagCache.hasBeenRevalidated(tags, lastModified);
|
||||
}
|
||||
const _lastModified = await globalThis.tagCache.getLastModified(key, lastModified);
|
||||
return _lastModified === -1;
|
||||
}
|
||||
function getTagsFromValue(value) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const cacheTags = value.meta?.headers?.["x-next-cache-tags"]?.split(",") ?? [];
|
||||
delete value.meta?.headers?.["x-next-cache-tags"];
|
||||
return cacheTags;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function getTagKey(tag) {
|
||||
if (typeof tag === "string") {
|
||||
return tag;
|
||||
}
|
||||
return JSON.stringify({
|
||||
tag: tag.tag,
|
||||
path: tag.path
|
||||
});
|
||||
}
|
||||
async function writeTags(tags) {
|
||||
const store = globalThis.__openNextAls.getStore();
|
||||
debug("Writing tags", tags, store);
|
||||
if (!store || globalThis.openNextConfig.dangerous?.disableTagCache) {
|
||||
return;
|
||||
}
|
||||
const tagsToWrite = tags.filter((t) => {
|
||||
const tagKey = getTagKey(t);
|
||||
const shouldWrite = !store.writtenTags.has(tagKey);
|
||||
if (shouldWrite) {
|
||||
store.writtenTags.add(tagKey);
|
||||
}
|
||||
return shouldWrite;
|
||||
});
|
||||
if (tagsToWrite.length === 0) {
|
||||
return;
|
||||
}
|
||||
await globalThis.tagCache.writeTags(tagsToWrite);
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/binary.js
|
||||
var commonBinaryMimeTypes = /* @__PURE__ */ new Set([
|
||||
"application/octet-stream",
|
||||
// Docs
|
||||
"application/epub+zip",
|
||||
"application/msword",
|
||||
"application/pdf",
|
||||
"application/rtf",
|
||||
"application/vnd.amazon.ebook",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
// Fonts
|
||||
"font/otf",
|
||||
"font/woff",
|
||||
"font/woff2",
|
||||
// Images
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/tiff",
|
||||
"image/vnd.microsoft.icon",
|
||||
"image/webp",
|
||||
// Audio
|
||||
"audio/3gpp",
|
||||
"audio/aac",
|
||||
"audio/basic",
|
||||
"audio/flac",
|
||||
"audio/mpeg",
|
||||
"audio/ogg",
|
||||
"audio/wavaudio/webm",
|
||||
"audio/x-aiff",
|
||||
"audio/x-midi",
|
||||
"audio/x-wav",
|
||||
// Video
|
||||
"video/3gpp",
|
||||
"video/mp2t",
|
||||
"video/mpeg",
|
||||
"video/ogg",
|
||||
"video/quicktime",
|
||||
"video/webm",
|
||||
"video/x-msvideo",
|
||||
// Archives
|
||||
"application/java-archive",
|
||||
"application/vnd.apple.installer+xml",
|
||||
"application/x-7z-compressed",
|
||||
"application/x-apple-diskimage",
|
||||
"application/x-bzip",
|
||||
"application/x-bzip2",
|
||||
"application/x-gzip",
|
||||
"application/x-java-archive",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-tar",
|
||||
"application/x-zip",
|
||||
"application/zip",
|
||||
// Serialized data
|
||||
"application/x-protobuf"
|
||||
]);
|
||||
function isBinaryContentType(contentType) {
|
||||
if (!contentType)
|
||||
return false;
|
||||
const value = contentType.split(";")[0];
|
||||
return commonBinaryMimeTypes.has(value);
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/cache.js
|
||||
var SOFT_TAG_PREFIX = "_N_T_/";
|
||||
function isFetchCache(options) {
|
||||
if (typeof options === "boolean") {
|
||||
return options;
|
||||
}
|
||||
if (typeof options === "object") {
|
||||
return options.kindHint === "fetch" || options.fetchCache || options.kind === "FETCH";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
var Cache = class {
|
||||
async get(key, options) {
|
||||
if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) {
|
||||
return null;
|
||||
}
|
||||
const softTags = typeof options === "object" ? options.softTags : [];
|
||||
const tags = typeof options === "object" ? options.tags : [];
|
||||
return isFetchCache(options) ? this.getFetchCache(key, softTags, tags) : this.getIncrementalCache(key);
|
||||
}
|
||||
async getFetchCache(key, softTags, tags) {
|
||||
debug("get fetch cache", { key, softTags, tags });
|
||||
try {
|
||||
const cachedEntry = await globalThis.incrementalCache.get(key, "fetch");
|
||||
if (cachedEntry?.value === void 0)
|
||||
return null;
|
||||
const _tags = [...tags ?? [], ...softTags ?? []];
|
||||
const _lastModified = cachedEntry.lastModified ?? Date.now();
|
||||
const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache ? false : await hasBeenRevalidated(key, _tags, cachedEntry);
|
||||
if (_hasBeenRevalidated)
|
||||
return null;
|
||||
if ((tags ?? []).length === 0) {
|
||||
const path = softTags?.find((tag) => tag.startsWith(SOFT_TAG_PREFIX) && !tag.endsWith("layout") && !tag.endsWith("page"));
|
||||
if (path) {
|
||||
const hasPathBeenUpdated = cachedEntry.shouldBypassTagCache ? false : await hasBeenRevalidated(path.replace(SOFT_TAG_PREFIX, ""), [], cachedEntry);
|
||||
if (hasPathBeenUpdated) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
lastModified: _lastModified,
|
||||
value: cachedEntry.value
|
||||
};
|
||||
} catch (e) {
|
||||
debug("Failed to get fetch cache", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async getIncrementalCache(key) {
|
||||
try {
|
||||
const cachedEntry = await globalThis.incrementalCache.get(key, "cache");
|
||||
if (!cachedEntry?.value) {
|
||||
return null;
|
||||
}
|
||||
const cacheData = cachedEntry.value;
|
||||
const meta = cacheData.meta;
|
||||
const tags = getTagsFromValue(cacheData);
|
||||
const _lastModified = cachedEntry.lastModified ?? Date.now();
|
||||
const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache ? false : await hasBeenRevalidated(key, tags, cachedEntry);
|
||||
if (_hasBeenRevalidated)
|
||||
return null;
|
||||
const store = globalThis.__openNextAls.getStore();
|
||||
if (store) {
|
||||
store.lastModified = _lastModified;
|
||||
}
|
||||
if (cacheData?.type === "route") {
|
||||
return {
|
||||
lastModified: _lastModified,
|
||||
value: {
|
||||
kind: globalThis.isNextAfter15 ? "APP_ROUTE" : "ROUTE",
|
||||
body: Buffer.from(cacheData.body ?? Buffer.alloc(0), isBinaryContentType(String(meta?.headers?.["content-type"])) ? "base64" : "utf8"),
|
||||
status: meta?.status,
|
||||
headers: meta?.headers
|
||||
}
|
||||
};
|
||||
}
|
||||
if (cacheData?.type === "page" || cacheData?.type === "app") {
|
||||
if (globalThis.isNextAfter15 && cacheData?.type === "app") {
|
||||
const segmentData = /* @__PURE__ */ new Map();
|
||||
if (cacheData.segmentData) {
|
||||
for (const [segmentPath, segmentContent] of Object.entries(cacheData.segmentData ?? {})) {
|
||||
segmentData.set(segmentPath, Buffer.from(segmentContent));
|
||||
}
|
||||
}
|
||||
return {
|
||||
lastModified: _lastModified,
|
||||
value: {
|
||||
kind: "APP_PAGE",
|
||||
html: cacheData.html,
|
||||
rscData: Buffer.from(cacheData.rsc),
|
||||
status: meta?.status,
|
||||
headers: meta?.headers,
|
||||
postponed: meta?.postponed,
|
||||
segmentData
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
lastModified: _lastModified,
|
||||
value: {
|
||||
kind: globalThis.isNextAfter15 ? "PAGES" : "PAGE",
|
||||
html: cacheData.html,
|
||||
pageData: cacheData.type === "page" ? cacheData.json : cacheData.rsc,
|
||||
status: meta?.status,
|
||||
headers: meta?.headers
|
||||
}
|
||||
};
|
||||
}
|
||||
if (cacheData?.type === "redirect") {
|
||||
return {
|
||||
lastModified: _lastModified,
|
||||
value: {
|
||||
kind: "REDIRECT",
|
||||
props: cacheData.props
|
||||
}
|
||||
};
|
||||
}
|
||||
warn("Unknown cache type", cacheData);
|
||||
return null;
|
||||
} catch (e) {
|
||||
debug("Failed to get body cache", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async set(key, data, ctx) {
|
||||
if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) {
|
||||
return;
|
||||
}
|
||||
const detachedPromise = globalThis.__openNextAls.getStore()?.pendingPromiseRunner.withResolvers();
|
||||
try {
|
||||
if (data === null || data === void 0) {
|
||||
await globalThis.incrementalCache.delete(key);
|
||||
} else {
|
||||
const revalidate = this.extractRevalidateForSet(ctx);
|
||||
switch (data.kind) {
|
||||
case "ROUTE":
|
||||
case "APP_ROUTE": {
|
||||
const { body, status, headers } = data;
|
||||
await globalThis.incrementalCache.set(key, {
|
||||
type: "route",
|
||||
body: body.toString(isBinaryContentType(String(headers["content-type"])) ? "base64" : "utf8"),
|
||||
meta: {
|
||||
status,
|
||||
headers
|
||||
},
|
||||
revalidate
|
||||
}, "cache");
|
||||
break;
|
||||
}
|
||||
case "PAGE":
|
||||
case "PAGES": {
|
||||
const { html, pageData, status, headers } = data;
|
||||
const isAppPath = typeof pageData === "string";
|
||||
if (isAppPath) {
|
||||
await globalThis.incrementalCache.set(key, {
|
||||
type: "app",
|
||||
html,
|
||||
rsc: pageData,
|
||||
meta: {
|
||||
status,
|
||||
headers
|
||||
},
|
||||
revalidate
|
||||
}, "cache");
|
||||
} else {
|
||||
await globalThis.incrementalCache.set(key, {
|
||||
type: "page",
|
||||
html,
|
||||
json: pageData,
|
||||
revalidate
|
||||
}, "cache");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "APP_PAGE": {
|
||||
const { html, rscData, headers, status } = data;
|
||||
await globalThis.incrementalCache.set(key, {
|
||||
type: "app",
|
||||
html,
|
||||
rsc: rscData.toString("utf8"),
|
||||
meta: {
|
||||
status,
|
||||
headers
|
||||
},
|
||||
revalidate
|
||||
}, "cache");
|
||||
break;
|
||||
}
|
||||
case "FETCH":
|
||||
await globalThis.incrementalCache.set(key, data, "fetch");
|
||||
break;
|
||||
case "REDIRECT":
|
||||
await globalThis.incrementalCache.set(key, {
|
||||
type: "redirect",
|
||||
props: data.props,
|
||||
revalidate
|
||||
}, "cache");
|
||||
break;
|
||||
case "IMAGE":
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.updateTagsOnSet(key, data, ctx);
|
||||
debug("Finished setting cache");
|
||||
} catch (e) {
|
||||
error("Failed to set cache", e);
|
||||
} finally {
|
||||
detachedPromise?.resolve();
|
||||
}
|
||||
}
|
||||
async revalidateTag(tags) {
|
||||
const config = globalThis.openNextConfig.dangerous;
|
||||
if (config?.disableTagCache || config?.disableIncrementalCache) {
|
||||
return;
|
||||
}
|
||||
const _tags = Array.isArray(tags) ? tags : [tags];
|
||||
if (_tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (globalThis.tagCache.mode === "nextMode") {
|
||||
const paths = await globalThis.tagCache.getPathsByTags?.(_tags) ?? [];
|
||||
await writeTags(_tags);
|
||||
if (paths.length > 0) {
|
||||
await globalThis.cdnInvalidationHandler.invalidatePaths(paths.map((path) => ({
|
||||
initialPath: path,
|
||||
rawPath: path,
|
||||
resolvedRoutes: [
|
||||
{
|
||||
route: path,
|
||||
// TODO: ideally here we should check if it's an app router page or route
|
||||
type: "app"
|
||||
}
|
||||
]
|
||||
})));
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const tag of _tags) {
|
||||
debug("revalidateTag", tag);
|
||||
const paths = await globalThis.tagCache.getByTag(tag);
|
||||
debug("Items", paths);
|
||||
const toInsert = paths.map((path) => ({
|
||||
path,
|
||||
tag
|
||||
}));
|
||||
if (tag.startsWith(SOFT_TAG_PREFIX)) {
|
||||
for (const path of paths) {
|
||||
const _tags2 = await globalThis.tagCache.getByPath(path);
|
||||
const hardTags = _tags2.filter((t) => !t.startsWith(SOFT_TAG_PREFIX));
|
||||
for (const hardTag of hardTags) {
|
||||
const _paths = await globalThis.tagCache.getByTag(hardTag);
|
||||
debug({ hardTag, _paths });
|
||||
toInsert.push(..._paths.map((path2) => ({
|
||||
path: path2,
|
||||
tag: hardTag
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
await writeTags(toInsert);
|
||||
const uniquePaths = Array.from(new Set(toInsert.filter((t) => t.tag.startsWith(SOFT_TAG_PREFIX)).map((t) => `/${t.path}`)));
|
||||
if (uniquePaths.length > 0) {
|
||||
await globalThis.cdnInvalidationHandler.invalidatePaths(uniquePaths.map((path) => ({
|
||||
initialPath: path,
|
||||
rawPath: path,
|
||||
resolvedRoutes: [
|
||||
{
|
||||
route: path,
|
||||
// TODO: ideally here we should check if it's an app router page or route
|
||||
type: "app"
|
||||
}
|
||||
]
|
||||
})));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error("Failed to revalidate tag", e);
|
||||
}
|
||||
}
|
||||
// TODO: We should delete/update tags in this method
|
||||
// This will require an update to the tag cache interface
|
||||
async updateTagsOnSet(key, data, ctx) {
|
||||
if (globalThis.openNextConfig.dangerous?.disableTagCache || globalThis.tagCache.mode === "nextMode" || // Here it means it's a delete
|
||||
!data) {
|
||||
return;
|
||||
}
|
||||
const derivedTags = data?.kind === "FETCH" ? (
|
||||
//@ts-expect-error - On older versions of next, ctx was a number, but for these cases we use data?.data?.tags
|
||||
ctx?.tags ?? data?.data?.tags ?? []
|
||||
) : data?.kind === "PAGE" ? data.headers?.["x-next-cache-tags"]?.split(",") ?? [] : [];
|
||||
debug("derivedTags", derivedTags);
|
||||
const storedTags = await globalThis.tagCache.getByPath(key);
|
||||
const tagsToWrite = derivedTags.filter((tag) => !storedTags.includes(tag));
|
||||
if (tagsToWrite.length > 0) {
|
||||
await writeTags(tagsToWrite.map((tag) => ({
|
||||
path: key,
|
||||
tag,
|
||||
// In case the tags are not there we just need to create them
|
||||
// but we don't want them to return from `getLastModified` as they are not stale
|
||||
revalidatedAt: 1
|
||||
})));
|
||||
}
|
||||
}
|
||||
extractRevalidateForSet(ctx) {
|
||||
if (ctx === void 0) {
|
||||
return void 0;
|
||||
}
|
||||
if (typeof ctx === "number" || ctx === false) {
|
||||
return ctx;
|
||||
}
|
||||
if ("revalidate" in ctx) {
|
||||
return ctx.revalidate;
|
||||
}
|
||||
if ("cacheControl" in ctx) {
|
||||
return ctx.cacheControl?.revalidate;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
SOFT_TAG_PREFIX
|
||||
});
|
||||
@ -1,195 +0,0 @@
|
||||
globalThis.disableIncrementalCache = false;globalThis.disableDynamoDBCache = false;globalThis.isNextAfter15 = true;globalThis.openNextDebug = false;globalThis.openNextVersion = "3.9.12";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/composable-cache.js
|
||||
var composable_cache_exports = {};
|
||||
__export(composable_cache_exports, {
|
||||
default: () => composable_cache_default
|
||||
});
|
||||
module.exports = __toCommonJS(composable_cache_exports);
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/logger.js
|
||||
function debug(...args) {
|
||||
if (globalThis.openNextDebug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/cache.js
|
||||
function getTagKey(tag) {
|
||||
if (typeof tag === "string") {
|
||||
return tag;
|
||||
}
|
||||
return JSON.stringify({
|
||||
tag: tag.tag,
|
||||
path: tag.path
|
||||
});
|
||||
}
|
||||
async function writeTags(tags) {
|
||||
const store = globalThis.__openNextAls.getStore();
|
||||
debug("Writing tags", tags, store);
|
||||
if (!store || globalThis.openNextConfig.dangerous?.disableTagCache) {
|
||||
return;
|
||||
}
|
||||
const tagsToWrite = tags.filter((t) => {
|
||||
const tagKey = getTagKey(t);
|
||||
const shouldWrite = !store.writtenTags.has(tagKey);
|
||||
if (shouldWrite) {
|
||||
store.writtenTags.add(tagKey);
|
||||
}
|
||||
return shouldWrite;
|
||||
});
|
||||
if (tagsToWrite.length === 0) {
|
||||
return;
|
||||
}
|
||||
await globalThis.tagCache.writeTags(tagsToWrite);
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/stream.js
|
||||
var import_web = require("node:stream/web");
|
||||
async function fromReadableStream(stream, base64) {
|
||||
const chunks = [];
|
||||
let totalLength = 0;
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
totalLength += chunk.length;
|
||||
}
|
||||
if (chunks.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (chunks.length === 1) {
|
||||
return Buffer.from(chunks[0]).toString(base64 ? "base64" : "utf8");
|
||||
}
|
||||
const buffer = Buffer.alloc(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
buffer.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return buffer.toString(base64 ? "base64" : "utf8");
|
||||
}
|
||||
function toReadableStream(value, isBase64) {
|
||||
return new import_web.ReadableStream({
|
||||
pull(controller) {
|
||||
controller.enqueue(Buffer.from(value, isBase64 ? "base64" : "utf8"));
|
||||
controller.close();
|
||||
}
|
||||
}, { highWaterMark: 0 });
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/composable-cache.js
|
||||
var pendingWritePromiseMap = /* @__PURE__ */ new Map();
|
||||
var composable_cache_default = {
|
||||
async get(cacheKey) {
|
||||
try {
|
||||
if (pendingWritePromiseMap.has(cacheKey)) {
|
||||
const stored = pendingWritePromiseMap.get(cacheKey);
|
||||
if (stored) {
|
||||
return stored.then((entry) => ({
|
||||
...entry,
|
||||
value: toReadableStream(entry.value)
|
||||
}));
|
||||
}
|
||||
}
|
||||
const result = await globalThis.incrementalCache.get(cacheKey, "composable");
|
||||
if (!result?.value?.value) {
|
||||
return void 0;
|
||||
}
|
||||
debug("composable cache result", result);
|
||||
if (globalThis.tagCache.mode === "nextMode" && result.value.tags.length > 0) {
|
||||
const hasBeenRevalidated = result.shouldBypassTagCache ? false : await globalThis.tagCache.hasBeenRevalidated(result.value.tags, result.lastModified);
|
||||
if (hasBeenRevalidated)
|
||||
return void 0;
|
||||
} else if (globalThis.tagCache.mode === "original" || globalThis.tagCache.mode === void 0) {
|
||||
const hasBeenRevalidated = result.shouldBypassTagCache ? false : await globalThis.tagCache.getLastModified(cacheKey, result.lastModified) === -1;
|
||||
if (hasBeenRevalidated)
|
||||
return void 0;
|
||||
}
|
||||
return {
|
||||
...result.value,
|
||||
value: toReadableStream(result.value.value)
|
||||
};
|
||||
} catch (e) {
|
||||
debug("Cannot read composable cache entry");
|
||||
return void 0;
|
||||
}
|
||||
},
|
||||
async set(cacheKey, pendingEntry) {
|
||||
const promiseEntry = pendingEntry.then(async (entry2) => ({
|
||||
...entry2,
|
||||
value: await fromReadableStream(entry2.value)
|
||||
}));
|
||||
pendingWritePromiseMap.set(cacheKey, promiseEntry);
|
||||
const entry = await promiseEntry.finally(() => {
|
||||
pendingWritePromiseMap.delete(cacheKey);
|
||||
});
|
||||
await globalThis.incrementalCache.set(cacheKey, {
|
||||
...entry,
|
||||
value: entry.value
|
||||
}, "composable");
|
||||
if (globalThis.tagCache.mode === "original") {
|
||||
const storedTags = await globalThis.tagCache.getByPath(cacheKey);
|
||||
const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
|
||||
if (tagsToWrite.length > 0) {
|
||||
await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
|
||||
}
|
||||
}
|
||||
},
|
||||
async refreshTags() {
|
||||
return;
|
||||
},
|
||||
/**
|
||||
* The signature has changed in Next.js 16
|
||||
* - Before Next.js 16, the method takes `...tags: string[]`
|
||||
* - From Next.js 16, the method takes `tags: string[]`
|
||||
*/
|
||||
async getExpiration(...tags) {
|
||||
if (globalThis.tagCache.mode === "nextMode") {
|
||||
return globalThis.tagCache.getLastRevalidated(tags.flat());
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
/**
|
||||
* This method is only used before Next.js 16
|
||||
*/
|
||||
async expireTags(...tags) {
|
||||
if (globalThis.tagCache.mode === "nextMode") {
|
||||
return writeTags(tags);
|
||||
}
|
||||
const tagCache = globalThis.tagCache;
|
||||
const revalidatedAt = Date.now();
|
||||
const pathsToUpdate = await Promise.all(tags.map(async (tag) => {
|
||||
const paths = await tagCache.getByTag(tag);
|
||||
return paths.map((path) => ({
|
||||
path,
|
||||
tag,
|
||||
revalidatedAt
|
||||
}));
|
||||
}));
|
||||
const setToWrite = /* @__PURE__ */ new Set();
|
||||
for (const entry of pathsToUpdate.flat()) {
|
||||
setToWrite.add(entry);
|
||||
}
|
||||
await writeTags(Array.from(setToWrite));
|
||||
},
|
||||
// This one is necessary for older versions of next
|
||||
async receiveExpiredTags(...tags) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -1,169 +0,0 @@
|
||||
globalThis.openNextDebug = false;globalThis.openNextVersion = "3.9.12";
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/durable-objects/bucket-cache-purge.js
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/error.js
|
||||
function isOpenNextError(e) {
|
||||
try {
|
||||
return "__openNextInternal" in e;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/logger.js
|
||||
function debug(...args) {
|
||||
if (globalThis.openNextDebug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
function warn(...args) {
|
||||
console.warn(...args);
|
||||
}
|
||||
var DOWNPLAYED_ERROR_LOGS = [
|
||||
{
|
||||
clientName: "S3Client",
|
||||
commandName: "GetObjectCommand",
|
||||
errorName: "NoSuchKey"
|
||||
}
|
||||
];
|
||||
var isDownplayedErrorLog = (errorLog) => DOWNPLAYED_ERROR_LOGS.some((downplayedInput) => downplayedInput.clientName === errorLog?.clientName && downplayedInput.commandName === errorLog?.commandName && (downplayedInput.errorName === errorLog?.error?.name || downplayedInput.errorName === errorLog?.error?.Code));
|
||||
function error(...args) {
|
||||
if (args.some((arg) => isDownplayedErrorLog(arg))) {
|
||||
return debug(...args);
|
||||
}
|
||||
if (args.some((arg) => isOpenNextError(arg))) {
|
||||
const error2 = args.find((arg) => isOpenNextError(arg));
|
||||
if (error2.logLevel < getOpenNextErrorLogLevel()) {
|
||||
return;
|
||||
}
|
||||
if (error2.logLevel === 0) {
|
||||
return console.log(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
if (error2.logLevel === 1) {
|
||||
return warn(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
return console.error(...args);
|
||||
}
|
||||
console.error(...args);
|
||||
}
|
||||
function getOpenNextErrorLogLevel() {
|
||||
const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1";
|
||||
switch (strLevel.toLowerCase()) {
|
||||
case "debug":
|
||||
case "0":
|
||||
return 0;
|
||||
case "error":
|
||||
case "2":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/cloudflare-context.js
|
||||
var cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/overrides/internal.js
|
||||
var debugCache = (name, ...args) => {
|
||||
if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
|
||||
console.log(`[${name}] `, ...args);
|
||||
}
|
||||
};
|
||||
async function internalPurgeCacheByTags(env, tags) {
|
||||
if (!env.CACHE_PURGE_ZONE_ID || !env.CACHE_PURGE_API_TOKEN) {
|
||||
error("No cache zone ID or API token provided. Skipping cache purge.");
|
||||
return "missing-credentials";
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(`https://api.cloudflare.com/client/v4/zones/${env.CACHE_PURGE_ZONE_ID}/purge_cache`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.CACHE_PURGE_API_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
tags
|
||||
})
|
||||
});
|
||||
if (response.status === 429) {
|
||||
error("purgeCacheByTags: Rate limit exceeded. Skipping cache purge.");
|
||||
return "rate-limit-exceeded";
|
||||
}
|
||||
const bodyResponse = await response.json();
|
||||
if (!bodyResponse.success) {
|
||||
error("purgeCacheByTags: Cache purge failed. Errors:", bodyResponse.errors.map((error2) => `${error2.code}: ${error2.message}`));
|
||||
return "purge-failed";
|
||||
}
|
||||
debugCache("purgeCacheByTags", "Cache purged successfully for tags:", tags);
|
||||
return "purge-success";
|
||||
} catch (error2) {
|
||||
console.error("Error purging cache by tags:", error2);
|
||||
return "purge-failed";
|
||||
} finally {
|
||||
try {
|
||||
await response?.body?.cancel();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/durable-objects/bucket-cache-purge.js
|
||||
var DEFAULT_BUFFER_TIME_IN_SECONDS = 5;
|
||||
var MAX_NUMBER_OF_TAGS_PER_PURGE = 100;
|
||||
var BucketCachePurge = class extends DurableObject {
|
||||
bufferTimeInSeconds;
|
||||
constructor(state, env) {
|
||||
super(state, env);
|
||||
this.bufferTimeInSeconds = env.NEXT_CACHE_DO_PURGE_BUFFER_TIME_IN_SECONDS ? parseInt(env.NEXT_CACHE_DO_PURGE_BUFFER_TIME_IN_SECONDS) : DEFAULT_BUFFER_TIME_IN_SECONDS;
|
||||
state.blockConcurrencyWhile(async () => {
|
||||
state.storage.sql.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cache_purge (
|
||||
tag TEXT NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS tag_index ON cache_purge (tag);
|
||||
`);
|
||||
});
|
||||
}
|
||||
async purgeCacheByTags(tags) {
|
||||
for (const tag of tags) {
|
||||
this.ctx.storage.sql.exec(`
|
||||
INSERT OR REPLACE INTO cache_purge (tag)
|
||||
VALUES (?)`, [tag]);
|
||||
}
|
||||
const nextAlarm = await this.ctx.storage.getAlarm();
|
||||
if (!nextAlarm) {
|
||||
this.ctx.storage.setAlarm(Date.now() + this.bufferTimeInSeconds * 1e3);
|
||||
}
|
||||
}
|
||||
async alarm() {
|
||||
let tags = this.ctx.storage.sql.exec(`
|
||||
SELECT * FROM cache_purge LIMIT ${MAX_NUMBER_OF_TAGS_PER_PURGE}
|
||||
`).toArray();
|
||||
do {
|
||||
if (tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
const result = await internalPurgeCacheByTags(this.env, tags.map((row) => row.tag));
|
||||
if (result === "rate-limit-exceeded") {
|
||||
throw new Error("Rate limit exceeded");
|
||||
}
|
||||
this.ctx.storage.sql.exec(`
|
||||
DELETE FROM cache_purge
|
||||
WHERE tag IN (${tags.map(() => "?").join(",")})
|
||||
`, tags.map((row) => row.tag));
|
||||
if (tags.length < MAX_NUMBER_OF_TAGS_PER_PURGE) {
|
||||
tags = [];
|
||||
} else {
|
||||
tags = this.ctx.storage.sql.exec(`
|
||||
SELECT * FROM cache_purge LIMIT ${MAX_NUMBER_OF_TAGS_PER_PURGE}
|
||||
`).toArray();
|
||||
}
|
||||
} while (tags.length >= 0);
|
||||
}
|
||||
};
|
||||
export {
|
||||
BucketCachePurge
|
||||
};
|
||||
@ -1,283 +0,0 @@
|
||||
globalThis.openNextDebug = false;globalThis.openNextVersion = "3.9.12";
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/error.js
|
||||
var IgnorableError = class extends Error {
|
||||
__openNextInternal = true;
|
||||
canIgnore = true;
|
||||
logLevel = 0;
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "IgnorableError";
|
||||
}
|
||||
};
|
||||
var RecoverableError = class extends Error {
|
||||
__openNextInternal = true;
|
||||
canIgnore = true;
|
||||
logLevel = 1;
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "RecoverableError";
|
||||
}
|
||||
};
|
||||
var FatalError = class extends Error {
|
||||
__openNextInternal = true;
|
||||
canIgnore = false;
|
||||
logLevel = 2;
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "FatalError";
|
||||
}
|
||||
};
|
||||
function isOpenNextError(e) {
|
||||
try {
|
||||
return "__openNextInternal" in e;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/logger.js
|
||||
function debug(...args) {
|
||||
if (globalThis.openNextDebug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
function warn(...args) {
|
||||
console.warn(...args);
|
||||
}
|
||||
var DOWNPLAYED_ERROR_LOGS = [
|
||||
{
|
||||
clientName: "S3Client",
|
||||
commandName: "GetObjectCommand",
|
||||
errorName: "NoSuchKey"
|
||||
}
|
||||
];
|
||||
var isDownplayedErrorLog = (errorLog) => DOWNPLAYED_ERROR_LOGS.some((downplayedInput) => downplayedInput.clientName === errorLog?.clientName && downplayedInput.commandName === errorLog?.commandName && (downplayedInput.errorName === errorLog?.error?.name || downplayedInput.errorName === errorLog?.error?.Code));
|
||||
function error(...args) {
|
||||
if (args.some((arg) => isDownplayedErrorLog(arg))) {
|
||||
return debug(...args);
|
||||
}
|
||||
if (args.some((arg) => isOpenNextError(arg))) {
|
||||
const error2 = args.find((arg) => isOpenNextError(arg));
|
||||
if (error2.logLevel < getOpenNextErrorLogLevel()) {
|
||||
return;
|
||||
}
|
||||
if (error2.logLevel === 0) {
|
||||
return console.log(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
if (error2.logLevel === 1) {
|
||||
return warn(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
return console.error(...args);
|
||||
}
|
||||
console.error(...args);
|
||||
}
|
||||
function getOpenNextErrorLogLevel() {
|
||||
const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1";
|
||||
switch (strLevel.toLowerCase()) {
|
||||
case "debug":
|
||||
case "0":
|
||||
return 0;
|
||||
case "error":
|
||||
case "2":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/durable-objects/queue.js
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
var DEFAULT_MAX_REVALIDATION = 5;
|
||||
var DEFAULT_REVALIDATION_TIMEOUT_MS = 1e4;
|
||||
var DEFAULT_RETRY_INTERVAL_MS = 2e3;
|
||||
var DEFAULT_MAX_RETRIES = 6;
|
||||
var DOQueueHandler = class extends DurableObject {
|
||||
// Ongoing revalidations are deduped by the deduplication id
|
||||
// Since this is running in waitUntil, we expect the durable object state to persist this during the duration of the revalidation
|
||||
// TODO: handle incremental cache with only eventual consistency (i.e. KV or R2/D1 with the optional cache layer on top)
|
||||
ongoingRevalidations = /* @__PURE__ */ new Map();
|
||||
sql;
|
||||
routeInFailedState = /* @__PURE__ */ new Map();
|
||||
service;
|
||||
// Configurable params
|
||||
maxRevalidations;
|
||||
revalidationTimeout;
|
||||
revalidationRetryInterval;
|
||||
maxRetries;
|
||||
disableSQLite;
|
||||
constructor(ctx, env) {
|
||||
super(ctx, env);
|
||||
this.service = env.WORKER_SELF_REFERENCE;
|
||||
if (!this.service)
|
||||
throw new IgnorableError("No service binding for cache revalidation worker");
|
||||
this.sql = ctx.storage.sql;
|
||||
this.maxRevalidations = env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION ? parseInt(env.NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION) : DEFAULT_MAX_REVALIDATION;
|
||||
this.revalidationTimeout = env.NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS ? parseInt(env.NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS) : DEFAULT_REVALIDATION_TIMEOUT_MS;
|
||||
this.revalidationRetryInterval = env.NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS ? parseInt(env.NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS) : DEFAULT_RETRY_INTERVAL_MS;
|
||||
this.maxRetries = env.NEXT_CACHE_DO_QUEUE_MAX_RETRIES ? parseInt(env.NEXT_CACHE_DO_QUEUE_MAX_RETRIES) : DEFAULT_MAX_RETRIES;
|
||||
this.disableSQLite = env.NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE === "true";
|
||||
ctx.blockConcurrencyWhile(async () => {
|
||||
debug(`Restoring the state of the durable object`);
|
||||
await this.initState();
|
||||
});
|
||||
debug(`Durable object initialized`);
|
||||
}
|
||||
async revalidate(msg) {
|
||||
if (this.ongoingRevalidations.size > 2 * this.maxRevalidations) {
|
||||
warn(`Your durable object has 2 times the maximum number of revalidations (${this.maxRevalidations}) in progress. If this happens often, you should consider increasing the NEXT_CACHE_DO_QUEUE_MAX_REVALIDATION or the number of durable objects with the MAX_REVALIDATE_CONCURRENCY env var.`);
|
||||
}
|
||||
if (this.ongoingRevalidations.has(msg.MessageDeduplicationId))
|
||||
return;
|
||||
if (this.routeInFailedState.has(msg.MessageDeduplicationId))
|
||||
return;
|
||||
if (this.checkSyncTable(msg))
|
||||
return;
|
||||
if (this.ongoingRevalidations.size >= this.maxRevalidations) {
|
||||
debug(`The maximum number of revalidations (${this.maxRevalidations}) is reached. Blocking until one of the revalidations finishes.`);
|
||||
while (this.ongoingRevalidations.size >= this.maxRevalidations) {
|
||||
const ongoingRevalidations = this.ongoingRevalidations.values();
|
||||
debug(`Waiting for one of the revalidations to finish`);
|
||||
await Promise.race(ongoingRevalidations);
|
||||
}
|
||||
}
|
||||
const revalidationPromise = this.executeRevalidation(msg);
|
||||
this.ongoingRevalidations.set(msg.MessageDeduplicationId, revalidationPromise);
|
||||
this.ctx.waitUntil(revalidationPromise);
|
||||
}
|
||||
async executeRevalidation(msg) {
|
||||
let response;
|
||||
try {
|
||||
debug(`Revalidating ${msg.MessageBody.host}${msg.MessageBody.url}`);
|
||||
const { MessageBody: { host, url } } = msg;
|
||||
const protocol = host.includes("localhost") ? "http" : "https";
|
||||
response = await this.service.fetch(`${protocol}://${host}${url}`, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
// This is defined during build
|
||||
"x-prerender-revalidate": "9212b8c54d8f2aaa5cb15dca46a16684",
|
||||
"x-isr": "1"
|
||||
},
|
||||
// This one is kind of problematic, it will always show the wall time of the revalidation to `this.revalidationTimeout`
|
||||
signal: AbortSignal.timeout(this.revalidationTimeout)
|
||||
});
|
||||
if (response.status === 200 && response.headers.get("x-nextjs-cache") !== "REVALIDATED") {
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
throw new FatalError(`The revalidation for ${host}${url} cannot be done. This error should never happen.`);
|
||||
} else if (response.status === 404) {
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
throw new IgnorableError(`The revalidation for ${host}${url} cannot be done because the page is not found. It's either expected or an error in user code itself`);
|
||||
} else if (response.status === 500) {
|
||||
await this.addToFailedState(msg);
|
||||
throw new IgnorableError(`Something went wrong while revalidating ${host}${url}`);
|
||||
} else if (response.status !== 200) {
|
||||
await this.addToFailedState(msg);
|
||||
throw new RecoverableError(`An unknown error occurred while revalidating ${host}${url}`);
|
||||
}
|
||||
if (!this.disableSQLite) {
|
||||
this.sql.exec(
|
||||
"INSERT OR REPLACE INTO sync (id, lastSuccess, buildId) VALUES (?, unixepoch(), ?)",
|
||||
// We cannot use the deduplication id because it's not unique per route - every time a route is revalidated, the deduplication id is different.
|
||||
`${host}${url}`,
|
||||
"Bi3XXPy0fxaJxYXDBsshm"
|
||||
);
|
||||
}
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
} catch (e) {
|
||||
if (!isOpenNextError(e)) {
|
||||
await this.addToFailedState(msg);
|
||||
}
|
||||
error(e);
|
||||
} finally {
|
||||
this.ongoingRevalidations.delete(msg.MessageDeduplicationId);
|
||||
try {
|
||||
await response?.body?.cancel();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
async alarm() {
|
||||
const currentDateTime = Date.now();
|
||||
const nextEventToRetry = Array.from(this.routeInFailedState.values()).filter(({ nextAlarmMs }) => nextAlarmMs > currentDateTime).sort(({ nextAlarmMs: a }, { nextAlarmMs: b }) => a - b)[0];
|
||||
const expiredEvents = Array.from(this.routeInFailedState.values()).filter(({ nextAlarmMs }) => nextAlarmMs <= currentDateTime);
|
||||
const allEventsToRetry = nextEventToRetry ? [nextEventToRetry, ...expiredEvents] : expiredEvents;
|
||||
for (const event of allEventsToRetry) {
|
||||
debug(`Retrying revalidation for ${event.msg.MessageBody.host}${event.msg.MessageBody.url}`);
|
||||
await this.executeRevalidation(event.msg);
|
||||
}
|
||||
}
|
||||
async addToFailedState(msg) {
|
||||
debug(`Adding ${msg.MessageBody.host}${msg.MessageBody.url} to the failed state`);
|
||||
const existingFailedState = this.routeInFailedState.get(msg.MessageDeduplicationId);
|
||||
let updatedFailedState;
|
||||
if (existingFailedState) {
|
||||
if (existingFailedState.retryCount >= this.maxRetries) {
|
||||
error(`The revalidation for ${msg.MessageBody.host}${msg.MessageBody.url} has failed after ${this.maxRetries} retries. It will not be tried again, but subsequent ISR requests will retry.`);
|
||||
this.routeInFailedState.delete(msg.MessageDeduplicationId);
|
||||
return;
|
||||
}
|
||||
const nextAlarmMs = Date.now() + Math.pow(2, existingFailedState.retryCount + 1) * this.revalidationRetryInterval;
|
||||
updatedFailedState = {
|
||||
...existingFailedState,
|
||||
retryCount: existingFailedState.retryCount + 1,
|
||||
nextAlarmMs
|
||||
};
|
||||
} else {
|
||||
updatedFailedState = {
|
||||
msg,
|
||||
retryCount: 1,
|
||||
nextAlarmMs: Date.now() + 2e3
|
||||
};
|
||||
}
|
||||
this.routeInFailedState.set(msg.MessageDeduplicationId, updatedFailedState);
|
||||
if (!this.disableSQLite) {
|
||||
this.sql.exec("INSERT OR REPLACE INTO failed_state (id, data, buildId) VALUES (?, ?, ?)", msg.MessageDeduplicationId, JSON.stringify(updatedFailedState), "Bi3XXPy0fxaJxYXDBsshm");
|
||||
}
|
||||
await this.addAlarm();
|
||||
}
|
||||
async addAlarm() {
|
||||
const existingAlarm = await this.ctx.storage.getAlarm({ allowConcurrency: false });
|
||||
if (existingAlarm)
|
||||
return;
|
||||
if (this.routeInFailedState.size === 0)
|
||||
return;
|
||||
let nextAlarmToSetup = Math.min(...Array.from(this.routeInFailedState.values()).map(({ nextAlarmMs }) => nextAlarmMs));
|
||||
if (nextAlarmToSetup < Date.now()) {
|
||||
nextAlarmToSetup = Date.now() + this.revalidationRetryInterval;
|
||||
}
|
||||
await this.ctx.storage.setAlarm(nextAlarmToSetup);
|
||||
}
|
||||
// This function is used to restore the state of the durable object
|
||||
// We don't restore the ongoing revalidations because we cannot know in which state they are
|
||||
// We only restore the failed state and the alarm
|
||||
async initState() {
|
||||
if (this.disableSQLite)
|
||||
return;
|
||||
this.sql.exec("CREATE TABLE IF NOT EXISTS failed_state (id TEXT PRIMARY KEY, data TEXT, buildId TEXT)");
|
||||
this.sql.exec("CREATE TABLE IF NOT EXISTS sync (id TEXT PRIMARY KEY, lastSuccess INTEGER, buildId TEXT)");
|
||||
this.sql.exec("DELETE FROM failed_state WHERE buildId != ?", "Bi3XXPy0fxaJxYXDBsshm");
|
||||
this.sql.exec("DELETE FROM sync WHERE buildId != ?", "Bi3XXPy0fxaJxYXDBsshm");
|
||||
const failedStateCursor = this.sql.exec("SELECT * FROM failed_state");
|
||||
for (const row of failedStateCursor) {
|
||||
this.routeInFailedState.set(row.id, JSON.parse(row.data));
|
||||
}
|
||||
await this.addAlarm();
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param msg
|
||||
* @returns `true` if the route has been revalidated since the lastModified from the message, `false` otherwise
|
||||
*/
|
||||
checkSyncTable(msg) {
|
||||
try {
|
||||
if (this.disableSQLite)
|
||||
return false;
|
||||
return this.sql.exec("SELECT 1 FROM sync WHERE id = ? AND lastSuccess > ? LIMIT 1", `${msg.MessageBody.host}${msg.MessageBody.url}`, Math.round(msg.MessageBody.lastModified / 1e3)).toArray().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
export {
|
||||
DOQueueHandler
|
||||
};
|
||||
@ -1,55 +0,0 @@
|
||||
globalThis.openNextDebug = false;globalThis.openNextVersion = "3.9.12";
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/durable-objects/sharded-tag-cache.js
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/cloudflare-context.js
|
||||
var cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/overrides/internal.js
|
||||
var debugCache = (name, ...args) => {
|
||||
if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
|
||||
console.log(`[${name}] `, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/api/durable-objects/sharded-tag-cache.js
|
||||
var DOShardedTagCache = class extends DurableObject {
|
||||
sql;
|
||||
constructor(state, env) {
|
||||
super(state, env);
|
||||
this.sql = state.storage.sql;
|
||||
state.blockConcurrencyWhile(async () => {
|
||||
this.sql.exec(`CREATE TABLE IF NOT EXISTS revalidations (tag TEXT PRIMARY KEY, revalidatedAt INTEGER)`);
|
||||
});
|
||||
}
|
||||
async getLastRevalidated(tags) {
|
||||
try {
|
||||
const result = this.sql.exec(`SELECT MAX(revalidatedAt) AS time FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")})`, ...tags).toArray();
|
||||
const timeMs = result[0]?.time ?? 0;
|
||||
debugCache("DOShardedTagCache", `getLastRevalidated tags=${tags} -> time=${timeMs}`);
|
||||
return timeMs;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
async hasBeenRevalidated(tags, lastModified) {
|
||||
const revalidated = this.sql.exec(`SELECT 1 FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")}) AND revalidatedAt > ? LIMIT 1`, ...tags, lastModified ?? Date.now()).toArray().length > 0;
|
||||
debugCache("DOShardedTagCache", `hasBeenRevalidated tags=${tags} -> revalidated=${revalidated}`);
|
||||
return revalidated;
|
||||
}
|
||||
async writeTags(tags, lastModified) {
|
||||
debugCache("DOShardedTagCache", `writeTags tags=${tags} time=${lastModified}`);
|
||||
tags.forEach((tag) => {
|
||||
this.sql.exec(`INSERT OR REPLACE INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`, tag, lastModified);
|
||||
});
|
||||
}
|
||||
async getRevalidationTimes(tags) {
|
||||
const result = this.sql.exec(`SELECT tag, revalidatedAt FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")})`, ...tags).toArray();
|
||||
return Object.fromEntries(result.map((row) => [row.tag, row.revalidatedAt]));
|
||||
}
|
||||
};
|
||||
export {
|
||||
DOShardedTagCache
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
// open-next.config.ts
|
||||
var config = {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: "cloudflare-node",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
tagCache: "dummy",
|
||||
queue: "dummy"
|
||||
}
|
||||
},
|
||||
edgeExternals: ["node:crypto"],
|
||||
middleware: {
|
||||
external: true,
|
||||
override: {
|
||||
wrapper: "cloudflare-edge",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
tagCache: "dummy",
|
||||
queue: "dummy"
|
||||
}
|
||||
}
|
||||
};
|
||||
var open_next_config_default = config;
|
||||
export {
|
||||
open_next_config_default as default
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
import { createRequire as topLevelCreateRequire } from 'module';const require = topLevelCreateRequire(import.meta.url);import bannerUrl from 'url';const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
// open-next.config.ts
|
||||
var config = {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: "cloudflare-node",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
tagCache: "dummy",
|
||||
queue: "dummy"
|
||||
}
|
||||
},
|
||||
edgeExternals: ["node:crypto"],
|
||||
middleware: {
|
||||
external: true,
|
||||
override: {
|
||||
wrapper: "cloudflare-edge",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
tagCache: "dummy",
|
||||
queue: "dummy"
|
||||
}
|
||||
}
|
||||
};
|
||||
var open_next_config_default = config;
|
||||
export {
|
||||
open_next_config_default as default
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
55
.open-next 2/cloudflare-templates/images.d.ts
vendored
55
.open-next 2/cloudflare-templates/images.d.ts
vendored
@ -1,55 +0,0 @@
|
||||
export type RemotePattern = {
|
||||
protocol?: "http" | "https";
|
||||
hostname: string;
|
||||
port?: string;
|
||||
pathname: string;
|
||||
search?: string;
|
||||
};
|
||||
export type LocalPattern = {
|
||||
pathname: string;
|
||||
search?: string;
|
||||
};
|
||||
/**
|
||||
* Handles requests to /_next/image(/), including image optimizations.
|
||||
*
|
||||
* Image optimization is disabled and the original image is returned if `env.IMAGES` is undefined.
|
||||
*
|
||||
* Throws an exception on unexpected errors.
|
||||
*
|
||||
* @param requestURL
|
||||
* @param requestHeaders
|
||||
* @param env
|
||||
* @returns A promise that resolves to the resolved request.
|
||||
*/
|
||||
export declare function handleImageRequest(requestURL: URL, requestHeaders: Headers, env: CloudflareEnv): Promise<Response>;
|
||||
export type OptimizedImageFormat = "image/avif" | "image/webp";
|
||||
export declare function matchLocalPattern(pattern: LocalPattern, url: {
|
||||
pathname: string;
|
||||
search: string;
|
||||
}): boolean;
|
||||
export declare function matchRemotePattern(pattern: RemotePattern, url: URL): boolean;
|
||||
type ImageContentType = "image/avif" | "image/webp" | "image/png" | "image/jpeg" | "image/jxl" | "image/jp2" | "image/heic" | "image/gif" | "image/svg+xml" | "image/x-icon" | "image/x-icns" | "image/tiff" | "image/bmp";
|
||||
/**
|
||||
* Detects the content type by looking at the first few bytes of a file
|
||||
*
|
||||
* Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155
|
||||
*
|
||||
* @param buffer The image bytes
|
||||
* @returns a content type of undefined for unsupported content
|
||||
*/
|
||||
export declare function detectImageContentType(buffer: Uint8Array): ImageContentType | null;
|
||||
declare global {
|
||||
var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
|
||||
var __IMAGES_LOCAL_PATTERNS__: LocalPattern[];
|
||||
var __IMAGES_DEVICE_SIZES__: number[];
|
||||
var __IMAGES_IMAGE_SIZES__: number[];
|
||||
var __IMAGES_QUALITIES__: number[];
|
||||
var __IMAGES_FORMATS__: NextConfigImageFormat[];
|
||||
var __IMAGES_MINIMUM_CACHE_TTL_SEC__: number;
|
||||
var __IMAGES_ALLOW_SVG__: boolean;
|
||||
var __IMAGES_CONTENT_SECURITY_POLICY__: string;
|
||||
var __IMAGES_CONTENT_DISPOSITION__: string;
|
||||
var __IMAGES_MAX_REDIRECTS__: number;
|
||||
type NextConfigImageFormat = "image/avif" | "image/webp";
|
||||
}
|
||||
export {};
|
||||
@ -1,573 +0,0 @@
|
||||
import { error, warn } from "@opennextjs/aws/adapters/logger.js";
|
||||
/**
|
||||
* Handles requests to /_next/image(/), including image optimizations.
|
||||
*
|
||||
* Image optimization is disabled and the original image is returned if `env.IMAGES` is undefined.
|
||||
*
|
||||
* Throws an exception on unexpected errors.
|
||||
*
|
||||
* @param requestURL
|
||||
* @param requestHeaders
|
||||
* @param env
|
||||
* @returns A promise that resolves to the resolved request.
|
||||
*/
|
||||
export async function handleImageRequest(requestURL, requestHeaders, env) {
|
||||
const parseResult = parseImageRequest(requestURL, requestHeaders);
|
||||
if (!parseResult.ok) {
|
||||
return new Response(parseResult.message, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
let imageResponse;
|
||||
if (parseResult.url.startsWith("/")) {
|
||||
if (env.ASSETS === undefined) {
|
||||
error("env.ASSETS binding is not defined");
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
const absoluteURL = new URL(parseResult.url, requestURL);
|
||||
imageResponse = await env.ASSETS.fetch(absoluteURL);
|
||||
}
|
||||
else {
|
||||
let fetchImageResult;
|
||||
try {
|
||||
fetchImageResult = await fetchWithRedirects(parseResult.url, 7_000, __IMAGES_MAX_REDIRECTS__);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error("Failed to fetch image", { cause: e });
|
||||
}
|
||||
if (!fetchImageResult.ok) {
|
||||
if (fetchImageResult.error === "timed_out") {
|
||||
return new Response('"url" parameter is valid but upstream response timed out', {
|
||||
status: 504,
|
||||
});
|
||||
}
|
||||
if (fetchImageResult.error === "too_many_redirects") {
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: 508,
|
||||
});
|
||||
}
|
||||
throw new Error("Failed to fetch image");
|
||||
}
|
||||
imageResponse = fetchImageResult.response;
|
||||
}
|
||||
if (!imageResponse.ok || imageResponse.body === null) {
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: imageResponse.status,
|
||||
});
|
||||
}
|
||||
let immutable = false;
|
||||
if (parseResult.static) {
|
||||
immutable = true;
|
||||
}
|
||||
else {
|
||||
const cacheControlHeader = imageResponse.headers.get("Cache-Control");
|
||||
if (cacheControlHeader !== null) {
|
||||
// TODO: Properly parse header
|
||||
immutable = cacheControlHeader.includes("immutable");
|
||||
}
|
||||
}
|
||||
const [contentTypeImageStream, imageStream] = imageResponse.body.tee();
|
||||
const imageHeaderBytes = new Uint8Array(32);
|
||||
const contentTypeImageReader = contentTypeImageStream.getReader({
|
||||
mode: "byob",
|
||||
});
|
||||
const readImageHeaderBytesResult = await contentTypeImageReader.readAtLeast(32, imageHeaderBytes);
|
||||
if (readImageHeaderBytesResult.value === undefined) {
|
||||
await imageResponse.body.cancel();
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
const contentType = detectImageContentType(readImageHeaderBytesResult.value);
|
||||
if (contentType === null) {
|
||||
warn(`Failed to detect content type of "${parseResult.url}"`);
|
||||
return new Response('"url" parameter is valid but image type is not allowed', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (contentType === SVG) {
|
||||
if (!__IMAGES_ALLOW_SVG__) {
|
||||
return new Response('"url" parameter is valid but image type is not allowed', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
const response = createImageResponse(imageStream, contentType, {
|
||||
immutable,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
if (contentType === GIF) {
|
||||
if (env.IMAGES === undefined) {
|
||||
warn("env.IMAGES binding is not defined");
|
||||
const response = createImageResponse(imageStream, contentType, {
|
||||
immutable,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
const imageSource = env.IMAGES.input(imageStream);
|
||||
const imageTransformationResult = await imageSource
|
||||
.transform({
|
||||
width: parseResult.width,
|
||||
fit: "scale-down",
|
||||
})
|
||||
.output({
|
||||
quality: parseResult.quality,
|
||||
format: GIF,
|
||||
});
|
||||
const outputImageStream = imageTransformationResult.image();
|
||||
const response = createImageResponse(outputImageStream, GIF, {
|
||||
immutable,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
if (contentType === AVIF || contentType === WEBP || contentType === JPEG || contentType === PNG) {
|
||||
if (env.IMAGES === undefined) {
|
||||
warn("env.IMAGES binding is not defined");
|
||||
const response = createImageResponse(imageStream, contentType, {
|
||||
immutable,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
const outputFormat = parseResult.format ?? contentType;
|
||||
const imageSource = env.IMAGES.input(imageStream);
|
||||
const imageTransformationResult = await imageSource
|
||||
.transform({
|
||||
width: parseResult.width,
|
||||
fit: "scale-down",
|
||||
})
|
||||
.output({
|
||||
quality: parseResult.quality,
|
||||
format: outputFormat,
|
||||
});
|
||||
const outputImageStream = imageTransformationResult.image();
|
||||
const response = createImageResponse(outputImageStream, outputFormat, {
|
||||
immutable,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
warn(`Image content type ${contentType} not supported`);
|
||||
const response = createImageResponse(imageStream, contentType, {
|
||||
immutable,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
/**
|
||||
* Fetch call with max redirects and timeouts.
|
||||
*
|
||||
* Re-throws the exception thrown by a fetch call.
|
||||
* @param url
|
||||
* @param timeoutMS Timeout for a single fetch call.
|
||||
* @param maxRedirectCount
|
||||
* @returns
|
||||
*/
|
||||
async function fetchWithRedirects(url, timeoutMS, maxRedirectCount) {
|
||||
// TODO: Add dangerouslyAllowLocalIP support
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(timeoutMS),
|
||||
redirect: "manual",
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error && e.name === "TimeoutError") {
|
||||
const result = {
|
||||
ok: false,
|
||||
error: "timed_out",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (redirectResponseStatuses.includes(response.status)) {
|
||||
const locationHeader = response.headers.get("Location");
|
||||
if (locationHeader !== null) {
|
||||
if (maxRedirectCount < 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
error: "too_many_redirects",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
let redirectTarget;
|
||||
if (locationHeader.startsWith("/")) {
|
||||
redirectTarget = new URL(locationHeader, url).href;
|
||||
}
|
||||
else {
|
||||
redirectTarget = locationHeader;
|
||||
}
|
||||
const result = await fetchWithRedirects(redirectTarget, timeoutMS, maxRedirectCount - 1);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
ok: true,
|
||||
response: response,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const redirectResponseStatuses = [301, 302, 303, 307, 308];
|
||||
function createImageResponse(image, contentType, imageResponseFlags) {
|
||||
const response = new Response(image, {
|
||||
headers: {
|
||||
Vary: "Accept",
|
||||
"Content-Type": contentType,
|
||||
"Content-Disposition": __IMAGES_CONTENT_DISPOSITION__,
|
||||
"Content-Security-Policy": __IMAGES_CONTENT_SECURITY_POLICY__,
|
||||
},
|
||||
});
|
||||
if (imageResponseFlags.immutable) {
|
||||
response.headers.set("Cache-Control", "public, max-age=315360000, immutable");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
/**
|
||||
* Parses the image request URL and headers.
|
||||
*
|
||||
* This function validates the parameters and returns either the parsed result or an error message.
|
||||
*
|
||||
* @param requestURL request URL
|
||||
* @param requestHeaders request headers
|
||||
* @returns an instance of `ParseImageRequestURLSuccessResult` when successful, or an instance of `ErrorResult` when failed.
|
||||
*/
|
||||
function parseImageRequest(requestURL, requestHeaders) {
|
||||
const formats = __IMAGES_FORMATS__;
|
||||
const parsedUrlOrError = validateUrlQueryParameter(requestURL);
|
||||
if (!("url" in parsedUrlOrError)) {
|
||||
return parsedUrlOrError;
|
||||
}
|
||||
const widthOrError = validateWidthQueryParameter(requestURL);
|
||||
if (typeof widthOrError !== "number") {
|
||||
return widthOrError;
|
||||
}
|
||||
const qualityOrError = validateQualityQueryParameter(requestURL);
|
||||
if (typeof qualityOrError !== "number") {
|
||||
return qualityOrError;
|
||||
}
|
||||
const acceptHeader = requestHeaders.get("Accept") ?? "";
|
||||
let format = null;
|
||||
// Find a more specific format that the client accepts.
|
||||
for (const allowedFormat of formats) {
|
||||
if (acceptHeader.includes(allowedFormat)) {
|
||||
format = allowedFormat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
ok: true,
|
||||
url: parsedUrlOrError.url,
|
||||
width: widthOrError,
|
||||
quality: qualityOrError,
|
||||
format,
|
||||
static: parsedUrlOrError.static,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Validates that there is exactly one "url" query parameter.
|
||||
*
|
||||
* @returns the validated URL or an error result.
|
||||
*/
|
||||
function validateUrlQueryParameter(requestURL) {
|
||||
// There should be a single "url" parameter.
|
||||
const urls = requestURL.searchParams.getAll("url");
|
||||
if (urls.length < 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is required',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (urls.length > 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter cannot be an array',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
// The url parameter value should be a valid URL or a valid relative URL.
|
||||
const url = urls[0];
|
||||
if (url.length > 3072) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is too long',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (url.startsWith("//")) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter cannot be a protocol-relative URL (//)',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (url.startsWith("/")) {
|
||||
const staticAsset = url.startsWith(`${__NEXT_BASE_PATH__ || ""}/_next/static/media`);
|
||||
const pathname = getPathnameFromRelativeURL(url);
|
||||
if (/\/_next\/image($|\/)/.test(decodeURIComponent(pathname))) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter cannot be recursive',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (!staticAsset) {
|
||||
if (!hasLocalMatch(__IMAGES_LOCAL_PATTERNS__, url)) {
|
||||
const result = { ok: false, message: '"url" parameter is not allowed' };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { url, static: staticAsset };
|
||||
}
|
||||
let parsedURL;
|
||||
try {
|
||||
parsedURL = new URL(url);
|
||||
}
|
||||
catch {
|
||||
const result = { ok: false, message: '"url" parameter is invalid' };
|
||||
return result;
|
||||
}
|
||||
const validProtocols = ["http:", "https:"];
|
||||
if (!validProtocols.includes(parsedURL.protocol)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is invalid',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (!hasRemoteMatch(__IMAGES_REMOTE_PATTERNS__, parsedURL)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is not allowed',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
return { url: parsedURL.href, static: false };
|
||||
}
|
||||
/**
|
||||
* Validates the "w" (width) query parameter.
|
||||
*
|
||||
* @returns the validated width number or an error result.
|
||||
*/
|
||||
function validateWidthQueryParameter(requestURL) {
|
||||
const widthQueryValues = requestURL.searchParams.getAll("w");
|
||||
if (widthQueryValues.length < 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) is required',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (widthQueryValues.length > 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) cannot be an array',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const widthQueryValue = widthQueryValues[0];
|
||||
if (!/^[0-9]+$/.test(widthQueryValue)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) must be an integer greater than 0',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const width = parseInt(widthQueryValue, 10);
|
||||
if (width <= 0 || isNaN(width)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) must be an integer greater than 0',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const sizeValid = __IMAGES_DEVICE_SIZES__.includes(width) || __IMAGES_IMAGE_SIZES__.includes(width);
|
||||
if (!sizeValid) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: `"w" parameter (width) of ${width} is not allowed`,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
/**
|
||||
* Validates the "q" (quality) query parameter.
|
||||
*
|
||||
* @returns the validated quality number or an error result.
|
||||
*/
|
||||
function validateQualityQueryParameter(requestURL) {
|
||||
const qualityQueryValues = requestURL.searchParams.getAll("q");
|
||||
if (qualityQueryValues.length < 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) is required',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (qualityQueryValues.length > 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) cannot be an array',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const qualityQueryValue = qualityQueryValues[0];
|
||||
if (!/^[0-9]+$/.test(qualityQueryValue)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) must be an integer between 1 and 100',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const quality = parseInt(qualityQueryValue, 10);
|
||||
if (isNaN(quality) || quality < 1 || quality > 100) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) must be an integer between 1 and 100',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (!__IMAGES_QUALITIES__.includes(quality)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: `"q" parameter (quality) of ${quality} is not allowed`,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
return quality;
|
||||
}
|
||||
function getPathnameFromRelativeURL(relativeURL) {
|
||||
return relativeURL.split("?")[0];
|
||||
}
|
||||
function hasLocalMatch(localPatterns, relativeURL) {
|
||||
const parseRelativeURLResult = parseRelativeURL(relativeURL);
|
||||
for (const localPattern of localPatterns) {
|
||||
const matched = matchLocalPattern(localPattern, parseRelativeURLResult);
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function parseRelativeURL(relativeURL) {
|
||||
if (!relativeURL.includes("?")) {
|
||||
const result = {
|
||||
pathname: relativeURL,
|
||||
search: "",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const parts = relativeURL.split("?");
|
||||
const pathname = parts[0];
|
||||
const search = "?" + parts.slice(1).join("?");
|
||||
const result = {
|
||||
pathname,
|
||||
search,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
export function matchLocalPattern(pattern, url) {
|
||||
if (pattern.search !== undefined && pattern.search !== url.search) {
|
||||
return false;
|
||||
}
|
||||
return new RegExp(pattern.pathname).test(url.pathname);
|
||||
}
|
||||
function hasRemoteMatch(remotePatterns, url) {
|
||||
for (const remotePattern of remotePatterns) {
|
||||
const matched = matchRemotePattern(remotePattern, url);
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function matchRemotePattern(pattern, url) {
|
||||
// https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts
|
||||
if (pattern.protocol !== undefined &&
|
||||
pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "")) {
|
||||
return false;
|
||||
}
|
||||
if (pattern.port !== undefined && pattern.port !== url.port) {
|
||||
return false;
|
||||
}
|
||||
if (pattern.hostname === undefined || !new RegExp(pattern.hostname).test(url.hostname)) {
|
||||
return false;
|
||||
}
|
||||
if (pattern.search !== undefined && pattern.search !== url.search) {
|
||||
return false;
|
||||
}
|
||||
// Should be the same as writeImagesManifest()
|
||||
return new RegExp(pattern.pathname).test(url.pathname);
|
||||
}
|
||||
const AVIF = "image/avif";
|
||||
const WEBP = "image/webp";
|
||||
const PNG = "image/png";
|
||||
const JPEG = "image/jpeg";
|
||||
const JXL = "image/jxl";
|
||||
const JP2 = "image/jp2";
|
||||
const HEIC = "image/heic";
|
||||
const GIF = "image/gif";
|
||||
const SVG = "image/svg+xml";
|
||||
const ICO = "image/x-icon";
|
||||
const ICNS = "image/x-icns";
|
||||
const TIFF = "image/tiff";
|
||||
const BMP = "image/bmp";
|
||||
/**
|
||||
* Detects the content type by looking at the first few bytes of a file
|
||||
*
|
||||
* Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155
|
||||
*
|
||||
* @param buffer The image bytes
|
||||
* @returns a content type of undefined for unsupported content
|
||||
*/
|
||||
export function detectImageContentType(buffer) {
|
||||
if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
|
||||
return JPEG;
|
||||
}
|
||||
if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) {
|
||||
return PNG;
|
||||
}
|
||||
if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
|
||||
return GIF;
|
||||
}
|
||||
if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) {
|
||||
return WEBP;
|
||||
}
|
||||
if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
|
||||
return SVG;
|
||||
}
|
||||
if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) {
|
||||
return SVG;
|
||||
}
|
||||
if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) {
|
||||
return AVIF;
|
||||
}
|
||||
if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) {
|
||||
return ICO;
|
||||
}
|
||||
if ([0x69, 0x63, 0x6e, 0x73].every((b, i) => buffer[i] === b)) {
|
||||
return ICNS;
|
||||
}
|
||||
if ([0x49, 0x49, 0x2a, 0x00].every((b, i) => buffer[i] === b)) {
|
||||
return TIFF;
|
||||
}
|
||||
if ([0x42, 0x4d].every((b, i) => buffer[i] === b)) {
|
||||
return BMP;
|
||||
}
|
||||
if ([0xff, 0x0a].every((b, i) => buffer[i] === b)) {
|
||||
return JXL;
|
||||
}
|
||||
if ([0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a].every((b, i) => buffer[i] === b)) {
|
||||
return JXL;
|
||||
}
|
||||
if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63].every((b, i) => !b || buffer[i] === b)) {
|
||||
return HEIC;
|
||||
}
|
||||
if ([0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a].every((b, i) => buffer[i] === b)) {
|
||||
return JP2;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
16
.open-next 2/cloudflare-templates/init.d.ts
vendored
16
.open-next 2/cloudflare-templates/init.d.ts
vendored
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Initialization for the workerd runtime.
|
||||
*
|
||||
* The file must be imported at the top level the worker.
|
||||
*/
|
||||
/**
|
||||
* Executes the handler with the Cloudflare context.
|
||||
*/
|
||||
export declare function runWithCloudflareRequestContext(request: Request, env: CloudflareEnv, ctx: ExecutionContext, handler: () => Promise<Response>): Promise<Response>;
|
||||
declare global {
|
||||
var __BUILD_TIMESTAMP_MS__: number;
|
||||
var __NEXT_BASE_PATH__: string;
|
||||
var __ASSETS_RUN_WORKER_FIRST__: boolean | string[] | undefined;
|
||||
var __DEPLOYMENT_ID__: string;
|
||||
var __TRAILING_SLASH__: boolean;
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Initialization for the workerd runtime.
|
||||
*
|
||||
* The file must be imported at the top level the worker.
|
||||
*/
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import process from "node:process";
|
||||
import stream from "node:stream";
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
import * as nextEnvVars from "./next-env.mjs";
|
||||
const cloudflareContextALS = new AsyncLocalStorage();
|
||||
// Note: this symbol needs to be kept in sync with `src/api/get-cloudflare-context.ts`
|
||||
Object.defineProperty(globalThis, Symbol.for("__cloudflare-context__"), {
|
||||
get() {
|
||||
return cloudflareContextALS.getStore();
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Executes the handler with the Cloudflare context.
|
||||
*/
|
||||
export async function runWithCloudflareRequestContext(request, env, ctx, handler) {
|
||||
init(request, env);
|
||||
return cloudflareContextALS.run({ env, ctx, cf: request.cf }, handler);
|
||||
}
|
||||
let initialized = false;
|
||||
/**
|
||||
* Initializes the runtime on the first call,
|
||||
* no-op on subsequent invocations.
|
||||
*/
|
||||
function init(request, env) {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
const url = new URL(request.url);
|
||||
initRuntime();
|
||||
populateProcessEnv(url, env);
|
||||
}
|
||||
function initRuntime() {
|
||||
// Some packages rely on `process.version` and `process.versions.node` (i.e. Jose@4)
|
||||
// TODO: Remove when https://github.com/unjs/unenv/pull/493 is merged
|
||||
Object.assign(process, { version: process.version || "v22.14.0" });
|
||||
// @ts-expect-error Node type does not match workerd
|
||||
Object.assign(process.versions, { node: "22.14.0", ...process.versions });
|
||||
globalThis.__dirname ??= "";
|
||||
globalThis.__filename ??= "";
|
||||
// Some packages rely on `import.meta.url` but it is undefined in workerd
|
||||
// For example it causes a bunch of issues, and will make even import crash with payload
|
||||
import.meta.url ??= "file:///worker.js";
|
||||
// Do not crash on cache not supported
|
||||
// https://github.com/cloudflare/workerd/pull/2434
|
||||
// compatibility flag "cache_option_enabled" -> does not support "force-cache"
|
||||
const __original_fetch = globalThis.fetch;
|
||||
globalThis.fetch = (input, init) => {
|
||||
if (init) {
|
||||
delete init.cache;
|
||||
}
|
||||
return __original_fetch(input, init);
|
||||
};
|
||||
const CustomRequest = class extends globalThis.Request {
|
||||
constructor(input, init) {
|
||||
if (init) {
|
||||
delete init.cache;
|
||||
// https://github.com/cloudflare/workerd/issues/2746
|
||||
// https://github.com/cloudflare/workerd/issues/3245
|
||||
Object.defineProperty(init, "body", {
|
||||
// @ts-ignore
|
||||
value: init.body instanceof stream.Readable ? ReadableStream.from(init.body) : init.body,
|
||||
});
|
||||
}
|
||||
super(input, init);
|
||||
}
|
||||
};
|
||||
Object.assign(globalThis, {
|
||||
Request: CustomRequest,
|
||||
__BUILD_TIMESTAMP_MS__,
|
||||
__NEXT_BASE_PATH__,
|
||||
__ASSETS_RUN_WORKER_FIRST__,
|
||||
__TRAILING_SLASH__,
|
||||
// The external middleware will use the convertTo function of the `edge` converter
|
||||
// by default it will try to fetch the request, but since we are running everything in the same worker
|
||||
// we need to use the request as is.
|
||||
__dangerous_ON_edge_converter_returns_request: true,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Populate process.env with:
|
||||
* - the environment variables and secrets from the cloudflare platform
|
||||
* - the variables from Next .env* files
|
||||
* - the origin resolver information
|
||||
*/
|
||||
function populateProcessEnv(url, env) {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (typeof value === "string") {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
const mode = env.NEXTJS_ENV ?? "production";
|
||||
if (nextEnvVars[mode]) {
|
||||
for (const key in nextEnvVars[mode]) {
|
||||
process.env[key] ??= nextEnvVars[mode][key];
|
||||
}
|
||||
}
|
||||
// Set the default Origin for the origin resolver.
|
||||
// This is only needed for an external middleware bundle
|
||||
process.env.OPEN_NEXT_ORIGIN = JSON.stringify({
|
||||
default: {
|
||||
host: url.hostname,
|
||||
protocol: url.protocol.slice(0, -1),
|
||||
port: url.port,
|
||||
},
|
||||
});
|
||||
/* We need to set this environment variable to make redirects work properly in preview mode.
|
||||
* Next sets this in standalone mode during `startServer`. Without this the protocol would always be `https` here:
|
||||
* https://github.com/vercel/next.js/blob/6b1e48080e896e0d44a05fe009cb79d2d3f91774/packages/next/src/server/app-render/action-handler.ts#L307-L316
|
||||
*/
|
||||
process.env.__NEXT_PRIVATE_ORIGIN = url.origin;
|
||||
// `__DEPLOYMENT_ID__` is a string (passed via ESBuild).
|
||||
if (__DEPLOYMENT_ID__) {
|
||||
process.env.DEPLOYMENT_ID = __DEPLOYMENT_ID__;
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
declare const _default: {};
|
||||
export default _default;
|
||||
@ -1 +0,0 @@
|
||||
export default {};
|
||||
@ -1 +0,0 @@
|
||||
export declare function loadEnvConfig(): void;
|
||||
@ -1 +0,0 @@
|
||||
export function loadEnvConfig() { }
|
||||
@ -1 +0,0 @@
|
||||
export default fetch;
|
||||
@ -1 +0,0 @@
|
||||
export default fetch;
|
||||
@ -1,2 +0,0 @@
|
||||
declare const _default: {};
|
||||
export default _default;
|
||||
@ -1,2 +0,0 @@
|
||||
throw "OpenNext shim";
|
||||
export default {};
|
||||
@ -1,28 +0,0 @@
|
||||
/** Name of the env var containing the mapping */
|
||||
export declare const DEPLOYMENT_MAPPING_ENV_NAME = "CF_DEPLOYMENT_MAPPING";
|
||||
/** Version used for the latest worker */
|
||||
export declare const CURRENT_VERSION_ID = "current";
|
||||
/**
|
||||
* Routes the request to the requested deployment.
|
||||
*
|
||||
* A specific deployment can be requested via:
|
||||
* - the `dpl` search parameter for assets
|
||||
* - the `x-deployment-id` for other requests
|
||||
*
|
||||
* When a specific deployment is requested, we route to that deployment via the preview URLs.
|
||||
* See https://developers.cloudflare.com/workers/configuration/previews/
|
||||
*
|
||||
* When the requested deployment is not supported a 400 response is returned.
|
||||
*
|
||||
* Notes:
|
||||
* - The re-routing is only active for the deployed version of the app (on a custom domain)
|
||||
* - Assets are also handled when `run_worker_first` is enabled.
|
||||
* See https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first
|
||||
*
|
||||
* @param request
|
||||
* @returns
|
||||
*/
|
||||
export declare function maybeGetSkewProtectionResponse(request: Request): Promise<Response> | Response | undefined;
|
||||
declare global {
|
||||
var __SKEW_PROTECTION_ENABLED__: boolean;
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
import process from "node:process";
|
||||
/** Name of the env var containing the mapping */
|
||||
export const DEPLOYMENT_MAPPING_ENV_NAME = "CF_DEPLOYMENT_MAPPING";
|
||||
/** Version used for the latest worker */
|
||||
export const CURRENT_VERSION_ID = "current";
|
||||
let deploymentMapping;
|
||||
/**
|
||||
* Routes the request to the requested deployment.
|
||||
*
|
||||
* A specific deployment can be requested via:
|
||||
* - the `dpl` search parameter for assets
|
||||
* - the `x-deployment-id` for other requests
|
||||
*
|
||||
* When a specific deployment is requested, we route to that deployment via the preview URLs.
|
||||
* See https://developers.cloudflare.com/workers/configuration/previews/
|
||||
*
|
||||
* When the requested deployment is not supported a 400 response is returned.
|
||||
*
|
||||
* Notes:
|
||||
* - The re-routing is only active for the deployed version of the app (on a custom domain)
|
||||
* - Assets are also handled when `run_worker_first` is enabled.
|
||||
* See https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first
|
||||
*
|
||||
* @param request
|
||||
* @returns
|
||||
*/
|
||||
export function maybeGetSkewProtectionResponse(request) {
|
||||
// no early return as esbuild would not treeshake the code.
|
||||
if (__SKEW_PROTECTION_ENABLED__) {
|
||||
const url = new URL(request.url);
|
||||
// Skew protection is only active for the latest version of the app served on a custom domain.
|
||||
if (url.hostname === "localhost" || url.hostname.endsWith(".workers.dev")) {
|
||||
return undefined;
|
||||
}
|
||||
const requestDeploymentId = request.headers.get("x-deployment-id") ?? url.searchParams.get("dpl");
|
||||
if (!requestDeploymentId || requestDeploymentId === process.env.DEPLOYMENT_ID) {
|
||||
// The request does not specify a deployment id or it is the current deployment id
|
||||
return undefined;
|
||||
}
|
||||
deploymentMapping ??= process.env[DEPLOYMENT_MAPPING_ENV_NAME]
|
||||
? JSON.parse(process.env[DEPLOYMENT_MAPPING_ENV_NAME])
|
||||
: {};
|
||||
if (!(requestDeploymentId in deploymentMapping)) {
|
||||
// Unknown deployment id, serve the current version
|
||||
return undefined;
|
||||
}
|
||||
const version = deploymentMapping[requestDeploymentId];
|
||||
if (!version || version === CURRENT_VERSION_ID) {
|
||||
return undefined;
|
||||
}
|
||||
const versionDomain = version.split("-")[0];
|
||||
const hostname = `${versionDomain}-${process.env.CF_WORKER_NAME}.${process.env.CF_PREVIEW_DOMAIN}.workers.dev`;
|
||||
url.hostname = hostname;
|
||||
const requestToOlderDeployment = new Request(url, request);
|
||||
// Remove the origin header to prevent an error with POST requests
|
||||
const headers = new Headers(request.headers);
|
||||
headers.delete("origin");
|
||||
return fetch(requestToOlderDeployment, { headers });
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
declare const _default: {
|
||||
fetch(request: Request<unknown, IncomingRequestCfProperties<unknown>>, env: CloudflareEnv, ctx: ExecutionContext<unknown>): Promise<any>;
|
||||
};
|
||||
export default _default;
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS tags (tag TEXT NOT NULL, path TEXT NOT NULL, UNIQUE(tag, path) ON CONFLICT REPLACE);
|
||||
CREATE TABLE IF NOT EXISTS revalidations (tag TEXT NOT NULL, revalidatedAt INTEGER NOT NULL, UNIQUE(tag) ON CONFLICT REPLACE);
|
||||
INSERT INTO tags (tag, path) VALUES ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/layout", "Bi3XXPy0fxaJxYXDBsshm/_global-error"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/_global-error/layout", "Bi3XXPy0fxaJxYXDBsshm/_global-error"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/_global-error/page", "Bi3XXPy0fxaJxYXDBsshm/_global-error"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/_global-error", "Bi3XXPy0fxaJxYXDBsshm/_global-error"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/layout", "Bi3XXPy0fxaJxYXDBsshm/_not-found"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/_not-found/layout", "Bi3XXPy0fxaJxYXDBsshm/_not-found"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/_not-found/page", "Bi3XXPy0fxaJxYXDBsshm/_not-found"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/_not-found", "Bi3XXPy0fxaJxYXDBsshm/_not-found"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/layout", "Bi3XXPy0fxaJxYXDBsshm/login"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/login/layout", "Bi3XXPy0fxaJxYXDBsshm/login"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/login/page", "Bi3XXPy0fxaJxYXDBsshm/login"), ("Bi3XXPy0fxaJxYXDBsshm/_N_T_/login", "Bi3XXPy0fxaJxYXDBsshm/login");
|
||||
@ -1,585 +0,0 @@
|
||||
// <define:__IMAGES_DEVICE_SIZES__>
|
||||
var define_IMAGES_DEVICE_SIZES_default = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
||||
|
||||
// <define:__IMAGES_FORMATS__>
|
||||
var define_IMAGES_FORMATS_default = ["image/webp"];
|
||||
|
||||
// <define:__IMAGES_IMAGE_SIZES__>
|
||||
var define_IMAGES_IMAGE_SIZES_default = [32, 48, 64, 96, 128, 256, 384];
|
||||
|
||||
// <define:__IMAGES_LOCAL_PATTERNS__>
|
||||
var define_IMAGES_LOCAL_PATTERNS_default = [{ pathname: "^(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)\\/?)$", search: "" }];
|
||||
|
||||
// <define:__IMAGES_QUALITIES__>
|
||||
var define_IMAGES_QUALITIES_default = [75];
|
||||
|
||||
// <define:__IMAGES_REMOTE_PATTERNS__>
|
||||
var define_IMAGES_REMOTE_PATTERNS_default = [{ protocol: "https", hostname: "^(?:^(?:m\\.media\\-amazon\\.com)$)$", pathname: "^(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)\\/?)$" }];
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/utils/error.js
|
||||
function isOpenNextError(e) {
|
||||
try {
|
||||
return "__openNextInternal" in e;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/aws/dist/adapters/logger.js
|
||||
function debug(...args) {
|
||||
if (globalThis.openNextDebug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
function warn(...args) {
|
||||
console.warn(...args);
|
||||
}
|
||||
var DOWNPLAYED_ERROR_LOGS = [
|
||||
{
|
||||
clientName: "S3Client",
|
||||
commandName: "GetObjectCommand",
|
||||
errorName: "NoSuchKey"
|
||||
}
|
||||
];
|
||||
var isDownplayedErrorLog = (errorLog) => DOWNPLAYED_ERROR_LOGS.some((downplayedInput) => downplayedInput.clientName === errorLog?.clientName && downplayedInput.commandName === errorLog?.commandName && (downplayedInput.errorName === errorLog?.error?.name || downplayedInput.errorName === errorLog?.error?.Code));
|
||||
function error(...args) {
|
||||
if (args.some((arg) => isDownplayedErrorLog(arg))) {
|
||||
return debug(...args);
|
||||
}
|
||||
if (args.some((arg) => isOpenNextError(arg))) {
|
||||
const error2 = args.find((arg) => isOpenNextError(arg));
|
||||
if (error2.logLevel < getOpenNextErrorLogLevel()) {
|
||||
return;
|
||||
}
|
||||
if (error2.logLevel === 0) {
|
||||
return console.log(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
if (error2.logLevel === 1) {
|
||||
return warn(...args.map((arg) => isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg));
|
||||
}
|
||||
return console.error(...args);
|
||||
}
|
||||
console.error(...args);
|
||||
}
|
||||
function getOpenNextErrorLogLevel() {
|
||||
const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1";
|
||||
switch (strLevel.toLowerCase()) {
|
||||
case "debug":
|
||||
case "0":
|
||||
return 0;
|
||||
case "error":
|
||||
case "2":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@opennextjs/cloudflare/dist/cli/templates/images.js
|
||||
async function handleImageRequest(requestURL, requestHeaders, env) {
|
||||
const parseResult = parseImageRequest(requestURL, requestHeaders);
|
||||
if (!parseResult.ok) {
|
||||
return new Response(parseResult.message, {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
let imageResponse;
|
||||
if (parseResult.url.startsWith("/")) {
|
||||
if (env.ASSETS === void 0) {
|
||||
error("env.ASSETS binding is not defined");
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
const absoluteURL = new URL(parseResult.url, requestURL);
|
||||
imageResponse = await env.ASSETS.fetch(absoluteURL);
|
||||
} else {
|
||||
let fetchImageResult;
|
||||
try {
|
||||
fetchImageResult = await fetchWithRedirects(parseResult.url, 7e3, 3);
|
||||
} catch (e) {
|
||||
throw new Error("Failed to fetch image", { cause: e });
|
||||
}
|
||||
if (!fetchImageResult.ok) {
|
||||
if (fetchImageResult.error === "timed_out") {
|
||||
return new Response('"url" parameter is valid but upstream response timed out', {
|
||||
status: 504
|
||||
});
|
||||
}
|
||||
if (fetchImageResult.error === "too_many_redirects") {
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: 508
|
||||
});
|
||||
}
|
||||
throw new Error("Failed to fetch image");
|
||||
}
|
||||
imageResponse = fetchImageResult.response;
|
||||
}
|
||||
if (!imageResponse.ok || imageResponse.body === null) {
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: imageResponse.status
|
||||
});
|
||||
}
|
||||
let immutable = false;
|
||||
if (parseResult.static) {
|
||||
immutable = true;
|
||||
} else {
|
||||
const cacheControlHeader = imageResponse.headers.get("Cache-Control");
|
||||
if (cacheControlHeader !== null) {
|
||||
immutable = cacheControlHeader.includes("immutable");
|
||||
}
|
||||
}
|
||||
const [contentTypeImageStream, imageStream] = imageResponse.body.tee();
|
||||
const imageHeaderBytes = new Uint8Array(32);
|
||||
const contentTypeImageReader = contentTypeImageStream.getReader({
|
||||
mode: "byob"
|
||||
});
|
||||
const readImageHeaderBytesResult = await contentTypeImageReader.readAtLeast(32, imageHeaderBytes);
|
||||
if (readImageHeaderBytesResult.value === void 0) {
|
||||
await imageResponse.body.cancel();
|
||||
return new Response('"url" parameter is valid but upstream response is invalid', {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
const contentType = detectImageContentType(readImageHeaderBytesResult.value);
|
||||
if (contentType === null) {
|
||||
warn(`Failed to detect content type of "${parseResult.url}"`);
|
||||
return new Response('"url" parameter is valid but image type is not allowed', {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
if (contentType === SVG) {
|
||||
if (true) {
|
||||
return new Response('"url" parameter is valid but image type is not allowed', {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
const response2 = createImageResponse(imageStream, contentType, {
|
||||
immutable
|
||||
});
|
||||
return response2;
|
||||
}
|
||||
if (contentType === GIF) {
|
||||
if (env.IMAGES === void 0) {
|
||||
warn("env.IMAGES binding is not defined");
|
||||
const response3 = createImageResponse(imageStream, contentType, {
|
||||
immutable
|
||||
});
|
||||
return response3;
|
||||
}
|
||||
const imageSource = env.IMAGES.input(imageStream);
|
||||
const imageTransformationResult = await imageSource.transform({
|
||||
width: parseResult.width,
|
||||
fit: "scale-down"
|
||||
}).output({
|
||||
quality: parseResult.quality,
|
||||
format: GIF
|
||||
});
|
||||
const outputImageStream = imageTransformationResult.image();
|
||||
const response2 = createImageResponse(outputImageStream, GIF, {
|
||||
immutable
|
||||
});
|
||||
return response2;
|
||||
}
|
||||
if (contentType === AVIF || contentType === WEBP || contentType === JPEG || contentType === PNG) {
|
||||
if (env.IMAGES === void 0) {
|
||||
warn("env.IMAGES binding is not defined");
|
||||
const response3 = createImageResponse(imageStream, contentType, {
|
||||
immutable
|
||||
});
|
||||
return response3;
|
||||
}
|
||||
const outputFormat = parseResult.format ?? contentType;
|
||||
const imageSource = env.IMAGES.input(imageStream);
|
||||
const imageTransformationResult = await imageSource.transform({
|
||||
width: parseResult.width,
|
||||
fit: "scale-down"
|
||||
}).output({
|
||||
quality: parseResult.quality,
|
||||
format: outputFormat
|
||||
});
|
||||
const outputImageStream = imageTransformationResult.image();
|
||||
const response2 = createImageResponse(outputImageStream, outputFormat, {
|
||||
immutable
|
||||
});
|
||||
return response2;
|
||||
}
|
||||
warn(`Image content type ${contentType} not supported`);
|
||||
const response = createImageResponse(imageStream, contentType, {
|
||||
immutable
|
||||
});
|
||||
return response;
|
||||
}
|
||||
async function fetchWithRedirects(url, timeoutMS, maxRedirectCount) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(timeoutMS),
|
||||
redirect: "manual"
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name === "TimeoutError") {
|
||||
const result2 = {
|
||||
ok: false,
|
||||
error: "timed_out"
|
||||
};
|
||||
return result2;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (redirectResponseStatuses.includes(response.status)) {
|
||||
const locationHeader = response.headers.get("Location");
|
||||
if (locationHeader !== null) {
|
||||
if (maxRedirectCount < 1) {
|
||||
const result3 = {
|
||||
ok: false,
|
||||
error: "too_many_redirects"
|
||||
};
|
||||
return result3;
|
||||
}
|
||||
let redirectTarget;
|
||||
if (locationHeader.startsWith("/")) {
|
||||
redirectTarget = new URL(locationHeader, url).href;
|
||||
} else {
|
||||
redirectTarget = locationHeader;
|
||||
}
|
||||
const result2 = await fetchWithRedirects(redirectTarget, timeoutMS, maxRedirectCount - 1);
|
||||
return result2;
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
ok: true,
|
||||
response
|
||||
};
|
||||
return result;
|
||||
}
|
||||
var redirectResponseStatuses = [301, 302, 303, 307, 308];
|
||||
function createImageResponse(image, contentType, imageResponseFlags) {
|
||||
const response = new Response(image, {
|
||||
headers: {
|
||||
Vary: "Accept",
|
||||
"Content-Type": contentType,
|
||||
"Content-Disposition": "attachment",
|
||||
"Content-Security-Policy": "script-src 'none'; frame-src 'none'; sandbox;"
|
||||
}
|
||||
});
|
||||
if (imageResponseFlags.immutable) {
|
||||
response.headers.set("Cache-Control", "public, max-age=315360000, immutable");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
function parseImageRequest(requestURL, requestHeaders) {
|
||||
const formats = define_IMAGES_FORMATS_default;
|
||||
const parsedUrlOrError = validateUrlQueryParameter(requestURL);
|
||||
if (!("url" in parsedUrlOrError)) {
|
||||
return parsedUrlOrError;
|
||||
}
|
||||
const widthOrError = validateWidthQueryParameter(requestURL);
|
||||
if (typeof widthOrError !== "number") {
|
||||
return widthOrError;
|
||||
}
|
||||
const qualityOrError = validateQualityQueryParameter(requestURL);
|
||||
if (typeof qualityOrError !== "number") {
|
||||
return qualityOrError;
|
||||
}
|
||||
const acceptHeader = requestHeaders.get("Accept") ?? "";
|
||||
let format = null;
|
||||
for (const allowedFormat of formats) {
|
||||
if (acceptHeader.includes(allowedFormat)) {
|
||||
format = allowedFormat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
ok: true,
|
||||
url: parsedUrlOrError.url,
|
||||
width: widthOrError,
|
||||
quality: qualityOrError,
|
||||
format,
|
||||
static: parsedUrlOrError.static
|
||||
};
|
||||
return result;
|
||||
}
|
||||
function validateUrlQueryParameter(requestURL) {
|
||||
const urls = requestURL.searchParams.getAll("url");
|
||||
if (urls.length < 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is required'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (urls.length > 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter cannot be an array'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const url = urls[0];
|
||||
if (url.length > 3072) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is too long'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (url.startsWith("//")) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter cannot be a protocol-relative URL (//)'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (url.startsWith("/")) {
|
||||
const staticAsset = url.startsWith(`${__NEXT_BASE_PATH__ || ""}/_next/static/media`);
|
||||
const pathname = getPathnameFromRelativeURL(url);
|
||||
if (/\/_next\/image($|\/)/.test(decodeURIComponent(pathname))) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter cannot be recursive'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (!staticAsset) {
|
||||
if (!hasLocalMatch(define_IMAGES_LOCAL_PATTERNS_default, url)) {
|
||||
const result = { ok: false, message: '"url" parameter is not allowed' };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { url, static: staticAsset };
|
||||
}
|
||||
let parsedURL;
|
||||
try {
|
||||
parsedURL = new URL(url);
|
||||
} catch {
|
||||
const result = { ok: false, message: '"url" parameter is invalid' };
|
||||
return result;
|
||||
}
|
||||
const validProtocols = ["http:", "https:"];
|
||||
if (!validProtocols.includes(parsedURL.protocol)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is invalid'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (!hasRemoteMatch(define_IMAGES_REMOTE_PATTERNS_default, parsedURL)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"url" parameter is not allowed'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
return { url: parsedURL.href, static: false };
|
||||
}
|
||||
function validateWidthQueryParameter(requestURL) {
|
||||
const widthQueryValues = requestURL.searchParams.getAll("w");
|
||||
if (widthQueryValues.length < 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) is required'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (widthQueryValues.length > 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) cannot be an array'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const widthQueryValue = widthQueryValues[0];
|
||||
if (!/^[0-9]+$/.test(widthQueryValue)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) must be an integer greater than 0'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const width = parseInt(widthQueryValue, 10);
|
||||
if (width <= 0 || isNaN(width)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"w" parameter (width) must be an integer greater than 0'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const sizeValid = define_IMAGES_DEVICE_SIZES_default.includes(width) || define_IMAGES_IMAGE_SIZES_default.includes(width);
|
||||
if (!sizeValid) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: `"w" parameter (width) of ${width} is not allowed`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
function validateQualityQueryParameter(requestURL) {
|
||||
const qualityQueryValues = requestURL.searchParams.getAll("q");
|
||||
if (qualityQueryValues.length < 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) is required'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (qualityQueryValues.length > 1) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) cannot be an array'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const qualityQueryValue = qualityQueryValues[0];
|
||||
if (!/^[0-9]+$/.test(qualityQueryValue)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) must be an integer between 1 and 100'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
const quality = parseInt(qualityQueryValue, 10);
|
||||
if (isNaN(quality) || quality < 1 || quality > 100) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: '"q" parameter (quality) must be an integer between 1 and 100'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
if (!define_IMAGES_QUALITIES_default.includes(quality)) {
|
||||
const result = {
|
||||
ok: false,
|
||||
message: `"q" parameter (quality) of ${quality} is not allowed`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
return quality;
|
||||
}
|
||||
function getPathnameFromRelativeURL(relativeURL) {
|
||||
return relativeURL.split("?")[0];
|
||||
}
|
||||
function hasLocalMatch(localPatterns, relativeURL) {
|
||||
const parseRelativeURLResult = parseRelativeURL(relativeURL);
|
||||
for (const localPattern of localPatterns) {
|
||||
const matched = matchLocalPattern(localPattern, parseRelativeURLResult);
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function parseRelativeURL(relativeURL) {
|
||||
if (!relativeURL.includes("?")) {
|
||||
const result2 = {
|
||||
pathname: relativeURL,
|
||||
search: ""
|
||||
};
|
||||
return result2;
|
||||
}
|
||||
const parts = relativeURL.split("?");
|
||||
const pathname = parts[0];
|
||||
const search = "?" + parts.slice(1).join("?");
|
||||
const result = {
|
||||
pathname,
|
||||
search
|
||||
};
|
||||
return result;
|
||||
}
|
||||
function matchLocalPattern(pattern, url) {
|
||||
if (pattern.search !== void 0 && pattern.search !== url.search) {
|
||||
return false;
|
||||
}
|
||||
return new RegExp(pattern.pathname).test(url.pathname);
|
||||
}
|
||||
function hasRemoteMatch(remotePatterns, url) {
|
||||
for (const remotePattern of remotePatterns) {
|
||||
const matched = matchRemotePattern(remotePattern, url);
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function matchRemotePattern(pattern, url) {
|
||||
if (pattern.protocol !== void 0 && pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "")) {
|
||||
return false;
|
||||
}
|
||||
if (pattern.port !== void 0 && pattern.port !== url.port) {
|
||||
return false;
|
||||
}
|
||||
if (pattern.hostname === void 0 || !new RegExp(pattern.hostname).test(url.hostname)) {
|
||||
return false;
|
||||
}
|
||||
if (pattern.search !== void 0 && pattern.search !== url.search) {
|
||||
return false;
|
||||
}
|
||||
return new RegExp(pattern.pathname).test(url.pathname);
|
||||
}
|
||||
var AVIF = "image/avif";
|
||||
var WEBP = "image/webp";
|
||||
var PNG = "image/png";
|
||||
var JPEG = "image/jpeg";
|
||||
var JXL = "image/jxl";
|
||||
var JP2 = "image/jp2";
|
||||
var HEIC = "image/heic";
|
||||
var GIF = "image/gif";
|
||||
var SVG = "image/svg+xml";
|
||||
var ICO = "image/x-icon";
|
||||
var ICNS = "image/x-icns";
|
||||
var TIFF = "image/tiff";
|
||||
var BMP = "image/bmp";
|
||||
function detectImageContentType(buffer) {
|
||||
if ([255, 216, 255].every((b, i) => buffer[i] === b)) {
|
||||
return JPEG;
|
||||
}
|
||||
if ([137, 80, 78, 71, 13, 10, 26, 10].every((b, i) => buffer[i] === b)) {
|
||||
return PNG;
|
||||
}
|
||||
if ([71, 73, 70, 56].every((b, i) => buffer[i] === b)) {
|
||||
return GIF;
|
||||
}
|
||||
if ([82, 73, 70, 70, 0, 0, 0, 0, 87, 69, 66, 80].every((b, i) => !b || buffer[i] === b)) {
|
||||
return WEBP;
|
||||
}
|
||||
if ([60, 63, 120, 109, 108].every((b, i) => buffer[i] === b)) {
|
||||
return SVG;
|
||||
}
|
||||
if ([60, 115, 118, 103].every((b, i) => buffer[i] === b)) {
|
||||
return SVG;
|
||||
}
|
||||
if ([0, 0, 0, 0, 102, 116, 121, 112, 97, 118, 105, 102].every((b, i) => !b || buffer[i] === b)) {
|
||||
return AVIF;
|
||||
}
|
||||
if ([0, 0, 1, 0].every((b, i) => buffer[i] === b)) {
|
||||
return ICO;
|
||||
}
|
||||
if ([105, 99, 110, 115].every((b, i) => buffer[i] === b)) {
|
||||
return ICNS;
|
||||
}
|
||||
if ([73, 73, 42, 0].every((b, i) => buffer[i] === b)) {
|
||||
return TIFF;
|
||||
}
|
||||
if ([66, 77].every((b, i) => buffer[i] === b)) {
|
||||
return BMP;
|
||||
}
|
||||
if ([255, 10].every((b, i) => buffer[i] === b)) {
|
||||
return JXL;
|
||||
}
|
||||
if ([0, 0, 0, 12, 74, 88, 76, 32, 13, 10, 135, 10].every((b, i) => buffer[i] === b)) {
|
||||
return JXL;
|
||||
}
|
||||
if ([0, 0, 0, 0, 102, 116, 121, 112, 104, 101, 105, 99].every((b, i) => !b || buffer[i] === b)) {
|
||||
return HEIC;
|
||||
}
|
||||
if ([0, 0, 0, 12, 106, 80, 32, 32, 13, 10, 135, 10].every((b, i) => buffer[i] === b)) {
|
||||
return JP2;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export {
|
||||
detectImageContentType,
|
||||
handleImageRequest,
|
||||
matchLocalPattern,
|
||||
matchRemotePattern
|
||||
};
|
||||
@ -1,88 +0,0 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import process from "node:process";
|
||||
import stream from "node:stream";
|
||||
import * as nextEnvVars from "./next-env.mjs";
|
||||
const cloudflareContextALS = new AsyncLocalStorage();
|
||||
Object.defineProperty(globalThis, Symbol.for("__cloudflare-context__"), {
|
||||
get() {
|
||||
return cloudflareContextALS.getStore();
|
||||
}
|
||||
});
|
||||
async function runWithCloudflareRequestContext(request, env, ctx, handler) {
|
||||
init(request, env);
|
||||
return cloudflareContextALS.run({ env, ctx, cf: request.cf }, handler);
|
||||
}
|
||||
let initialized = false;
|
||||
function init(request, env) {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
const url = new URL(request.url);
|
||||
initRuntime();
|
||||
populateProcessEnv(url, env);
|
||||
}
|
||||
function initRuntime() {
|
||||
Object.assign(process, { version: process.version || "v22.14.0" });
|
||||
Object.assign(process.versions, { node: "22.14.0", ...process.versions });
|
||||
globalThis.__dirname ??= "";
|
||||
globalThis.__filename ??= "";
|
||||
import.meta.url ??= "file:///worker.js";
|
||||
const __original_fetch = globalThis.fetch;
|
||||
globalThis.fetch = (input, init2) => {
|
||||
if (init2) {
|
||||
delete init2.cache;
|
||||
}
|
||||
return __original_fetch(input, init2);
|
||||
};
|
||||
const CustomRequest = class extends globalThis.Request {
|
||||
constructor(input, init2) {
|
||||
if (init2) {
|
||||
delete init2.cache;
|
||||
Object.defineProperty(init2, "body", {
|
||||
// @ts-ignore
|
||||
value: init2.body instanceof stream.Readable ? ReadableStream.from(init2.body) : init2.body
|
||||
});
|
||||
}
|
||||
super(input, init2);
|
||||
}
|
||||
};
|
||||
Object.assign(globalThis, {
|
||||
Request: CustomRequest,
|
||||
__BUILD_TIMESTAMP_MS__: 1769585780929,
|
||||
__NEXT_BASE_PATH__: "",
|
||||
__ASSETS_RUN_WORKER_FIRST__: false,
|
||||
__TRAILING_SLASH__: false,
|
||||
// The external middleware will use the convertTo function of the `edge` converter
|
||||
// by default it will try to fetch the request, but since we are running everything in the same worker
|
||||
// we need to use the request as is.
|
||||
__dangerous_ON_edge_converter_returns_request: true
|
||||
});
|
||||
}
|
||||
function populateProcessEnv(url, env) {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (typeof value === "string") {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
const mode = env.NEXTJS_ENV ?? "production";
|
||||
if (nextEnvVars[mode]) {
|
||||
for (const key in nextEnvVars[mode]) {
|
||||
process.env[key] ??= nextEnvVars[mode][key];
|
||||
}
|
||||
}
|
||||
process.env.OPEN_NEXT_ORIGIN = JSON.stringify({
|
||||
default: {
|
||||
host: url.hostname,
|
||||
protocol: url.protocol.slice(0, -1),
|
||||
port: url.port
|
||||
}
|
||||
});
|
||||
process.env.__NEXT_PRIVATE_ORIGIN = url.origin;
|
||||
if ("") {
|
||||
process.env.DEPLOYMENT_ID = "";
|
||||
}
|
||||
}
|
||||
export {
|
||||
runWithCloudflareRequestContext
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export const production = {"DATABASE_URL":"file:./prisma/dev.db","WORKOS_CLIENT_ID":"client_01KFPE94C2VGN7KHB59DD4RR52","WORKOS_API_KEY":"sk_test_a2V5XzAxS0ZWU1AyWTFDNkNDS1BSV1lKMERaNkNXLDBmUU5TN0RKYzJXNlFUeUR6Qk96cTY2ZVg","WORKOS_REDIRECT_URI":"http://localhost:3000/api/auth/callback","NEXT_PUBLIC_VAPID_PUBLIC_KEY":"BG7JgPf8BGBj-msDaGWl3osAa8OSbNjUm1ktC_0oNuyqNPzd6XJ91VT40RaPzyC5n91HkGifLLYxSG-Uj5DBh94","VAPID_PRIVATE_KEY":"f6Vd3ECXGOB3FTuNl1y2q77NzjK-Xq5Z0pekcD8qTVY","VAPID_SUBJECT":"mailto:admin@quittraq.com"};
|
||||
export const development = {"DATABASE_URL":"file:./prisma/dev.db","WORKOS_CLIENT_ID":"client_01KFPE94C2VGN7KHB59DD4RR52","WORKOS_API_KEY":"sk_test_a2V5XzAxS0ZWU1AyWTFDNkNDS1BSV1lKMERaNkNXLDBmUU5TN0RKYzJXNlFUeUR6Qk96cTY2ZVg","WORKOS_REDIRECT_URI":"http://localhost:3000/api/auth/callback","NEXT_PUBLIC_VAPID_PUBLIC_KEY":"BG7JgPf8BGBj-msDaGWl3osAa8OSbNjUm1ktC_0oNuyqNPzd6XJ91VT40RaPzyC5n91HkGifLLYxSG-Uj5DBh94","VAPID_PRIVATE_KEY":"f6Vd3ECXGOB3FTuNl1y2q77NzjK-Xq5Z0pekcD8qTVY","VAPID_SUBJECT":"mailto:admin@quittraq.com"};
|
||||
export const test = {"DATABASE_URL":"file:./dev.db"};
|
||||
@ -1,36 +0,0 @@
|
||||
import process from "node:process";
|
||||
const DEPLOYMENT_MAPPING_ENV_NAME = "CF_DEPLOYMENT_MAPPING";
|
||||
const CURRENT_VERSION_ID = "current";
|
||||
let deploymentMapping;
|
||||
function maybeGetSkewProtectionResponse(request) {
|
||||
if (false) {
|
||||
const url = new URL(request.url);
|
||||
if (url.hostname === "localhost" || url.hostname.endsWith(".workers.dev")) {
|
||||
return void 0;
|
||||
}
|
||||
const requestDeploymentId = request.headers.get("x-deployment-id") ?? url.searchParams.get("dpl");
|
||||
if (!requestDeploymentId || requestDeploymentId === process.env.DEPLOYMENT_ID) {
|
||||
return void 0;
|
||||
}
|
||||
deploymentMapping ??= process.env[DEPLOYMENT_MAPPING_ENV_NAME] ? JSON.parse(process.env[DEPLOYMENT_MAPPING_ENV_NAME]) : {};
|
||||
if (!(requestDeploymentId in deploymentMapping)) {
|
||||
return void 0;
|
||||
}
|
||||
const version = deploymentMapping[requestDeploymentId];
|
||||
if (!version || version === CURRENT_VERSION_ID) {
|
||||
return void 0;
|
||||
}
|
||||
const versionDomain = version.split("-")[0];
|
||||
const hostname = `${versionDomain}-${process.env.CF_WORKER_NAME}.${process.env.CF_PREVIEW_DOMAIN}.workers.dev`;
|
||||
url.hostname = hostname;
|
||||
const requestToOlderDeployment = new Request(url, request);
|
||||
const headers = new Headers(request.headers);
|
||||
headers.delete("origin");
|
||||
return fetch(requestToOlderDeployment, { headers });
|
||||
}
|
||||
}
|
||||
export {
|
||||
CURRENT_VERSION_ID,
|
||||
DEPLOYMENT_MAPPING_ENV_NAME,
|
||||
maybeGetSkewProtectionResponse
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
[{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/layout"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_global-error"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/_global-error/layout"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_global-error"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/_global-error/page"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_global-error"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/_global-error"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_global-error"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/layout"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_not-found"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/_not-found/layout"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_not-found"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/_not-found/page"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_not-found"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/_not-found"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/_not-found"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/layout"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/login"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/login/layout"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/login"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/login/page"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/login"},"revalidatedAt":{"N":"1"}},{"tag":{"S":"Bi3XXPy0fxaJxYXDBsshm/_N_T_/login"},"path":{"S":"Bi3XXPy0fxaJxYXDBsshm/login"},"revalidatedAt":{"N":"1"}}]
|
||||
@ -1,31 +0,0 @@
|
||||
import { createRequire as topLevelCreateRequire } from 'module';const require = topLevelCreateRequire(import.meta.url);import bannerUrl from 'url';const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
// open-next.config.ts
|
||||
var config = {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: "cloudflare-node",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
tagCache: "dummy",
|
||||
queue: "dummy"
|
||||
}
|
||||
},
|
||||
edgeExternals: ["node:crypto"],
|
||||
middleware: {
|
||||
external: true,
|
||||
override: {
|
||||
wrapper: "cloudflare-edge",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
tagCache: "dummy",
|
||||
queue: "dummy"
|
||||
}
|
||||
}
|
||||
};
|
||||
var open_next_config_default = config;
|
||||
export {
|
||||
open_next_config_default as default
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { handleImageRequest } from "./cloudflare/images.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
||||
// @ts-expect-error: Will be resolved by wrangler build
|
||||
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
|
||||
//@ts-expect-error: Will be resolved by wrangler build
|
||||
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
const response = maybeGetSkewProtectionResponse(request);
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// Serve images in development.
|
||||
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
||||
if (url.pathname.startsWith("/cdn-cgi/image/")) {
|
||||
const m = url.pathname.match(/\/cdn-cgi\/image\/.+?\/(?<url>.+)$/);
|
||||
if (m === null) {
|
||||
return new Response("Not Found!", { status: 404 });
|
||||
}
|
||||
const imageUrl = m.groups.url;
|
||||
return imageUrl.match(/^https?:\/\//)
|
||||
? fetch(imageUrl, { cf: { cacheEverything: true } })
|
||||
: env.ASSETS?.fetch(new URL(`/${imageUrl}`, url));
|
||||
}
|
||||
// Fallback for the Next default image loader.
|
||||
if (url.pathname ===
|
||||
`${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`) {
|
||||
return await handleImageRequest(url, request.headers, env);
|
||||
}
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, env, ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
// @ts-expect-error: resolved by wrangler build
|
||||
const { handler } = await import("./server-functions/default/handler.mjs");
|
||||
return handler(reqOrResp, env, ctx, request.signal);
|
||||
});
|
||||
},
|
||||
};
|
||||
1
bun.lock
1
bun.lock
@ -28,6 +28,7 @@
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "19.2.3",
|
||||
"recharts": "^3.7.0",
|
||||
"styled-jsx": "^5.1.6",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"web-push": "^3.6.7",
|
||||
},
|
||||
|
||||
2
migrations/0007_add_mood_score.sql
Normal file
2
migrations/0007_add_mood_score.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Migration number: 0007 2024-01-30T23:33:00.000Z
|
||||
ALTER TABLE MoodEntry ADD COLUMN score INTEGER DEFAULT 50;
|
||||
@ -41,7 +41,8 @@
|
||||
"react-dom": "19.2.3",
|
||||
"recharts": "^3.7.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"web-push": "^3.6.7"
|
||||
"web-push": "^3.6.7",
|
||||
"styled-jsx": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250121.0",
|
||||
|
||||
@ -93,7 +93,8 @@ model SavingsConfig {
|
||||
model MoodEntry {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
mood String // "good", "neutral", "bad"
|
||||
mood String // "good", "neutral", "bad" (kept for backward compat or categorical)
|
||||
score Int @default(50) // 0-100
|
||||
date String // YYYY-MM-DD
|
||||
comment String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@ -3,27 +3,76 @@ import { getSession } from '@/lib/session';
|
||||
import { getMoodEntriesD1, saveMoodEntryD1 } from '@/lib/d1';
|
||||
import { getTodayString } from '@/lib/date-utils';
|
||||
|
||||
const AFFIRMATIONS = {
|
||||
const AFFIRMATIONS: Record<string, string[]> = {
|
||||
amazing: [
|
||||
"Incredible! Use this energy to stay smoke-free!",
|
||||
"You're on fire! Keep living your best life.",
|
||||
"So happy for you! This is what freedom feels like.",
|
||||
"Outstanding! Bottle this feeling for later.",
|
||||
"You're unstoppable today! Keep it up.",
|
||||
"Radiating positivity! Your journey is inspiring.",
|
||||
"Top of the world! Enjoy every moment of this success.",
|
||||
"Absolutely brilliant! You're glowing with health.",
|
||||
"Peak performance! This is the real you shining through.",
|
||||
"Marvelous! Your commitment is paying off in spades.",
|
||||
"What a feeling! You're crushing your goals.",
|
||||
"Sky high! nothing can bring you down today."
|
||||
],
|
||||
good: [
|
||||
"That's wonderful! Keep riding this positive wave.",
|
||||
"Your strength is inspiring. Keep going!",
|
||||
"Happiness is a great companion on this journey.",
|
||||
"So glad you're feeling good! You've got this.",
|
||||
"Keep that momentum! You're doing amazing."
|
||||
"Keep that momentum! You're doing amazing.",
|
||||
"Great vibes! A healthy life looks good on you.",
|
||||
"Feeling good is just the beginning. It gets better.",
|
||||
"Solid progress! You're building a beautiful future.",
|
||||
"Nice! Enjoy this clarity and energy.",
|
||||
"You're doing it! Every good day is a victory.",
|
||||
"Beautiful! Your body thanks you for your choices.",
|
||||
"Keep smiling! You're on the right path."
|
||||
],
|
||||
neutral: [
|
||||
"Steady as she goes. Every day is progress.",
|
||||
"It's okay to just 'be' sometimes. Stay the course.",
|
||||
"Focus on your 'why' today. You're doing great.",
|
||||
"One step at a time. You're still moving forward.",
|
||||
"Balance is key. Keep your goals in sight."
|
||||
"Balance is key. Keep your goals in sight.",
|
||||
"Calm waters run deep. You're staying strong.",
|
||||
"Consistency is your superpower. Keep showing up.",
|
||||
"A quiet day is a good day. Stay committed.",
|
||||
"Just keep swimming. You're doing just fine.",
|
||||
"Neutral ground is safe ground. protecting your progress.",
|
||||
"Peaceful and present. That's a win in itself.",
|
||||
"Center yourself. You are in control."
|
||||
],
|
||||
bad: [
|
||||
"I'm sorry things are tough. This feeling is temporary.",
|
||||
"Be kind to yourself today. You're still stronger than you think.",
|
||||
"Tough times don't last, but tough people do. Hang in there.",
|
||||
"Take a deep breath. Tomorrow is a fresh start.",
|
||||
"It's okay to struggle. What matters is that you keep trying."
|
||||
"It's okay to struggle. What matters is that you keep trying.",
|
||||
"Storms run out of rain. This too shall pass.",
|
||||
"Your journey is worth it, even on hard days.",
|
||||
"Don't lose hope. You've come so far already.",
|
||||
"Courage is keeping on when it's hard. You valid.",
|
||||
"Treat yourself with extra care today. You deserve it.",
|
||||
"One breath at a time. You can get through this.",
|
||||
"Sending you strength. You are not alone."
|
||||
],
|
||||
terrible: [
|
||||
"It's okay to not be okay. Just breathe.",
|
||||
"This moment will pass. You are stronger than this usage.",
|
||||
"Reaching out for support is a sign of strength.",
|
||||
"Be gentle with yourself. You're fighting a hard battle.",
|
||||
"Don't give up. The sun will rise again.",
|
||||
"You are resilient. You can weather this storm.",
|
||||
"Hold on. Better days are coming.",
|
||||
"Your worth is not defined by this moment.",
|
||||
"Scream if you need to, but don't give up.",
|
||||
"It's darkest before dawn. Keep fighting.",
|
||||
"You are loved and you are capable. Stay with us.",
|
||||
"This pain is temporary, your freedom is forever."
|
||||
]
|
||||
};
|
||||
|
||||
@ -49,23 +98,34 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json() as { mood: 'good' | 'neutral' | 'bad'; comment?: string };
|
||||
const { mood, comment } = body;
|
||||
const body = await request.json() as { mood: 'amazing' | 'good' | 'neutral' | 'bad' | 'terrible'; score: number; comment?: string; date?: string };
|
||||
const { mood, score, comment, date } = body;
|
||||
|
||||
if (!mood || !['good', 'neutral', 'bad'].includes(mood)) {
|
||||
if (!mood) {
|
||||
return NextResponse.json({ error: 'Invalid mood' }, { status: 400 });
|
||||
}
|
||||
|
||||
const today = getTodayString();
|
||||
const entry = await saveMoodEntryD1(session.user.id, mood, today, comment);
|
||||
// Validate score is between 0 and 100
|
||||
const finalScore = Math.max(0, Math.min(100, score ?? 50));
|
||||
|
||||
const entryDate = date || getTodayString();
|
||||
const entry = await saveMoodEntryD1(session.user.id, mood, finalScore, entryDate, comment);
|
||||
|
||||
if (!entry) {
|
||||
return NextResponse.json({ error: 'Failed to save mood entry' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Pick a random affirmation
|
||||
const moodAffirmations = AFFIRMATIONS[mood];
|
||||
const affirmation = moodAffirmations[Math.floor(Math.random() * moodAffirmations.length)];
|
||||
let affirmation = "You're doing great! Keep it up.";
|
||||
try {
|
||||
const moodAffirmations = AFFIRMATIONS[mood] || AFFIRMATIONS['neutral'];
|
||||
if (moodAffirmations && moodAffirmations.length > 0) {
|
||||
affirmation = moodAffirmations[Math.floor(Math.random() * moodAffirmations.length)];
|
||||
}
|
||||
} catch (affError) {
|
||||
console.error('Error selecting affirmation:', affError);
|
||||
// Fallback is already set
|
||||
}
|
||||
|
||||
return NextResponse.json({ entry, affirmation });
|
||||
} catch (error) {
|
||||
|
||||
@ -14,70 +14,108 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md bg-card/80 backdrop-blur-sm border-white/10">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-3xl font-bold">QuitTraq</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Track your journey to a smoke-free life
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-12 text-base"
|
||||
onClick={() => handleLogin('GoogleOAuth')}
|
||||
<div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900 overflow-hidden relative">
|
||||
{/* Background Orbs */}
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/10 rounded-full blur-[120px] pointer-events-none" />
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-purple-500/10 rounded-full blur-[120px] pointer-events-none" />
|
||||
|
||||
<div className="w-full max-w-5xl grid lg:grid-cols-2 gap-8 items-center relative z-10">
|
||||
{/* Left Side: Video Demo */}
|
||||
<div className="hidden lg:block relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-purple-500/20 rounded-[2.5rem] blur-2xl group-hover:blur-3xl transition-all duration-500 opacity-50" />
|
||||
<div className="relative aspect-[9/16] max-h-[80vh] mx-auto rounded-[2rem] overflow-hidden border border-white/10 shadow-2xl shadow-black/20">
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="w-full h-full object-cover"
|
||||
>
|
||||
<svg className="mr-2 h-5 w-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-12 text-base"
|
||||
onClick={() => handleLogin('AppleOAuth')}
|
||||
>
|
||||
<svg className="mr-2 h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||
</svg>
|
||||
Continue with Apple
|
||||
</Button>
|
||||
<source src="/demo-video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none" />
|
||||
<div className="absolute bottom-8 left-8 right-8 text-white">
|
||||
<p className="text-sm font-bold uppercase tracking-widest opacity-70 mb-2">Live Preview</p>
|
||||
<h3 className="text-xl font-bold">Experience the journey</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="stayLoggedIn"
|
||||
checked={stayLoggedIn}
|
||||
onCheckedChange={(checked) => setStayLoggedIn(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="stayLoggedIn" className="text-sm cursor-pointer">
|
||||
Keep me logged in on this device
|
||||
</Label>
|
||||
</div>
|
||||
{/* Right Side: Login Form */}
|
||||
<div className="w-full max-w-md mx-auto">
|
||||
<Card className="bg-white/70 dark:bg-slate-900/70 backdrop-blur-xl border-white/20 dark:border-white/10 shadow-2xl rounded-[2rem] overflow-hidden">
|
||||
<CardHeader className="text-center pt-8 pb-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-primary to-purple-600 rounded-2xl mx-auto mb-6 flex items-center justify-center shadow-lg shadow-primary/20 rotate-3 group-hover:rotate-0 transition-transform">
|
||||
<span className="text-2xl font-black text-white">Q</span>
|
||||
</div>
|
||||
<CardTitle className="text-4xl font-black tracking-tight bg-gradient-to-br from-slate-900 to-slate-700 dark:from-white dark:to-slate-400 bg-clip-text text-transparent">
|
||||
QuitTraq
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base font-medium mt-2">
|
||||
Your companion to a smoke-free life
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 pb-8">
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-14 text-base font-bold rounded-xl border-slate-200 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:scale-[1.02] transition-all shadow-sm"
|
||||
onClick={() => handleLogin('GoogleOAuth')}
|
||||
>
|
||||
<svg className="mr-3 h-5 w-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
By continuing, you agree to our Terms of Service and Privacy Policy
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-14 text-base font-bold rounded-xl border-slate-200 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:scale-[1.02] transition-all shadow-sm"
|
||||
onClick={() => handleLogin('AppleOAuth')}
|
||||
>
|
||||
<svg className="mr-3 h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||
</svg>
|
||||
Continue with Apple
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl border border-slate-100 dark:border-slate-700/50">
|
||||
<Checkbox
|
||||
id="stayLoggedIn"
|
||||
checked={stayLoggedIn}
|
||||
onCheckedChange={(checked) => setStayLoggedIn(checked === true)}
|
||||
className="w-5 h-5 rounded-md border-slate-300 dark:border-slate-700"
|
||||
/>
|
||||
<Label htmlFor="stayLoggedIn" className="text-sm font-medium cursor-pointer select-none opacity-80">
|
||||
Keep me logged in on this device
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<p className="text-center text-[10px] uppercase font-bold tracking-widest text-slate-400 dark:text-slate-500 leading-relaxed max-w-[200px] mx-auto">
|
||||
By continuing, you agree to our Terms & Privacy Policy
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,16 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Smile, Meh, Frown, TrendingUp, ChevronLeft, ChevronRight, MessageSquare, Quote, Sparkles } from 'lucide-react';
|
||||
import { Smile, Meh, Frown, TrendingUp, ChevronLeft, ChevronRight, Quote, Sparkles, Heart, AlertCircle } from 'lucide-react';
|
||||
import { MoodEntry, fetchMoodEntries, saveMoodEntry } from '@/lib/storage';
|
||||
import { ResponsiveContainer, BarChart, Bar, XAxis, Tooltip, Cell } from 'recharts';
|
||||
import { format, subDays, startOfWeek, endOfWeek, eachDayOfInterval } from 'date-fns';
|
||||
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Cell } from 'recharts';
|
||||
import { format, subDays, startOfWeek, endOfWeek, eachDayOfInterval, isSameDay } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useTheme } from '@/lib/theme-context';
|
||||
|
||||
type MoodType = 'amazing' | 'good' | 'neutral' | 'terrible';
|
||||
|
||||
const moodConfig = [
|
||||
{ id: 'amazing', icon: Heart, label: 'Amazing!', color: 'fuchsia', score: 100 },
|
||||
{ id: 'good', icon: Smile, label: 'Good', color: 'emerald', score: 75 },
|
||||
{ id: 'neutral', icon: Meh, label: 'Okay', color: 'amber', score: 50 },
|
||||
{ id: 'terrible', icon: AlertCircle, label: 'Terrible', color: 'red', score: 0 }
|
||||
];
|
||||
|
||||
function MoodTrackerComponent() {
|
||||
const { theme } = useTheme();
|
||||
const [entries, setEntries] = useState<MoodEntry[]>([]);
|
||||
@ -18,7 +26,7 @@ function MoodTrackerComponent() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [affirmation, setAffirmation] = useState<string | null>(null);
|
||||
const [weekOffset, setWeekOffset] = useState(0);
|
||||
const [activeMood, setActiveMood] = useState<'good' | 'neutral' | 'bad' | null>(null);
|
||||
const [activeMood, setActiveMood] = useState<MoodType | null>(null);
|
||||
const [currentTimeout, setCurrentTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -27,18 +35,25 @@ function MoodTrackerComponent() {
|
||||
setEntries(data);
|
||||
if (data.length > 0) {
|
||||
// If the most recent entry is today, set it as active
|
||||
const today = format(new Date(), 'yyyy-MM-dd');
|
||||
const lastEntry = data[0];
|
||||
if (lastEntry.date === today) {
|
||||
setActiveMood(lastEntry.mood as any);
|
||||
}
|
||||
// REMOVED: Auto-selection of mood on load per user request
|
||||
// const today = format(new Date(), 'yyyy-MM-dd');
|
||||
// const lastEntry = data[0];
|
||||
// if (lastEntry.date === today) {
|
||||
// if (['amazing', 'good', 'neutral', 'terrible'].includes(lastEntry.mood)) {
|
||||
// setActiveMood(lastEntry.mood as MoodType);
|
||||
// } else if (lastEntry.mood === 'bad') {
|
||||
// // Map legacy 'bad' to 'terrible' or 'neutral' for UI state?
|
||||
// // Let's map legacy 'bad' to 'terrible' for now or just generic fallback
|
||||
// setActiveMood('terrible');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
loadMoods();
|
||||
}, []);
|
||||
|
||||
const handleMoodSelect = async (mood: 'good' | 'neutral' | 'bad') => {
|
||||
const handleMoodSelect = useCallback(async (mood: MoodType) => {
|
||||
setIsSaving(true);
|
||||
setAffirmation(null);
|
||||
setActiveMood(mood);
|
||||
@ -48,14 +63,32 @@ function MoodTrackerComponent() {
|
||||
setCurrentTimeout(null);
|
||||
}
|
||||
|
||||
let score = 50;
|
||||
if (mood === 'amazing') score = 100;
|
||||
else if (mood === 'good') score = 75;
|
||||
else if (mood === 'neutral') score = 50;
|
||||
else if (mood === 'terrible') score = 0;
|
||||
|
||||
// Optimistic Update
|
||||
const tempId = `temp-${Date.now()}`;
|
||||
const todayStr = format(new Date(), 'yyyy-MM-dd');
|
||||
const tempEntry: MoodEntry = {
|
||||
id: tempId,
|
||||
userId: 'current',
|
||||
mood,
|
||||
score,
|
||||
date: todayStr,
|
||||
comment: null,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
setEntries(prev => [tempEntry, ...prev]);
|
||||
|
||||
try {
|
||||
const result = await saveMoodEntry(mood);
|
||||
const result = await saveMoodEntry(mood, score, undefined, todayStr);
|
||||
if (result) {
|
||||
setEntries(prev => {
|
||||
// Remove existing entry for today if it exists to avoid duplicates in state
|
||||
const filtered = prev.filter(e => e.date !== result.entry.date);
|
||||
return [result.entry, ...filtered];
|
||||
});
|
||||
// Replace temp entry with real one
|
||||
setEntries(prev => prev.map(e => e.id === tempId ? result.entry : e));
|
||||
setAffirmation(result.affirmation);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
@ -63,11 +96,17 @@ function MoodTrackerComponent() {
|
||||
setCurrentTimeout(null);
|
||||
}, 8000);
|
||||
setCurrentTimeout(timeout);
|
||||
} else {
|
||||
// Revert if failed
|
||||
setEntries(prev => prev.filter(e => e.id !== tempId));
|
||||
}
|
||||
} catch (error) {
|
||||
// Revert if error
|
||||
setEntries(prev => prev.filter(e => e.id !== tempId));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
}, [currentTimeout]);
|
||||
|
||||
const weeklyData = useMemo(() => {
|
||||
const today = new Date();
|
||||
@ -77,23 +116,39 @@ function MoodTrackerComponent() {
|
||||
|
||||
return days.map(day => {
|
||||
const dateStr = format(day, 'yyyy-MM-dd');
|
||||
// Find the *latest* entry for this day
|
||||
const dayEntry = entries.find(e => e.date === dateStr);
|
||||
// Find ALL entries for this day
|
||||
const dayEntries = entries.filter(e => e.date === dateStr);
|
||||
|
||||
// Map mood to numeric values: bad=1, neutral=2, good=3
|
||||
let value = 0;
|
||||
if (dayEntry) {
|
||||
if (dayEntry.mood === 'good') value = 3;
|
||||
else if (dayEntry.mood === 'neutral') value = 2;
|
||||
else if (dayEntry.mood === 'bad') value = 1;
|
||||
let averageScore = 0;
|
||||
let hasData = false;
|
||||
|
||||
if (dayEntries.length > 0) {
|
||||
hasData = true;
|
||||
const totalScore = dayEntries.reduce((sum, entry) => {
|
||||
// Use entry.score if available, otherwise fallback to mapping mood
|
||||
let s = entry.score;
|
||||
if (typeof s !== 'number') {
|
||||
if (entry.mood === 'amazing') s = 100;
|
||||
else if (entry.mood === 'good') s = 75;
|
||||
else if (entry.mood === 'neutral') s = 50;
|
||||
else if (entry.mood === 'bad') s = 25; // Legacy bad
|
||||
else if (entry.mood === 'terrible') s = 0;
|
||||
else s = 50;
|
||||
}
|
||||
return sum + s;
|
||||
}, 0);
|
||||
averageScore = totalScore / dayEntries.length;
|
||||
}
|
||||
|
||||
return {
|
||||
name: format(day, 'EEE'),
|
||||
fullDate: dateStr,
|
||||
value: value === 0 ? 0.2 : value, // 0.2 provides a small placeholder bar
|
||||
isPlaceholder: value === 0,
|
||||
mood: dayEntry?.mood
|
||||
// Make sure even 0 score has significant visual height (e.g. 15% of height)
|
||||
// But keep originalScore accurate for tooltip colors
|
||||
value: hasData ? Math.max(15, averageScore) : 2,
|
||||
originalScore: hasData ? averageScore : 0,
|
||||
isPlaceholder: !hasData,
|
||||
count: dayEntries.length
|
||||
};
|
||||
});
|
||||
}, [entries, weekOffset]);
|
||||
@ -106,37 +161,48 @@ function MoodTrackerComponent() {
|
||||
return `${format(start, 'MMM d')} - ${format(end, 'MMM d')}`;
|
||||
}, [weekOffset]);
|
||||
|
||||
const getGradient = () => {
|
||||
const weeklyAverage = useMemo(() => {
|
||||
const daysWithData = weeklyData.filter(d => !d.isPlaceholder && d.count > 0);
|
||||
if (daysWithData.length === 0) return null;
|
||||
const totalScore = daysWithData.reduce((sum, d) => sum + d.originalScore, 0);
|
||||
return Math.round(totalScore / daysWithData.length);
|
||||
}, [weeklyData]);
|
||||
|
||||
const gradientClass = useMemo(() => {
|
||||
if (theme === 'light') {
|
||||
switch (activeMood) {
|
||||
case 'amazing':
|
||||
return 'from-fuchsia-100 via-pink-50 to-fuchsia-100 border-fuchsia-200 shadow-fuchsia-500/5';
|
||||
case 'good':
|
||||
return 'from-emerald-100 via-teal-50 to-emerald-100 border-emerald-200 shadow-emerald-500/5';
|
||||
case 'neutral':
|
||||
return 'from-amber-100 via-orange-50 to-amber-100 border-amber-200 shadow-amber-500/5';
|
||||
case 'bad':
|
||||
return 'from-rose-100 via-red-50 to-rose-100 border-rose-200 shadow-rose-500/5';
|
||||
case 'terrible':
|
||||
return 'from-slate-200 via-gray-100 to-slate-200 border-slate-300 shadow-slate-500/5';
|
||||
default:
|
||||
return 'from-indigo-50 via-white to-indigo-50 border-indigo-100 shadow-indigo-500/5';
|
||||
}
|
||||
}
|
||||
|
||||
switch (activeMood) {
|
||||
case 'amazing':
|
||||
return 'from-fuchsia-500/10 via-pink-500/5 to-fuchsia-500/10 border-fuchsia-500/20 shadow-fuchsia-500/10';
|
||||
case 'good':
|
||||
return 'from-emerald-500/10 via-teal-500/5 to-emerald-500/10 border-emerald-500/20 shadow-emerald-500/10';
|
||||
case 'neutral':
|
||||
return 'from-amber-500/10 via-orange-500/5 to-amber-500/10 border-amber-500/20 shadow-amber-500/10';
|
||||
case 'bad':
|
||||
return 'from-rose-500/10 via-red-500/5 to-rose-500/10 border-rose-500/20 shadow-rose-500/10';
|
||||
case 'terrible':
|
||||
return 'from-red-900/20 via-red-900/10 to-red-900/20 border-red-500/20 shadow-red-500/10';
|
||||
default:
|
||||
return 'from-violet-500/10 via-indigo-500/5 to-violet-500/10 border-white/10 shadow-indigo-500/5';
|
||||
}
|
||||
};
|
||||
}, [activeMood, theme]);
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
"overflow-hidden transition-all duration-700 backdrop-blur-xl border shadow-xl",
|
||||
"overflow-hidden transition-all duration-700 ease-in-out backdrop-blur-xl border shadow-xl",
|
||||
"bg-gradient-to-br",
|
||||
getGradient()
|
||||
gradientClass
|
||||
)}>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -145,10 +211,11 @@ function MoodTrackerComponent() {
|
||||
theme === 'light' ? "text-slate-700" : "text-white/90"
|
||||
)}>
|
||||
<div className={cn("p-1.5 rounded-lg transition-colors duration-500",
|
||||
activeMood === 'good' ? (theme === 'light' ? "bg-emerald-100 text-emerald-600" : "bg-emerald-500/20 text-emerald-400") :
|
||||
activeMood === 'neutral' ? (theme === 'light' ? "bg-amber-100 text-amber-600" : "bg-amber-500/20 text-amber-400") :
|
||||
activeMood === 'bad' ? (theme === 'light' ? "bg-rose-100 text-rose-600" : "bg-rose-500/20 text-rose-400") :
|
||||
(theme === 'light' ? "bg-indigo-100 text-indigo-500" : "bg-white/10 text-white/70")
|
||||
activeMood === 'amazing' ? (theme === 'light' ? "bg-fuchsia-100 text-fuchsia-600" : "bg-fuchsia-500/20 text-fuchsia-400") :
|
||||
activeMood === 'good' ? (theme === 'light' ? "bg-emerald-100 text-emerald-600" : "bg-emerald-500/20 text-emerald-400") :
|
||||
activeMood === 'neutral' ? (theme === 'light' ? "bg-amber-100 text-amber-600" : "bg-amber-500/20 text-amber-400") :
|
||||
activeMood === 'terrible' ? (theme === 'light' ? "bg-red-100 text-red-600" : "bg-red-500/20 text-red-400") :
|
||||
(theme === 'light' ? "bg-indigo-100 text-indigo-500" : "bg-white/10 text-white/70")
|
||||
)}>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</div>
|
||||
@ -196,16 +263,31 @@ function MoodTrackerComponent() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{weeklyAverage !== null && (
|
||||
<div className={cn(
|
||||
"mt-1 flex justify-end",
|
||||
theme === 'light' ? "text-slate-500" : "text-white/60"
|
||||
)}>
|
||||
<span className="text-[10px] font-medium uppercase tracking-wider flex items-center gap-1.5">
|
||||
Weekly Score:
|
||||
<span className={cn(
|
||||
"font-bold text-xs px-1.5 py-0.5 rounded-md",
|
||||
weeklyAverage >= 85 ? (theme === 'light' ? "bg-fuchsia-100 text-fuchsia-600" : "bg-fuchsia-500/20 text-fuchsia-300") :
|
||||
weeklyAverage >= 65 ? (theme === 'light' ? "bg-emerald-100 text-emerald-600" : "bg-emerald-500/20 text-emerald-300") :
|
||||
weeklyAverage >= 35 ? (theme === 'light' ? "bg-amber-100 text-amber-600" : "bg-amber-500/20 text-amber-300") :
|
||||
(theme === 'light' ? "bg-red-100 text-red-600" : "bg-red-500/20 text-red-300")
|
||||
)}>
|
||||
{weeklyAverage}%
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-2 space-y-4">
|
||||
{/* Mood Selection */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ id: 'good', icon: Smile, label: 'Good', color: 'emerald' },
|
||||
{ id: 'neutral', icon: Meh, label: 'Okay', color: 'amber' },
|
||||
{ id: 'bad', icon: Frown, label: 'Bad', color: 'rose' }
|
||||
].map((item) => {
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{moodConfig.map((item) => {
|
||||
const isSelected = activeMood === item.id;
|
||||
const Icon = item.icon;
|
||||
|
||||
@ -215,7 +297,7 @@ function MoodTrackerComponent() {
|
||||
onClick={() => handleMoodSelect(item.id as any)}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
"group relative flex flex-col items-center justify-center gap-2 p-3 rounded-2xl transition-all duration-300",
|
||||
"group relative flex flex-col items-center justify-center gap-2 p-2 rounded-2xl transition-all duration-300 hover:scale-105 active:scale-95",
|
||||
"border",
|
||||
isSelected
|
||||
? (theme === 'light'
|
||||
@ -227,17 +309,17 @@ function MoodTrackerComponent() {
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-2 rounded-xl transition-all duration-300",
|
||||
"p-1.5 rounded-xl transition-all duration-300",
|
||||
isSelected
|
||||
? `bg-${item.color}-500 text-white shadow-lg scale-110`
|
||||
: (theme === 'light'
|
||||
? `bg-slate-100 text-slate-400 group-hover:text-${item.color}-500 group-hover:scale-110`
|
||||
: `bg-white/5 text-white/60 group-hover:text-${item.color}-400 group-hover:scale-110`)
|
||||
)}>
|
||||
<Icon className={cn("w-5 h-5", isSelected && "animate-pulse-subtle")} />
|
||||
<Icon className={cn("w-4 h-4", isSelected && "animate-pulse-subtle")} />
|
||||
</div>
|
||||
<span className={cn(
|
||||
"text-[10px] font-semibold tracking-wide uppercase transition-colors",
|
||||
"text-[9px] font-semibold tracking-wide uppercase transition-colors truncate w-full text-center",
|
||||
isSelected
|
||||
? (theme === 'light' ? `text-${item.color}-700` : "text-white")
|
||||
: (theme === 'light' ? "text-slate-400 group-hover:text-slate-600" : "text-white/40 group-hover:text-white/80")
|
||||
@ -282,7 +364,7 @@ function MoodTrackerComponent() {
|
||||
theme === 'light' ? "text-slate-400" : "text-white/40"
|
||||
)}>
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
<span>Mood Tracking</span>
|
||||
<span>Daily Average Mood</span>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
@ -308,6 +390,15 @@ function MoodTrackerComponent() {
|
||||
const data = payload[0].payload;
|
||||
if (data.isPlaceholder) return null;
|
||||
|
||||
const score = Math.round(data.originalScore);
|
||||
let moodLabel = 'Neutral';
|
||||
let colorClass = theme === 'light' ? "text-amber-600" : "text-amber-400";
|
||||
|
||||
if (score >= 85) { moodLabel = 'Amazing'; colorClass = theme === 'light' ? "text-fuchsia-600" : "text-fuchsia-400"; }
|
||||
else if (score >= 65) { moodLabel = 'Good'; colorClass = theme === 'light' ? "text-emerald-600" : "text-emerald-400"; }
|
||||
else if (score >= 35) { moodLabel = 'Okay'; colorClass = theme === 'light' ? "text-amber-600" : "text-amber-400"; }
|
||||
else { moodLabel = 'Terrible'; colorClass = theme === 'light' ? "text-red-600" : "text-red-400"; }
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"p-2 rounded-lg shadow-xl backdrop-blur-md border",
|
||||
@ -316,13 +407,13 @@ function MoodTrackerComponent() {
|
||||
: "bg-slate-900/90 border-white/10 text-white"
|
||||
)}>
|
||||
<p className={cn("text-xs font-medium mb-1", theme === 'light' ? "text-slate-600" : "text-white")}>{data.fullDate}</p>
|
||||
<p className={cn(
|
||||
"text-xs font-bold capitalize",
|
||||
data.mood === 'good' ? (theme === 'light' ? "text-emerald-600" : "text-emerald-400") :
|
||||
data.mood === 'neutral' ? (theme === 'light' ? "text-amber-600" : "text-amber-400") :
|
||||
(theme === 'light' ? "text-rose-600" : "text-rose-400")
|
||||
)}>
|
||||
{data.mood}
|
||||
<div className="flex items-center gap-2">
|
||||
<p className={cn("text-xs font-bold", colorClass)}>
|
||||
{moodLabel} ({score}%)
|
||||
</p>
|
||||
</div>
|
||||
<p className={cn("text-[10px] mt-1 opacity-70", theme === 'light' ? "text-slate-500" : "text-white/60")}>
|
||||
{data.count} {data.count === 1 ? 'entry' : 'entries'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@ -336,22 +427,23 @@ function MoodTrackerComponent() {
|
||||
tickLine={false}
|
||||
tick={{ fill: theme === 'light' ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.4)', fontSize: 10, dy: 10 }}
|
||||
/>
|
||||
<YAxis hide domain={[0, 100]} />
|
||||
<Bar dataKey="value" radius={[4, 4, 4, 4]}>
|
||||
{weeklyData.map((entry, index) => {
|
||||
const score = entry.originalScore;
|
||||
let fillColor;
|
||||
if (entry.isPlaceholder) {
|
||||
fillColor = theme === 'light' ? 'rgba(0,0,0,0.05)' : 'rgba(255,255,255,0.05)';
|
||||
} else if (entry.mood === 'good') {
|
||||
fillColor = theme === 'light' ? '#10b981' : '#34d399'; // emerald-500 : emerald-400
|
||||
} else if (entry.mood === 'neutral') {
|
||||
fillColor = theme === 'light' ? '#f59e0b' : '#fbbf24'; // amber-500 : amber-400
|
||||
} else {
|
||||
fillColor = theme === 'light' ? '#f43f5e' : '#fb7185'; // rose-500 : rose-400
|
||||
if (score >= 85) fillColor = theme === 'light' ? '#d946ef' : '#e879f9'; // Amazing (100) -> Purple
|
||||
else if (score >= 60) fillColor = theme === 'light' ? '#10b981' : '#34d399'; // Good (75) -> Green
|
||||
else if (score >= 50) fillColor = theme === 'light' ? '#f59e0b' : '#fbbf24'; // Neutral (50) -> Yellow
|
||||
else fillColor = theme === 'light' ? '#ef4444' : '#f87171'; // Terrible (0) -> Red
|
||||
}
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
key={`cell-${index}-${fillColor}-${score}`}
|
||||
fill={fillColor}
|
||||
className="transition-all duration-300 hover:opacity-80"
|
||||
/>
|
||||
|
||||
@ -433,6 +433,7 @@ export interface MoodEntryRow {
|
||||
id: string;
|
||||
userId: string;
|
||||
mood: string;
|
||||
score: number;
|
||||
date: string;
|
||||
comment: string | null;
|
||||
createdAt: string;
|
||||
@ -453,6 +454,7 @@ export async function getMoodEntriesD1(userId: string, limit: number = 50): Prom
|
||||
export async function saveMoodEntryD1(
|
||||
userId: string,
|
||||
mood: string,
|
||||
score: number,
|
||||
date: string,
|
||||
comment?: string | null
|
||||
): Promise<MoodEntryRow | null> {
|
||||
@ -464,14 +466,15 @@ export async function saveMoodEntryD1(
|
||||
|
||||
// Mood tracking is flexible, multiple entries per day are allowed
|
||||
await db.prepare(
|
||||
`INSERT INTO MoodEntry (id, userId, mood, date, comment, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
).bind(id, userId, mood, date, comment ?? null, now, now).run();
|
||||
`INSERT INTO MoodEntry (id, userId, mood, score, date, comment, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).bind(id, userId, mood, score, date, comment ?? null, now, now).run();
|
||||
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
mood,
|
||||
score,
|
||||
date,
|
||||
comment: comment ?? null,
|
||||
createdAt: now,
|
||||
|
||||
@ -72,7 +72,8 @@ export interface HealthMilestone {
|
||||
export interface MoodEntry {
|
||||
id: string;
|
||||
userId: string;
|
||||
mood: 'good' | 'neutral' | 'bad';
|
||||
mood: 'good' | 'neutral' | 'bad' | string;
|
||||
score: number;
|
||||
date: string;
|
||||
comment: string | null;
|
||||
createdAt: string;
|
||||
@ -369,12 +370,12 @@ export async function fetchMoodEntries(): Promise<MoodEntry[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveMoodEntry(mood: 'good' | 'neutral' | 'bad', comment?: string): Promise<{ entry: MoodEntry; affirmation: string } | null> {
|
||||
export async function saveMoodEntry(mood: 'good' | 'neutral' | 'bad' | string, score: number, comment?: string, date?: string): Promise<{ entry: MoodEntry; affirmation: string } | null> {
|
||||
try {
|
||||
const response = await fetch('/api/mood', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mood, comment }),
|
||||
body: JSON.stringify({ mood, score, comment, date }),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json() as { entry: MoodEntry; affirmation: string };
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user