@scenar/cli 0.1.20 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/example-bundle/assets/index-BLQ14SEo.js +147 -0
- package/example-bundle/assets/logo-f-5LjVqJ.png +0 -0
- package/example-bundle/assets/style-Dnzx_jUO.css +1 -0
- package/example-bundle/index.html +13 -0
- package/example-bundle/pack-manifest.json +36 -0
- package/example-bundle/scenario.json +9 -0
- package/package.json +17 -5
- package/src/__tests__/pack-generate-embed-entry.test.ts +59 -6
- package/src/__tests__/publish-command.test.ts +46 -0
- package/src/__tests__/publish-flow.test.ts +220 -0
- package/src/__tests__/read-viewport.test.ts +45 -0
- package/src/__tests__/run-publish.test.ts +92 -0
- package/src/__tests__/serve-command.test.ts +44 -0
- package/src/__tests__/serve-request-path.test.ts +67 -0
- package/src/__tests__/serve-static-server.test.ts +88 -0
- package/src/__tests__/try-command.test.ts +21 -0
- package/src/api.d.ts +31 -0
- package/src/api.d.ts.map +1 -0
- package/src/api.js +28 -0
- package/src/api.js.map +1 -0
- package/src/api.ts +55 -0
- package/src/bundle/read-viewport.d.ts +23 -0
- package/src/bundle/read-viewport.d.ts.map +1 -0
- package/src/bundle/read-viewport.js +25 -0
- package/src/bundle/read-viewport.js.map +1 -0
- package/src/bundle/read-viewport.ts +36 -0
- package/src/commands/deploy.d.ts.map +1 -1
- package/src/commands/deploy.js +8 -24
- package/src/commands/deploy.js.map +1 -1
- package/src/commands/deploy.ts +10 -28
- package/src/commands/narrate.d.ts.map +1 -1
- package/src/commands/narrate.js +39 -170
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +48 -289
- package/src/commands/pack.d.ts.map +1 -1
- package/src/commands/pack.js +12 -112
- package/src/commands/pack.js.map +1 -1
- package/src/commands/pack.ts +12 -128
- package/src/commands/publish.d.ts +3 -0
- package/src/commands/publish.d.ts.map +1 -0
- package/src/commands/publish.js +64 -0
- package/src/commands/publish.js.map +1 -0
- package/src/commands/publish.ts +86 -0
- package/src/commands/render.d.ts.map +1 -1
- package/src/commands/render.js +12 -194
- package/src/commands/render.js.map +1 -1
- package/src/commands/render.ts +19 -286
- package/src/commands/serve.d.ts +3 -0
- package/src/commands/serve.d.ts.map +1 -0
- package/src/commands/serve.js +91 -0
- package/src/commands/serve.js.map +1 -0
- package/src/commands/serve.ts +112 -0
- package/src/commands/try.d.ts +3 -0
- package/src/commands/try.d.ts.map +1 -0
- package/src/commands/try.js +99 -0
- package/src/commands/try.js.map +1 -0
- package/src/commands/try.ts +122 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +6 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +6 -0
- package/src/narrate/run-narrate.d.ts +43 -0
- package/src/narrate/run-narrate.d.ts.map +1 -0
- package/src/narrate/run-narrate.js +174 -0
- package/src/narrate/run-narrate.js.map +1 -0
- package/src/narrate/run-narrate.ts +298 -0
- package/src/pack/generate-embed-entry.d.ts +11 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +81 -10
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +81 -10
- package/src/pack/run-pack.d.ts +34 -0
- package/src/pack/run-pack.d.ts.map +1 -0
- package/src/pack/run-pack.js +117 -0
- package/src/pack/run-pack.js.map +1 -0
- package/src/pack/run-pack.ts +163 -0
- package/src/publish/github.d.ts +11 -0
- package/src/publish/github.d.ts.map +1 -0
- package/src/publish/github.js +197 -0
- package/src/publish/github.js.map +1 -0
- package/src/publish/github.ts +216 -0
- package/src/publish/publish-flow.d.ts +110 -0
- package/src/publish/publish-flow.d.ts.map +1 -0
- package/src/publish/publish-flow.js +117 -0
- package/src/publish/publish-flow.js.map +1 -0
- package/src/publish/publish-flow.ts +199 -0
- package/src/publish/run-publish.d.ts +41 -0
- package/src/publish/run-publish.d.ts.map +1 -0
- package/src/publish/run-publish.js +77 -0
- package/src/publish/run-publish.js.map +1 -0
- package/src/publish/run-publish.ts +131 -0
- package/src/render/run-render.d.ts +31 -0
- package/src/render/run-render.d.ts.map +1 -0
- package/src/render/run-render.js +196 -0
- package/src/render/run-render.js.map +1 -0
- package/src/render/run-render.ts +287 -0
- package/src/serve/request-path.d.ts +23 -0
- package/src/serve/request-path.d.ts.map +1 -0
- package/src/serve/request-path.js +52 -0
- package/src/serve/request-path.js.map +1 -0
- package/src/serve/request-path.ts +55 -0
- package/src/serve/run-serve.d.ts +31 -0
- package/src/serve/run-serve.d.ts.map +1 -0
- package/src/serve/run-serve.js +35 -0
- package/src/serve/run-serve.js.map +1 -0
- package/src/serve/run-serve.ts +68 -0
- package/src/serve/static-server.d.ts +38 -0
- package/src/serve/static-server.d.ts.map +1 -0
- package/src/serve/static-server.js +91 -0
- package/src/serve/static-server.js.map +1 -0
- package/src/serve/static-server.ts +118 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { stat, mkdir, access, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { loadBundle, type CliBundle } from "../util/load-bundle.js";
|
|
5
|
+
import { generateRemotionEntry } from "./generate-entry.js";
|
|
6
|
+
import { resolveProvidersPath } from "./resolve-providers.js";
|
|
7
|
+
import { detectRenderExport } from "./detect-render-export.js";
|
|
8
|
+
|
|
9
|
+
/** Options for {@link runRender}. Paths may be relative; resolved here. */
|
|
10
|
+
export interface RunRenderOptions {
|
|
11
|
+
readonly scenarioDir: string;
|
|
12
|
+
readonly out?: string;
|
|
13
|
+
readonly fps?: number;
|
|
14
|
+
readonly width?: number;
|
|
15
|
+
readonly height?: number;
|
|
16
|
+
readonly compositionId?: string;
|
|
17
|
+
readonly entry?: string;
|
|
18
|
+
readonly webpackOverride?: string;
|
|
19
|
+
readonly onLog?: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The outcome of a successful render. */
|
|
23
|
+
export interface RenderResult {
|
|
24
|
+
readonly scenarioId: string;
|
|
25
|
+
readonly outputPath: string;
|
|
26
|
+
readonly steps: number;
|
|
27
|
+
readonly hasNarration: boolean;
|
|
28
|
+
readonly entryAutoGenerated: boolean;
|
|
29
|
+
readonly fps: number;
|
|
30
|
+
readonly width: number;
|
|
31
|
+
readonly height: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render a scenario directory to an MP4 via Remotion — the pure orchestration
|
|
36
|
+
* behind `scenar render`, with no process/exit coupling. Throws on any failure
|
|
37
|
+
* (missing directory, missing Remotion peer deps, render error). The caller
|
|
38
|
+
* formats the result/error.
|
|
39
|
+
*/
|
|
40
|
+
export async function runRender(options: RunRenderOptions): Promise<RenderResult> {
|
|
41
|
+
const onLog = options.onLog ?? (() => {});
|
|
42
|
+
const scenarioDir = resolve(options.scenarioDir);
|
|
43
|
+
|
|
44
|
+
const info = await stat(scenarioDir).catch(() => null);
|
|
45
|
+
if (!info) {
|
|
46
|
+
throw new Error(`${options.scenarioDir} does not exist.`);
|
|
47
|
+
}
|
|
48
|
+
if (!info.isDirectory()) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${options.scenarioDir} is not a directory. ` +
|
|
51
|
+
"The render command requires a scenario directory (with steps.ts).",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fps = options.fps ?? 30;
|
|
56
|
+
const width = options.width ?? 1920;
|
|
57
|
+
const height = options.height ?? 1080;
|
|
58
|
+
|
|
59
|
+
let tempDir: string | undefined;
|
|
60
|
+
try {
|
|
61
|
+
const bundle = await loadBundle(scenarioDir);
|
|
62
|
+
const scenarioId = bundle.id;
|
|
63
|
+
const compositionId = options.compositionId ?? scenarioId;
|
|
64
|
+
const outputPath = resolveOutputPath(options.out, scenarioId);
|
|
65
|
+
|
|
66
|
+
const { entryPoint, generated } = await resolveEntryPoint({
|
|
67
|
+
entryOption: options.entry,
|
|
68
|
+
scenarioDir,
|
|
69
|
+
scenarioId,
|
|
70
|
+
compositionId,
|
|
71
|
+
hasNarration: !!bundle.narrationManifest,
|
|
72
|
+
fps,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
});
|
|
76
|
+
tempDir = generated?.tempDir;
|
|
77
|
+
|
|
78
|
+
const webpackOverride = options.webpackOverride
|
|
79
|
+
? await loadWebpackOverride(options.webpackOverride)
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
onLog(`Scenario: ${scenarioId}`);
|
|
83
|
+
onLog(`Steps: ${bundle.steps.length}`);
|
|
84
|
+
onLog(`Audio: ${bundle.narrationManifest ? "yes (manifest found)" : "none"}`);
|
|
85
|
+
onLog(`Entry: ${generated ? "(auto-generated)" : entryPoint}`);
|
|
86
|
+
if (generated?.providersPath) {
|
|
87
|
+
onLog(`Providers: ${generated.providersPath}`);
|
|
88
|
+
}
|
|
89
|
+
if (webpackOverride) {
|
|
90
|
+
onLog("Webpack: custom override loaded");
|
|
91
|
+
}
|
|
92
|
+
onLog(`Output: ${outputPath}`);
|
|
93
|
+
onLog(`Config: ${width}x${height} @ ${fps}fps`);
|
|
94
|
+
|
|
95
|
+
await renderScenario({
|
|
96
|
+
bundle,
|
|
97
|
+
entryPoint,
|
|
98
|
+
compositionId,
|
|
99
|
+
outputPath,
|
|
100
|
+
fps,
|
|
101
|
+
width,
|
|
102
|
+
height,
|
|
103
|
+
webpackOverride,
|
|
104
|
+
onLog,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
scenarioId,
|
|
109
|
+
outputPath,
|
|
110
|
+
steps: bundle.steps.length,
|
|
111
|
+
hasNarration: !!bundle.narrationManifest,
|
|
112
|
+
entryAutoGenerated: !!generated,
|
|
113
|
+
fps,
|
|
114
|
+
width,
|
|
115
|
+
height,
|
|
116
|
+
};
|
|
117
|
+
} finally {
|
|
118
|
+
if (tempDir) {
|
|
119
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveOutputPath(outOption: string | undefined, scenarioId: string): string {
|
|
125
|
+
if (!outOption) {
|
|
126
|
+
return resolve(`./${scenarioId}.mp4`);
|
|
127
|
+
}
|
|
128
|
+
if (outOption.endsWith(".mp4")) {
|
|
129
|
+
return resolve(outOption);
|
|
130
|
+
}
|
|
131
|
+
return resolve(join(outOption, `${scenarioId}.mp4`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface EntryResolutionInput {
|
|
135
|
+
entryOption: string | undefined;
|
|
136
|
+
scenarioDir: string;
|
|
137
|
+
scenarioId: string;
|
|
138
|
+
compositionId: string;
|
|
139
|
+
hasNarration: boolean;
|
|
140
|
+
fps: number;
|
|
141
|
+
width: number;
|
|
142
|
+
height: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface EntryResolutionResult {
|
|
146
|
+
entryPoint: string;
|
|
147
|
+
generated?: {
|
|
148
|
+
tempDir: string;
|
|
149
|
+
providersPath: string | null;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function resolveEntryPoint(input: EntryResolutionInput): Promise<EntryResolutionResult> {
|
|
154
|
+
// Explicit --entry: use as-is, no generation.
|
|
155
|
+
if (input.entryOption) {
|
|
156
|
+
const resolved = resolve(input.entryOption);
|
|
157
|
+
try {
|
|
158
|
+
await access(resolved);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error(`Remotion entry point not found: ${input.entryOption}`);
|
|
161
|
+
}
|
|
162
|
+
return { entryPoint: resolved };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Auto-generate: detect renderStep export, find providers, write temp entry.
|
|
166
|
+
const renderFilePath = await detectRenderExport(input.scenarioDir);
|
|
167
|
+
const providersPath = await resolveProvidersPath(input.scenarioDir);
|
|
168
|
+
|
|
169
|
+
const entrySource = generateRemotionEntry({
|
|
170
|
+
scenarioDir: input.scenarioDir,
|
|
171
|
+
renderFilePath,
|
|
172
|
+
scenarioId: input.scenarioId,
|
|
173
|
+
hasNarration: input.hasNarration,
|
|
174
|
+
providersPath,
|
|
175
|
+
fps: input.fps,
|
|
176
|
+
width: input.width,
|
|
177
|
+
height: input.height,
|
|
178
|
+
compositionId: input.compositionId,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Write the entry inside the scenario directory so webpack's standard
|
|
182
|
+
// node_modules resolution walks up from here into the consumer project.
|
|
183
|
+
// A /tmp/ location would fail because no node_modules exist there.
|
|
184
|
+
const tempDir = join(input.scenarioDir, ".scenar-render");
|
|
185
|
+
await mkdir(tempDir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
const entryPath = join(tempDir, "index.tsx");
|
|
188
|
+
await writeFile(entryPath, entrySource, "utf-8");
|
|
189
|
+
|
|
190
|
+
return { entryPoint: entryPath, generated: { tempDir, providersPath } };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function loadWebpackOverride(overridePath: string): Promise<(config: unknown) => unknown> {
|
|
194
|
+
const resolved = resolve(overridePath);
|
|
195
|
+
try {
|
|
196
|
+
await access(resolved);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(`Webpack override file not found: ${overridePath}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let mod: Record<string, unknown>;
|
|
202
|
+
try {
|
|
203
|
+
mod = (await import(pathToFileURL(resolved).href)) as Record<string, unknown>;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
206
|
+
throw new Error(`Failed to import webpack override from ${overridePath}:\n${detail}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const override = (mod.default ?? mod.webpackOverride) as unknown;
|
|
210
|
+
if (typeof override !== "function") {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`${overridePath} does not export a webpack override function.\n\n` +
|
|
213
|
+
"Expected a default export or named 'webpackOverride' export:\n\n" +
|
|
214
|
+
' import type { WebpackOverrideFn } from "@remotion/bundler";\n\n' +
|
|
215
|
+
" const webpackOverride: WebpackOverrideFn = (config) => ({\n" +
|
|
216
|
+
" ...config,\n" +
|
|
217
|
+
" // your overrides\n" +
|
|
218
|
+
" });\n\n" +
|
|
219
|
+
" export default webpackOverride;\n",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return override as (config: unknown) => unknown;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface RenderConfig {
|
|
227
|
+
bundle: CliBundle;
|
|
228
|
+
entryPoint: string;
|
|
229
|
+
compositionId: string;
|
|
230
|
+
outputPath: string;
|
|
231
|
+
fps: number;
|
|
232
|
+
width: number;
|
|
233
|
+
height: number;
|
|
234
|
+
webpackOverride?: (config: unknown) => unknown;
|
|
235
|
+
onLog: (message: string) => void;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function renderScenario(config: RenderConfig): Promise<void> {
|
|
239
|
+
const bundler = await import("@remotion/bundler").catch(() => {
|
|
240
|
+
throw new Error(
|
|
241
|
+
"Could not load @remotion/bundler.\nInstall it: pnpm add @remotion/bundler@4.0.448",
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const renderer = await import("@remotion/renderer").catch(() => {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"Could not load @remotion/renderer.\nInstall it: pnpm add @remotion/renderer@4.0.448",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
config.onLog("Bundling Remotion project...");
|
|
252
|
+
|
|
253
|
+
const bundleOptions: Record<string, unknown> = { entryPoint: config.entryPoint };
|
|
254
|
+
if (config.webpackOverride) {
|
|
255
|
+
bundleOptions.webpackOverride = config.webpackOverride;
|
|
256
|
+
}
|
|
257
|
+
const serveUrl = await (bundler.bundle as (opts: Record<string, unknown>) => Promise<string>)(
|
|
258
|
+
bundleOptions,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
config.onLog(`Selecting composition: ${config.compositionId}`);
|
|
262
|
+
const composition = await renderer.selectComposition({
|
|
263
|
+
serveUrl,
|
|
264
|
+
id: config.compositionId,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
config.onLog(
|
|
268
|
+
`Composition: ${composition.width}x${composition.height} @ ${composition.fps}fps, ` +
|
|
269
|
+
`${composition.durationInFrames} frames ` +
|
|
270
|
+
`(${(composition.durationInFrames / composition.fps).toFixed(1)}s)`,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const outDir = resolve(config.outputPath, "..");
|
|
274
|
+
await mkdir(outDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
config.onLog("Rendering...");
|
|
277
|
+
await renderer.renderMedia({
|
|
278
|
+
composition,
|
|
279
|
+
serveUrl,
|
|
280
|
+
codec: "h264" as const,
|
|
281
|
+
outputLocation: config.outputPath,
|
|
282
|
+
onProgress: ({ progress }: { progress: number }) => {
|
|
283
|
+
const pct = Math.round(progress * 100);
|
|
284
|
+
config.onLog(` Progress: ${pct}%`);
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** File served for a directory request ("/" or any path ending in "/"). */
|
|
2
|
+
export declare const INDEX_FILE = "index.html";
|
|
3
|
+
/** Content type for an extension the bundle contract does not recognise. */
|
|
4
|
+
export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve an incoming request URL path to a clean, bundle-relative file path —
|
|
7
|
+
* or null if the request is malformed or attempts to escape the bundle root.
|
|
8
|
+
*
|
|
9
|
+
* Pure: no filesystem access. The caller maps the returned relative path onto
|
|
10
|
+
* disk (and 404s if the file is absent). This is the security-critical core of
|
|
11
|
+
* the local server, so it is isolated and exhaustively unit-tested:
|
|
12
|
+
* - percent-decoding (rejecting malformed encodings),
|
|
13
|
+
* - directory requests mapped to {@link INDEX_FILE},
|
|
14
|
+
* - rejection of "", ".", ".." segments and backslash/NUL injection.
|
|
15
|
+
*
|
|
16
|
+
* A defence-in-depth absolute-path containment check still runs in the server
|
|
17
|
+
* after this resolves, but this function alone must never yield a traversing
|
|
18
|
+
* path.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveStaticPath(urlPath: string): string | null;
|
|
21
|
+
/** Canonical content type for a bundle file path, keyed on its final extension. */
|
|
22
|
+
export declare function contentTypeFor(relativePath: string): string;
|
|
23
|
+
//# sourceMappingURL=request-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-path.d.ts","sourceRoot":"","sources":["../../../src/serve/request-path.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBhE;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG3D"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CONTENT_TYPE_BY_EXTENSION, finalExtension } from "../pack/bundle-contract.js";
|
|
2
|
+
/** File served for a directory request ("/" or any path ending in "/"). */
|
|
3
|
+
export const INDEX_FILE = "index.html";
|
|
4
|
+
/** Content type for an extension the bundle contract does not recognise. */
|
|
5
|
+
export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve an incoming request URL path to a clean, bundle-relative file path —
|
|
8
|
+
* or null if the request is malformed or attempts to escape the bundle root.
|
|
9
|
+
*
|
|
10
|
+
* Pure: no filesystem access. The caller maps the returned relative path onto
|
|
11
|
+
* disk (and 404s if the file is absent). This is the security-critical core of
|
|
12
|
+
* the local server, so it is isolated and exhaustively unit-tested:
|
|
13
|
+
* - percent-decoding (rejecting malformed encodings),
|
|
14
|
+
* - directory requests mapped to {@link INDEX_FILE},
|
|
15
|
+
* - rejection of "", ".", ".." segments and backslash/NUL injection.
|
|
16
|
+
*
|
|
17
|
+
* A defence-in-depth absolute-path containment check still runs in the server
|
|
18
|
+
* after this resolves, but this function alone must never yield a traversing
|
|
19
|
+
* path.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveStaticPath(urlPath) {
|
|
22
|
+
// Keep only the path; drop any query string or fragment defensively.
|
|
23
|
+
const pathOnly = urlPath.split(/[?#]/, 1)[0] ?? "";
|
|
24
|
+
let decoded;
|
|
25
|
+
try {
|
|
26
|
+
decoded = decodeURIComponent(pathOnly);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null; // malformed percent-encoding
|
|
30
|
+
}
|
|
31
|
+
// Requests are absolute-path form; reject separator/NUL injection outright.
|
|
32
|
+
if (!decoded.startsWith("/"))
|
|
33
|
+
return null;
|
|
34
|
+
if (decoded.includes("\0") || decoded.includes("\\"))
|
|
35
|
+
return null;
|
|
36
|
+
// A directory request ("/" or trailing slash) serves the index document.
|
|
37
|
+
const withIndex = decoded === "/" || decoded.endsWith("/") ? decoded + INDEX_FILE : decoded;
|
|
38
|
+
const segments = withIndex.replace(/^\/+/, "").split("/");
|
|
39
|
+
for (const segment of segments) {
|
|
40
|
+
// Empty (double-slash), current-dir, and parent-dir segments are all
|
|
41
|
+
// rejected: the charset is otherwise permissive and "." is a valid char.
|
|
42
|
+
if (segment === "" || segment === "." || segment === "..")
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return segments.join("/");
|
|
46
|
+
}
|
|
47
|
+
/** Canonical content type for a bundle file path, keyed on its final extension. */
|
|
48
|
+
export function contentTypeFor(relativePath) {
|
|
49
|
+
const ext = finalExtension(relativePath);
|
|
50
|
+
return CONTENT_TYPE_BY_EXTENSION[ext] ?? DEFAULT_CONTENT_TYPE;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=request-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-path.js","sourceRoot":"","sources":["../../../src/serve/request-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEvF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC;AAEvC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,qEAAqE;IACrE,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAEnD,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,6BAA6B;IAC5C,CAAC;IAED,4EAA4E;IAC5E,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAElE,yEAAyE;IACzE,MAAM,SAAS,GAAG,OAAO,KAAK,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;IAE5F,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,qEAAqE;QACrE,yEAAyE;QACzE,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;IACzE,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,YAAoB;IACjD,MAAM,GAAG,GAAG,cAAc,CAAC,YAAY,CAA2C,CAAC;IACnF,OAAO,yBAAyB,CAAC,GAAG,CAAC,IAAI,oBAAoB,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CONTENT_TYPE_BY_EXTENSION, finalExtension } from "../pack/bundle-contract.js";
|
|
2
|
+
|
|
3
|
+
/** File served for a directory request ("/" or any path ending in "/"). */
|
|
4
|
+
export const INDEX_FILE = "index.html";
|
|
5
|
+
|
|
6
|
+
/** Content type for an extension the bundle contract does not recognise. */
|
|
7
|
+
export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve an incoming request URL path to a clean, bundle-relative file path —
|
|
11
|
+
* or null if the request is malformed or attempts to escape the bundle root.
|
|
12
|
+
*
|
|
13
|
+
* Pure: no filesystem access. The caller maps the returned relative path onto
|
|
14
|
+
* disk (and 404s if the file is absent). This is the security-critical core of
|
|
15
|
+
* the local server, so it is isolated and exhaustively unit-tested:
|
|
16
|
+
* - percent-decoding (rejecting malformed encodings),
|
|
17
|
+
* - directory requests mapped to {@link INDEX_FILE},
|
|
18
|
+
* - rejection of "", ".", ".." segments and backslash/NUL injection.
|
|
19
|
+
*
|
|
20
|
+
* A defence-in-depth absolute-path containment check still runs in the server
|
|
21
|
+
* after this resolves, but this function alone must never yield a traversing
|
|
22
|
+
* path.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveStaticPath(urlPath: string): string | null {
|
|
25
|
+
// Keep only the path; drop any query string or fragment defensively.
|
|
26
|
+
const pathOnly = urlPath.split(/[?#]/, 1)[0] ?? "";
|
|
27
|
+
|
|
28
|
+
let decoded: string;
|
|
29
|
+
try {
|
|
30
|
+
decoded = decodeURIComponent(pathOnly);
|
|
31
|
+
} catch {
|
|
32
|
+
return null; // malformed percent-encoding
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Requests are absolute-path form; reject separator/NUL injection outright.
|
|
36
|
+
if (!decoded.startsWith("/")) return null;
|
|
37
|
+
if (decoded.includes("\0") || decoded.includes("\\")) return null;
|
|
38
|
+
|
|
39
|
+
// A directory request ("/" or trailing slash) serves the index document.
|
|
40
|
+
const withIndex = decoded === "/" || decoded.endsWith("/") ? decoded + INDEX_FILE : decoded;
|
|
41
|
+
|
|
42
|
+
const segments = withIndex.replace(/^\/+/, "").split("/");
|
|
43
|
+
for (const segment of segments) {
|
|
44
|
+
// Empty (double-slash), current-dir, and parent-dir segments are all
|
|
45
|
+
// rejected: the charset is otherwise permissive and "." is a valid char.
|
|
46
|
+
if (segment === "" || segment === "." || segment === "..") return null;
|
|
47
|
+
}
|
|
48
|
+
return segments.join("/");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Canonical content type for a bundle file path, keyed on its final extension. */
|
|
52
|
+
export function contentTypeFor(relativePath: string): string {
|
|
53
|
+
const ext = finalExtension(relativePath) as keyof typeof CONTENT_TYPE_BY_EXTENSION;
|
|
54
|
+
return CONTENT_TYPE_BY_EXTENSION[ext] ?? DEFAULT_CONTENT_TYPE;
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Viewport } from "../pack/viewport.js";
|
|
2
|
+
import { type BundleServerHandle } from "./static-server.js";
|
|
3
|
+
/** Options for {@link runServe}. */
|
|
4
|
+
export interface RunServeOptions {
|
|
5
|
+
readonly bundleDir: string;
|
|
6
|
+
/** Port to bind; 0 lets the OS choose a free one. */
|
|
7
|
+
readonly port: number;
|
|
8
|
+
/** Host/interface to bind (default: localhost). */
|
|
9
|
+
readonly host?: string;
|
|
10
|
+
}
|
|
11
|
+
/** A running preview server plus the derived presentation details. */
|
|
12
|
+
export interface ServeResult {
|
|
13
|
+
/** The running server (call .close() to stop). */
|
|
14
|
+
readonly handle: BundleServerHandle;
|
|
15
|
+
/** The browser-clickable URL (https→http downgraded for *.localhost). */
|
|
16
|
+
readonly url: string;
|
|
17
|
+
/** A ready-to-paste responsive <iframe> snippet for this URL. */
|
|
18
|
+
readonly snippet: string;
|
|
19
|
+
/** The viewport used for the snippet (recorded, or the default). */
|
|
20
|
+
readonly viewport: Viewport;
|
|
21
|
+
/** Whether the snippet's viewport came from the bundle (vs. the default). */
|
|
22
|
+
readonly recordedViewport: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validate a packed bundle directory and start serving it — the orchestration
|
|
26
|
+
* behind `scenar serve`, with no process/exit coupling so both the CLI command
|
|
27
|
+
* and the MCP server can call it. The returned handle keeps running until the
|
|
28
|
+
* caller closes it. Throws on an invalid bundle or an unavailable port.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runServe(options: RunServeOptions): Promise<ServeResult>;
|
|
31
|
+
//# sourceMappingURL=run-serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-serve.d.ts","sourceRoot":"","sources":["../../../src/serve/run-serve.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAKhF,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CA2B7E"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { buildEmbedSnippet } from "../deploy/embed-snippet.js";
|
|
4
|
+
import { localViewUrl } from "../deploy/deploy-flow.js";
|
|
5
|
+
import { readBundleViewport } from "../bundle/read-viewport.js";
|
|
6
|
+
import { startBundleServer } from "./static-server.js";
|
|
7
|
+
/** The servable entry every packed bundle contains at its root. */
|
|
8
|
+
const INDEX_FILE = "index.html";
|
|
9
|
+
/**
|
|
10
|
+
* Validate a packed bundle directory and start serving it — the orchestration
|
|
11
|
+
* behind `scenar serve`, with no process/exit coupling so both the CLI command
|
|
12
|
+
* and the MCP server can call it. The returned handle keeps running until the
|
|
13
|
+
* caller closes it. Throws on an invalid bundle or an unavailable port.
|
|
14
|
+
*/
|
|
15
|
+
export async function runServe(options) {
|
|
16
|
+
const resolvedDir = resolve(options.bundleDir);
|
|
17
|
+
const info = await stat(resolvedDir).catch(() => null);
|
|
18
|
+
if (!info || !info.isDirectory()) {
|
|
19
|
+
throw new Error(`${options.bundleDir} is not a directory. Pass a bundle produced by \`scenar pack\`.`);
|
|
20
|
+
}
|
|
21
|
+
const indexInfo = await stat(join(resolvedDir, INDEX_FILE)).catch(() => null);
|
|
22
|
+
if (!indexInfo || !indexInfo.isFile()) {
|
|
23
|
+
throw new Error(`no ${INDEX_FILE} in ${options.bundleDir}. Run \`scenar pack\` to produce a servable bundle first.`);
|
|
24
|
+
}
|
|
25
|
+
const handle = await startBundleServer({
|
|
26
|
+
rootDir: resolvedDir,
|
|
27
|
+
port: options.port,
|
|
28
|
+
host: options.host,
|
|
29
|
+
});
|
|
30
|
+
const url = localViewUrl(handle.url);
|
|
31
|
+
const { viewport, recorded } = await readBundleViewport(resolvedDir);
|
|
32
|
+
const snippet = buildEmbedSnippet({ embedUrl: url, viewport });
|
|
33
|
+
return { handle, url, snippet, viewport, recordedViewport: recorded };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=run-serve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-serve.js","sourceRoot":"","sources":["../../../src/serve/run-serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAA2B,MAAM,oBAAoB,CAAC;AAEhF,mEAAmE;AACnE,MAAM,UAAU,GAAG,YAAY,CAAC;AAyBhC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAwB;IACrD,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAE/C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACvD,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,GAAG,OAAO,CAAC,SAAS,iEAAiE,CACtF,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC9E,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,MAAM,UAAU,OAAO,OAAO,CAAC,SAAS,2DAA2D,CACpG,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC;QACrC,OAAO,EAAE,WAAW;QACpB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;KACnB,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,iBAAiB,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IAE/D,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { buildEmbedSnippet } from "../deploy/embed-snippet.js";
|
|
4
|
+
import { localViewUrl } from "../deploy/deploy-flow.js";
|
|
5
|
+
import { readBundleViewport } from "../bundle/read-viewport.js";
|
|
6
|
+
import type { Viewport } from "../pack/viewport.js";
|
|
7
|
+
import { startBundleServer, type BundleServerHandle } from "./static-server.js";
|
|
8
|
+
|
|
9
|
+
/** The servable entry every packed bundle contains at its root. */
|
|
10
|
+
const INDEX_FILE = "index.html";
|
|
11
|
+
|
|
12
|
+
/** Options for {@link runServe}. */
|
|
13
|
+
export interface RunServeOptions {
|
|
14
|
+
readonly bundleDir: string;
|
|
15
|
+
/** Port to bind; 0 lets the OS choose a free one. */
|
|
16
|
+
readonly port: number;
|
|
17
|
+
/** Host/interface to bind (default: localhost). */
|
|
18
|
+
readonly host?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A running preview server plus the derived presentation details. */
|
|
22
|
+
export interface ServeResult {
|
|
23
|
+
/** The running server (call .close() to stop). */
|
|
24
|
+
readonly handle: BundleServerHandle;
|
|
25
|
+
/** The browser-clickable URL (https→http downgraded for *.localhost). */
|
|
26
|
+
readonly url: string;
|
|
27
|
+
/** A ready-to-paste responsive <iframe> snippet for this URL. */
|
|
28
|
+
readonly snippet: string;
|
|
29
|
+
/** The viewport used for the snippet (recorded, or the default). */
|
|
30
|
+
readonly viewport: Viewport;
|
|
31
|
+
/** Whether the snippet's viewport came from the bundle (vs. the default). */
|
|
32
|
+
readonly recordedViewport: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate a packed bundle directory and start serving it — the orchestration
|
|
37
|
+
* behind `scenar serve`, with no process/exit coupling so both the CLI command
|
|
38
|
+
* and the MCP server can call it. The returned handle keeps running until the
|
|
39
|
+
* caller closes it. Throws on an invalid bundle or an unavailable port.
|
|
40
|
+
*/
|
|
41
|
+
export async function runServe(options: RunServeOptions): Promise<ServeResult> {
|
|
42
|
+
const resolvedDir = resolve(options.bundleDir);
|
|
43
|
+
|
|
44
|
+
const info = await stat(resolvedDir).catch(() => null);
|
|
45
|
+
if (!info || !info.isDirectory()) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`${options.bundleDir} is not a directory. Pass a bundle produced by \`scenar pack\`.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const indexInfo = await stat(join(resolvedDir, INDEX_FILE)).catch(() => null);
|
|
51
|
+
if (!indexInfo || !indexInfo.isFile()) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`no ${INDEX_FILE} in ${options.bundleDir}. Run \`scenar pack\` to produce a servable bundle first.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handle = await startBundleServer({
|
|
58
|
+
rootDir: resolvedDir,
|
|
59
|
+
port: options.port,
|
|
60
|
+
host: options.host,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const url = localViewUrl(handle.url);
|
|
64
|
+
const { viewport, recorded } = await readBundleViewport(resolvedDir);
|
|
65
|
+
const snippet = buildEmbedSnippet({ embedUrl: url, viewport });
|
|
66
|
+
|
|
67
|
+
return { handle, url, snippet, viewport, recordedViewport: recorded };
|
|
68
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
/** A running bundle server: where it is reachable and how to stop it. */
|
|
3
|
+
export interface BundleServerHandle {
|
|
4
|
+
/** The base URL the bundle is served at (e.g. http://localhost:4173/). */
|
|
5
|
+
readonly url: string;
|
|
6
|
+
/** The port the server actually bound (useful when port 0 was requested). */
|
|
7
|
+
readonly port: number;
|
|
8
|
+
/** Stop accepting connections and release the port. */
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** Options for {@link startBundleServer}. */
|
|
12
|
+
export interface StartBundleServerOptions {
|
|
13
|
+
/** Bundle directory to serve (a `scenar pack` output). */
|
|
14
|
+
readonly rootDir: string;
|
|
15
|
+
/** Port to bind; 0 lets the OS pick a free one (handy in tests). */
|
|
16
|
+
readonly port: number;
|
|
17
|
+
/** Host/interface to bind. Defaults to "localhost". */
|
|
18
|
+
readonly host?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create (but do not start) an HTTP server that serves a packed bundle as a
|
|
22
|
+
* static site. Only GET/HEAD are allowed; every response carries the bundle
|
|
23
|
+
* contract's canonical content type and `Cache-Control: no-cache` (a local
|
|
24
|
+
* preview should always reflect the latest pack, never a stale asset).
|
|
25
|
+
*
|
|
26
|
+
* No CSP or other security headers are emitted: that envelope is the serving
|
|
27
|
+
* edge's responsibility (and is enforced + tested in scenar-cloud). A local
|
|
28
|
+
* preview deliberately matches the headerless behaviour of a plain static host
|
|
29
|
+
* such as GitHub Pages — the other Phase 1 target — so what you see locally is
|
|
30
|
+
* what a `scenar publish` visitor sees.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createBundleServer(rootDir: string): Server;
|
|
33
|
+
/**
|
|
34
|
+
* Start an HTTP server for a packed bundle and resolve once it is listening.
|
|
35
|
+
* Rejects if the port is already in use (the caller surfaces a clear message).
|
|
36
|
+
*/
|
|
37
|
+
export declare function startBundleServer(options: StartBundleServerOptions): Promise<BundleServerHandle>;
|
|
38
|
+
//# sourceMappingURL=static-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-server.d.ts","sourceRoot":"","sources":["../../../src/serve/static-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAMjG,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ1D;AA4CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuBhG"}
|