feat: scaffold Astro + Tailwind project

This commit is contained in:
TerryM
2026-05-12 16:16:03 +08:00
parent 906eb5c763
commit 03d3800c6c
12097 changed files with 1266600 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import type { ComponentInstance, RoutesList } from '../../types/astro.js';
import type { SSRManifest } from '../../types/public/internal.js';
export declare const SERVER_ISLAND_ROUTE = "/_server-islands/[name]";
export declare const SERVER_ISLAND_COMPONENT = "_server-islands.astro";
type ConfigFields = Pick<SSRManifest, 'base' | 'trailingSlash'>;
export declare function injectServerIslandRoute(config: ConfigFields, routeManifest: RoutesList): void;
export type RenderOptions = {
encryptedComponentExport: string;
encryptedProps: string;
encryptedSlots: string;
};
export declare function getRequestData(request: Request, bodySizeLimit?: number): Promise<Response | RenderOptions>;
export declare function createEndpoint(manifest: SSRManifest): ComponentInstance;
export {};

173
node_modules/astro/dist/core/server-islands/endpoint.js generated vendored Normal file
View File

@@ -0,0 +1,173 @@
import {
renderComponent,
renderTemplate
} from "../../runtime/server/index.js";
import { isAstroComponentFactory } from "../../runtime/server/render/astro/factory.js";
import { createSlotValueFromString } from "../../runtime/server/render/slot.js";
import { decryptString } from "../encryption.js";
import { BodySizeLimitError, readBodyWithLimit } from "../request-body.js";
import { getPattern } from "../routing/pattern.js";
const SERVER_ISLAND_ROUTE = "/_server-islands/[name]";
const SERVER_ISLAND_COMPONENT = "_server-islands.astro";
function getServerIslandRouteData(config) {
const segments = [
[{ content: "_server-islands", dynamic: false, spread: false }],
[{ content: "name", dynamic: true, spread: false }]
];
const route = {
type: "page",
component: SERVER_ISLAND_COMPONENT,
params: ["name"],
segments,
pattern: getPattern(segments, config.base, config.trailingSlash),
prerender: false,
isIndex: false,
fallbackRoutes: [],
route: SERVER_ISLAND_ROUTE,
origin: "internal",
distURL: []
};
return route;
}
function injectServerIslandRoute(config, routeManifest) {
routeManifest.routes.unshift(getServerIslandRouteData(config));
}
function badRequest(reason) {
return new Response(null, {
status: 400,
statusText: "Bad request: " + reason
});
}
const DEFAULT_BODY_SIZE_LIMIT = 1024 * 1024;
async function getRequestData(request, bodySizeLimit = DEFAULT_BODY_SIZE_LIMIT) {
switch (request.method) {
case "GET": {
const url = new URL(request.url);
const params = url.searchParams;
if (!params.has("s") || !params.has("e") || !params.has("p")) {
return badRequest("Missing required query parameters.");
}
const encryptedSlots = params.get("s");
return {
encryptedComponentExport: params.get("e"),
encryptedProps: params.get("p"),
encryptedSlots
};
}
case "POST": {
try {
const body = await readBodyWithLimit(request, bodySizeLimit);
const raw = new TextDecoder().decode(body);
const data = JSON.parse(raw);
if (Object.hasOwn(data, "slots") && typeof data.slots === "object") {
return badRequest("Plaintext slots are not allowed. Slots must be encrypted.");
}
if (Object.hasOwn(data, "componentExport") && typeof data.componentExport === "string") {
return badRequest(
"Plaintext componentExport is not allowed. componentExport must be encrypted."
);
}
return data;
} catch (e) {
if (e instanceof BodySizeLimitError) {
return new Response(null, {
status: 413,
statusText: e.message
});
}
if (e instanceof SyntaxError) {
return badRequest("Request format is invalid.");
}
throw e;
}
}
default: {
return new Response(null, { status: 405 });
}
}
}
function createEndpoint(manifest) {
const page = async (result) => {
const params = result.params;
if (!params.name) {
return new Response(null, {
status: 400,
statusText: "Bad request"
});
}
const componentId = params.name;
const data = await getRequestData(result.request, manifest.serverIslandBodySizeLimit);
if (data instanceof Response) {
return data;
}
const serverIslandMappings = await manifest.serverIslandMappings?.();
const serverIslandMap = await serverIslandMappings?.serverIslandMap;
let imp = serverIslandMap?.get(componentId);
if (!imp) {
return new Response(null, {
status: 404,
statusText: "Not found"
});
}
const key = await manifest.key;
let componentExport;
try {
componentExport = await decryptString(
key,
data.encryptedComponentExport,
`export:${componentId}`
);
} catch (_e) {
return badRequest("Encrypted componentExport value is invalid.");
}
const encryptedProps = data.encryptedProps;
let props = {};
if (encryptedProps !== "") {
try {
const propString = await decryptString(key, encryptedProps, `props:${componentId}`);
props = JSON.parse(propString);
} catch (_e) {
return badRequest("Encrypted props value is invalid.");
}
}
let decryptedSlots = {};
const encryptedSlots = data.encryptedSlots;
if (encryptedSlots !== "") {
try {
const slotsString = await decryptString(key, encryptedSlots, `slots:${componentId}`);
decryptedSlots = JSON.parse(slotsString);
} catch (_e) {
return badRequest("Encrypted slots value is invalid.");
}
}
const componentModule = await imp();
let Component = componentModule[componentExport];
const slots = {};
for (const prop in decryptedSlots) {
slots[prop] = createSlotValueFromString(decryptedSlots[prop]);
}
result.response.headers.set("X-Robots-Tag", "noindex");
if (isAstroComponentFactory(Component)) {
const ServerIsland = Component;
Component = function(...args) {
return ServerIsland.apply(this, args);
};
Object.assign(Component, ServerIsland);
Component.propagation = "self";
}
return renderTemplate`${renderComponent(result, "Component", Component, props, slots)}`;
};
page.isAstroComponentFactory = true;
const instance = {
default: page,
partial: true
};
return instance;
}
export {
SERVER_ISLAND_COMPONENT,
SERVER_ISLAND_ROUTE,
createEndpoint,
getRequestData,
injectServerIslandRoute
};

View File

@@ -0,0 +1,44 @@
type ServerIslandDiscovery = {
resolvedPath: string;
localName: string;
specifier: string;
importer: string;
};
type ServerIslandRecord = Omit<ServerIslandDiscovery, 'resolvedPath'> & {
islandName: string;
};
export declare class ServerIslandsState {
private islandsByResolvedPath;
private resolvedPathByIslandName;
private referenceIdByResolvedPath;
hasIslands(): boolean;
/**
* Record a discovered server island.
*
* Dedupe is based on `resolvedPath`: if the same resolved path is discovered
* again from a different importer/specifier, the first record is preserved.
* This keeps island names stable across repeated scans.
*/
discover(island: ServerIslandDiscovery): ServerIslandRecord;
getDiscoveredIslands(): Iterable<ServerIslandRecord>;
hasReferenceId(resolvedPath: string): boolean;
setReferenceId(resolvedPath: string, referenceId: string): void;
getDiscoveredIslandEntries(): Iterable<[string, ServerIslandRecord]>;
/**
* Build import-map source from discovered islands.
*
* Used by non-SSR build output and dev replacement paths where we can import
* directly from discovered component paths.
*/
createImportMapSourceFromDiscovered(toImportPath: (fileName: string) => string): string;
/**
* Build import-map source from Rollup reference ids.
*
* Used by SSR build output: reference ids are resolved to final emitted chunk
* file names before generating import() mappings.
*/
createImportMapSourceFromReferences(resolveFileName: (referenceId: string) => string, toImportPath: (fileName: string) => string): string;
createNameMapSource(): string;
private createImportMapSource;
}
export {};

View File

@@ -0,0 +1,96 @@
class ServerIslandsState {
// Canonical source of discovered islands keyed by resolved component path.
islandsByResolvedPath = /* @__PURE__ */ new Map();
// Reverse lookup used to keep island names unique and stable.
resolvedPathByIslandName = /* @__PURE__ */ new Map();
// Rollup reference ids emitted for SSR chunks, keyed by resolved path.
referenceIdByResolvedPath = /* @__PURE__ */ new Map();
hasIslands() {
return this.islandsByResolvedPath.size > 0;
}
/**
* Record a discovered server island.
*
* Dedupe is based on `resolvedPath`: if the same resolved path is discovered
* again from a different importer/specifier, the first record is preserved.
* This keeps island names stable across repeated scans.
*/
discover(island) {
const { resolvedPath, ...discovery } = island;
const existing = this.islandsByResolvedPath.get(resolvedPath);
if (existing) {
return existing;
}
let name = island.localName;
let idx = 1;
while (this.resolvedPathByIslandName.has(name)) {
name += idx++;
}
const record = {
...discovery,
islandName: name
};
this.islandsByResolvedPath.set(resolvedPath, record);
this.resolvedPathByIslandName.set(name, resolvedPath);
return record;
}
getDiscoveredIslands() {
return this.islandsByResolvedPath.values();
}
hasReferenceId(resolvedPath) {
return this.referenceIdByResolvedPath.has(resolvedPath);
}
setReferenceId(resolvedPath, referenceId) {
this.referenceIdByResolvedPath.set(resolvedPath, referenceId);
}
getDiscoveredIslandEntries() {
return this.islandsByResolvedPath.entries();
}
/**
* Build import-map source from discovered islands.
*
* Used by non-SSR build output and dev replacement paths where we can import
* directly from discovered component paths.
*/
createImportMapSourceFromDiscovered(toImportPath) {
const entries = Array.from(
this.islandsByResolvedPath,
([resolvedPath, island]) => [island.islandName, resolvedPath]
);
return this.createImportMapSource(entries, toImportPath);
}
/**
* Build import-map source from Rollup reference ids.
*
* Used by SSR build output: reference ids are resolved to final emitted chunk
* file names before generating import() mappings.
*/
createImportMapSourceFromReferences(resolveFileName, toImportPath) {
const entries = [];
for (const [resolvedPath, referenceId] of this.referenceIdByResolvedPath) {
const island = this.islandsByResolvedPath.get(resolvedPath);
if (!island) continue;
entries.push([island.islandName, resolveFileName(referenceId)]);
}
return this.createImportMapSource(entries, toImportPath);
}
createNameMapSource() {
const entries = Array.from(
this.islandsByResolvedPath,
([resolvedPath, island]) => [resolvedPath, island.islandName]
);
return `new Map(${JSON.stringify(entries, null, 2)})`;
}
createImportMapSource(entries, toImportPath) {
const mappings = Array.from(entries, ([islandName, fileName]) => {
const importPath = toImportPath(fileName);
return ` [${JSON.stringify(islandName)}, () => import(${JSON.stringify(importPath)})],`;
});
return `new Map([
${mappings.join("\n")}
])`;
}
}
export {
ServerIslandsState
};

View File

@@ -0,0 +1,8 @@
import type { Plugin as VitePlugin } from 'vite';
import type { AstroPluginOptions } from '../../types/astro.js';
import type { ServerIslandsState } from './shared-state.js';
export declare const SERVER_ISLAND_MANIFEST = "virtual:astro:server-island-manifest";
export declare const SERVER_ISLAND_MAP_MARKER = "$$server-islands-map$$";
export declare function vitePluginServerIslands({ settings, serverIslandsState, }: AstroPluginOptions & {
serverIslandsState: ServerIslandsState;
}): VitePlugin;

View File

@@ -0,0 +1,170 @@
import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../constants.js";
import { AstroError, AstroErrorData } from "../errors/index.js";
const SERVER_ISLAND_MANIFEST = "virtual:astro:server-island-manifest";
const RESOLVED_SERVER_ISLAND_MANIFEST = "\0" + SERVER_ISLAND_MANIFEST;
const serverIslandPlaceholderMap = "'$$server-islands-map$$'";
const serverIslandPlaceholderNameMap = "'$$server-islands-name-map$$'";
const SERVER_ISLAND_MAP_MARKER = "$$server-islands-map$$";
const serverIslandMapReplaceExp = /['"]\$\$server-islands-map\$\$['"]/g;
const serverIslandNameMapReplaceExp = /['"]\$\$server-islands-name-map\$\$['"]/g;
function vitePluginServerIslands({
settings,
serverIslandsState
}) {
let command = "serve";
let serverEnvironments = [];
function ensureServerIslandReferenceIds(ctx) {
for (const [resolvedPath, island] of serverIslandsState.getDiscoveredIslandEntries()) {
if (serverIslandsState.hasReferenceId(resolvedPath)) continue;
const referenceId = ctx.emitFile({
type: "chunk",
id: island.specifier,
importer: island.importer,
name: island.islandName
});
serverIslandsState.setReferenceId(resolvedPath, referenceId);
}
}
return {
name: "astro:server-islands",
enforce: "post",
config(_config, { command: _command }) {
command = _command;
},
buildStart() {
if (command !== "build" || this.environment?.name !== ASTRO_VITE_ENVIRONMENT_NAMES.ssr) {
return;
}
ensureServerIslandReferenceIds(this);
},
configureServer(server) {
serverEnvironments = [];
for (const name of [
ASTRO_VITE_ENVIRONMENT_NAMES.ssr,
ASTRO_VITE_ENVIRONMENT_NAMES.prerender,
ASTRO_VITE_ENVIRONMENT_NAMES.astro
]) {
const env = server.environments[name];
if (env) {
serverEnvironments.push(env);
}
}
},
resolveId: {
filter: {
id: new RegExp(`^${SERVER_ISLAND_MANIFEST}$`)
},
handler() {
return RESOLVED_SERVER_ISLAND_MANIFEST;
}
},
load: {
filter: {
id: new RegExp(`^${RESOLVED_SERVER_ISLAND_MANIFEST}$`)
},
handler() {
return {
code: `export const serverIslandMap = ${serverIslandPlaceholderMap};
export const serverIslandNameMap = ${serverIslandPlaceholderNameMap};`
};
}
},
transform: {
filter: {
id: {
include: [/\.(astro|mdx)$/, new RegExp(`^${RESOLVED_SERVER_ISLAND_MANIFEST}$`)]
}
},
async handler(_code, id) {
const info = this.getModuleInfo(id);
const astro = info ? info.meta.astro : void 0;
const isBuildSsr = command === "build" && this.environment?.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr;
if (astro) {
for (const comp of astro.serverComponents) {
if (!settings.adapter) {
throw new AstroError(AstroErrorData.NoAdapterInstalledServerIslands);
}
const island = serverIslandsState.discover({
resolvedPath: comp.resolvedPath,
localName: comp.localName,
specifier: comp.specifier ?? comp.resolvedPath,
importer: id
});
if (isBuildSsr && !serverIslandsState.hasReferenceId(comp.resolvedPath)) {
const referenceId = this.emitFile({
type: "chunk",
id: island.specifier,
importer: island.importer,
name: island.islandName
});
serverIslandsState.setReferenceId(comp.resolvedPath, referenceId);
}
}
}
if (serverIslandsState.hasIslands()) {
for (const env of serverEnvironments) {
const mod = env.moduleGraph.getModuleById(RESOLVED_SERVER_ISLAND_MANIFEST);
if (mod) {
env.moduleGraph.invalidateModule(mod);
}
}
}
if (id === RESOLVED_SERVER_ISLAND_MANIFEST) {
if (command === "build" && settings.buildOutput) {
const hasServerIslands = serverIslandsState.hasIslands();
if (hasServerIslands && settings.buildOutput !== "server") {
throw new AstroError(AstroErrorData.NoAdapterInstalledServerIslands);
}
}
if (command !== "build" && serverIslandsState.hasIslands()) {
const mapSource = serverIslandsState.createImportMapSourceFromDiscovered(
(fileName) => fileName
);
const nameMapSource = serverIslandsState.createNameMapSource();
return {
code: `
export const serverIslandMap = ${mapSource};
export const serverIslandNameMap = ${nameMapSource};
`
};
}
}
}
},
renderChunk(code, chunk) {
if (!code.includes(SERVER_ISLAND_MAP_MARKER)) return;
if (command === "build") {
const envName = this.environment?.name;
let mapSource;
if (envName === ASTRO_VITE_ENVIRONMENT_NAMES.ssr) {
const isRelativeChunk = !chunk.isEntry;
const dots = isRelativeChunk ? ".." : ".";
mapSource = serverIslandsState.createImportMapSourceFromReferences(
(referenceId) => this.getFileName(referenceId),
(fileName) => `${dots}/${fileName}`
);
} else {
mapSource = serverIslandsState.createImportMapSourceFromDiscovered(
(fileName) => fileName
);
}
const nameMapSource = serverIslandsState.createNameMapSource();
return {
code: code.replace(serverIslandMapReplaceExp, mapSource).replace(serverIslandNameMapReplaceExp, nameMapSource),
map: null
};
}
return {
code: code.replace(serverIslandMapReplaceExp, "new Map();").replace(serverIslandNameMapReplaceExp, "new Map()"),
map: null
};
}
};
}
export {
SERVER_ISLAND_MANIFEST,
SERVER_ISLAND_MAP_MARKER,
vitePluginServerIslands
};