External Template-Layer Composition RFC
This document records the architecture contract for external template-layer composition on top of the built-in shared scaffold model.
It closes the RFC thread from issue #198. The goal is not to replace the
current scaffold pipeline or the existing remote-template adapter. The goal is
to define how another package can extend the built-in shared layer graph
without forking the whole scaffold system.
Design goals
Section titled “Design goals”- keep built-in
_sharedlayers as the canonical internal scaffold model - let external packages add reusable layers on top of that model
- define deterministic
extendsand override rules - keep conflict handling explicit instead of implicit
- preserve the current trust model for third-party template sources
What this RFC is and is not
Section titled “What this RFC is and is not”This RFC defines a package contract for reusable external scaffold layers.
It does not:
- replace current external template seed support
- replace the typed built-in generator architecture from
docs/block-generator-architecture.md - change the official empty workspace template package contract
- define discovery UX beyond the current interactive selector plus canonical flags
The implementation follow-through was tracked in issue #268.
Relationship to current template support
Section titled “Relationship to current template support”Today wp-typia supports three remote-template shapes:
- a remote/local
wp-typiatemplate directory - an official create-block external template config
- a create-block-style subset (
block.jsonplussrc/index|edit|save)
Those flows are still seed-oriented. wp-typia normalizes them into a scaffold
project and then re-applies its own package/tooling/runtime conventions around
that seed.
External template-layer composition is different:
- it is layer-oriented instead of seed-oriented
- it composes on top of built-in
_sharedscaffold layers - it does not replace built-in emitter ownership
- it is intended for reusable agencies/vendors/community overlays rather than one-off remote template forks
Package contract
Section titled “Package contract”An external layer package should publish a root manifest named
wp-typia.layers.json.
The manifest is data, not executable code. A minimal shape is:
{ "version": 1, "layers": { "acme/persistence-observability": { "path": "layers/persistence-observability", "extends": [ "builtin:shared/base", "builtin:shared/rest-helpers/shared", "builtin:shared/persistence/core" ], "description": "Adds shared observability files for persistence-capable blocks" } }}Manifest fields
Section titled “Manifest fields”versionRequired. Initial contract version is1.layersRequired object keyed by external layer id.layers.<id>.pathRequired relative directory path inside the package. It must stay within the package root and may not resolve through symlinks.layers.<id>.extendsOptional ordered array of ancestor layer ids. Ancestors may reference canonical built-in ids or other external layer ids from the same package.layers.<id>.descriptionOptional human-readable description.
This RFC intentionally does not add package-level JavaScript transformers to the layer manifest. The manifest itself should stay statically inspectable.
Canonical built-in layer ids
Section titled “Canonical built-in layer ids”Built-in _shared layers remain the canonical internal model. The RFC maps
those directories to stable layer ids using the rule:
packages/wp-typia-project-tools/templates/_shared/<path>- becomes
builtin:shared/<path>
Examples:
templates/_shared/base->builtin:shared/basetemplates/_shared/rest-helpers/shared->builtin:shared/rest-helpers/sharedtemplates/_shared/rest-helpers/public->builtin:shared/rest-helpers/publictemplates/_shared/rest-helpers/auth->builtin:shared/rest-helpers/authtemplates/_shared/persistence/core->builtin:shared/persistence/coretemplates/_shared/persistence/public->builtin:shared/persistence/publictemplates/_shared/persistence/auth->builtin:shared/persistence/authtemplates/_shared/compound/core->builtin:shared/compound/coretemplates/_shared/compound/persistence->builtin:shared/compound/persistencetemplates/_shared/compound/persistence-public->builtin:shared/compound/persistence-publictemplates/_shared/compound/persistence-auth->builtin:shared/compound/persistence-authtemplates/_shared/migration-ui/common->builtin:shared/migration-ui/commontemplates/_shared/presets/wp-env->builtin:shared/presets/wp-envtemplates/_shared/presets/test-preset->builtin:shared/presets/test-presettemplates/_shared/workspace/persistence-public->builtin:shared/workspace/persistence-publictemplates/_shared/workspace/persistence-auth->builtin:shared/workspace/persistence-auth
Resolution rules
Section titled “Resolution rules”Layer resolution is deterministic and order-sensitive.
Given a selected external layer:
- Resolve ancestors from
extendsdepth-first, left to right. - Materialize each ancestor exactly once.
- Apply the selected layer’s own
pathlast. - Apply the built-in template-specific overlay and emitter-owned files after shared/external layer copy.
That means:
- earlier
extendsentries are lower precedence - later
extendsentries override earlier ones - the selected layer overrides every ancestor
- the typed built-in generator still owns emitter-written artifacts after layer copy completes
Conflict handling and precedence
Section titled “Conflict handling and precedence”The contract distinguishes between ordinary copied assets and protected
wp-typia-owned outputs.
Ordinary copied assets
Section titled “Ordinary copied assets”Examples:
- shared scripts
- non-emitter README/bootstrap fragments
- copied helper files that still live in
_shared
For these, later layers win deterministically.
Protected paths
Section titled “Protected paths”External layers may not override paths owned by the generator or by
wp-typia’s package/tooling/bootstrap contract.
Protected outputs include:
- emitter-owned built-in artifacts such as
types.ts,block.json, built-in TS/TSX scaffold bodies, built-in styles, and block-localrender.php - starter
typia.manifest.json - package/tooling bootstrap files that
wp-typiaexplicitly normalizes, such as package-manager metadata and sync/runtime setup
If an external layer writes a protected path, the implementation should fail hard with an explicit conflict error instead of silently accepting drift.
Trust model
Section titled “Trust model”External layer packages are third-party code and should be treated with the same trust posture as existing external template sources.
- local paths, GitHub locators, and npm package sources are all trusted inputs
- layer packages must not contain symlinks
- the layer manifest should stay data-only so it can be inspected without executing package JavaScript
- if a package also exposes the current create-block-style external config entrypoint, that JavaScript path keeps the existing trusted-JS model
Relation to workspace templates
Section titled “Relation to workspace templates”The official empty workspace template package remains a separate concept.
@wp-typia/create-workspace-templatestill defines the empty workspace root used bywp-typia create --template @wp-typia/create-workspace-templatewp-typia add blockcontinues to grow that workspace through the built-in generator path- external template-layer composition is for reusable shared scaffold layers, not for replacing the official empty workspace template contract
Future implementation may let workspace-aware flows reuse the same external layer ids, but the official workspace template itself remains canonical.
Relation to the typed generator architecture
Section titled “Relation to the typed generator architecture”This RFC extends the current generator architecture; it does not compete with it.
- built-in block planning/validation/render/apply still flows through
BlockSpecandBlockGeneratorService - built-in emitter-owned files remain authoritative
- external layer composition only broadens the shared copied-layer graph around that typed core
That is why issue #193 could close independently while #198 stayed open:
the typed generator is implemented, while the external layer contract is a
separate follow-through.
Current implementation status
Section titled “Current implementation status”The current implementation is available through the canonical built-in CLI flags and the built-in generator runtime API.
CLI callers can compose a built-in family with an external layer package using:
wp-typia create <project-dir> --template <basic|interactivity|persistence|compound> --external-layer-source <locator> [--external-layer-id <layer-id>]wp-typia add block <name> --template <basic|interactivity|persistence|compound> --external-layer-source <locator> [--external-layer-id <layer-id>]
Programmatic callers can use the same contract through
scaffoldProject(...), BlockGeneratorService, or inspectBlockGeneration(...)
using:
templateIdThe built-in block family that still owns the typed generator/emitter path.externalLayerSourceA local path, GitHub locator, or npm package spec that resolves to a package root containingwp-typia.layers.json.externalLayerIdOptional explicit layer id. Omit it when the package exposes a single public layer. Built-in interactive CLI flows can prompt when a package exposes multiple public roots. Programmatic and non-interactive callers still choose one explicitly in that case.
The implemented behavior now includes:
- manifest loading and validation
- canonical built-in layer id mapping
extendsresolution with depth-first, left-to-right precedence- protected-path conflict errors for emitter-owned outputs and built-in package bootstrap files
- regression coverage for built-in plus external layer composition