Skip to content

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 15 Sep 18:14
· 42 commits to main since this release

What's Changed

Added

  • fields-migrate maps the ACF relationship, range and message
    field types.
    Measured across the downstream fleet: 9 relationship
    fields, 2 message, 1 range — every one of them refused the WHOLE
    component before, not just the one field. relationship maps to
    type: reference with multiple: true, an of: post:<type>[,...] target
    and wp.acf_type: relationship (it is always multi-value by field
    design, unlike post_object's opt-in multiple). range maps to
    type: number with wp.acf_type: range — the same min/max/step
    constraint-lifting as a plain number. message holds no author-set
    value, so it is captured the same way as accordion (identity + "which
    field did it precede") and replayed on generation, rather than invented
    as a data field with no abstract home. Tests: mapper and reverse-mapper
    unit tests for all three types, a completeness audit, and a generation
    round trip on a new related-content corpus fixture covering a
    fully-baseline relationship, a relationship deviating on
    filters/elements/taxonomy/min/max, a range with every
    constraint authored, and both a leading and a trailing message (one with
    a residual esc_html deviation, one with the real corpus's empty
    name). composer check: 906 tests OK, PHPStan clean.

    End to end on four real downstream components that failed before
    (aleszejdl/career-list, loyaltiq/estimate-list, anyever/testimonial-feature,
    umbili/image-promo): all four now migrate and regenerate with every
    relationship/range/message field intact, including image-promo's
    message-then-accordion stacking ahead of its first real field.
    Regeneration on career-list and estimate-list shows the same class of
    pre-existing, documented residual already tolerated elsewhere in this
    tool (legacy ACF-export sentinel conventions — post_type as a bare
    string vs. an array, missing allow_in_bindings, string vs. 0
    dimension sentinels) — unrelated to this change, not a regression.
    Closes #77.

Fixed

  • fields-migrate no longer discards a twig fields: annotation on a
    component with no acf.json.
    element/part/section/utility
    components take their values from the calling template, not from ACF, so
    the CLI synthesised an empty ACF document for them and previously wrote
    fields: {} — losing the annotation entirely once ADR 0007 strips the
    front-comment. Measured on one downstream project: 16 of 22 non-block
    components carried such an annotation. AcfJsonReader now falls back to
    the new TwigMetadataReader::readFields() / TwigFieldTypeMapper when
    acf.json has no fields but the twig annotation does, translating the
    twig type vocabulary (text/textarea/url/link/select/image/…)
    into the abstract schema. When acf.json genuinely has fields, behaviour
    is unchanged. A twig field
    annotated type: array (with or without nested fields:) is refused with
    a \DomainException naming the field, rather than guessed — see the
    linked issue for the decision and its rejected alternatives. A twig
    annotation prop with no mapping to the abstract schema (e.g. a typo, or a
    genuinely unhandled key) is likewise refused by field path rather than
    silently dropped, and placeholder: is now carried across. A select
    with no (or empty) options:/choices:, or a non-string option label,
    is refused locally instead of migrating to schema-invalid YAML. A
    select's options: written as a YAML list ([a, b, c]) is read like
    the comma-string shorthand instead of collapsing to a single bogus
    Array: Array option; choices: and options: given together, a
    malformed choices:, and a non-canonical required: value (e.g.
    required: yes) are refused by field path instead of one silently
    winning over the other or the constraint silently vanishing. A fields:
    annotation whose value is a scalar rather than a map is likewise refused,
    distinct from the "explicitly no fields" bare/null fields: shape. The
    twig fallback now only fires when acf.json is genuinely absent from
    disk (AcfJsonReader::read()'s new $acfJsonExists parameter) — a real
    acf.json with an intentionally empty field group is no longer
    overridable by a stale twig annotation. A select's options: list
    entry that is a boolean or number, or two entries (list or comma-string)
    that collide once trimmed, is refused instead of silently coercing or
    overwriting. An associative options: map (its own keys, e.g.
    draft: Draft) is refused rather than silently re-keyed by value — use
    choices: for that shape. title:, description: and placeholder:
    now require an actual string; a YAML list or map there used to be cast
    straight to the literal string "Array" via PHP's array-to-string
    coercion, producing schema-valid but corrupted output. Provenance
    (role:) is no longer inferred from missing acf.json alone
    — absence
    of acf.json proves only that a field is not editor-authored, not that
    it is parent (it could be query, e.g. a PHP sidecar's own database
    read, or global). A field's own twig role: annotation always wins
    (validated against the full schema role enum; role: derived requires
    from:); absent that, fields-migrate now requires an explicit
    --assume-role=parent|query|global for the whole component and refuses
    the component by field path when neither is given. The flag is restricted
    to those three roles (field/inherited/derived make no sense as a
    blanket assumption) and is ignored entirely for an acf.json-backed
    component. AcfJsonReader::readFields()'s twig-annotation parse now also
    only runs when acf.json is genuinely absent, so a stale/malformed twig
    fields: block next to a valid acf.json no longer aborts migration for
    fields that were never going to be used. An un-annotated child field now
    inherits its container's own resolved role: (e.g. role: query on a
    repeater) instead of falling straight back to --assume-role, matching
    the schema's "a descendant inherits its ancestor's role" contract; this
    also means an explicit container role: lets its un-annotated children
    through even without --assume-role at all. A select's choices:/
    options: keyed with the sequential integers 0, 1, 2, … from zero
    (e.g. choices: {0: None, 1: One}) is refused by field name — PHP
    cannot keep such a key as a string, so it is otherwise indistinguishable
    from a plain list once written, and used to fail later at YAML-write
    time with a message naming neither the field nor the cause. role: derived is no longer inherited by an un-annotated child — it always
    carries its own from: naming a specific sibling, which is meaningless
    to copy onto a child with different siblings; inheriting it produced a
    schema-invalid role: derived with no from: and rejected the whole
    component for an annotation that looked completely valid. Such a child
    now falls back to --assume-role (or ambiguous-provenance) instead,
    same as if the container had no role at all. Closes #75.
  • A nested accordion/message inside a group/repeater's sub_fields
    or a flexible_content layout is now rejected loudly, not silently
    dropped.
    Both pseudo-field kinds are captured and replayed ONLY at a
    component's root — the reader used to continue past a nested one with
    no diagnostic at all, generation never reproduced it, and a container
    whose only child was a pseudo-field threw a misleading "zero sub-fields"
    error with no hint why. AcfJsonReader::rejectNestedPseudoField() now
    throws immediately, naming the field, its container and its ACF type.
    MigrationCompletenessAuditor now flags a nested pseudo-field it
    encounters directly as silent data loss too, instead of skipping it the
    same way it correctly skips a ROOT-level one. Found in review of #78.
  • An unrestricted relationship/post_object (post_type: [] or
    missing — "every post type", a normal and common ACF shape) used to
    migrate to the schema-invalid of: "".
    Empty/missing post_type now
    normalizes to WordPress's own reserved "no restriction" sentinel,
    of: post:any (mirroring WP_Query's post_type: 'any'), which
    reverses back to ACF's genuinely-unrestricted post_type: [] — not a
    literal, nonexistent any post type. Found in review of #78.
  • A message immediately followed by an accordion (or vice versa), both
    anchored on the same real field, now reproduces its true authored
    order on regeneration.
    It used to always emit accordions first
    regardless of which was actually authored first. Migration\AcfJsonReader
    now attaches a seq ordering hint, but ONLY on an anchor that mixes
    both kinds — a single-kind anchor (the fleet's overwhelming majority)
    never gets one, so wp.accordions stays byte-for-byte unchanged for
    every definition with no messages. Found in review of #78.

Pull Requests

  • #76 — fix(migration): preserve twig fields: annotation without acf.json
  • #78 — feat(migration): map ACF relationship, range and message fields

Full Changelog: v0.13.0...v0.14.0