start-vibing 4.3.1 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/template/.claude/skills/super-design/SKILL.md +88 -2
- package/template/.claude/skills/super-design/audit-state.schema.json +226 -0
- package/template/.claude/skills/super-design/scripts/build-import-graph.sh +208 -0
- package/template/.claude/skills/super-design/scripts/detect-apps.sh +180 -0
- package/template/.claude/skills/super-design/scripts/detect-changes.sh +73 -12
- package/template/.claude/skills/super-design/scripts/discover-routes.sh +120 -13
- package/template/.claude/skills/super-design/scripts/extract-tokens.mjs +153 -9
- package/template/.claude/skills/super-design/scripts/hash-pages.sh +208 -28
- package/template/.claude/skills/super-design/scripts/validate-state.sh +46 -15
- package/template/.claude/skills/super-design/scripts/verify-audit.sh +62 -9
- package/template/.claude/skills/super-design/scripts/visual-regression.sh +275 -0
- package/template/.claude/skills/super-design/scripts/write-state.sh +29 -2
- package/template/.claude/skills/super-design/templates/audit-state.schema.json +0 -57
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: detect-apps.sh [<repo_root>]
|
|
3
|
+
#
|
|
4
|
+
# Detects monorepo layout for super-design per-app audit state (artifact §11
|
|
5
|
+
# line 902). Reads workspace declarations from the known manifests:
|
|
6
|
+
# - pnpm-workspace.yaml (pnpm)
|
|
7
|
+
# - package.json#workspaces (npm / yarn / bun)
|
|
8
|
+
# - turbo.json (Turborepo — infers apps from pipeline scope)
|
|
9
|
+
# - nx.json / workspace.json (Nx)
|
|
10
|
+
# - bunfig.toml (Bun workspaces)
|
|
11
|
+
# For each matched glob we enumerate directories that also contain a
|
|
12
|
+
# package.json; those are the apps. When no workspace config is found we
|
|
13
|
+
# emit a single "single"-layout entry with path ".".
|
|
14
|
+
#
|
|
15
|
+
# Output JSON:
|
|
16
|
+
# {
|
|
17
|
+
# "layout": "monorepo" | "single",
|
|
18
|
+
# "apps": [
|
|
19
|
+
# { "name": "web",
|
|
20
|
+
# "path": "apps/web",
|
|
21
|
+
# "state_path": "apps/web/docs/super-design/.audit-state.json" }
|
|
22
|
+
# ]
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# POSIX-ish bash. Requires: jq. Works under git-bash on Windows (no GNU
|
|
26
|
+
# find -printf, no readarray).
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
ROOT="${1:-.}"
|
|
30
|
+
cd "$ROOT"
|
|
31
|
+
|
|
32
|
+
log() { printf '[detect-apps] %s\n' "$*" >&2; }
|
|
33
|
+
|
|
34
|
+
# --- Collect workspace globs from whichever manifests exist ------------------
|
|
35
|
+
GLOBS=""
|
|
36
|
+
|
|
37
|
+
append_glob() {
|
|
38
|
+
# $1 = glob string; skip empty, comments, and negative (!) patterns for now.
|
|
39
|
+
local g
|
|
40
|
+
g="$(printf '%s' "$1" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
41
|
+
[ -z "$g" ] && return 0
|
|
42
|
+
case "$g" in
|
|
43
|
+
'#'*) return 0 ;;
|
|
44
|
+
'!'*) return 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
GLOBS="${GLOBS}
|
|
47
|
+
${g}"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# pnpm-workspace.yaml — very small YAML subset parser: lines under
|
|
51
|
+
# "packages:" that start with "- ".
|
|
52
|
+
if [ -f "pnpm-workspace.yaml" ]; then
|
|
53
|
+
in_pkgs=0
|
|
54
|
+
while IFS= read -r line; do
|
|
55
|
+
case "$line" in
|
|
56
|
+
packages:*) in_pkgs=1; continue ;;
|
|
57
|
+
esac
|
|
58
|
+
if [ "$in_pkgs" = "1" ]; then
|
|
59
|
+
case "$line" in
|
|
60
|
+
-\ *|\ *-\ *)
|
|
61
|
+
entry="$(printf '%s' "$line" | sed -e 's/^[[:space:]]*-[[:space:]]*//' -e 's/^["'\'']//' -e 's/["'\'']$//')"
|
|
62
|
+
append_glob "$entry"
|
|
63
|
+
;;
|
|
64
|
+
[A-Za-z]*:) in_pkgs=0 ;;
|
|
65
|
+
esac
|
|
66
|
+
fi
|
|
67
|
+
done < "pnpm-workspace.yaml"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# package.json#workspaces (array or { packages: [...] })
|
|
71
|
+
if [ -f "package.json" ] && command -v jq >/dev/null 2>&1; then
|
|
72
|
+
WS_JSON="$(jq -r '
|
|
73
|
+
(.workspaces // empty)
|
|
74
|
+
| if type == "array" then .[]
|
|
75
|
+
elif type == "object" then (.packages // [])[]
|
|
76
|
+
else empty end
|
|
77
|
+
' package.json 2>/dev/null || true)"
|
|
78
|
+
if [ -n "$WS_JSON" ]; then
|
|
79
|
+
while IFS= read -r g; do
|
|
80
|
+
[ -n "$g" ] && append_glob "$g"
|
|
81
|
+
done <<EOF
|
|
82
|
+
$WS_JSON
|
|
83
|
+
EOF
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# turbo.json — Turborepo does not declare apps itself but relies on the
|
|
88
|
+
# package manager's workspaces. Treat its presence as confirmation that
|
|
89
|
+
# we are in a monorepo; globs come from package.json/pnpm-workspace.yaml
|
|
90
|
+
# which we've already read. If only turbo.json exists (rare), fall back
|
|
91
|
+
# to the conventional apps/* + packages/* layout.
|
|
92
|
+
if [ -f "turbo.json" ] && [ -z "$GLOBS" ]; then
|
|
93
|
+
append_glob "apps/*"
|
|
94
|
+
append_glob "packages/*"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# nx.json / workspace.json — Nx. Apps live under apps/, libs/ (or the
|
|
98
|
+
# workspaceLayout override).
|
|
99
|
+
if [ -f "nx.json" ]; then
|
|
100
|
+
apps_dir="apps"
|
|
101
|
+
libs_dir="libs"
|
|
102
|
+
if command -v jq >/dev/null 2>&1; then
|
|
103
|
+
override_apps="$(jq -r '.workspaceLayout.appsDir // empty' nx.json 2>/dev/null || true)"
|
|
104
|
+
override_libs="$(jq -r '.workspaceLayout.libsDir // empty' nx.json 2>/dev/null || true)"
|
|
105
|
+
[ -n "$override_apps" ] && apps_dir="$override_apps"
|
|
106
|
+
[ -n "$override_libs" ] && libs_dir="$override_libs"
|
|
107
|
+
fi
|
|
108
|
+
append_glob "${apps_dir}/*"
|
|
109
|
+
append_glob "${libs_dir}/*"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# bunfig.toml — Bun workspaces. Bun primarily reads workspaces from
|
|
113
|
+
# package.json#workspaces (already handled); bunfig.toml presence alone
|
|
114
|
+
# is a monorepo hint only if nothing else matched.
|
|
115
|
+
if [ -f "bunfig.toml" ] && [ -z "$GLOBS" ]; then
|
|
116
|
+
append_glob "apps/*"
|
|
117
|
+
append_glob "packages/*"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# --- Expand globs to real directories that contain a package.json ------------
|
|
121
|
+
APPS_JSON='[]'
|
|
122
|
+
seen_paths=""
|
|
123
|
+
|
|
124
|
+
expand_glob() {
|
|
125
|
+
# $1 = glob like "apps/*" or "packages/marketing-site"
|
|
126
|
+
local g="$1"
|
|
127
|
+
# shellcheck disable=SC2086
|
|
128
|
+
# We deliberately let the shell expand the glob. POSIX shells don't set
|
|
129
|
+
# nullglob, so an unmatched pattern comes back as the literal string —
|
|
130
|
+
# we filter that out below by checking directory existence.
|
|
131
|
+
for d in $g; do
|
|
132
|
+
[ -d "$d" ] || continue
|
|
133
|
+
[ -f "$d/package.json" ] || continue
|
|
134
|
+
# Normalize: strip trailing slash, leading ./
|
|
135
|
+
p="$(printf '%s' "$d" | sed -e 's#^\./##' -e 's#/$##')"
|
|
136
|
+
case "$seen_paths" in
|
|
137
|
+
*"|${p}|"*) continue ;;
|
|
138
|
+
esac
|
|
139
|
+
seen_paths="${seen_paths}|${p}|"
|
|
140
|
+
# Name = package.json "name" field (last path segment for scoped names),
|
|
141
|
+
# fallback to basename.
|
|
142
|
+
name="$(basename "$p")"
|
|
143
|
+
if command -v jq >/dev/null 2>&1; then
|
|
144
|
+
pkg_name="$(jq -r '.name // empty' "$p/package.json" 2>/dev/null || true)"
|
|
145
|
+
if [ -n "$pkg_name" ]; then
|
|
146
|
+
# Strip @scope/ prefix so "web" beats "@acme/web".
|
|
147
|
+
name="$(printf '%s' "$pkg_name" | sed -e 's#^@[^/]*/##')"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
APPS_JSON="$(printf '%s' "$APPS_JSON" | jq --arg name "$name" --arg path "$p" \
|
|
151
|
+
--arg state "$p/docs/super-design/.audit-state.json" \
|
|
152
|
+
'. + [{name:$name, path:$path, state_path:$state}]')"
|
|
153
|
+
done
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if [ -n "$GLOBS" ]; then
|
|
157
|
+
while IFS= read -r g; do
|
|
158
|
+
[ -z "$g" ] && continue
|
|
159
|
+
expand_glob "$g"
|
|
160
|
+
done <<EOF
|
|
161
|
+
$GLOBS
|
|
162
|
+
EOF
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
APP_COUNT="$(printf '%s' "$APPS_JSON" | jq 'length')"
|
|
166
|
+
|
|
167
|
+
if [ "$APP_COUNT" -eq 0 ]; then
|
|
168
|
+
# No monorepo config matched OR matched but produced no apps with
|
|
169
|
+
# package.json. Emit single-app layout.
|
|
170
|
+
jq -n '{
|
|
171
|
+
layout: "single",
|
|
172
|
+
apps: [ {name: ".", path: ".", state_path: "docs/super-design/.audit-state.json"} ]
|
|
173
|
+
}'
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
jq -n --argjson apps "$APPS_JSON" '{
|
|
178
|
+
layout: "monorepo",
|
|
179
|
+
apps: $apps
|
|
180
|
+
}'
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Usage:
|
|
3
|
-
#
|
|
2
|
+
# Usage:
|
|
3
|
+
# detect-changes.sh <last_sha> [<last_iso>] (single app)
|
|
4
|
+
# detect-changes.sh --app <path> <last_sha> [<last_iso>] (one app, path-scoped)
|
|
5
|
+
# detect-changes.sh --all-apps (monorepo loop)
|
|
6
|
+
#
|
|
7
|
+
# Emits JSON. Single-app form:
|
|
8
|
+
# { mode, range_start, commits, files, classified }
|
|
9
|
+
#
|
|
10
|
+
# Monorepo form (--all-apps) — one wrapper with per-app results; each
|
|
11
|
+
# app reads its own `.audit-state.json` via detect-apps.sh:
|
|
12
|
+
# { "layout": "monorepo"|"single",
|
|
13
|
+
# "apps": [ { "name", "path", "state_path",
|
|
14
|
+
# "last_sha", "changes": {<single-app form>} } ] }
|
|
4
15
|
#
|
|
5
16
|
# Implements the fallback ladder from artifact §6 + §14:
|
|
6
17
|
# 1. Anchor exists → diff with rename detection (-M90%).
|
|
@@ -11,16 +22,59 @@
|
|
|
11
22
|
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904 (artifact line 900).
|
|
12
23
|
# Also uses --first-parent + --cherry-pick --right-only when walking
|
|
13
24
|
# history (artifact §2.5 line 167-172).
|
|
25
|
+
#
|
|
26
|
+
# Monorepo per-app state (artifact §11 line 902): when --app is given,
|
|
27
|
+
# `git diff --name-status` is narrowed via `-- <app_path>/` pathspec so
|
|
28
|
+
# only files under that app show up. `--all-apps` discovers apps via
|
|
29
|
+
# detect-apps.sh and runs the per-app path for each.
|
|
14
30
|
set -euo pipefail
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
LAST_ISO="${2:-}"
|
|
32
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
33
|
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
19
34
|
|
|
20
35
|
log() { printf '[detect-changes] %s\n' "$*" >&2; }
|
|
21
36
|
|
|
37
|
+
# --- Arg parsing -------------------------------------------------------------
|
|
38
|
+
MODE_SELECT="single" # single | app | all
|
|
39
|
+
APP_PATH="."
|
|
40
|
+
if [ "${1:-}" = "--all-apps" ]; then
|
|
41
|
+
MODE_SELECT="all"; shift
|
|
42
|
+
elif [ "${1:-}" = "--app" ]; then
|
|
43
|
+
MODE_SELECT="app"; APP_PATH="${2:-.}"; shift 2
|
|
44
|
+
fi
|
|
45
|
+
LAST_SHA="${1:-}"
|
|
46
|
+
LAST_ISO="${2:-}"
|
|
47
|
+
|
|
22
48
|
if ! git rev-parse --git-dir >/dev/null 2>&1; then echo '{"error":"not-a-git-repo"}'; exit 3; fi
|
|
23
49
|
|
|
50
|
+
# --- --all-apps: discover via detect-apps.sh, recurse per app ----------------
|
|
51
|
+
if [ "$MODE_SELECT" = "all" ]; then
|
|
52
|
+
APPS_DOC="$(bash "$SCRIPT_DIR/detect-apps.sh" .)"
|
|
53
|
+
LAYOUT="$(printf '%s' "$APPS_DOC" | jq -r '.layout')"
|
|
54
|
+
RESULT='[]'
|
|
55
|
+
APP_COUNT="$(printf '%s' "$APPS_DOC" | jq '.apps | length')"
|
|
56
|
+
i=0
|
|
57
|
+
while [ "$i" -lt "$APP_COUNT" ]; do
|
|
58
|
+
APP_NAME="$(printf '%s' "$APPS_DOC" | jq -r ".apps[$i].name")"
|
|
59
|
+
A_PATH="$(printf '%s' "$APPS_DOC" | jq -r ".apps[$i].path")"
|
|
60
|
+
A_STATE="$(printf '%s' "$APPS_DOC" | jq -r ".apps[$i].state_path")"
|
|
61
|
+
A_LAST_SHA=""; A_LAST_ISO=""
|
|
62
|
+
if [ -f "$A_STATE" ]; then
|
|
63
|
+
A_LAST_SHA="$(jq -r '.git_sha_at_audit // ""' "$A_STATE" 2>/dev/null || true)"
|
|
64
|
+
A_LAST_ISO="$(jq -r '.last_audit_at // ""' "$A_STATE" 2>/dev/null || true)"
|
|
65
|
+
fi
|
|
66
|
+
A_CHANGES="$(bash "$0" --app "$A_PATH" "$A_LAST_SHA" "$A_LAST_ISO")"
|
|
67
|
+
RESULT="$(printf '%s' "$RESULT" | jq \
|
|
68
|
+
--arg name "$APP_NAME" --arg path "$A_PATH" --arg state "$A_STATE" \
|
|
69
|
+
--arg lsha "$A_LAST_SHA" --argjson ch "$A_CHANGES" '
|
|
70
|
+
. + [{name:$name, path:$path, state_path:$state, last_sha:$lsha, changes:$ch}]
|
|
71
|
+
')"
|
|
72
|
+
i=$((i + 1))
|
|
73
|
+
done
|
|
74
|
+
jq -n --arg layout "$LAYOUT" --argjson apps "$RESULT" '{layout:$layout, apps:$apps}'
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
24
78
|
# Determine HEAD availability — empty repos have no commits at all.
|
|
25
79
|
if ! git rev-parse --verify --quiet HEAD >/dev/null; then
|
|
26
80
|
log "no HEAD commit; using empty-tree SHA fallback"
|
|
@@ -67,11 +121,19 @@ if [[ -z "${RANGE_START:-}" ]]; then
|
|
|
67
121
|
resolve_anchor || true
|
|
68
122
|
fi
|
|
69
123
|
|
|
124
|
+
# Build the pathspec used to narrow diff/log to a single app (monorepo
|
|
125
|
+
# per-app state, artifact §11 line 902). For the default "." the scope
|
|
126
|
+
# is the repo root, preserving pre-existing single-app behavior.
|
|
127
|
+
case "$APP_PATH" in
|
|
128
|
+
.|"") SCOPE_PATH="." ;;
|
|
129
|
+
*) SCOPE_PATH="${APP_PATH%/}" ;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
70
132
|
# Ladder step (c): time-based fallback.
|
|
71
133
|
if [[ -z "${RANGE_START:-}" && -n "$LAST_ISO" ]]; then
|
|
72
|
-
log "using --since=$LAST_ISO time-based fallback"
|
|
73
|
-
FILES="$(git log --since="$LAST_ISO" --name-only --pretty=format: 2>/dev/null | sort -u | sed '/^$/d' || true)"
|
|
74
|
-
COMMITS="$(git log --first-parent --since="$LAST_ISO" --pretty=format:'%H|%s|%an|%aI' 2>/dev/null || true)"
|
|
134
|
+
log "using --since=$LAST_ISO time-based fallback (scope=$SCOPE_PATH)"
|
|
135
|
+
FILES="$(git log --since="$LAST_ISO" --name-only --pretty=format: -- "$SCOPE_PATH" 2>/dev/null | sort -u | sed '/^$/d' || true)"
|
|
136
|
+
COMMITS="$(git log --first-parent --since="$LAST_ISO" --pretty=format:'%H|%s|%an|%aI' -- "$SCOPE_PATH" 2>/dev/null || true)"
|
|
75
137
|
MODE="since-time"
|
|
76
138
|
RANGE_START=""
|
|
77
139
|
elif [[ -z "${RANGE_START:-}" ]]; then
|
|
@@ -91,7 +153,7 @@ else
|
|
|
91
153
|
# the current file layout.
|
|
92
154
|
FILES="$(
|
|
93
155
|
git diff --name-status -M90% -z \
|
|
94
|
-
--
|
|
156
|
+
-- "$SCOPE_PATH" ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml' ':!yarn.lock' \
|
|
95
157
|
':!.github/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/*.stories.*' \
|
|
96
158
|
$RANGE 2>/dev/null |
|
|
97
159
|
awk -v RS='\0' '
|
|
@@ -136,6 +198,7 @@ while IFS= read -r p; do
|
|
|
136
198
|
done <<< "$FILES"
|
|
137
199
|
|
|
138
200
|
jq -Rn --arg mode "$MODE" --arg range_start "${RANGE_START:-}" --arg last_iso "$LAST_ISO" \
|
|
201
|
+
--arg app_path "$APP_PATH" \
|
|
139
202
|
--arg tokens "${CLASSIFIED[tokens]:-}" --arg components "${CLASSIFIED[components]:-}" \
|
|
140
203
|
--arg routes "${CLASSIFIED[routes]:-}" --arg imagery "${CLASSIFIED[imagery]:-}" \
|
|
141
204
|
--arg deps "${CLASSIFIED[deps]:-}" --arg theory "${CLASSIFIED[theory]:-}" \
|
|
@@ -143,7 +206,8 @@ jq -Rn --arg mode "$MODE" --arg range_start "${RANGE_START:-}" --arg last_iso "$
|
|
|
143
206
|
--argjson files "$(printf '%s\n' "$FILES" | jq -R . | jq -s .)" \
|
|
144
207
|
--argjson commits "$(printf '%s\n' "$COMMITS" | jq -R . | jq -s .)" '
|
|
145
208
|
def tolist(s): s | split(",") | map(select(length>0));
|
|
146
|
-
{
|
|
209
|
+
{app_path:$app_path,
|
|
210
|
+
mode:$mode, range_start:$range_start, since_iso:$last_iso,
|
|
147
211
|
commits:$commits, files:$files,
|
|
148
212
|
classified: {
|
|
149
213
|
tokens: tolist($tokens), components: tolist($components),
|
|
@@ -151,6 +215,3 @@ jq -Rn --arg mode "$MODE" --arg range_start "${RANGE_START:-}" --arg last_iso "$
|
|
|
151
215
|
deps: tolist($deps), theory: tolist($theory), content: tolist($content)
|
|
152
216
|
}
|
|
153
217
|
}'
|
|
154
|
-
|
|
155
|
-
# TODO(sd-audit-state §11/artifact line 902): monorepo per-app state
|
|
156
|
-
# (apps/*/docs/super-design/.audit-state.json) not yet supported.
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
2
|
+
# discover-routes.sh — discover route URLs for the detected framework.
|
|
3
|
+
#
|
|
4
|
+
# Dynamic routes (Next `[slug]` / `[[...foo]]` / `[...bar]`, Remix `$slug`,
|
|
5
|
+
# React-Router/Vue `:slug`) are emitted with a `@fixture-<id>` suffix so
|
|
6
|
+
# the route map has a stable identity across audits (artifact §2.7 + §7:
|
|
7
|
+
# "/posts/[id]@fixture-post-123"). Fixtures are resolved via:
|
|
8
|
+
#
|
|
9
|
+
# 1. Sibling file: <route-dir>/<name>.fixture.json|ts (array of IDs or
|
|
10
|
+
# objects with {id|slug|key}).
|
|
11
|
+
# 2. Nearby directory: <route-dir>/fixtures/*.json.
|
|
12
|
+
# 3. Env override: $SUPER_DESIGN_FIXTURES — a JSON object mapping the
|
|
13
|
+
# raw pattern (e.g. "/posts/[slug]") to an array of fixture IDs.
|
|
14
|
+
# 4. Placeholder fallback: `@fixture-default` (with stderr warning).
|
|
15
|
+
#
|
|
16
|
+
# Consumers (hash-pages.sh, sd-audit) MUST strip the `@fixture-<id>`
|
|
17
|
+
# suffix before navigating; the suffix is identity-only.
|
|
18
|
+
#
|
|
19
|
+
# Output: JSON array of URL strings.
|
|
9
20
|
set -euo pipefail
|
|
10
21
|
|
|
22
|
+
log() { printf '[discover-routes] %s\n' "$*" >&2; }
|
|
23
|
+
|
|
11
24
|
detect_framework() {
|
|
12
25
|
if [[ -f next.config.js || -f next.config.ts || -f next.config.mjs ]]; then echo "next"
|
|
13
26
|
elif [[ -f remix.config.js || -d app/routes ]]; then echo "remix"
|
|
@@ -20,6 +33,96 @@ detect_framework() {
|
|
|
20
33
|
else echo "unknown"; fi
|
|
21
34
|
}
|
|
22
35
|
|
|
36
|
+
# is_dynamic <url> → 0 if URL contains a dynamic segment, else 1.
|
|
37
|
+
is_dynamic() {
|
|
38
|
+
local u="$1"
|
|
39
|
+
# Next-style: [foo], [[foo]], [...foo], [[...foo]]
|
|
40
|
+
[[ "$u" == *"["*"]"* ]] && return 0
|
|
41
|
+
# Remix-style: $foo
|
|
42
|
+
[[ "$u" == *"$"* ]] && return 0
|
|
43
|
+
# RR/Vue-style: :foo
|
|
44
|
+
[[ "$u" == *":"* ]] && return 0
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Look for a fixtures source for a given route URL and emit IDs one per
|
|
49
|
+
# line. Echo `__DEFAULT__` if nothing found.
|
|
50
|
+
resolve_fixture_ids() {
|
|
51
|
+
local url="$1"
|
|
52
|
+
local segment
|
|
53
|
+
# Strip leading / and use the last dynamic-bearing segment to find a file.
|
|
54
|
+
# Rough heuristic: pick the last path component that is a bare identifier
|
|
55
|
+
# (strip brackets/dollar/colon) to look up fixtures/<name>.*.
|
|
56
|
+
segment="$(printf '%s' "$url" \
|
|
57
|
+
| sed -E 's|^/||' \
|
|
58
|
+
| tr '/' '\n' \
|
|
59
|
+
| grep -E '^(\[\[?\.\.\.?.+\]\]?|\[.+\]|\$.+|:.+)$' \
|
|
60
|
+
| tail -1 \
|
|
61
|
+
| sed -E 's|^\[\[?\.?\.?\.?||; s|\]\]?$||; s|^\$||; s|^:||')"
|
|
62
|
+
[[ -z "$segment" ]] && { echo "__DEFAULT__"; return; }
|
|
63
|
+
|
|
64
|
+
# 1. Env override.
|
|
65
|
+
if [[ -n "${SUPER_DESIGN_FIXTURES:-}" ]]; then
|
|
66
|
+
local ids
|
|
67
|
+
ids="$(printf '%s' "$SUPER_DESIGN_FIXTURES" \
|
|
68
|
+
| jq -r --arg k "$url" '.[$k][]? // empty' 2>/dev/null || true)"
|
|
69
|
+
if [[ -n "$ids" ]]; then
|
|
70
|
+
printf '%s\n' "$ids"; return
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# 2. Sibling / nearby fixture files. Search in common locations.
|
|
75
|
+
local candidate ids=""
|
|
76
|
+
for candidate in \
|
|
77
|
+
"${segment}.fixture.json" \
|
|
78
|
+
"fixtures/${segment}.json" \
|
|
79
|
+
"tests/fixtures/${segment}.json" \
|
|
80
|
+
"__fixtures__/${segment}.json" \
|
|
81
|
+
".super-design/fixtures/${segment}.json"; do
|
|
82
|
+
local found
|
|
83
|
+
found="$(find . -type f -name "$(basename "$candidate")" 2>/dev/null \
|
|
84
|
+
| grep -F "/$candidate" | head -1 || true)"
|
|
85
|
+
if [[ -n "$found" && -f "$found" ]]; then
|
|
86
|
+
ids="$(jq -r '
|
|
87
|
+
if type == "array" then
|
|
88
|
+
.[] | if type == "object" then (.id // .slug // .key // empty) else . end
|
|
89
|
+
elif type == "object" and (.fixtures|type=="array") then
|
|
90
|
+
.fixtures[] | if type=="object" then (.id // .slug // .key // empty) else . end
|
|
91
|
+
else empty end
|
|
92
|
+
' "$found" 2>/dev/null | head -5 || true)"
|
|
93
|
+
if [[ -n "$ids" ]]; then
|
|
94
|
+
printf '%s\n' "$ids"; return
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
# 3. Give up → default placeholder with warning.
|
|
100
|
+
log "no fixtures for $url; using @fixture-default"
|
|
101
|
+
echo "__DEFAULT__"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# suffix_dynamic_routes: read raw URLs on stdin, write suffixed URLs on
|
|
105
|
+
# stdout. Static routes pass through untouched.
|
|
106
|
+
suffix_dynamic_routes() {
|
|
107
|
+
local url ids id
|
|
108
|
+
while IFS= read -r url; do
|
|
109
|
+
[[ -z "$url" ]] && continue
|
|
110
|
+
if is_dynamic "$url"; then
|
|
111
|
+
ids="$(resolve_fixture_ids "$url")"
|
|
112
|
+
if [[ "$ids" == "__DEFAULT__" ]]; then
|
|
113
|
+
printf '%s@fixture-default\n' "$url"
|
|
114
|
+
else
|
|
115
|
+
while IFS= read -r id; do
|
|
116
|
+
[[ -z "$id" ]] && continue
|
|
117
|
+
printf '%s@fixture-%s\n' "$url" "$id"
|
|
118
|
+
done <<<"$ids"
|
|
119
|
+
fi
|
|
120
|
+
else
|
|
121
|
+
printf '%s\n' "$url"
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
}
|
|
125
|
+
|
|
23
126
|
FW="$(detect_framework)"
|
|
24
127
|
case "$FW" in
|
|
25
128
|
next)
|
|
@@ -29,24 +132,28 @@ case "$FW" in
|
|
|
29
132
|
PG="$(find pages src/pages -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' -o -name '*.md' -o -name '*.mdx' \) 2>/dev/null \
|
|
30
133
|
| grep -vE '(^|/)(pages|src/pages)/(_app|_document|_error|404|500|api/)' \
|
|
31
134
|
| sed -E 's|(^|/)(pages|src/pages)/||; s|\.(tsx|ts|jsx|js|md|mdx)$||; s|index$||' | sort -u || true)"
|
|
32
|
-
printf '%s\n%s\n' "$APP" "$PG" | awk 'NF' | sed -E 's|^|/|; s|//|/|g' | sort -u
|
|
135
|
+
printf '%s\n%s\n' "$APP" "$PG" | awk 'NF' | sed -E 's|^|/|; s|//|/|g' | sort -u \
|
|
136
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
33
137
|
sveltekit)
|
|
34
138
|
find src/routes -type f -name '+page.svelte' 2>/dev/null \
|
|
35
139
|
| sed -E 's|^src/routes||; s|/\+page\.svelte$||; s|\([^)]+\)/||g' \
|
|
36
|
-
| awk 'NF==0 {print "/"; next} {print}' | sort -u
|
|
140
|
+
| awk 'NF==0 {print "/"; next} {print}' | sort -u \
|
|
141
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
37
142
|
astro)
|
|
38
143
|
find src/pages -type f \( -name '*.astro' -o -name '*.md' -o -name '*.mdx' \) 2>/dev/null \
|
|
39
|
-
| sed -E 's|^src/pages||; s|\.(astro|md|mdx)$||; s|/index$||' | sort -u
|
|
144
|
+
| sed -E 's|^src/pages||; s|\.(astro|md|mdx)$||; s|/index$||' | sort -u \
|
|
145
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
40
146
|
nuxt)
|
|
41
147
|
find pages app/pages -type f -name '*.vue' 2>/dev/null \
|
|
42
|
-
| sed -E 's|^(app/)?pages||; s|\.vue$||; s|/index$||' | sort -u
|
|
148
|
+
| sed -E 's|^(app/)?pages||; s|\.vue$||; s|/index$||' | sort -u \
|
|
149
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
43
150
|
remix)
|
|
44
151
|
find app/routes -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' -o -name '*.md' -o -name '*.mdx' \) 2>/dev/null \
|
|
45
152
|
| sed -E 's|^app/routes/||; s|\.(tsx|ts|jsx|js|md|mdx)$||; s|\.|/|g; s|_index$||; s|^|/|' \
|
|
46
|
-
| sort -u | jq -Rn '[inputs]';;
|
|
153
|
+
| sort -u | suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
47
154
|
solid-start)
|
|
48
155
|
find src/routes -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' \) 2>/dev/null \
|
|
49
156
|
| sed -E 's|^src/routes||; s|\.(tsx|ts|jsx|js)$||; s|/index$||; s|\([^)]+\)/||g' \
|
|
50
|
-
| sort -u | jq -Rn '[inputs]';;
|
|
157
|
+
| sort -u | suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
51
158
|
*) echo '[]';;
|
|
52
159
|
esac
|