afv-library/skills/experience-ui-bundle-localize/scripts/check-i18n-wired.sh

196 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail # exit on error (-e), undefined vars (-u), and propagate pipeline failures (-o pipefail)
#
# check-i18n-wired.sh — Confirm the i18n init exists and is called at boot.
#
# Whether the app already has i18n wiring is a file-existence plus call-site
# check, so run this rather than reading for it by hand. It looks for an init
# file that defines initI18n() and for a boot-time initI18n() call in an entry
# file.
#
# Usage (run from the UI bundle dir, or pass its src path):
# bash <skill-dir>/scripts/check-i18n-wired.sh [src-dir]
#
# src-dir defaults to "src".
#
# When wiring exists, it also reports whether the label manifest is imported AND
# actually passed into the SalesforceBackend's backendOptions (not merely
# imported and left unused), so the reconcile decision is a script result rather
# than a by-hand read.
#
# Exit codes (each actionable state has its own code so the workflow branches on
# the code, not on the message text):
# 0 fully wired — initI18n() defined, called at boot, AND the manifest is
# passed into the SalesforceBackend's backendOptions. Just add new keys.
# 1 no i18n init found — no initI18n() definition. Scaffold the whole setup
# (see references/i18n-setup.md).
# 2 init defined but not called at boot — add only the boot-time initI18n()
# call in the entry file; do NOT re-scaffold or overwrite the init.
# 3 wired at boot but the manifest could not be confirmed as passed into the
# backend (not imported, no backendOptions/SalesforceBackend config, or
# imported but not seen inside a backendOptions array). This is a textual
# heuristic: the manifest may be wired via a variable/spread this check
# cannot see. VERIFY by hand; reconcile only if it genuinely dangles, and
# never clobber the existing init.
# 64 usage error — the source dir does not exist (bad argument or wrong cwd).
# This is NOT a "no init found" result; do not scaffold. Fix the path and
# re-run. (64 = EX_USAGE, kept out of the 0-3 semantic range so exit 1
# uniquely means "no init found".)
# The message still names the specific file/symbol/gap for the report, but the
# decision is the exit code.
SRC_DIR="${1:-src}"
if [ ! -d "$SRC_DIR" ]; then
echo "ERROR: source dir not found: $SRC_DIR (run from the UI bundle dir, or pass its src path)" >&2
exit 64
fi
# An init file that DEFINES initI18n (function/const declaration, not just a call).
# Use POSIX character classes, not \s / \b: BSD/macOS grep -E does not honor those.
# A trailing [^A-Za-z0-9_] (or end of line) stands in for the word boundary so
# initI18nFoo does not match.
DEFINES=$(grep -rlE '(function|const)[[:space:]]+initI18n([^A-Za-z0-9_]|$)|initI18n[[:space:]]*=' "$SRC_DIR" \
--include='*.ts' --include='*.tsx' 2>/dev/null | head -1 || true)
# A boot-time call to initI18n() somewhere other than its own definition file.
CALLS=$(grep -rlE '(^|[^A-Za-z0-9_])initI18n[[:space:]]*\(' "$SRC_DIR" \
--include='*.ts' --include='*.tsx' 2>/dev/null \
| grep -vF "${DEFINES:-/dev/null}" | head -1 || true)
if [ -n "$DEFINES" ] && [ -n "$CALLS" ]; then
# Wiring exists. Determine whether the manifest is already reconciled into the
# backend. Importing the manifest is not enough: an init can `import
# { labelManifest }` and still omit it from backendOptions, in which case the
# SalesforceBackend fetches nothing and every key renders as its own literal
# name. So confirm three things, not just token presence:
# 1. the manifest is imported (capture the symbol it binds),
# 2. a backendOptions construction exists (where the SDK backend is fed),
# 3. that symbol appears INSIDE the backendOptions array, not merely
# somewhere in the source.
# (3) is what separates "imported and wired" from "imported but dropped". A
# bare reference elsewhere (e.g. console.log(labelManifest)) must not count:
# the backend only receives the manifest if it sits in backendOptions.
# Search the WHOLE src tree, not just the init file: a valid app may define
# initI18n() in one file and build the SalesforceBackend (with the manifest)
# in a separate backend module or the entry file. Scanning only $DEFINES
# falsely reports "not reconciled" for that layout. Find the manifest import
# wherever it lives, and the file that actually constructs backendOptions.
# Is the manifest imported anywhere at all? (Used only for the "manifest
# import missing" message; the wired/dangling decision is made per-file below.)
MANIFEST_IMPORT_LINE=$(grep -rhnE '^[[:space:]]*import[^A-Za-z0-9_].*(label-manifest|labelManifest)' "$SRC_DIR" \
--include='*.ts' --include='*.tsx' 2>/dev/null | head -1 || true)
# EVERY file that constructs backendOptions, not just the first: a decoy (a
# comment, a types file, an unused second backend) can sort earlier in the
# tree walk, so we check whether the manifest lands in ANY file's backendOptions.
BACKEND_FILES=$(grep -rlE 'backendOptions' "$SRC_DIR" \
--include='*.ts' --include='*.tsx' 2>/dev/null || true)
# Any backend config token at all, for the "no backend config" message.
BACKEND_OPTS=$(grep -rlE 'backendOptions|SalesforceBackend' "$SRC_DIR" \
--include='*.ts' --include='*.tsx' 2>/dev/null | head -1 || true)
# sed program that resolves the manifest's LOCAL name from an import line.
# Two forms bind a local name, plus the default import:
# named: import { ..., labelManifest, ... } -> labelManifest
# aliased: import { ..., labelManifest as foo } -> foo
# default: import labelManifest from ... -> labelManifest
# Avoid \b: BSD/macOS sed does not support it.
RESOLVE_SYMBOL='s/.*[{,][[:space:]]*labelManifest[[:space:]]+as[[:space:]]+([A-Za-z0-9_]+).*/\1/; t
s/.*[{,][[:space:]]*labelManifest[[:space:]]*[,}].*/labelManifest/; t
s/.*import[[:space:]]+labelManifest[[:space:]]+from.*/labelManifest/; t
s/.*/labelManifest/'
# Determine whether the manifest is actually passed into a backendOptions
# array (not merely imported and left unused). Importing is not enough: an
# init can `import { labelManifest }` and still omit it from backendOptions,
# in which case the backend fetches nothing and every key renders as its own
# literal name. Resolve the symbol PER FILE from that file's own import: an
# imported binding can only be used in the file that imports it, so the name
# passed into a file's backendOptions is whatever that same file bound (named,
# aliased, or default). Resolving per file avoids a cross-file alias mismatch
# (file A imports `labelManifest`, file B imports it `as m` and uses `m`).
# The awk emits every backendOptions array block in the file (bracket-balanced
# from each "[" after a "backendOptions" token); we grep those blocks for the
# file's own symbol. A bare reference elsewhere (console.log(labelManifest))
# is outside every block, so it does not count.
MANIFEST_USED=""
MANIFEST_SYMBOL="labelManifest" # fallback for the message if no hit is found
if [ -n "$BACKEND_FILES" ]; then
while IFS= read -r BACKEND_FILE; do
[ -z "$BACKEND_FILE" ] && continue
FILE_IMPORT=$(grep -nE '^[[:space:]]*import[^A-Za-z0-9_].*(label-manifest|labelManifest)' \
"$BACKEND_FILE" 2>/dev/null | head -1 || true)
[ -z "$FILE_IMPORT" ] && continue # this file cannot use a binding it never imported
SYM=$(printf '%s\n' "$FILE_IMPORT" | sed -E "$RESOLVE_SYMBOL")
[ -z "$SYM" ] && continue
# Emit every backendOptions array block. State (started/seeking/capturing/
# depth/out) is awk-global and persists across lines so a multi-line array
# is captured whole. After a "backendOptions" token we SEEK its opening
# "[": only whitespace and ":" may sit between token and bracket. If any
# other non-space char comes first, the token is not followed by an array
# literal (e.g. `backendOptions: buildOpts()`), so abandon it and re-scan —
# without this, `started` would leak forward and capture an unrelated later
# array as if it were the options array (a false "wired" result).
# Known limit: a "]" inside a string literal in the array (e.g.
# `["a]b", labelManifest]`) closes the block early and can miss the symbol.
# That only ever pushes toward exit 3 (verify by hand), never a false exit
# 0, so it is safe under the heuristic framing; not worth a string parser.
BLOCKS=$(sed -E 's://.*$::' "$BACKEND_FILE" | awk '
{
line = $0; pos = 1; n = length(line)
while (pos <= n) {
if (!seeking && !capturing) {
idx = index(substr(line, pos), "backendOptions")
if (idx == 0) break
seeking = 1
pos = pos + idx - 1 + 14 # 14 = length("backendOptions")
continue
}
c = substr(line, pos, 1)
if (seeking) {
if (c == "[") { seeking = 0; capturing = 1; depth = 1; out = c; pos++; continue }
if (c == " " || c == "\t" || c == ":") { pos++; continue }
seeking = 0; continue # not an array literal -> re-scan for next token
}
# capturing
out = out c
if (c == "[") depth++
if (c == "]") { depth--; if (depth <= 0) { print out; out=""; capturing=0 } }
pos++
}
if (capturing) out = out "\n"
}
')
if printf '%s' "$BLOCKS" \
| grep -qE "(^|[^A-Za-z0-9_])${SYM}([^A-Za-z0-9_]|$)" 2>/dev/null; then
MANIFEST_USED="yes"; MANIFEST_SYMBOL="$SYM"; break
fi
done <<EOF
$BACKEND_FILES
EOF
fi
if [ -n "$MANIFEST_USED" ]; then
echo "i18n wired: initI18n() defined in $DEFINES, called at boot in $CALLS; the manifest ($MANIFEST_SYMBOL) is imported and passed into the backend config -> add new keys to the manifest, do not clobber the init"
exit 0
fi
# Could not confirm the manifest is passed into a backendOptions array. This
# is a heuristic textual check, so treat exit 3 as "verify, do not clobber":
# the manifest may be wired through a form this script cannot see (passed via
# a variable, spread, or helper). Report the specific gap for the human/agent
# to confirm before editing; only reconcile if it genuinely dangles.
MISSING=""
[ -z "$MANIFEST_IMPORT_LINE" ] && MISSING="manifest import"
[ -z "$BACKEND_OPTS" ] && MISSING="${MISSING:+$MISSING and }backendOptions/SalesforceBackend config"
[ -n "$MANIFEST_IMPORT_LINE" ] && [ -n "$BACKEND_OPTS" ] \
&& MISSING="${MISSING:+$MISSING and }could not confirm the manifest is passed into backendOptions (it may dangle unused, or be wired via a variable this check cannot see)"
echo "i18n wired: initI18n() defined in $DEFINES, called at boot in $CALLS; but $MISSING -> verify by hand; if it genuinely dangles, reconcile (import the manifest AND pass it into the SalesforceBackend's backendOptions) without clobbering"
exit 3
fi
if [ -z "$DEFINES" ]; then
echo "no i18n init found (no initI18n() definition under $SRC_DIR) -> scaffold it"
exit 1
fi
echo "initI18n() defined in $DEFINES but not called at boot -> add the boot-time call"
exit 2