@multiplatform.one/cli 7.11.0 → 7.15.0
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/README.md +121 -111
- package/bin/createMultiplatformApp.mjs +39 -37
- package/lib/bin/multiplatformOne.mjs +65 -75
- package/lib/commands/addApp.mjs +78 -0
- package/lib/commands/adoptApp.mjs +11 -7
- package/lib/commands/doctypeTypes.mjs +148 -0
- package/lib/commands/e2e.mjs +2 -2
- package/lib/commands/init.mjs +142 -459
- package/lib/commands/merge.mjs +359 -0
- package/lib/commands/pieces.mjs +236 -0
- package/lib/commands/provenance.mjs +108 -0
- package/lib/commands/scaffold.mjs +105 -0
- package/lib/commands/updateApp.mjs +211 -528
- package/lib/source.json +5 -0
- package/lib/source.mjs +63 -0
- package/package.json +6 -6
- package/scripts/frappe-bootstrap.sh +29 -3
- package/scripts/frappe-dev.sh +1 -2
- package/scripts/frappe-helpers.sh +50 -2
- package/scripts/frappe.sh +5 -0
- package/scripts/scaffold.sh +937 -0
- package/scripts/stamp-source.mjs +51 -0
- package/scripts/stamp-source.spec.ts +86 -0
- package/src/bin/multiplatformOne.ts +116 -226
- package/src/commands/addApp.ts +102 -0
- package/src/commands/adoptApp.spec.ts +5 -1
- package/src/commands/adoptApp.ts +15 -10
- package/src/commands/doctypeTypes.spec.ts +155 -0
- package/src/commands/doctypeTypes.ts +214 -0
- package/src/commands/init.spec.ts +66 -0
- package/src/commands/init.ts +201 -643
- package/src/commands/merge.spec.ts +81 -0
- package/src/commands/merge.ts +411 -0
- package/src/commands/pieces.spec.ts +62 -0
- package/src/commands/pieces.ts +340 -0
- package/src/commands/provenance.spec.ts +92 -0
- package/src/commands/provenance.ts +169 -0
- package/src/commands/scaffold.ts +131 -0
- package/src/commands/updateApp.spec.ts +540 -331
- package/src/commands/updateApp.ts +255 -668
- package/src/source.spec.ts +45 -0
- package/src/source.ts +88 -0
- package/templates/AGENTS.md +161 -0
- package/templates/README.md +62 -0
- package/templates/pieces/frappe/universal/README.md.partial +7 -32
- package/templates/pieces/frappe/universal/env.example.partial +3 -3
- package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
- package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
- package/templates/pieces/tauri/universal/README.md.partial +7 -5
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
- package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
- package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
- package/types/bin/multiplatformOne.d.ts.map +1 -1
- package/types/commands/addApp.d.ts +20 -0
- package/types/commands/addApp.d.ts.map +1 -0
- package/types/commands/adoptApp.d.ts +4 -3
- package/types/commands/adoptApp.d.ts.map +1 -1
- package/types/commands/doctypeTypes.d.ts +40 -0
- package/types/commands/doctypeTypes.d.ts.map +1 -0
- package/types/commands/init.d.ts +30 -13
- package/types/commands/init.d.ts.map +1 -1
- package/types/commands/merge.d.ts +54 -0
- package/types/commands/merge.d.ts.map +1 -0
- package/types/commands/pieces.d.ts +54 -0
- package/types/commands/pieces.d.ts.map +1 -0
- package/types/commands/provenance.d.ts +52 -0
- package/types/commands/provenance.d.ts.map +1 -0
- package/types/commands/scaffold.d.ts +43 -0
- package/types/commands/scaffold.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +19 -27
- package/types/commands/updateApp.d.ts.map +1 -1
- package/types/source.d.ts +35 -0
- package/types/source.d.ts.map +1 -0
- package/lib/commands/initApp.mjs +0 -431
- package/lib/generateVscode.mjs +0 -184
- package/scripts/clone.sh +0 -17
- package/scripts/update.sh +0 -46
- package/src/commands/initApp.spec.ts +0 -720
- package/src/commands/initApp.ts +0 -706
- package/src/generateVscode.ts +0 -238
- package/templates/app/AGENTS.md +0 -142
- package/templates/app/README.md +0 -62
- package/templates/app/apps/__NAME__/index.html +0 -36
- package/templates/app/apps/__NAME__/package.json +0 -31
- package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/app/apps/__NAME__/src/App.tsx +0 -9
- package/templates/app/apps/__NAME__/src/main.tsx +0 -14
- package/templates/app/apps/__NAME__/tsconfig.json +0 -8
- package/templates/app/apps/__NAME__/vite.config.ts +0 -17
- package/templates/app/features/__NAME__/package.json +0 -34
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
- package/templates/app/features/__NAME__/src/index.ts +0 -1
- package/templates/app/features/__NAME__/tsconfig.json +0 -5
- package/templates/app/gitignore +0 -9
- package/templates/app/npmrc +0 -2
- package/templates/app/oxlintrc.json +0 -39
- package/templates/app/package.json +0 -38
- package/templates/app/packages/config/config.json +0 -4
- package/templates/app/packages/config/index.ts +0 -5
- package/templates/app/packages/config/package.json +0 -17
- package/templates/app/packages/config/tsconfig.json +0 -5
- package/templates/app/pnpm-workspace.yaml +0 -4
- package/templates/app/tsconfig.base.json +0 -16
- package/templates/universal/AGENTS.md +0 -142
- package/templates/universal/README.md +0 -97
- package/templates/universal/apps/__NAME__/app.config.js +0 -31
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
- package/templates/universal/apps/__NAME__/package.json +0 -62
- package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
- package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
- package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
- package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
- package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
- package/templates/universal/apps/__NAME__/src/app.ts +0 -10
- package/templates/universal/apps/__NAME__/tamagui.css +0 -3
- package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
- package/templates/universal/apps/__NAME__/types.d.ts +0 -9
- package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
- package/templates/universal/features/__NAME__/package.json +0 -35
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
- package/templates/universal/features/__NAME__/src/index.ts +0 -4
- package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
- package/templates/universal/features/__NAME__/tsconfig.json +0 -5
- package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
- package/templates/universal/gitignore +0 -14
- package/templates/universal/npmrc +0 -3
- package/templates/universal/oxlintrc.json +0 -39
- package/templates/universal/package.json +0 -51
- package/templates/universal/packages/config/config.json +0 -4
- package/templates/universal/packages/config/index.ts +0 -5
- package/templates/universal/packages/config/package.json +0 -17
- package/templates/universal/packages/config/tsconfig.json +0 -5
- package/templates/universal/packages/i18n/en/common.json +0 -30
- package/templates/universal/packages/i18n/es/common.json +0 -30
- package/templates/universal/packages/i18n/i18n.d.ts +0 -16
- package/templates/universal/packages/i18n/index.ts +0 -26
- package/templates/universal/packages/i18n/package.json +0 -20
- package/templates/universal/packages/i18n/resources.ts +0 -13
- package/templates/universal/packages/i18n/tsconfig.json +0 -5
- package/templates/universal/packages/themes/accent.ts +0 -32
- package/templates/universal/packages/themes/base.ts +0 -50
- package/templates/universal/packages/themes/fonts.ts +0 -8
- package/templates/universal/packages/themes/index.ts +0 -33
- package/templates/universal/packages/themes/package.json +0 -23
- package/templates/universal/packages/themes/tamagui.config.ts +0 -14
- package/templates/universal/packages/themes/tsconfig.json +0 -5
- package/templates/universal/pnpm-workspace.yaml +0 -4
- package/templates/universal/tsconfig.base.json +0 -21
- package/templates/universal/types/env.d.ts +0 -9
- package/types/commands/initApp.d.ts +0 -114
- package/types/commands/initApp.d.ts.map +0 -1
- package/types/generateVscode.d.ts +0 -7
- package/types/generateVscode.d.ts.map +0 -1
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Scaffold a consumer project from the multiplatform.one monorepo.
|
|
3
|
+
#
|
|
4
|
+
# The consumer project IS the monorepo at one commit, pruned: the published
|
|
5
|
+
# packages under public/ go (they come back from the registry), the singleton
|
|
6
|
+
# apps not asked for go, and apps/one (the One platform app) is renamed to each
|
|
7
|
+
# product app. `mpo init` and `mpo update` call this script; it also runs on its
|
|
8
|
+
# own with nothing but git, node and a POSIX shell.
|
|
9
|
+
#
|
|
10
|
+
# Usage: scaffold.sh [options] <source> <ref> <target>
|
|
11
|
+
# source path to a local clone (taken with git archive) or a git URL
|
|
12
|
+
# (shallow fetch of the ref)
|
|
13
|
+
# ref branch, tag or commit
|
|
14
|
+
# target directory to write; must not exist or be empty (see --into)
|
|
15
|
+
#
|
|
16
|
+
# Options:
|
|
17
|
+
# --app <name> product app derived from apps/one (repeatable)
|
|
18
|
+
# --service <name> singleton app under apps/ to keep (repeatable):
|
|
19
|
+
# ethereum frappe keycloak solana storybook
|
|
20
|
+
# storybook-expo sui uxpin vocs
|
|
21
|
+
# --mpo-version <range> range written for @multiplatform.one/* deps
|
|
22
|
+
# (default: ^<version of public/cli in the source tree>)
|
|
23
|
+
# --keep <dir> keep a pruned top-level path (repeatable):
|
|
24
|
+
# public agent-os .changeset docs deploy
|
|
25
|
+
# --into target may already hold files: a path that already
|
|
26
|
+
# exists there is skipped, listed, and pinned in
|
|
27
|
+
# .updateignore so `mpo update` never rewrites it
|
|
28
|
+
# --source-url <url> url recorded in .mpo.json when <source> is a path
|
|
29
|
+
# --name <name> root package.json name (default: the target's basename)
|
|
30
|
+
|
|
31
|
+
set -eu
|
|
32
|
+
|
|
33
|
+
usage() {
|
|
34
|
+
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
die() {
|
|
38
|
+
printf 'scaffold: %s\n' "$*" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pascal_case() {
|
|
43
|
+
printf '%s\n' "$1" | awk -F- '{ for (i = 1; i <= NF; i++) printf "%s%s", toupper(substr($i, 1, 1)), substr($i, 2); print "" }'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
valid_name() {
|
|
47
|
+
case "$1" in
|
|
48
|
+
'' | *[!a-z0-9-]* | [!a-z]*) return 1 ;;
|
|
49
|
+
esac
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# The ls-tree lines on stdin whose path is a word of $1, printed without the
|
|
53
|
+
# directory prefix $2 (mktree input for that directory).
|
|
54
|
+
keep_entries() {
|
|
55
|
+
while IFS="$TAB" read -r _meta _path; do
|
|
56
|
+
case " $1 " in
|
|
57
|
+
*" $_path "*) printf '%s\t%s\n' "$_meta" "${_path#"$2"}" ;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Rewrite one file in place with a sed script ($2), atomically.
|
|
63
|
+
sed_file() {
|
|
64
|
+
_tmp="$1.scaffold.tmp"
|
|
65
|
+
sed "$2" "$1" >"$_tmp" && mv "$_tmp" "$1"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
positional() {
|
|
69
|
+
if [ -z "$SOURCE" ]; then
|
|
70
|
+
SOURCE="$1"
|
|
71
|
+
elif [ -z "$REF" ]; then
|
|
72
|
+
REF="$1"
|
|
73
|
+
elif [ -z "$TARGET" ]; then
|
|
74
|
+
TARGET="$1"
|
|
75
|
+
else
|
|
76
|
+
die "unexpected argument $1 (see --help)"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Text files under $1 that mention any of the patterns given after it.
|
|
81
|
+
grep_files() {
|
|
82
|
+
_dir="$1"
|
|
83
|
+
shift
|
|
84
|
+
grep -rIl "$@" "$_dir" 2>/dev/null || true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
APPS=""
|
|
88
|
+
SERVICES=""
|
|
89
|
+
KEEP=""
|
|
90
|
+
MPO_VERSION=""
|
|
91
|
+
INTO=0
|
|
92
|
+
SOURCE_URL=""
|
|
93
|
+
PROJECT_NAME=""
|
|
94
|
+
SOURCE=""
|
|
95
|
+
REF=""
|
|
96
|
+
TARGET=""
|
|
97
|
+
PRUNE="public agent-os .changeset docs deploy typedoc.json .gitlab-ci.yml"
|
|
98
|
+
TAB="$(printf '\t')"
|
|
99
|
+
|
|
100
|
+
while [ $# -gt 0 ]; do
|
|
101
|
+
case "$1" in
|
|
102
|
+
--app)
|
|
103
|
+
[ $# -ge 2 ] || die "--app needs a name"
|
|
104
|
+
valid_name "$2" || die "app name \"$2\" must be lowercase letters, digits and hyphens, starting with a letter"
|
|
105
|
+
[ "$2" != one ] || die "the product app cannot be named one (that is the monorepo's One app)"
|
|
106
|
+
APPS="$APPS $2"
|
|
107
|
+
shift 2
|
|
108
|
+
;;
|
|
109
|
+
--service)
|
|
110
|
+
[ $# -ge 2 ] || die "--service needs a name"
|
|
111
|
+
valid_name "$2" || die "service name \"$2\" must be lowercase letters, digits and hyphens"
|
|
112
|
+
[ "$2" != one ] || die "one is the product app, not a service (use --app)"
|
|
113
|
+
SERVICES="$SERVICES $2"
|
|
114
|
+
shift 2
|
|
115
|
+
;;
|
|
116
|
+
--mpo-version)
|
|
117
|
+
[ $# -ge 2 ] || die "--mpo-version needs a range"
|
|
118
|
+
MPO_VERSION="$2"
|
|
119
|
+
shift 2
|
|
120
|
+
;;
|
|
121
|
+
--keep)
|
|
122
|
+
[ $# -ge 2 ] || die "--keep needs a path"
|
|
123
|
+
KEEP="$KEEP $2"
|
|
124
|
+
shift 2
|
|
125
|
+
;;
|
|
126
|
+
--into)
|
|
127
|
+
INTO=1
|
|
128
|
+
shift
|
|
129
|
+
;;
|
|
130
|
+
--source-url)
|
|
131
|
+
[ $# -ge 2 ] || die "--source-url needs a url"
|
|
132
|
+
SOURCE_URL="$2"
|
|
133
|
+
shift 2
|
|
134
|
+
;;
|
|
135
|
+
--name)
|
|
136
|
+
[ $# -ge 2 ] || die "--name needs a name"
|
|
137
|
+
PROJECT_NAME="$2"
|
|
138
|
+
shift 2
|
|
139
|
+
;;
|
|
140
|
+
-h | --help)
|
|
141
|
+
usage
|
|
142
|
+
exit 0
|
|
143
|
+
;;
|
|
144
|
+
--)
|
|
145
|
+
shift
|
|
146
|
+
for _arg in "$@"; do positional "$_arg"; done
|
|
147
|
+
break
|
|
148
|
+
;;
|
|
149
|
+
-*)
|
|
150
|
+
die "unknown option $1 (see --help)"
|
|
151
|
+
;;
|
|
152
|
+
*)
|
|
153
|
+
positional "$1"
|
|
154
|
+
shift
|
|
155
|
+
;;
|
|
156
|
+
esac
|
|
157
|
+
done
|
|
158
|
+
|
|
159
|
+
[ -n "$SOURCE" ] && [ -n "$REF" ] && [ -n "$TARGET" ] || {
|
|
160
|
+
usage >&2
|
|
161
|
+
exit 1
|
|
162
|
+
}
|
|
163
|
+
APPS="${APPS# }"
|
|
164
|
+
SERVICES="${SERVICES# }"
|
|
165
|
+
KEEP="${KEEP# }"
|
|
166
|
+
FIRST_APP="${APPS%% *}"
|
|
167
|
+
|
|
168
|
+
for _app in $APPS; do
|
|
169
|
+
for _service in $SERVICES; do
|
|
170
|
+
[ "$_app" != "$_service" ] || die "$_app is both an --app and a --service"
|
|
171
|
+
done
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
command -v git >/dev/null 2>&1 || die "git is required"
|
|
175
|
+
command -v node >/dev/null 2>&1 || die "node is required (JSON edits)"
|
|
176
|
+
|
|
177
|
+
if [ -e "$TARGET" ]; then
|
|
178
|
+
[ -d "$TARGET" ] || die "target $TARGET exists and is not a directory"
|
|
179
|
+
if [ -n "$(ls -A "$TARGET")" ] && [ "$INTO" -eq 0 ]; then
|
|
180
|
+
die "target $TARGET is not empty (pass --into to scaffold into an existing directory)"
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
WORK="$(mktemp -d "${TMPDIR:-/tmp}/mpo-scaffold.XXXXXX")"
|
|
185
|
+
trap 'rm -rf "$WORK"' EXIT INT TERM
|
|
186
|
+
|
|
187
|
+
# ── 1. the repository and the commit ─────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
if [ -d "$SOURCE" ]; then
|
|
190
|
+
REPO="$(cd "$SOURCE" && pwd)"
|
|
191
|
+
SHA="$(git -C "$REPO" rev-parse --verify --quiet "$REF^{commit}")" ||
|
|
192
|
+
die "$REF is not a commit in $REPO"
|
|
193
|
+
[ -n "$SOURCE_URL" ] || SOURCE_URL="$REPO"
|
|
194
|
+
else
|
|
195
|
+
# A shallow, blobless fetch: commit and trees only. The blobs the project
|
|
196
|
+
# keeps are fetched in one batch in step 2, so nothing under public/ or
|
|
197
|
+
# docs/ crosses the wire (a server without filter support sends all).
|
|
198
|
+
REPO="$WORK/clone"
|
|
199
|
+
git init -q "$REPO"
|
|
200
|
+
git -C "$REPO" remote add origin "$SOURCE"
|
|
201
|
+
git -C "$REPO" fetch -q --depth 1 --filter=blob:none origin "$REF" ||
|
|
202
|
+
die "could not fetch $REF from $SOURCE"
|
|
203
|
+
SHA="$(git -C "$REPO" rev-parse --verify --quiet 'FETCH_HEAD^{commit}')" ||
|
|
204
|
+
die "$REF at $SOURCE is not a commit"
|
|
205
|
+
[ -n "$SOURCE_URL" ] || SOURCE_URL="$SOURCE"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
git -C "$REPO" rev-parse --verify --quiet "$SHA:apps/one/package.json" >/dev/null ||
|
|
209
|
+
die "$SOURCE at $REF has no apps/one: not the multiplatform.one monorepo"
|
|
210
|
+
|
|
211
|
+
if [ -z "$MPO_VERSION" ]; then
|
|
212
|
+
MPO_VERSION="^$(git -C "$REPO" show "$SHA:public/cli/package.json" | sed -n 's/^ "version": "\([^"]*\)".*/\1/p' | head -n 1)"
|
|
213
|
+
[ "$MPO_VERSION" != "^" ] || die "could not read the CLI version from public/cli/package.json at $SHA"
|
|
214
|
+
fi
|
|
215
|
+
CLI_VERSION="${MPO_VERSION#[~^]}"
|
|
216
|
+
|
|
217
|
+
# ── 2. take only what a consumer carries ─────────────────────────────────
|
|
218
|
+
|
|
219
|
+
# Top-level entries and apps/* at the commit decide what is archived; the
|
|
220
|
+
# pruned trees are never written, which is most of the wall time saved.
|
|
221
|
+
REMOVED=""
|
|
222
|
+
REMOVED_APPS=""
|
|
223
|
+
PATHS=""
|
|
224
|
+
for _entry in $(git -C "$REPO" ls-tree --name-only "$SHA"); do
|
|
225
|
+
case "$_entry" in
|
|
226
|
+
apps) continue ;;
|
|
227
|
+
esac
|
|
228
|
+
case " $PRUNE " in
|
|
229
|
+
*" $_entry "*)
|
|
230
|
+
case " $KEEP " in
|
|
231
|
+
*" $_entry "*) ;;
|
|
232
|
+
*)
|
|
233
|
+
REMOVED="$REMOVED $_entry"
|
|
234
|
+
printf 'removed: %s\n' "$_entry"
|
|
235
|
+
continue
|
|
236
|
+
;;
|
|
237
|
+
esac
|
|
238
|
+
;;
|
|
239
|
+
esac
|
|
240
|
+
PATHS="$PATHS $_entry"
|
|
241
|
+
done
|
|
242
|
+
for _entry in $(git -C "$REPO" ls-tree --name-only "$SHA" apps/); do
|
|
243
|
+
_name="${_entry#apps/}"
|
|
244
|
+
case " one $SERVICES " in
|
|
245
|
+
*" $_name "*)
|
|
246
|
+
PATHS="$PATHS $_entry"
|
|
247
|
+
continue
|
|
248
|
+
;;
|
|
249
|
+
esac
|
|
250
|
+
REMOVED_APPS="$REMOVED_APPS $_name"
|
|
251
|
+
REMOVED="$REMOVED $_entry"
|
|
252
|
+
printf 'removed: %s\n' "$_entry"
|
|
253
|
+
done
|
|
254
|
+
REMOVED="${REMOVED# }"
|
|
255
|
+
REMOVED_APPS="${REMOVED_APPS# }"
|
|
256
|
+
for _service in $SERVICES; do
|
|
257
|
+
case " $PATHS " in
|
|
258
|
+
*" apps/$_service "*) ;;
|
|
259
|
+
*) die "service $_service does not exist under apps/ at $SHA" ;;
|
|
260
|
+
esac
|
|
261
|
+
done
|
|
262
|
+
|
|
263
|
+
if [ "$INTO" -eq 1 ]; then
|
|
264
|
+
TREE="$WORK/tree"
|
|
265
|
+
else
|
|
266
|
+
TREE="$TARGET"
|
|
267
|
+
fi
|
|
268
|
+
mkdir -p "$TREE"
|
|
269
|
+
if [ "$(git -C "$REPO" config --get remote.origin.promisor || true)" = true ]; then
|
|
270
|
+
# git archive of a pathspec still reads blobs outside it, so a blobless
|
|
271
|
+
# clone archives a tree holding only $PATHS, its blobs fetched in one go.
|
|
272
|
+
_apps_tree="$(git -C "$REPO" ls-tree "$SHA" apps/ | keep_entries "$PATHS" apps/ | git -C "$REPO" mktree --missing)"
|
|
273
|
+
_tree="$({
|
|
274
|
+
git -C "$REPO" ls-tree "$SHA" | keep_entries "$PATHS" ""
|
|
275
|
+
printf '040000 tree %s\tapps\n' "$_apps_tree"
|
|
276
|
+
} | git -C "$REPO" mktree --missing)"
|
|
277
|
+
git -C "$REPO" ls-tree -r "$_tree" | awk '$2 == "blob" { print $3 }' >"$WORK/blobs"
|
|
278
|
+
# The same request git makes to fill a blobless clone: a "have" for a
|
|
279
|
+
# fetched ref would tell the server the blobs are already here.
|
|
280
|
+
if [ -s "$WORK/blobs" ]; then
|
|
281
|
+
git -C "$REPO" -c fetch.negotiationAlgorithm=noop fetch -q --no-tags --no-write-fetch-head \
|
|
282
|
+
--filter=blob:none --stdin origin <"$WORK/blobs" ||
|
|
283
|
+
die "could not fetch the project's files from $SOURCE"
|
|
284
|
+
fi
|
|
285
|
+
git -C "$REPO" archive --format=tar "$_tree" | tar -xf - -C "$TREE"
|
|
286
|
+
else
|
|
287
|
+
# shellcheck disable=SC2086
|
|
288
|
+
git -C "$REPO" archive --format=tar "$SHA" $PATHS | tar -xf - -C "$TREE"
|
|
289
|
+
fi
|
|
290
|
+
rm -rf "$TREE/apps/storybook/.lostpixel/baseline"
|
|
291
|
+
# patches/public/ patches packages only the published packages' build
|
|
292
|
+
# installs; pnpm fails an install whose patch matches nothing.
|
|
293
|
+
case " $REMOVED " in
|
|
294
|
+
*" public "*) rm -rf "$TREE/patches/public" ;;
|
|
295
|
+
esac
|
|
296
|
+
|
|
297
|
+
# The root AGENTS.md documents the monorepo; a project gets the consumer copy
|
|
298
|
+
# at the same commit, so an mpo update merge base carries the copy its commit
|
|
299
|
+
# shipped (a commit before the copy existed keeps the monorepo file).
|
|
300
|
+
if git -C "$REPO" rev-parse --verify --quiet "$SHA:public/cli/templates/AGENTS.md" >/dev/null; then
|
|
301
|
+
git -C "$REPO" show "$SHA:public/cli/templates/AGENTS.md" >"$TREE/AGENTS.md"
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# Commits from MPO-343 to MPO-289 shipped a pnpmfile mapping @multiplatform.one
|
|
305
|
+
# to Pulp. The scaffold at one of them still carries it, so an mpo update whose
|
|
306
|
+
# merge base is such a commit sees the file leave and removes it.
|
|
307
|
+
if git -C "$REPO" rev-parse --verify --quiet "$SHA:public/cli/templates/pnpmfile.cjs" >/dev/null; then
|
|
308
|
+
git -C "$REPO" show "$SHA:public/cli/templates/pnpmfile.cjs" >"$TREE/.pnpmfile.cjs"
|
|
309
|
+
fi
|
|
310
|
+
if [ -f "$TREE/.pnpmfile.cjs" ] && [ -f "$TREE/pnpm-lock.yaml" ]; then
|
|
311
|
+
node - "$TREE/.pnpmfile.cjs" "$TREE/pnpm-lock.yaml" <<'NODE'
|
|
312
|
+
const crypto = require("node:crypto");
|
|
313
|
+
const fs = require("node:fs");
|
|
314
|
+
const [pnpmfile, lockfile] = process.argv.slice(2);
|
|
315
|
+
// pnpm re-resolves every dependency when the lockfile's pnpmfileChecksum does
|
|
316
|
+
// not match, and the shipped lockfile would pin nothing. Same hash as pnpm 10.
|
|
317
|
+
const content = fs.readFileSync(pnpmfile, "utf8").split("\r\n").join("\n");
|
|
318
|
+
const checksum = `sha256-${crypto.createHash("sha256").update(content).digest("base64")}`;
|
|
319
|
+
let lock = fs.readFileSync(lockfile, "utf8").replace(/^pnpmfileChecksum: .*\n\n/m, "");
|
|
320
|
+
const at = lock.search(/^(patchedDependencies|importers):/m);
|
|
321
|
+
if (at < 0) process.exit(0);
|
|
322
|
+
lock = `${lock.slice(0, at)}pnpmfileChecksum: ${checksum}\n\n${lock.slice(at)}`;
|
|
323
|
+
fs.writeFileSync(lockfile, lock);
|
|
324
|
+
NODE
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
# Whatever names the One app outside it (the frappe Makefile's SPA copy,
|
|
328
|
+
# docker-bake.hcl, comments in features/) points at the first product app;
|
|
329
|
+
# the root files that need more than a rename are handled below.
|
|
330
|
+
if [ -n "$FIRST_APP" ]; then
|
|
331
|
+
for _file in $(grep_files "$TREE" -e '@platform/one' -e 'apps/one' -e 'features/one'); do
|
|
332
|
+
case "$_file" in
|
|
333
|
+
"$TREE/apps/one/"* | "$TREE/features/one/"* | "$TREE/.gitignore" | "$TREE/Makefile" | "$TREE/package.json" | "$TREE/.vscode/"*) continue ;;
|
|
334
|
+
esac
|
|
335
|
+
sed_file "$_file" "s|@platform/one|@app/$FIRST_APP|g
|
|
336
|
+
s|apps/one\\([^-a-zA-Z0-9_]\\)|apps/$FIRST_APP\\1|g
|
|
337
|
+
s|apps/one\$|apps/$FIRST_APP|g
|
|
338
|
+
s|features/one\\([^-a-zA-Z0-9_]\\)|features/$FIRST_APP\\1|g
|
|
339
|
+
s|features/one\$|features/$FIRST_APP|g"
|
|
340
|
+
done
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# ── 3. apps/one becomes each product app ─────────────────────────────────
|
|
344
|
+
|
|
345
|
+
for _app in $APPS; do
|
|
346
|
+
[ ! -e "$TREE/apps/$_app" ] || die "apps/$_app already exists in the source tree; pick another product name"
|
|
347
|
+
cp -R "$TREE/apps/one" "$TREE/apps/$_app"
|
|
348
|
+
# A feature module of that name already satisfies the app/features twin
|
|
349
|
+
# (features/pokemon is the One app's own pokemon feature). The index
|
|
350
|
+
# routes import features/<app>/screen, so a module without one gets
|
|
351
|
+
# features/one's.
|
|
352
|
+
if [ -d "$TREE/features/one" ]; then
|
|
353
|
+
if [ ! -e "$TREE/features/$_app" ]; then
|
|
354
|
+
cp -R "$TREE/features/one" "$TREE/features/$_app"
|
|
355
|
+
elif [ ! -e "$TREE/features/$_app/screen.tsx" ]; then
|
|
356
|
+
cp "$TREE/features/one/screen.tsx" "$TREE/features/$_app/screen.tsx"
|
|
357
|
+
fi
|
|
358
|
+
fi
|
|
359
|
+
_pascal="$(pascal_case "$_app")"
|
|
360
|
+
_appid="$(printf '%s' "$_app" | tr -d '-')"
|
|
361
|
+
_script="s|@platform/one|@app/$_app|g
|
|
362
|
+
s|apps/one\\([^-a-zA-Z0-9_]\\)|apps/$_app\\1|g
|
|
363
|
+
s|apps/one\$|apps/$_app|g
|
|
364
|
+
s|features/one\\([^-a-zA-Z0-9_]\\)|features/$_app\\1|g
|
|
365
|
+
s|features/one\$|features/$_app|g
|
|
366
|
+
s|one\\.multiplatform\\.app|com.$_appid.app|g
|
|
367
|
+
s|multiplatform-one|$_app|g
|
|
368
|
+
s|Multiplatform One|$_pascal|g
|
|
369
|
+
s|\"multiplatform\\.one\"|\"$_app\"|g"
|
|
370
|
+
for _dir in "$TREE/apps/$_app" "$TREE/features/$_app"; do
|
|
371
|
+
[ -d "$_dir" ] || continue
|
|
372
|
+
for _file in $(grep_files "$_dir" -e '@platform/one' -e 'apps/one' -e 'features/one' -e 'one\.multiplatform\.app' -e 'multiplatform-one' -e 'Multiplatform One' -e '"multiplatform\.one"'); do
|
|
373
|
+
sed_file "$_file" "$_script"
|
|
374
|
+
done
|
|
375
|
+
done
|
|
376
|
+
done
|
|
377
|
+
rm -rf "$TREE/apps/one" "$TREE/features/one"
|
|
378
|
+
|
|
379
|
+
# ── 4. root wiring: Makefile, .gitignore, .env, .vscode, package.json ─────
|
|
380
|
+
|
|
381
|
+
if [ -f "$TREE/Makefile" ]; then
|
|
382
|
+
awk -v apps="$APPS" -v removed="$REMOVED_APPS" '
|
|
383
|
+
BEGIN {
|
|
384
|
+
napps = split(apps, app, " ")
|
|
385
|
+
nremoved = split(removed, gone, " ")
|
|
386
|
+
for (i = 1; i <= nremoved; i++) dead[gone[i]] = 1
|
|
387
|
+
dead["vscode"] = 1
|
|
388
|
+
dead["webext"] = 1
|
|
389
|
+
}
|
|
390
|
+
# "name/%: configure FORCE" followed by its one recipe line.
|
|
391
|
+
match($0, /^[a-z0-9-]+\/%: /) {
|
|
392
|
+
name = substr($0, 1, RSTART + RLENGTH - 5)
|
|
393
|
+
getline recipe
|
|
394
|
+
if (name == "one") {
|
|
395
|
+
for (i = 1; i <= napps; i++) {
|
|
396
|
+
line = $0
|
|
397
|
+
sub(/^one\//, app[i] "/", line)
|
|
398
|
+
print line
|
|
399
|
+
r = recipe
|
|
400
|
+
gsub(/apps\/one/, "apps/" app[i], r)
|
|
401
|
+
print r
|
|
402
|
+
}
|
|
403
|
+
next
|
|
404
|
+
}
|
|
405
|
+
if (name in dead) next
|
|
406
|
+
print
|
|
407
|
+
print recipe
|
|
408
|
+
next
|
|
409
|
+
}
|
|
410
|
+
/^# vscode \+ webext are targets OF the One platform app/ { getline; next }
|
|
411
|
+
/^\.PHONY: / && /one\/%/ {
|
|
412
|
+
out = ".PHONY:"
|
|
413
|
+
for (i = 2; i <= NF; i++) {
|
|
414
|
+
word = $i
|
|
415
|
+
base = word
|
|
416
|
+
sub(/\/%$/, "", base)
|
|
417
|
+
if (base == "one") {
|
|
418
|
+
for (j = 1; j <= napps; j++) out = out " " app[j] "/%"
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
if (base in dead) continue
|
|
422
|
+
out = out " " word
|
|
423
|
+
}
|
|
424
|
+
print out
|
|
425
|
+
next
|
|
426
|
+
}
|
|
427
|
+
# Plain rule headers: drop prerequisites of removed apps (down: keycloak/down docker/down).
|
|
428
|
+
/^[a-z][a-z0-9\/._-]*: / && !/%/ {
|
|
429
|
+
out = $1
|
|
430
|
+
for (i = 2; i <= NF; i++) {
|
|
431
|
+
word = $i
|
|
432
|
+
base = word
|
|
433
|
+
sub(/\/.*$/, "", base)
|
|
434
|
+
if (base in dead) continue
|
|
435
|
+
out = out " " word
|
|
436
|
+
}
|
|
437
|
+
print out
|
|
438
|
+
next
|
|
439
|
+
}
|
|
440
|
+
{ print }
|
|
441
|
+
' "$TREE/Makefile" >"$TREE/Makefile.scaffold.tmp" && mv "$TREE/Makefile.scaffold.tmp" "$TREE/Makefile"
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
if [ -f "$TREE/.gitignore" ]; then
|
|
445
|
+
awk -v apps="$APPS" '
|
|
446
|
+
BEGIN { napps = split(apps, app, " ") }
|
|
447
|
+
/apps\/one(\/|$| )/ {
|
|
448
|
+
for (i = 1; i <= napps; i++) {
|
|
449
|
+
line = $0
|
|
450
|
+
gsub(/apps\/one/, "apps/" app[i], line)
|
|
451
|
+
print line
|
|
452
|
+
}
|
|
453
|
+
next
|
|
454
|
+
}
|
|
455
|
+
{ print }
|
|
456
|
+
' "$TREE/.gitignore" >"$TREE/.gitignore.scaffold.tmp" && mv "$TREE/.gitignore.scaffold.tmp" "$TREE/.gitignore"
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
# .env.example sections are headed "# <app>"; a removed app takes its section
|
|
460
|
+
# (header through the next blank line) with it. frappe also owns mariadb,
|
|
461
|
+
# and keycloak postgres.
|
|
462
|
+
ENV_SECTIONS="$REMOVED_APPS"
|
|
463
|
+
[ -z "$APPS" ] && [ -d "$TREE/apps" ] && ENV_SECTIONS="$ENV_SECTIONS one"
|
|
464
|
+
case " $REMOVED_APPS " in
|
|
465
|
+
*" frappe "*) ENV_SECTIONS="$ENV_SECTIONS mariadb" ;;
|
|
466
|
+
esac
|
|
467
|
+
case " $REMOVED_APPS " in
|
|
468
|
+
*" keycloak "*) ENV_SECTIONS="$ENV_SECTIONS postgres" ;;
|
|
469
|
+
esac
|
|
470
|
+
for _envfile in "$TREE/.env.example" "$TREE/.env"; do
|
|
471
|
+
[ -f "$_envfile" ] || continue
|
|
472
|
+
awk -v sections="$ENV_SECTIONS" '
|
|
473
|
+
BEGIN { n = split(sections, s, " "); for (i = 1; i <= n; i++) drop["# " s[i]] = 1 }
|
|
474
|
+
skipping && /^$/ { skipping = 0; next }
|
|
475
|
+
skipping { next }
|
|
476
|
+
($0 in drop) { skipping = 1; next }
|
|
477
|
+
{ print }
|
|
478
|
+
' "$_envfile" >"$_envfile.scaffold.tmp" && mv "$_envfile.scaffold.tmp" "$_envfile"
|
|
479
|
+
done
|
|
480
|
+
if [ -f "$TREE/.env.example" ] && [ ! -f "$TREE/.env" ]; then
|
|
481
|
+
cp "$TREE/.env.example" "$TREE/.env"
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
# Docker wiring that backs a pruned app goes with it. A compose service goes
|
|
485
|
+
# when one of its profiles is named after the app (frappe: the bench's mariadb,
|
|
486
|
+
# redis and mailpit; keycloak: its postgres), when it bind-mounts from under
|
|
487
|
+
# apps/<app>, or when it depends on a service that went, and so do the named
|
|
488
|
+
# volumes only those services used. With no service left the file is
|
|
489
|
+
# `services: {}`, which compose still reads, and `pnpm backend` goes with the
|
|
490
|
+
# services it started. A bake target building from apps/<app> and a
|
|
491
|
+
# `# <app>-block` of docker/Makefile go too. Nothing is rewritten while every
|
|
492
|
+
# app they name is kept.
|
|
493
|
+
if [ -n "$REMOVED_APPS" ] && [ -f "$TREE/docker/compose.yaml" ]; then
|
|
494
|
+
awk -v removed="$REMOVED_APPS" '
|
|
495
|
+
function record(svc, section, value, name) {
|
|
496
|
+
name = value
|
|
497
|
+
sub(/:.*/, "", name)
|
|
498
|
+
gsub(/[" \047]/, "", name)
|
|
499
|
+
if (section == "profiles" && (name in gone)) dead[svc] = 1
|
|
500
|
+
if (section == "depends_on" && name != "") needs[svc, ++nneeds[svc]] = name
|
|
501
|
+
if (section == "volumes" && name ~ /^[A-Za-z0-9_-]+$/ && value ~ /:/) uses[svc, ++nuses[svc]] = name
|
|
502
|
+
}
|
|
503
|
+
BEGIN {
|
|
504
|
+
nremoved = split(removed, list, " ")
|
|
505
|
+
for (i = 1; i <= nremoved; i++) gone[list[i]] = 1
|
|
506
|
+
}
|
|
507
|
+
{ line[++n] = $0 }
|
|
508
|
+
END {
|
|
509
|
+
for (i = 1; i <= n; i++) {
|
|
510
|
+
l = line[i]
|
|
511
|
+
if (l ~ /^[^ #]/) {
|
|
512
|
+
npending = 0
|
|
513
|
+
top = l
|
|
514
|
+
sub(/:.*/, "", top)
|
|
515
|
+
cur = ""
|
|
516
|
+
vol = ""
|
|
517
|
+
if (top == "services") services_at = i
|
|
518
|
+
if (top == "volumes") volumes_at = i
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
if (top == "volumes") {
|
|
522
|
+
if (l ~ /^ [A-Za-z0-9_.-]+:/) {
|
|
523
|
+
vol = l
|
|
524
|
+
sub(/^ /, "", vol)
|
|
525
|
+
sub(/:.*/, "", vol)
|
|
526
|
+
}
|
|
527
|
+
if (vol != "") vowner[i] = vol
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
if (top != "services") continue
|
|
531
|
+
if (l ~ /^ #/ || (npending && l ~ /^[ ]*$/)) {
|
|
532
|
+
pending[++npending] = i
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
if (l ~ /^ [A-Za-z0-9_.-]+:/) {
|
|
536
|
+
cur = l
|
|
537
|
+
sub(/^ /, "", cur)
|
|
538
|
+
sub(/:.*/, "", cur)
|
|
539
|
+
order[++nservices] = cur
|
|
540
|
+
for (j = 1; j <= npending; j++) owner[pending[j]] = cur
|
|
541
|
+
npending = 0
|
|
542
|
+
section = ""
|
|
543
|
+
owner[i] = cur
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
if (cur == "") continue
|
|
547
|
+
owner[i] = cur
|
|
548
|
+
if (l ~ /^[ ]*#/) continue
|
|
549
|
+
for (app in gone) if (index(l, "../apps/" app "/")) dead[cur] = 1
|
|
550
|
+
if (l ~ /^ [A-Za-z_]+:/) {
|
|
551
|
+
section = l
|
|
552
|
+
sub(/^ /, "", section)
|
|
553
|
+
sub(/:.*/, "", section)
|
|
554
|
+
rest = l
|
|
555
|
+
sub(/^ [A-Za-z_]+:[ ]*/, "", rest)
|
|
556
|
+
if (rest ~ /^\[/) {
|
|
557
|
+
gsub(/[][]/, "", rest)
|
|
558
|
+
nparts = split(rest, parts, ",")
|
|
559
|
+
for (j = 1; j <= nparts; j++) record(cur, section, parts[j])
|
|
560
|
+
}
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
563
|
+
if (l ~ /^ - / || l ~ /^ [A-Za-z0-9_.-]+:/) {
|
|
564
|
+
value = l
|
|
565
|
+
sub(/^ (- )?/, "", value)
|
|
566
|
+
record(cur, section, value)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
do {
|
|
570
|
+
changed = 0
|
|
571
|
+
for (s = 1; s <= nservices; s++) {
|
|
572
|
+
svc = order[s]
|
|
573
|
+
if (svc in dead) continue
|
|
574
|
+
for (k = 1; k <= nneeds[svc]; k++) {
|
|
575
|
+
if (needs[svc, k] in dead) {
|
|
576
|
+
dead[svc] = 1
|
|
577
|
+
changed = 1
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} while (changed)
|
|
582
|
+
live = 0
|
|
583
|
+
for (s = 1; s <= nservices; s++) {
|
|
584
|
+
svc = order[s]
|
|
585
|
+
if (!(svc in dead)) live++
|
|
586
|
+
for (k = 1; k <= nuses[svc]; k++) {
|
|
587
|
+
if (svc in dead) dead_use[uses[svc, k]] = 1
|
|
588
|
+
else live_use[uses[svc, k]] = 1
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (live == nservices) {
|
|
592
|
+
for (i = 1; i <= n; i++) print line[i]
|
|
593
|
+
exit
|
|
594
|
+
}
|
|
595
|
+
volumes_kept = 0
|
|
596
|
+
for (i in vowner) {
|
|
597
|
+
name = vowner[i]
|
|
598
|
+
if ((name in dead_use) && !(name in live_use)) vdrop[name] = 1
|
|
599
|
+
else volumes_kept = 1
|
|
600
|
+
}
|
|
601
|
+
m = 0
|
|
602
|
+
for (i = 1; i <= n; i++) {
|
|
603
|
+
if ((i in owner) && (owner[i] in dead)) continue
|
|
604
|
+
if ((i in vowner) && (vowner[i] in vdrop)) continue
|
|
605
|
+
if (i == volumes_at && !volumes_kept) continue
|
|
606
|
+
out[++m] = (i == services_at && !live) ? "services: {}" : line[i]
|
|
607
|
+
}
|
|
608
|
+
while (m > 0 && out[m] ~ /^[ ]*$/) m--
|
|
609
|
+
for (i = 1; i <= m; i++) print out[i]
|
|
610
|
+
}
|
|
611
|
+
' "$TREE/docker/compose.yaml" >"$TREE/docker/compose.yaml.scaffold.tmp" &&
|
|
612
|
+
mv "$TREE/docker/compose.yaml.scaffold.tmp" "$TREE/docker/compose.yaml"
|
|
613
|
+
if grep -q '^services: {}$' "$TREE/docker/compose.yaml"; then
|
|
614
|
+
node - "$TREE/package.json" <<'NODE'
|
|
615
|
+
const fs = require("node:fs");
|
|
616
|
+
const file = process.argv[2];
|
|
617
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
618
|
+
if (pkg.scripts?.backend?.includes("docker/compose.yaml")) {
|
|
619
|
+
delete pkg.scripts.backend;
|
|
620
|
+
fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
621
|
+
}
|
|
622
|
+
NODE
|
|
623
|
+
fi
|
|
624
|
+
fi
|
|
625
|
+
if [ -n "$REMOVED_APPS" ] && [ -f "$TREE/docker/docker-bake.hcl" ]; then
|
|
626
|
+
awk -v removed="$REMOVED_APPS" '
|
|
627
|
+
BEGIN { nremoved = split(removed, gone, " ") }
|
|
628
|
+
{ line[++n] = $0 }
|
|
629
|
+
END {
|
|
630
|
+
for (i = 1; i <= n; i++) {
|
|
631
|
+
if (line[i] !~ /^target "[^"]*" \{/) continue
|
|
632
|
+
name = line[i]
|
|
633
|
+
sub(/^target "/, "", name)
|
|
634
|
+
sub(/".*/, "", name)
|
|
635
|
+
for (end = i; end < n && line[end] != "}"; end++) continue
|
|
636
|
+
drop = 0
|
|
637
|
+
for (j = 1; j <= nremoved; j++) {
|
|
638
|
+
if (name == gone[j]) drop = 1
|
|
639
|
+
for (k = i; k <= end; k++)
|
|
640
|
+
if (line[k] ~ /^[ ]*dockerfile[ ]*=/ && index(line[k], "\"apps/" gone[j] "/")) drop = 1
|
|
641
|
+
}
|
|
642
|
+
if (drop) {
|
|
643
|
+
start = i
|
|
644
|
+
while (start > 1 && line[start - 1] ~ /^#/) start--
|
|
645
|
+
if (start > 1 && line[start - 1] == "") start--
|
|
646
|
+
for (k = start; k <= end; k++) skip[k] = 1
|
|
647
|
+
}
|
|
648
|
+
i = end
|
|
649
|
+
}
|
|
650
|
+
for (i = 1; i <= n; i++) if (!(i in skip)) print line[i]
|
|
651
|
+
}
|
|
652
|
+
' "$TREE/docker/docker-bake.hcl" >"$TREE/docker/docker-bake.hcl.scaffold.tmp" &&
|
|
653
|
+
mv "$TREE/docker/docker-bake.hcl.scaffold.tmp" "$TREE/docker/docker-bake.hcl"
|
|
654
|
+
fi
|
|
655
|
+
if [ -n "$REMOVED_APPS" ] && [ -f "$TREE/docker/Makefile" ]; then
|
|
656
|
+
awk -v removed=" $REMOVED_APPS " '
|
|
657
|
+
/^# [a-z0-9-]+-block$/ {
|
|
658
|
+
name = $2
|
|
659
|
+
sub(/-block$/, "", name)
|
|
660
|
+
skipping = index(removed, " " name " ") > 0
|
|
661
|
+
}
|
|
662
|
+
!skipping
|
|
663
|
+
' "$TREE/docker/Makefile" >"$TREE/docker/Makefile.scaffold.tmp" &&
|
|
664
|
+
mv "$TREE/docker/Makefile.scaffold.tmp" "$TREE/docker/Makefile"
|
|
665
|
+
fi
|
|
666
|
+
|
|
667
|
+
# The root README documents the monorepo; a project gets the consumer copy at
|
|
668
|
+
# the same commit. A block between <!-- service:<app> --> and its closing
|
|
669
|
+
# <!-- /service:<app> --> stays only when that service was kept, and a
|
|
670
|
+
# no-service block only when it was not.
|
|
671
|
+
if git -C "$REPO" rev-parse --verify --quiet "$SHA:public/cli/templates/README.md" >/dev/null; then
|
|
672
|
+
git -C "$REPO" show "$SHA:public/cli/templates/README.md" | awk -v kept=" $SERVICES " '
|
|
673
|
+
/^<!-- \/(no-)?service:[a-z0-9-]+ -->$/ {
|
|
674
|
+
skipping = 0
|
|
675
|
+
next
|
|
676
|
+
}
|
|
677
|
+
/^<!-- (no-)?service:[a-z0-9-]+ -->$/ {
|
|
678
|
+
name = $2
|
|
679
|
+
negate = sub(/^no-/, "", name)
|
|
680
|
+
sub(/^service:/, "", name)
|
|
681
|
+
present = index(kept, " " name " ") > 0
|
|
682
|
+
skipping = negate ? present : !present
|
|
683
|
+
next
|
|
684
|
+
}
|
|
685
|
+
skipping { next }
|
|
686
|
+
/^$/ && blank { next }
|
|
687
|
+
{ blank = ($0 == ""); print }
|
|
688
|
+
' >"$TREE/README.md"
|
|
689
|
+
fi
|
|
690
|
+
|
|
691
|
+
APPS_CSV="$(printf '%s' "$APPS" | tr ' ' ',')"
|
|
692
|
+
REMOVED_CSV="$(printf '%s' "$REMOVED_APPS" | tr ' ' ',')"
|
|
693
|
+
for _vsfile in "$TREE/.vscode/launch.json" "$TREE/.vscode/tasks.json"; do
|
|
694
|
+
[ -f "$_vsfile" ] || continue
|
|
695
|
+
node - "$_vsfile" "$APPS_CSV" "$REMOVED_CSV" <<'NODE'
|
|
696
|
+
const fs = require("node:fs");
|
|
697
|
+
const [file, appsCsv, removedCsv] = process.argv.slice(2);
|
|
698
|
+
const apps = appsCsv ? appsCsv.split(",") : [];
|
|
699
|
+
const removed = removedCsv ? removedCsv.split(",") : [];
|
|
700
|
+
const json = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
701
|
+
const mentions = (value, name) => {
|
|
702
|
+
const text = JSON.stringify(value);
|
|
703
|
+
return [`/apps/${name}/`, `/apps/${name}"`, `}/${name}/`, `}/${name}"`].some((needle) =>
|
|
704
|
+
text.includes(needle),
|
|
705
|
+
);
|
|
706
|
+
};
|
|
707
|
+
const forApp = (value, name) =>
|
|
708
|
+
JSON.parse(
|
|
709
|
+
JSON.stringify(value)
|
|
710
|
+
.split("/apps/one/").join(`/apps/${name}/`)
|
|
711
|
+
.split('/apps/one"').join(`/apps/${name}"`)
|
|
712
|
+
.split('"one dev"').join(`"${name} dev"`),
|
|
713
|
+
);
|
|
714
|
+
const multiply = (items) =>
|
|
715
|
+
items.flatMap((item) => {
|
|
716
|
+
if (removed.some((name) => mentions(item, name))) return [];
|
|
717
|
+
if (!mentions(item, "one")) return [item];
|
|
718
|
+
return apps.map((name) => {
|
|
719
|
+
const copy = forApp(item, name);
|
|
720
|
+
if (apps.length > 1 && copy.name && !copy.name.startsWith(`${name} `)) {
|
|
721
|
+
copy.name = `${name} ${copy.name}`;
|
|
722
|
+
}
|
|
723
|
+
return copy;
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
if (Array.isArray(json.configurations)) {
|
|
727
|
+
json.configurations = multiply(json.configurations);
|
|
728
|
+
const names = new Set(json.configurations.map((item) => item.name));
|
|
729
|
+
if (Array.isArray(json.compounds)) {
|
|
730
|
+
json.compounds = json.compounds
|
|
731
|
+
.map((compound) => ({
|
|
732
|
+
...compound,
|
|
733
|
+
configurations: (compound.configurations ?? []).flatMap((name) =>
|
|
734
|
+
name === "one dev" ? apps.map((app) => `${app} dev`) : [name],
|
|
735
|
+
).filter((name) => names.has(name)),
|
|
736
|
+
}))
|
|
737
|
+
.filter((compound) => compound.configurations.length > 0);
|
|
738
|
+
if (!json.compounds.length) delete json.compounds;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (Array.isArray(json.tasks)) json.tasks = multiply(json.tasks);
|
|
742
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
|
|
743
|
+
NODE
|
|
744
|
+
done
|
|
745
|
+
|
|
746
|
+
[ -n "$PROJECT_NAME" ] || PROJECT_NAME="$(basename "$(cd "$TARGET" 2>/dev/null && pwd || printf '%s' "$TARGET")")"
|
|
747
|
+
node - "$TREE/package.json" "$PROJECT_NAME" "$APPS_CSV" "$REMOVED_CSV" "$(printf '%s' "$REMOVED" | tr ' ' ',')" <<'NODE'
|
|
748
|
+
const fs = require("node:fs");
|
|
749
|
+
const [file, projectName, appsCsv, removedCsv, prunedCsv] = process.argv.slice(2);
|
|
750
|
+
const apps = appsCsv ? appsCsv.split(",") : [];
|
|
751
|
+
const removed = removedCsv ? removedCsv.split(",") : [];
|
|
752
|
+
const pruned = prunedCsv ? prunedCsv.split(",") : [];
|
|
753
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
754
|
+
pkg.name = projectName;
|
|
755
|
+
// A root patch can name a package a project never installs. Every versioned
|
|
756
|
+
// patch is pinned by an exact override, so an unused one cannot hide drift.
|
|
757
|
+
if (pkg.pnpm?.patchedDependencies) {
|
|
758
|
+
pkg.pnpm = Object.fromEntries(
|
|
759
|
+
Object.entries(pkg.pnpm).flatMap((entry) =>
|
|
760
|
+
entry[0] === "patchedDependencies" ? [entry, ["allowUnusedPatches", true]] : [entry],
|
|
761
|
+
),
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
// A script whose command lives in a removed tree goes with it; a chained
|
|
765
|
+
// script keeps the segments that still resolve, minus ignore patterns that
|
|
766
|
+
// named removed trees.
|
|
767
|
+
const removedTree = /^(node|tsx|sh|bash)\s+(public|docs|deploy|agent-os|\.changeset)\//;
|
|
768
|
+
const dropSegment = (segment) =>
|
|
769
|
+
removedTree.test(segment) ||
|
|
770
|
+
/^(changeset|typedoc)(\s|$)/.test(segment) ||
|
|
771
|
+
segment.includes("generate-tamagui-workspace-paths");
|
|
772
|
+
const namesPruned = (pattern) =>
|
|
773
|
+
pruned.some((tree) => pattern.startsWith(`${tree}/`) || pattern.includes(`/${tree}/`));
|
|
774
|
+
const cleanSegment = (segment) =>
|
|
775
|
+
segment.replace(/\s--ignore-pattern\s+'([^']*)'/g, (whole, pattern) =>
|
|
776
|
+
namesPruned(pattern) ? "" : whole,
|
|
777
|
+
);
|
|
778
|
+
const scripts = {};
|
|
779
|
+
for (const [name, value] of Object.entries(pkg.scripts ?? {})) {
|
|
780
|
+
if (/^(docs|docs:watch|changeset|changeset:publish|generate:tamagui-paths|prepare)$/.test(name)) continue;
|
|
781
|
+
// verify:lc audits the catalog in public/ against the rulebook in docs/.
|
|
782
|
+
if (name === "verify:lc" && (pruned.includes("public") || pruned.includes("docs"))) continue;
|
|
783
|
+
const kept = value.split(" && ").filter((segment) => !dropSegment(segment)).map(cleanSegment);
|
|
784
|
+
if (!kept.length) continue;
|
|
785
|
+
scripts[name] = kept.join(" && ");
|
|
786
|
+
}
|
|
787
|
+
// Chains that ran a script dropped above lose that call too.
|
|
788
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
789
|
+
const kept = value
|
|
790
|
+
.split(" && ")
|
|
791
|
+
.filter((segment) => {
|
|
792
|
+
const match = segment.match(/^pnpm\s+([\w:.-]+)$/);
|
|
793
|
+
return !match || match[1] in scripts;
|
|
794
|
+
});
|
|
795
|
+
if (kept.length) scripts[name] = kept.join(" && ");
|
|
796
|
+
else delete scripts[name];
|
|
797
|
+
}
|
|
798
|
+
if (typeof scripts.dev === "string" && /--filter=one\b/.test(scripts.dev)) {
|
|
799
|
+
if (!apps.length) {
|
|
800
|
+
delete scripts.dev;
|
|
801
|
+
} else {
|
|
802
|
+
scripts.dev = `pnpm --filter @app/${apps[0]} dev`;
|
|
803
|
+
if (apps.length > 1) for (const app of apps) scripts[`dev:${app}`] = `pnpm --filter @app/${app} dev`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
for (const name of Object.keys(scripts)) {
|
|
807
|
+
if (removed.some((app) => scripts[name].includes(`apps/${app}`) || scripts[name].includes(`@platform/${app}`))) {
|
|
808
|
+
delete scripts[name];
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
pkg.scripts = scripts;
|
|
812
|
+
if (pruned.includes("public") && pkg.pnpm?.patchedDependencies) {
|
|
813
|
+
for (const [name, patch] of Object.entries(pkg.pnpm.patchedDependencies)) {
|
|
814
|
+
if (patch.startsWith("patches/public/")) delete pkg.pnpm.patchedDependencies[name];
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
818
|
+
NODE
|
|
819
|
+
|
|
820
|
+
# ── 5. the published packages come from the registry now ─────────────────
|
|
821
|
+
|
|
822
|
+
# workspace:* / workspace:^ / workspace:~ on @multiplatform.one/* become the
|
|
823
|
+
# semver range; @package/* and other private workspace deps stay workspace:.
|
|
824
|
+
for _file in $(grep_files "$TREE" -e '"@multiplatform\.one/[^"]*": *"workspace:' --include=package.json); do
|
|
825
|
+
sed_file "$_file" "s|\\(\"@multiplatform\\.one/[^\"]*\": *\"\\)workspace:\\*\"|\\1$MPO_VERSION\"|
|
|
826
|
+
s|\\(\"@multiplatform\\.one/[^\"]*\": *\"\\)workspace:^\"|\\1^$CLI_VERSION\"|
|
|
827
|
+
s|\\(\"@multiplatform\\.one/[^\"]*\": *\"\\)workspace:~\"|\\1~$CLI_VERSION\"|"
|
|
828
|
+
done
|
|
829
|
+
|
|
830
|
+
if [ -f "$TREE/pnpm-workspace.yaml" ]; then
|
|
831
|
+
_ws='/^[[:space:]]*-[[:space:]]*["'"'"']\{0,1\}public\/\*["'"'"']\{0,1\}[[:space:]]*$/d'
|
|
832
|
+
if [ ! -d "$TREE/apps" ] || [ -z "$(ls -A "$TREE/apps")" ]; then
|
|
833
|
+
_ws="$_ws
|
|
834
|
+
/^[[:space:]]*-[[:space:]]*[\"']\\{0,1\\}apps\\/\\*[\"']\\{0,1\\}[[:space:]]*\$/d"
|
|
835
|
+
fi
|
|
836
|
+
if [ ! -d "$TREE/packages" ] || [ -z "$(ls -A "$TREE/packages")" ]; then
|
|
837
|
+
_ws="$_ws
|
|
838
|
+
/^[[:space:]]*-[[:space:]]*[\"']\\{0,1\\}packages\\/\\*[\"']\\{0,1\\}[[:space:]]*\$/d"
|
|
839
|
+
fi
|
|
840
|
+
sed_file "$TREE/pnpm-workspace.yaml" "$_ws"
|
|
841
|
+
fi
|
|
842
|
+
|
|
843
|
+
# tsconfig project references into public/ and the tsconfig.base.json that
|
|
844
|
+
# extended public/config directly: the published @multiplatform.one/config
|
|
845
|
+
# ships the same tsconfig/base.json.
|
|
846
|
+
for _file in $(grep_files "$TREE" -e '"path": *"\./public/' -e '"path": *"\.\./public/' -e '"path": *"\.\./\.\./public/' --include='tsconfig*.json'); do
|
|
847
|
+
node - "$_file" <<'NODE'
|
|
848
|
+
const fs = require("node:fs");
|
|
849
|
+
const [file] = process.argv.slice(2);
|
|
850
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
851
|
+
// tsconfig files carry full-line comments; a "//" inside a string (the
|
|
852
|
+
// $schema url) is not one, so only whole comment lines are stripped.
|
|
853
|
+
const json = JSON.parse(raw.replace(/^\s*\/\/[^\n]*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""));
|
|
854
|
+
if (Array.isArray(json.references)) {
|
|
855
|
+
json.references = json.references.filter((ref) => !/(^|\/)public\//.test(ref.path));
|
|
856
|
+
}
|
|
857
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
|
|
858
|
+
NODE
|
|
859
|
+
done
|
|
860
|
+
if [ -f "$TREE/tsconfig.base.json" ] && grep -q 'public/config/tsconfig' "$TREE/tsconfig.base.json"; then
|
|
861
|
+
node - "$TREE/tsconfig.base.json" <<'NODE'
|
|
862
|
+
const fs = require("node:fs");
|
|
863
|
+
const [file] = process.argv.slice(2);
|
|
864
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
865
|
+
// tsconfig files carry full-line comments; a "//" inside a string (the
|
|
866
|
+
// $schema url) is not one, so only whole comment lines are stripped.
|
|
867
|
+
const json = JSON.parse(raw.replace(/^\s*\/\/[^\n]*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""));
|
|
868
|
+
const extendsList = Array.isArray(json.extends) ? json.extends : json.extends ? [json.extends] : [];
|
|
869
|
+
json.extends = extendsList
|
|
870
|
+
.filter((entry) => !entry.includes("tamagui-workspace-paths.generated"))
|
|
871
|
+
.map((entry) => entry.replace(/^\.\/public\/config\/tsconfig\//, "@multiplatform.one/config/tsconfig/"));
|
|
872
|
+
if (json.extends.length === 1) json.extends = json.extends[0];
|
|
873
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
|
|
874
|
+
NODE
|
|
875
|
+
fi
|
|
876
|
+
|
|
877
|
+
# Stories are apps/storybook's input and import its framework, which only
|
|
878
|
+
# apps/storybook declares; without it the features program leaves them out.
|
|
879
|
+
case " $REMOVED_APPS " in
|
|
880
|
+
*" storybook "*)
|
|
881
|
+
if [ -f "$TREE/features/tsconfig.json" ]; then
|
|
882
|
+
sed_file "$TREE/features/tsconfig.json" 's|"exclude": \["node_modules"\]|"exclude": ["node_modules", "**/*.stories.tsx"]|'
|
|
883
|
+
fi
|
|
884
|
+
;;
|
|
885
|
+
esac
|
|
886
|
+
|
|
887
|
+
# ── 6. provenance ────────────────────────────────────────────────────────
|
|
888
|
+
|
|
889
|
+
node - "$TREE/.mpo.json" "$CLI_VERSION" "$SHA" "$SOURCE_URL" "$APPS_CSV" "$(printf '%s' "$SERVICES" | tr ' ' ',')" <<'NODE'
|
|
890
|
+
const fs = require("node:fs");
|
|
891
|
+
const [file, cliVersion, sha, url, appsCsv, servicesCsv] = process.argv.slice(2);
|
|
892
|
+
const provenance = {
|
|
893
|
+
cliVersion,
|
|
894
|
+
source: url ? { sha, url } : { sha },
|
|
895
|
+
apps: appsCsv ? appsCsv.split(",") : [],
|
|
896
|
+
services: servicesCsv ? servicesCsv.split(",").sort() : [],
|
|
897
|
+
pieces: [],
|
|
898
|
+
};
|
|
899
|
+
fs.writeFileSync(file, `${JSON.stringify(provenance, null, 2)}\n`);
|
|
900
|
+
NODE
|
|
901
|
+
|
|
902
|
+
# ── 7. --into: lay the tree over an existing directory ───────────────────
|
|
903
|
+
|
|
904
|
+
SKIPPED=""
|
|
905
|
+
if [ "$INTO" -eq 1 ]; then
|
|
906
|
+
mkdir -p "$TARGET"
|
|
907
|
+
_list="$WORK/files"
|
|
908
|
+
(cd "$TREE" && find . -type f -o -type l) | sed 's|^\./||' | sort >"$_list"
|
|
909
|
+
while IFS= read -r _rel; do
|
|
910
|
+
if [ -e "$TARGET/$_rel" ] || [ -L "$TARGET/$_rel" ]; then
|
|
911
|
+
SKIPPED="$SKIPPED $_rel"
|
|
912
|
+
printf 'skipped (exists): %s\n' "$_rel"
|
|
913
|
+
continue
|
|
914
|
+
fi
|
|
915
|
+
mkdir -p "$(dirname "$TARGET/$_rel")"
|
|
916
|
+
cp -P "$TREE/$_rel" "$TARGET/$_rel"
|
|
917
|
+
done <"$_list"
|
|
918
|
+
if [ -n "$SKIPPED" ]; then
|
|
919
|
+
{
|
|
920
|
+
[ -f "$TARGET/.updateignore" ] && [ -n "$(tail -c 1 "$TARGET/.updateignore")" ] && printf '\n'
|
|
921
|
+
printf '# scaffold --into: these paths existed before the scaffold and are yours\n'
|
|
922
|
+
for _rel in $SKIPPED; do printf '%s\n' "$_rel"; done
|
|
923
|
+
} >>"$TARGET/.updateignore"
|
|
924
|
+
fi
|
|
925
|
+
fi
|
|
926
|
+
|
|
927
|
+
# ── 8. report ────────────────────────────────────────────────────────────
|
|
928
|
+
|
|
929
|
+
printf 'scaffold: %s\n' "$TARGET"
|
|
930
|
+
printf ' source %s @ %s\n' "$SOURCE" "$SHA"
|
|
931
|
+
printf ' apps %s\n' "${APPS:-(none)}"
|
|
932
|
+
printf ' services %s\n' "${SERVICES:-(none)}"
|
|
933
|
+
printf ' packages @multiplatform.one/* %s\n' "$MPO_VERSION"
|
|
934
|
+
printf ' removed %s\n' "${REMOVED# }"
|
|
935
|
+
if [ -n "$SKIPPED" ]; then
|
|
936
|
+
printf ' skipped %s (pinned in .updateignore)\n' "$(printf '%s' "${SKIPPED# }" | wc -w | tr -d ' ') existing paths"
|
|
937
|
+
fi
|