Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1fe8122
docs: add Blueprints V2 TypeScript runner design and implementation plan
brandonpayton Mar 30, 2026
9c92b6c
feat(blueprints): add V2 TypeScript runner type definitions
brandonpayton Mar 30, 2026
62fbabc
feat(blueprints): add V2 step handler registry
brandonpayton Mar 30, 2026
c58f910
feat(blueprints): add V2 compilation pipeline skeleton
brandonpayton Mar 30, 2026
534ca09
feat(blueprints): add V2 module barrel export and update package exports
brandonpayton Mar 30, 2026
7877959
feat(blueprints): implement V2 data reference resolver
brandonpayton Mar 30, 2026
e98c399
feat(blueprints): implement V2 blueprint validation
brandonpayton Mar 30, 2026
f37c350
feat(blueprints): implement V2 runtime configuration extraction
brandonpayton Mar 30, 2026
e3d96a1
feat(blueprints): implement declarative-to-step transpilation
brandonpayton Mar 30, 2026
91bdff4
feat(blueprints): implement V2 step execution loop
brandonpayton Mar 30, 2026
ce63b2f
feat(blueprints): implement V2 step handlers (filesystem, constants, …
brandonpayton Mar 30, 2026
4310365
feat(blueprints): implement V2 step handlers (plugin, theme, language…
brandonpayton Mar 30, 2026
6e573be
feat(blueprints): implement V1 to V2 blueprint transpiler
brandonpayton Mar 30, 2026
cba9e94
feat(blueprints): implement V2 blueprint merge algorithm
brandonpayton Mar 30, 2026
9c21862
feat(playground): integrate V2 TS blueprint runner into CLI and websi…
brandonpayton Mar 30, 2026
99d970b
chore(blueprints): remove PHP .phar runner files
brandonpayton Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 381 additions & 0 deletions docs/plans/2026-03-30-blueprints-v2-ts-runner-design.md

Large diffs are not rendered by default.

1,663 changes: 1,663 additions & 0 deletions docs/plans/2026-03-30-blueprints-v2-ts-runner-plan.md

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions packages/playground/blueprints/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,28 @@ export {
} from './lib/v1/resources';
export * from './lib/steps';
export * from './lib/steps/handlers';
// V2 Blueprint types (keep existing type exports for now)
export type {
BlueprintV2,
BlueprintV2Declaration,
RawBlueprintV2Data,
ParsedBlueprintV1orV2String as ParsedBlueprintV2String,
} from './lib/v2/blueprint-v2-declaration';
export { getV2Runner } from './lib/v2/get-v2-runner';
export { runBlueprintV2 } from './lib/v2/run-blueprint-v2';
export type { BlueprintMessage } from './lib/v2/run-blueprint-v2';

// V2 TypeScript runner (new)
export {
compileBlueprintV2,
InvalidBlueprintV2Error,
BlueprintV2StepExecutionError,
DataReferenceResolutionError,
BlueprintMergeConflictError,
} from './lib/v2/index';

export type {
CompiledBlueprintV2,
CompileBlueprintV2Options,
V2RuntimeConfig,
} from './lib/v2/index';

export {
resolveRemoteBlueprint,
Expand Down
103 changes: 103 additions & 0 deletions packages/playground/blueprints/src/lib/v2/compile/compile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, it, expect } from 'vitest';
import { extractRuntimeConfig } from './compile';
import type { BlueprintV2Declaration } from '../types';

describe('extractRuntimeConfig', () => {
it('should extract a simple PHP version string', () => {
const blueprint: BlueprintV2Declaration = {
version: 2,
phpVersion: '8.1',
};
const config = extractRuntimeConfig(blueprint);
expect(config.phpVersion).toEqual({
preferred: '8.1',
});
});

it('should extract a PHP version constraint object', () => {
const blueprint = {
version: 2,
phpVersion: {
min: '8.0',
recommended: '8.2',
max: '8.4',
},
} as BlueprintV2Declaration;
const config = extractRuntimeConfig(blueprint);
expect(config.phpVersion).toEqual({
min: '8.0',
max: '8.4',
preferred: '8.2',
});
});

it('should extract a WordPress version string', () => {
const blueprint: BlueprintV2Declaration = {
version: 2,
wordpressVersion: '6.4',
};
const config = extractRuntimeConfig(blueprint);
expect(config.wordpressVersion).toEqual({
preferred: '6.4',
});
});

it('should extract application options', () => {
const blueprint: BlueprintV2Declaration = {
version: 2,
applicationOptions: {
'wordpress-playground': {
landingPage: '/wp-admin/plugins.php',
login: true,
networkAccess: false,
},
},
};
const config = extractRuntimeConfig(blueprint);
expect(config.applicationOptions).toEqual({
'wordpress-playground': {
landingPage: '/wp-admin/plugins.php',
login: true,
networkAccess: false,
},
});
});

it('should return an empty config for a minimal blueprint', () => {
const blueprint: BlueprintV2Declaration = {
version: 2,
};
const config = extractRuntimeConfig(blueprint);
expect(config).toEqual({});
});

it('should handle "latest" as a version string', () => {
const blueprint: BlueprintV2Declaration = {
version: 2,
phpVersion: 'latest',
wordpressVersion: 'latest',
};
const config = extractRuntimeConfig(blueprint);
expect(config.phpVersion).toEqual({
preferred: 'latest',
});
expect(config.wordpressVersion).toEqual({
preferred: 'latest',
});
});

it('should extract a WordPress version constraint object with preferred', () => {
const blueprint = {
version: 2,
wordpressVersion: {
min: '6.2',
preferred: '6.4',
},
} as BlueprintV2Declaration;
const config = extractRuntimeConfig(blueprint);
expect(config.wordpressVersion).toEqual({
min: '6.2',
preferred: '6.4',
});
});
});
201 changes: 201 additions & 0 deletions packages/playground/blueprints/src/lib/v2/compile/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { ProgressTracker } from '@php-wasm/progress';
import type { UniversalPHP } from '@php-wasm/universal';
import type {
BlueprintV2Declaration,
CompiledBlueprintV2,
CompiledV2Step,
CompileBlueprintV2Options,
V2RuntimeConfig,
V2VersionConstraint,
StepExecutionContext,
} from '../types';
import {
BlueprintV2StepExecutionError,
InvalidBlueprintV2Error,
} from '../types';
import type { DataReferenceResolverConfig } from '../data-references/types';
import { DataReferenceResolverImpl } from '../data-references/resolver';
import { v2StepHandlers } from '../steps/index';
import { validateBlueprintV2 } from './validate';
import { transpileDeclarativeToSteps } from './transpile-declarative';
import { transpileV1toV2 } from './v1-to-v2-transpiler';

/**
* Compiles a V2 blueprint declaration into an executable form.
*
* This is the main entry point for V2 blueprint processing.
* It validates the blueprint, extracts runtime configuration,
* transpiles declarative properties into ordered steps, and
* returns an object whose run() method executes the blueprint.
*/
export async function compileBlueprintV2(
blueprint: BlueprintV2Declaration,
options: CompileBlueprintV2Options = {}
): Promise<CompiledBlueprintV2> {
// Detect V1 blueprints (no `version` property) and
// transpile to V2 before proceeding.
let effectiveBlueprint = blueprint;
if (!hasVersionProperty(blueprint)) {
effectiveBlueprint = transpileV1toV2(
blueprint as unknown as Record<string, unknown>
);
}

const validation = validateBlueprintV2(effectiveBlueprint);
if (!validation.valid) {
throw new InvalidBlueprintV2Error(
'Blueprint validation failed: ' + validation.errors.join('; '),
validation.errors
);
}

const runtimeConfig = extractRuntimeConfig(effectiveBlueprint);
const steps = transpileDeclarativeToSteps(effectiveBlueprint);

return {
runtimeConfig,
steps,
run: async (playground: UniversalPHP) => {
await executeSteps(playground, steps, options);
},
};
}

/**
* Extracts runtime configuration from a V2 blueprint declaration.
*
* Converts phpVersion / wordpressVersion strings into
* `V2VersionConstraint` objects and passes applicationOptions
* through as-is.
*/
export function extractRuntimeConfig(
blueprint: BlueprintV2Declaration
): V2RuntimeConfig {
const config: V2RuntimeConfig = {};

if (blueprint.phpVersion !== undefined) {
config.phpVersion = toVersionConstraint(blueprint.phpVersion);
}

if (blueprint.wordpressVersion !== undefined) {
config.wordpressVersion = toVersionConstraint(
blueprint.wordpressVersion
);
}

if (blueprint.applicationOptions !== undefined) {
config.applicationOptions = blueprint.applicationOptions;
}

return config;
}

/**
* Normalizes a version field into a `V2VersionConstraint`.
*
* - Plain strings (e.g. `"8.1"`, `"latest"`) become
* `{ preferred: "<value>" }`.
* - Objects that look like version constraints are mapped
* field-by-field, using `preferred` or `recommended` as
* the preferred key.
* - Other values (e.g. DataReferences like URLs) are not
* representable as a version constraint and are ignored.
*/
function toVersionConstraint(value: unknown): V2VersionConstraint | undefined {
if (typeof value === 'string') {
return { preferred: value };
}

if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const obj = value as Record<string, unknown>;
const constraint: V2VersionConstraint = {};
if (typeof obj.min === 'string') {
constraint.min = obj.min;
}
if (typeof obj.max === 'string') {
constraint.max = obj.max;
}
// The PHP schema uses "recommended"; WordPress uses
// "preferred". Accept both.
if (typeof obj.preferred === 'string') {
constraint.preferred = obj.preferred;
} else if (typeof obj.recommended === 'string') {
constraint.preferred = obj.recommended;
}
return constraint;
}

// DataReference values (URLs, paths, etc.) cannot be
// represented as a simple version constraint — return
// undefined so the caller knows no constraint was set.
return undefined;
}

/**
* Returns true if the blueprint has a `version` property,
* indicating it's a V2 blueprint (or at least declares a
* version). Blueprints without this property are V1.
*/
function hasVersionProperty(blueprint: unknown): boolean {
return (
typeof blueprint === 'object' &&
blueprint !== null &&
'version' in blueprint
);
}

/**
* Executes an ordered list of compiled steps against a PHP
* runtime. Each step is dispatched to its registered handler.
* Progress reporting, data reference resolution, and error
* wrapping are handled automatically.
*/
async function executeSteps(
playground: UniversalPHP,
steps: CompiledV2Step[],
options: CompileBlueprintV2Options
): Promise<void> {
const resolverConfig: DataReferenceResolverConfig = {
semaphore: options.semaphore,
corsProxy: options.corsProxy,
executionContext: options.executionContext,
};
const resolver = new DataReferenceResolverImpl(resolverConfig);

const context: StepExecutionContext = {
php: playground,
progress: options.progress ?? new ProgressTracker(),
dataReferenceResolver: resolver,
};

for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const handler = v2StepHandlers[step.step];

if (!handler) {
throw new BlueprintV2StepExecutionError(
step.step,
`Unknown step handler: "${step.step}"`
);
}

try {
if (step.progressHints?.caption) {
context.progress.setCaption(step.progressHints.caption);
}
await handler(step.args, context);
options.onStepCompleted?.(step.step, i);
} catch (error) {
if (error instanceof BlueprintV2StepExecutionError) {
throw error;
}
throw new BlueprintV2StepExecutionError(
step.step,
`Step "${step.step}" (index ${i}) failed: ${
error instanceof Error ? error.message : String(error)
}`,
error
);
}
}
}
Loading
Loading