UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

329 lines (328 loc) • 11.2 kB
import "./polyfill.js"; import { posix } from "node:path"; import { getDefaultClientDirectives } from "../core/client-directive/index.js"; import { ASTRO_CONFIG_DEFAULTS } from "../core/config/schema.js"; import { validateConfig } from "../core/config/validate.js"; import { createKey } from "../core/encryption.js"; import { Logger } from "../core/logger/core.js"; import { nodeLogDestination } from "../core/logger/node.js"; import { NOOP_MIDDLEWARE_FN } from "../core/middleware/noop-middleware.js"; import { removeLeadingForwardSlash } from "../core/path.js"; import { RenderContext } from "../core/render-context.js"; import { getParts, validateSegment } from "../core/routing/manifest/create.js"; import { getPattern } from "../core/routing/manifest/pattern.js"; import { ContainerPipeline } from "./pipeline.js"; function createManifest(manifest, renderers, middleware) { function middlewareInstance() { return { onRequest: middleware ?? NOOP_MIDDLEWARE_FN }; } return { hrefRoot: import.meta.url, trailingSlash: manifest?.trailingSlash ?? ASTRO_CONFIG_DEFAULTS.trailingSlash, buildFormat: manifest?.buildFormat ?? ASTRO_CONFIG_DEFAULTS.build.format, compressHTML: manifest?.compressHTML ?? ASTRO_CONFIG_DEFAULTS.compressHTML, assets: manifest?.assets ?? /* @__PURE__ */ new Set(), assetsPrefix: manifest?.assetsPrefix ?? void 0, entryModules: manifest?.entryModules ?? {}, routes: manifest?.routes ?? [], adapterName: "", clientDirectives: manifest?.clientDirectives ?? getDefaultClientDirectives(), renderers: renderers ?? manifest?.renderers ?? [], base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base, componentMetadata: manifest?.componentMetadata ?? /* @__PURE__ */ new Map(), inlinedScripts: manifest?.inlinedScripts ?? /* @__PURE__ */ new Map(), i18n: manifest?.i18n, checkOrigin: false, middleware: manifest?.middleware ?? middlewareInstance, key: createKey() }; } class experimental_AstroContainer { #pipeline; /** * Internally used to check if the container was created with a manifest. * @private */ #withManifest = false; /** * Internal function responsible for importing a renderer * @private */ #getRenderer; constructor({ streaming = false, manifest, renderers, resolve, astroConfig }) { this.#pipeline = ContainerPipeline.create({ logger: new Logger({ level: "info", dest: nodeLogDestination }), manifest: createManifest(manifest, renderers), streaming, serverLike: true, renderers: renderers ?? manifest?.renderers ?? [], resolve: async (specifier) => { if (this.#withManifest) { return this.#containerResolve(specifier, astroConfig); } else if (resolve) { return resolve(specifier); } return specifier; } }); } async #containerResolve(specifier, astroConfig) { const found = this.#pipeline.manifest.entryModules[specifier]; if (found) { return new URL(found, astroConfig?.build.client).toString(); } return found; } /** * Creates a new instance of a container. * * @param {AstroContainerOptions=} containerOptions */ static async create(containerOptions = {}) { const { streaming = false, manifest, renderers = [], resolve } = containerOptions; const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), "container"); return new experimental_AstroContainer({ streaming, manifest, renderers, astroConfig, resolve }); } /** * Use this function to manually add a **server** renderer to the container. * * This function is preferred when you require to use the container with a renderer in environments such as on-demand pages. * * ## Example * * ```js * import reactRenderer from "@astrojs/react/server.js"; * import vueRenderer from "@astrojs/vue/server.js"; * import customRenderer from "../renderer/customRenderer.js"; * import { experimental_AstroContainer as AstroContainer } from "astro/container" * * const container = await AstroContainer.create(); * container.addServerRenderer(reactRenderer); * container.addServerRenderer(vueRenderer); * container.addServerRenderer("customRenderer", customRenderer); * ``` * * @param options {object} * @param options.name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package. * @param options.renderer The server renderer exported by integration. */ addServerRenderer(options) { const { renderer, name } = options; if (!renderer.check || !renderer.renderToStaticMarkup) { throw new Error( "The renderer you passed isn't valid. A renderer is usually an object that exposes the `check` and `renderToStaticMarkup` functions.\nUsually, the renderer is exported by a /server.js entrypoint e.g. `import renderer from '@astrojs/react/server.js'`" ); } if (isNamedRenderer(renderer)) { this.#pipeline.manifest.renderers.push({ name: renderer.name, ssr: renderer }); } else { this.#pipeline.manifest.renderers.push({ name, ssr: renderer }); } } /** * Use this function to manually add a **client** renderer to the container. * * When rendering components that use the `client:*` directives, you need to use this function. * * ## Example * * ```js * import reactRenderer from "@astrojs/react/server.js"; * import { experimental_AstroContainer as AstroContainer } from "astro/container" * * const container = await AstroContainer.create(); * container.addServerRenderer(reactRenderer); * container.addClientRenderer({ * name: "@astrojs/react", * entrypoint: "@astrojs/react/client.js" * }); * ``` * * @param options {object} * @param options.name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package. * @param options.entrypoint The entrypoint of the client renderer. */ addClientRenderer(options) { const { entrypoint, name } = options; const rendererIndex = this.#pipeline.manifest.renderers.findIndex((r) => r.name === name); if (rendererIndex === -1) { throw new Error( "You tried to add the " + name + " client renderer, but its server renderer wasn't added. You must add the server renderer first. Use the `addServerRenderer` function." ); } const renderer = this.#pipeline.manifest.renderers[rendererIndex]; renderer.clientEntrypoint = entrypoint; this.#pipeline.manifest.renderers[rendererIndex] = renderer; } // NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it. // @ematipico: I plan to use it for a possible integration that could help people static async createFromManifest(manifest) { const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), "container"); const container = new experimental_AstroContainer({ manifest, astroConfig }); container.#withManifest = true; return container; } #insertRoute({ path, componentInstance, params = {}, type = "page" }) { const pathUrl = new URL(path, "https://example.com"); const routeData = this.#createRoute(pathUrl, params, type); this.#pipeline.manifest.routes.push({ routeData, file: "", links: [], styles: [], scripts: [] }); this.#pipeline.insertRoute(routeData, componentInstance); return routeData; } /** * @description * It renders a component and returns the result as a string. * * ## Example * * ```js * import Card from "../src/components/Card.astro"; * * const container = await AstroContainer.create(); * const result = await container.renderToString(Card); * * console.log(result); // it's a string * ``` * * * @param {AstroComponentFactory} component The instance of the component. * @param {ContainerRenderOptions=} options Possible options to pass when rendering the component. */ async renderToString(component, options = {}) { const response = await this.renderToResponse(component, options); return await response.text(); } /** * @description * It renders a component and returns the `Response` as result of the rendering phase. * * ## Example * * ```js * import Card from "../src/components/Card.astro"; * * const container = await AstroContainer.create(); * const response = await container.renderToResponse(Card); * * console.log(response.status); // it's a number * ``` * * * @param {AstroComponentFactory} component The instance of the component. * @param {ContainerRenderOptions=} options Possible options to pass when rendering the component. */ async renderToResponse(component, options = {}) { const { routeType = "page", slots } = options; const request = options?.request ?? new Request("https://example.com/"); const url = new URL(request.url); const componentInstance = routeType === "endpoint" ? component : this.#wrapComponent(component, options.params); const routeData = this.#insertRoute({ path: request.url, componentInstance, params: options.params, type: routeType }); const renderContext = await RenderContext.create({ pipeline: this.#pipeline, routeData, status: 200, request, pathname: url.pathname, locals: options?.locals ?? {}, partial: options?.partial ?? true, clientAddress: "" }); if (options.params) { renderContext.params = options.params; } if (options.props) { renderContext.props = options.props; } return renderContext.render(componentInstance, slots); } #createRoute(url, params, type) { const segments = removeLeadingForwardSlash(url.pathname).split(posix.sep).filter(Boolean).map((s) => { validateSegment(s); return getParts(s, url.pathname); }); return { route: url.pathname, component: "", generate(_data) { return ""; }, params: Object.keys(params), pattern: getPattern( segments, ASTRO_CONFIG_DEFAULTS.base, ASTRO_CONFIG_DEFAULTS.trailingSlash ), prerender: false, segments, type, fallbackRoutes: [], isIndex: false, origin: "internal" }; } /** * If the provided component isn't a default export, the function wraps it in an object `{default: Component }` to mimic the default export. * @param componentFactory * @param params * @private */ #wrapComponent(componentFactory, params) { if (params) { return { default: componentFactory, getStaticPaths() { return [{ params }]; } }; } return { default: componentFactory }; } } function isNamedRenderer(renderer) { return !!renderer?.name; } export { experimental_AstroContainer };