@adonisjs/inertia
Version:
Official Inertia.js adapter for AdonisJS
413 lines (407 loc) • 12.9 kB
JavaScript
// src/server_renderer.ts
import { pathToFileURL } from "url";
var ServerRenderer = class _ServerRenderer {
constructor(config, vite) {
this.config = config;
this.vite = vite;
}
static runtime;
/**
* Render the page on the server
*
* On development, we use the Vite Runtime API
* On production, we just import and use the SSR bundle generated by Vite
*/
async render(pageObject) {
let render;
const devServer = this.vite?.getDevServer();
if (devServer) {
_ServerRenderer.runtime ??= await this.vite.createModuleRunner();
_ServerRenderer.runtime.clearCache();
render = await _ServerRenderer.runtime.import(this.config.ssr.entrypoint);
} else {
render = await import(pathToFileURL(this.config.ssr.bundle).href);
}
const result = await render.default(pageObject);
return { head: result.head, body: result.body };
}
};
// src/props.ts
var ignoreFirstLoadSymbol = Symbol("ignoreFirstLoad");
var MergeableProp = class {
shouldMerge = false;
merge() {
this.shouldMerge = true;
return this;
}
};
var OptionalProp = class {
constructor(callback) {
this.callback = callback;
}
[ignoreFirstLoadSymbol] = true;
};
var DeferProp = class extends MergeableProp {
constructor(callback, group) {
super();
this.callback = callback;
this.group = group;
}
[ignoreFirstLoadSymbol] = true;
getGroup() {
return this.group;
}
};
var MergeProp = class extends MergeableProp {
constructor(callback) {
super();
this.callback = callback;
this.shouldMerge = true;
}
};
var AlwaysProp = class extends MergeableProp {
constructor(callback) {
super();
this.callback = callback;
}
};
// src/headers.ts
var InertiaHeaders = {
Inertia: "x-inertia",
Reset: "x-inertia-reset",
Version: "x-inertia-version",
Location: "x-inertia-location",
ErrorBag: "X-Inertia-Error-Bag",
PartialOnly: "x-inertia-partial-data",
PartialExcept: "x-inertia-partial-except",
PartialComponent: "x-inertia-partial-component"
};
// src/inertia.ts
var Inertia = class {
constructor(ctx, config, vite) {
this.ctx = ctx;
this.config = config;
this.vite = vite;
this.#sharedData = config.sharedData;
this.#serverRenderer = new ServerRenderer(config, vite);
this.#shouldClearHistory = false;
this.#shouldEncryptHistory = config.history.encrypt;
}
#sharedData = {};
#serverRenderer;
#shouldClearHistory = false;
#shouldEncryptHistory = false;
/**
* Check if the current request is a partial request
*/
#isPartial(component) {
return this.ctx.request.header(InertiaHeaders.PartialComponent) === component;
}
/**
* Resolve the `only` partial request props.
* Only the props listed in the `x-inertia-partial-data` header
* will be returned
*/
#resolveOnly(props) {
const partialOnlyHeader = this.ctx.request.header(InertiaHeaders.PartialOnly);
const only = partialOnlyHeader.split(",").filter(Boolean);
let newProps = {};
for (const key of only) newProps[key] = props[key];
return newProps;
}
/**
* Resolve the `except` partial request props.
* Remove the props listed in the `x-inertia-partial-except` header
*/
#resolveExcept(props) {
const partialExceptHeader = this.ctx.request.header(InertiaHeaders.PartialExcept);
const except = partialExceptHeader.split(",").filter(Boolean);
for (const key of except) delete props[key];
return props;
}
/**
* Resolve the props for the current request
* by filtering out the props that are not needed
* based on the request headers
*/
#pickPropsToResolve(component, props = {}) {
const isPartial = this.#isPartial(component);
let newProps = props;
if (!isPartial) {
newProps = Object.fromEntries(
Object.entries(props).filter(([_, value]) => {
if (value && value[ignoreFirstLoadSymbol]) return false;
return true;
})
);
}
const partialOnlyHeader = this.ctx.request.header(InertiaHeaders.PartialOnly);
if (isPartial && partialOnlyHeader) newProps = this.#resolveOnly(props);
const partialExceptHeader = this.ctx.request.header(InertiaHeaders.PartialExcept);
if (isPartial && partialExceptHeader) newProps = this.#resolveExcept(newProps);
for (const [key, value] of Object.entries(props)) {
if (value instanceof AlwaysProp) newProps[key] = props[key];
}
return newProps;
}
/**
* Resolve a single prop
*/
async #resolveProp(key, value) {
if (value instanceof OptionalProp || value instanceof MergeProp || value instanceof DeferProp || value instanceof AlwaysProp) {
return [key, await value.callback()];
}
return [key, value];
}
/**
* Resolve a single prop by calling the callback or resolving the promise
*/
async #resolvePageProps(props = {}) {
return Object.fromEntries(
await Promise.all(
Object.entries(props).map(async ([key, value]) => {
if (typeof value === "function") {
const result = await value(this.ctx);
return this.#resolveProp(key, result);
}
return this.#resolveProp(key, value);
})
)
);
}
/**
* Resolve the deferred props listing. Will be returned only
* on the first visit to the page and will be used to make
* subsequent partial requests
*/
#resolveDeferredProps(component, pageProps) {
if (this.#isPartial(component)) return {};
const deferredProps = Object.entries(pageProps || {}).filter(([_, value]) => value instanceof DeferProp).map(([key, value]) => ({ key, group: value.getGroup() })).reduce(
(groups, { key, group }) => {
if (!groups[group]) groups[group] = [];
groups[group].push(key);
return groups;
},
{}
);
return Object.keys(deferredProps).length ? { deferredProps } : {};
}
/**
* Resolve the props that should be merged
*/
#resolveMergeProps(pageProps) {
const inertiaResetHeader = this.ctx.request.header(InertiaHeaders.Reset) || "";
const resetProps = new Set(inertiaResetHeader.split(",").filter(Boolean));
const mergeProps = Object.entries(pageProps || {}).filter(([_, value]) => value instanceof MergeableProp && value.shouldMerge).map(([key]) => key).filter((key) => !resetProps.has(key));
return mergeProps.length ? { mergeProps } : {};
}
/**
* Build the page object that will be returned to the client
*
* See https://inertiajs.com/the-protocol#the-page-object
*/
async #buildPageObject(component, pageProps) {
const propsToResolve = this.#pickPropsToResolve(component, {
...this.#sharedData,
...pageProps
});
return {
component,
url: this.ctx.request.url(true),
version: this.config.versionCache.getVersion(),
props: await this.#resolvePageProps(propsToResolve),
clearHistory: this.#shouldClearHistory,
encryptHistory: this.#shouldEncryptHistory,
...this.#resolveMergeProps(pageProps),
...this.#resolveDeferredProps(component, pageProps)
};
}
/**
* If the page should be rendered on the server or not
*
* The ssr.pages config can be a list of pages or a function that returns a boolean
*/
async #shouldRenderOnServer(component) {
const isSsrEnabled = this.config.ssr.enabled;
if (!isSsrEnabled) return false;
let isSsrEnabledForPage = false;
if (typeof this.config.ssr.pages === "function") {
isSsrEnabledForPage = await this.config.ssr.pages(this.ctx, component);
} else if (this.config.ssr.pages) {
isSsrEnabledForPage = this.config.ssr.pages?.includes(component);
} else {
isSsrEnabledForPage = true;
}
return isSsrEnabledForPage;
}
/**
* Resolve the root view
*/
#resolveRootView() {
return typeof this.config.rootView === "function" ? this.config.rootView(this.ctx) : this.config.rootView;
}
/**
* Render the page on the server
*/
async #renderOnServer(pageObject, viewProps) {
const { head, body } = await this.#serverRenderer.render(pageObject);
return this.ctx.view.render(this.#resolveRootView(), {
...viewProps,
page: { ssrHead: head, ssrBody: body, ...pageObject }
});
}
/**
* Share data for the current request.
* This data will override any shared data defined in the config.
*/
share(data) {
this.#sharedData = { ...this.#sharedData, ...data };
}
/**
* Render a page using Inertia
*/
async render(component, pageProps, viewProps) {
const pageObject = await this.#buildPageObject(component, pageProps);
const isInertiaRequest = !!this.ctx.request.header(InertiaHeaders.Inertia);
if (!isInertiaRequest) {
const shouldRenderOnServer = await this.#shouldRenderOnServer(component);
if (shouldRenderOnServer) return this.#renderOnServer(pageObject, viewProps);
return this.ctx.view.render(this.#resolveRootView(), { ...viewProps, page: pageObject });
}
this.ctx.response.header(InertiaHeaders.Inertia, "true");
return pageObject;
}
/**
* Clear history state.
*
* See https://v2.inertiajs.com/history-encryption#clearing-history
*/
clearHistory() {
this.#shouldClearHistory = true;
}
/**
* Encrypt history
*
* See https://v2.inertiajs.com/history-encryption
*/
encryptHistory(encrypt = true) {
this.#shouldEncryptHistory = encrypt;
}
/**
* Create a lazy prop
*
* Lazy props are never resolved on first visit, but only when the client
* request a partial reload explicitely with this value.
*
* See https://inertiajs.com/partial-reloads#lazy-data-evaluation
*
* @deprecated use `optional` instead
*/
lazy(callback) {
return new OptionalProp(callback);
}
/**
* Create an optional prop
*
* See https://inertiajs.com/partial-reloads#lazy-data-evaluation
*/
optional(callback) {
return new OptionalProp(callback);
}
/**
* Create a mergeable prop
*
* See https://v2.inertiajs.com/merging-props
*/
merge(callback) {
return new MergeProp(callback);
}
/**
* Create an always prop
*
* Always props are resolved on every request, no matter if it's a partial
* request or not.
*
* See https://inertiajs.com/partial-reloads#lazy-data-evaluation
*/
always(callback) {
return new AlwaysProp(callback);
}
/**
* Create a deferred prop
*
* Deferred props feature allows you to defer the loading of certain
* page data until after the initial page render.
*
* See https://v2.inertiajs.com/deferred-props
*/
defer(callback, group = "default") {
return new DeferProp(callback, group);
}
/**
* This method can be used to redirect the user to an external website
* or even a non-inertia route of your application.
*
* See https://inertiajs.com/redirects#external-redirects
*/
async location(url) {
this.ctx.response.header(InertiaHeaders.Location, url);
this.ctx.response.status(409);
}
};
// src/inertia_middleware.ts
var InertiaMiddleware = class {
constructor(config, vite) {
this.config = config;
this.vite = vite;
}
/**
* Resolves the validation errors to be shared with Inertia
*/
#resolveValidationErrors(ctx) {
const { session, request } = ctx;
if (!session) {
return {};
}
if (!session.flashMessages.has("errorsBag.E_VALIDATION_ERROR")) {
return session.flashMessages.get("errorsBag");
}
const errors = Object.entries(session.flashMessages.get("inputErrorsBag")).reduce(
(acc, [field, messages]) => {
acc[field] = Array.isArray(messages) ? messages[0] : messages;
return acc;
},
{}
);
const errorBag = request.header(InertiaHeaders.ErrorBag);
return errorBag ? { [errorBag]: errors } : errors;
}
/**
* Share validation and flashed errors with Inertia
*/
#shareErrors(ctx) {
ctx.inertia.share({ errors: ctx.inertia.always(() => this.#resolveValidationErrors(ctx)) });
}
async handle(ctx, next) {
const { response, request } = ctx;
ctx.inertia = new Inertia(ctx, this.config, this.vite);
this.#shareErrors(ctx);
await next();
const isInertiaRequest = !!request.header(InertiaHeaders.Inertia);
if (!isInertiaRequest) return;
response.header("Vary", InertiaHeaders.Inertia);
const method = request.method();
if (response.getStatus() === 302 && ["PUT", "PATCH", "DELETE"].includes(method)) {
response.status(303);
}
const version = this.config.versionCache.getVersion().toString();
if (method === "GET" && request.header(InertiaHeaders.Version, "") !== version) {
response.removeHeader(InertiaHeaders.Inertia);
response.header(InertiaHeaders.Location, request.url());
response.status(409);
}
}
};
export {
InertiaMiddleware
};