Skip to content

Commit 6d7f40c

Browse files
authored
Merge pull request #1005 from Automattic/issue/replay-export-snapshot-scoping
Scope replay export snapshots
2 parents a0a9b08 + 9d0b396 commit 6d7f40c

7 files changed

Lines changed: 279 additions & 12 deletions

File tree

packages/runtime-core/src/command-registry.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ export interface CommandDefinition {
1818
handler: CommandHandlerBinding
1919
}
2020

21+
const snapshotScopingAcceptedArgs: CommandDefinition["acceptedArgs"] = [
22+
{ name: "snapshot-include-wp-content", description: "Comma-separated wp-content-relative paths to include in the runtime snapshot. Defaults to all non-excluded paths.", format: "string" },
23+
{ name: "snapshot-exclude-wp-content", description: "Comma-separated wp-content-relative paths to exclude from the runtime snapshot.", format: "string" },
24+
{ name: "snapshot-database-tables", description: "Comma-separated database table base names to include, such as posts,postmeta,options.", format: "string" },
25+
{ name: "snapshot-exclude-database-tables", description: "Comma-separated database table base names to exclude from the runtime snapshot.", format: "string" },
26+
{ name: "snapshot-option-names", description: "Comma-separated option_name values or LIKE patterns using * for the options table.", format: "string" },
27+
{ name: "snapshot-post-types", description: "Comma-separated post types used to scope posts and postmeta table exports.", format: "string" },
28+
]
29+
2130
export const commandRegistry = [
2231
{
2332
id: "inspect-mounted-inputs",
@@ -57,6 +66,7 @@ export const commandRegistry = [
5766
description: "Capture the current WordPress runtime state as a portable state bundle source for replayable Playground artifacts.",
5867
acceptedArgs: [
5968
{ name: "label", description: "Optional human-readable capture label recorded in the command output.", format: "string" },
69+
...snapshotScopingAcceptedArgs,
6070
],
6171
outputShape: "wp-codebox/wordpress-state-bundle-capture/v1 JSON with runtime snapshot id, artifact refs, replay status, and capture summary.",
6272
policyRequirement: "Runtime policy commands must include wordpress.capture-state-bundle.",
@@ -71,6 +81,7 @@ export const commandRegistry = [
7181
{ name: "output-dir", description: "Optional package directory relative to the runtime artifact root; defaults to files/replay-package.", format: "relative path" },
7282
{ name: "landing-page", description: "Optional replay landing page recorded in blueprint.after.json.", format: "path" },
7383
{ name: "import-ms", description: "Optional importer duration supplied by the caller so replay export metrics can include the preceding import phase.", format: "non-negative integer" },
84+
...snapshotScopingAcceptedArgs,
7485
],
7586
outputShape: "wp-codebox/wordpress-replay-export/v1 JSON with import/materialization/snapshot/export metrics and manifest, blueprint.after.json, blueprint.after-notes.json, and files/runtime-snapshot.json artifact paths.",
7687
policyRequirement: "Runtime policy commands must include wordpress.export-replay-package.",

packages/runtime-core/src/runtime-contracts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ export interface Runtime {
859859
mount(spec: MountSpec): Promise<void>
860860
execute(spec: ExecutionSpec): Promise<ExecutionResult>
861861
observe(spec: ObservationSpec): Promise<ObservationResult>
862-
snapshot(): Promise<Snapshot>
862+
snapshot(options?: unknown): Promise<Snapshot>
863863
collectArtifacts(spec?: ArtifactSpec): Promise<ArtifactBundle>
864864
destroy(): Promise<void>
865865
}

packages/runtime-playground/src/playground-runtime.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { PlaygroundCliServer } from "./preview-server.js"
1616
import { collectPlaygroundArtifacts } from "./runtime-artifact-helpers.js"
1717
import { materializePlaygroundMountsFromVfs } from "./mount-materialization.js"
1818
import { runAbilityCommand, runBenchCommand, runCorePhpunitCommand, runPhpCommand, runPhpunitCommand, runPluginCheckCommand, runRestRequestCommand, runThemeCheckCommand } from "./wordpress-command-runners.js"
19-
import { PlaygroundSnapshotRestoreError, contentDigest, mountsFromSnapshot, runtimeSnapshotExportPayload, runtimeSnapshotExportPhp, runtimeSnapshotPayload, runtimeSnapshotRestorePhp, runtimeSpecFromSnapshot, snapshotDigest, type RuntimeSnapshotArtifact } from "./runtime-snapshot.js"
19+
import { PlaygroundSnapshotRestoreError, contentDigest, mountsFromSnapshot, runtimeSnapshotExportPayload, runtimeSnapshotExportPhp, runtimeSnapshotPayload, runtimeSnapshotRestorePhp, runtimeSpecFromSnapshot, snapshotDigest, type RuntimeSnapshotArtifact, type RuntimeSnapshotExportOptions } from "./runtime-snapshot.js"
2020
import { createRuntimeWpCliBridge, type RuntimeWpCliBridge } from "./runtime-wp-cli-bridge.js"
2121
import { writeReplayExportPackage } from "./replayable-wordpress-site-bundle.js"
2222
import { preflightPhpWasmRuntimeAssets } from "./php-wasm-preflight.js"
@@ -344,10 +344,10 @@ class PlaygroundRuntime implements Runtime {
344344
return observation
345345
}
346346

347-
async snapshot(): Promise<Snapshot> {
347+
async snapshot(options: RuntimeSnapshotExportOptions = {}): Promise<Snapshot> {
348348
const snapshotId = id("snapshot")
349349
const createdAt = now()
350-
const payload = await this.captureRuntimeSnapshotArtifact(snapshotId, createdAt)
350+
const payload = await this.captureRuntimeSnapshotArtifact(snapshotId, createdAt, options)
351351
const artifactPath = `files/runtime-snapshots/${snapshotId}.json`
352352
const absoluteArtifactPath = join(this.artifactRoot, artifactPath)
353353
const artifactJson = `${JSON.stringify(payload, null, 2)}\n`
@@ -398,10 +398,10 @@ class PlaygroundRuntime implements Runtime {
398398
return snapshot
399399
}
400400

401-
private async captureRuntimeSnapshotArtifact(snapshotId: string, createdAt: string): Promise<RuntimeSnapshotArtifact> {
401+
private async captureRuntimeSnapshotArtifact(snapshotId: string, createdAt: string, options: RuntimeSnapshotExportOptions = {}): Promise<RuntimeSnapshotArtifact> {
402402
const server = await this.bootPlayground()
403403
const response = await this.runPlaygroundCommand("runtime.snapshot", server, {
404-
code: bootstrapPhpCode(this.spec, runtimeSnapshotExportPhp({ excludedWpContentPaths: this.snapshotExcludedWpContentPaths() }), []),
404+
code: bootstrapPhpCode(this.spec, runtimeSnapshotExportPhp({ ...options, excludedWpContentPaths: [...this.snapshotExcludedWpContentPaths(), ...(options.excludedWpContentPaths ?? [])] }), []),
405405
})
406406
assertPlaygroundResponseOk("runtime.snapshot", response)
407407
const captured = await runtimeSnapshotExportPayload(server, response.text)
@@ -800,7 +800,9 @@ class PlaygroundRuntime implements Runtime {
800800

801801
async runCaptureStateBundle(spec: ExecutionSpec): Promise<string> {
802802
const label = stringArg(spec.args ?? [], "label")
803-
const snapshot = await this.snapshot()
803+
const snapshotOptions = snapshotOptionsFromArgs(spec.args ?? [])
804+
const snapshot = await this.snapshot(snapshotOptions)
805+
const snapshotOptionsMetadata = hasSnapshotOptions(snapshotOptions) ? { snapshotOptions } : {}
804806
const summary = snapshot.metadata.summary && typeof snapshot.metadata.summary === "object" && !Array.isArray(snapshot.metadata.summary)
805807
? snapshot.metadata.summary as Record<string, unknown>
806808
: {}
@@ -820,6 +822,7 @@ class PlaygroundRuntime implements Runtime {
820822
summary: {
821823
databaseTables: summary.databaseTables ?? 0,
822824
wpContentFiles: summary.wpContentFiles ?? 0,
825+
...snapshotOptionsMetadata,
823826
},
824827
}, null, 2)}\n`
825828
}
@@ -829,6 +832,8 @@ class PlaygroundRuntime implements Runtime {
829832
const landingPage = stringArg(spec.args ?? [], "landing-page")
830833
const outputDirectory = replayExportOutputDirectory(this.artifactRoot, stringArg(spec.args ?? [], "output-dir"))
831834
const importMs = nonNegativeIntegerStringArg(spec.args ?? [], "import-ms") ?? 0
835+
const snapshotOptions = snapshotOptionsFromArgs(spec.args ?? [])
836+
const snapshotOptionsMetadata = hasSnapshotOptions(snapshotOptions) ? { snapshotOptions } : {}
832837

833838
const materializeStartedAtMs = Date.now()
834839
let materialization: Awaited<ReturnType<typeof materializePlaygroundMountsFromVfs>> | undefined
@@ -841,7 +846,7 @@ class PlaygroundRuntime implements Runtime {
841846
const materializeMs = Date.now() - materializeStartedAtMs
842847

843848
const snapshotStartedAtMs = Date.now()
844-
const snapshot = await this.snapshot()
849+
const snapshot = await this.snapshot(snapshotOptions)
845850
const snapshotMs = Date.now() - snapshotStartedAtMs
846851
const payload = await runtimeSnapshotPayload(snapshot)
847852

@@ -857,6 +862,7 @@ class PlaygroundRuntime implements Runtime {
857862
command: "wordpress.export-replay-package",
858863
runtimeId: this.runtimeId,
859864
snapshotId: snapshot.id,
865+
...snapshotOptionsMetadata,
860866
artifactRoot: this.artifactRoot,
861867
...(materialization ? { materialization } : {}),
862868
},
@@ -882,6 +888,7 @@ class PlaygroundRuntime implements Runtime {
882888
contentDigest: replayPackage.manifest.contentDigest,
883889
createdAt: replayPackage.manifest.createdAt,
884890
},
891+
...snapshotOptionsMetadata,
885892
}, null, 2)}\n`
886893
}
887894

@@ -1137,6 +1144,39 @@ function nonNegativeIntegerStringArg(args: string[], name: string): number | und
11371144
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined
11381145
}
11391146

1147+
function stringListArg(args: string[], name: string): string[] | undefined {
1148+
const value = stringArg(args, name)
1149+
if (!value) {
1150+
return undefined
1151+
}
1152+
1153+
const values = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
1154+
return values.length > 0 ? values : undefined
1155+
}
1156+
1157+
function snapshotOptionsFromArgs(args: string[]): RuntimeSnapshotExportOptions {
1158+
const options: RuntimeSnapshotExportOptions = {}
1159+
const includedWpContentPaths = stringListArg(args, "snapshot-include-wp-content")
1160+
const excludedWpContentPaths = stringListArg(args, "snapshot-exclude-wp-content")
1161+
const includedDatabaseTables = stringListArg(args, "snapshot-database-tables")
1162+
const excludedDatabaseTables = stringListArg(args, "snapshot-exclude-database-tables")
1163+
const includedOptionNames = stringListArg(args, "snapshot-option-names")
1164+
const includedPostTypes = stringListArg(args, "snapshot-post-types")
1165+
1166+
if (includedWpContentPaths) options.includedWpContentPaths = includedWpContentPaths
1167+
if (excludedWpContentPaths) options.excludedWpContentPaths = excludedWpContentPaths
1168+
if (includedDatabaseTables) options.includedDatabaseTables = includedDatabaseTables
1169+
if (excludedDatabaseTables) options.excludedDatabaseTables = excludedDatabaseTables
1170+
if (includedOptionNames) options.includedOptionNames = includedOptionNames
1171+
if (includedPostTypes) options.includedPostTypes = includedPostTypes
1172+
1173+
return options
1174+
}
1175+
1176+
function hasSnapshotOptions(options: RuntimeSnapshotExportOptions): boolean {
1177+
return Object.keys(options).length > 0
1178+
}
1179+
11401180
function replayExportOutputDirectory(artifactRoot: string, requested: string | undefined): string {
11411181
const relativePath = requested?.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "") || "files/replay-package"
11421182
if (relativePath.length === 0 || relativePath.includes("..")) {

0 commit comments

Comments
 (0)