@edgedev/create-edge-app 1.2.35 → 1.2.36

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/agents.md CHANGED
@@ -99,25 +99,51 @@ Adjust props (search, filters, pagination, save overrides) using the existing co
99
99
  - Array loop aliases use `as` (for example `{{{#array {"field":"items","as":"card"}}}}`).
100
100
  - Use `subarray` for nested arrays and `entries` for object key/value iteration.
101
101
  - Use `renderBlocks` when an item contains nested CMS block content (for example post body content).
102
+ - Inside `renderBlocks`, use `{{renderItem.someField}}` to render values directly from the current item passed into that block render.
102
103
 
103
104
  ### Array data loading (Firestore/API)
104
105
  - Firestore arrays use `collection` config with `path`, `uniqueKey`, `query`, `order`, `limit`.
105
106
  - `path` is org-scoped under `organizations/{orgId}`.
106
- - `uniqueKey` supports template tokens like `{orgId}` and `{siteId}`.
107
+ - `uniqueKey` supports runtime tokens such as `{orgId}` and `{siteId}` and is resolved in memory at runtime.
108
+ - `collection.canonicalLookup.key` is optional and supports runtime token replacement for direct canonical fetches.
107
109
  - API arrays use `api`, optional `apiQuery`, and `apiField`.
108
110
  - `queryOptions` creates CMS filter controls; selected values are stored in `meta.queryItems`.
111
+ - `query` and `queryItems` stay saved exactly as authored; supported runtime tokens in `query`, `queryItems`, `uniqueKey`, and `collection.canonicalLookup.key` include `{orgId}`, `{siteId}`, and `{routeLastSegment}`.
112
+ - If you manually author a key in `queryItems`, that value overrides a `queryOption` on the same key. In practice, do not manually set a `queryItems` key that you want CMS users to edit through `queryOptions`.
109
113
  - Loading state tokens are supported while async array data resolves: `{{loading}}` and `{{loaded}}`.
110
114
 
111
115
  ### Array query execution model (critical)
116
+ - Before runtime fetches, tokens in `collection.query`, `queryItems`, `uniqueKey`, and `collection.canonicalLookup.key` are resolved in memory only. Saved blocks keep the original tokens.
112
117
  - Each `queryItems` entry performs its own KV index lookup via `kvClient.queryIndex`.
113
118
  - Query keys only work if the field is present in KV mirror config: include keys in `indexKeys`, and also in `metadataKeys` when needed for rendering/sorting.
114
119
  - Multiple `queryItems` are unioned first (OR behavior at lookup stage).
115
120
  - Candidate duplicates are removed by canonical key.
116
121
  - `collection.query` then runs in JavaScript as final filtering (AND behavior across rules).
117
122
  - `collection.order` sorts the filtered result set.
123
+ - If `collection.canonicalLookup.key` is present, runtime can fetch the exact document directly instead of relying on indexed lookups.
118
124
  - Final output is written to `values[field]`.
119
125
  - If load fails, runtime falls back to inline `value` or `[]` when no fallback value exists.
120
126
 
127
+ ### Array query strategy examples
128
+ - Indexed detail lookup by slug/name:
129
+ ```hbs
130
+ {{{#array {"field":"list","collection":{"path":"posts","queryItems":{"name":"{routeLastSegment}"},"order":[]},"queryOptions":[],"limit":1,"value":[]}}}}
131
+ {{{#renderBlocks {"field":"item"}}}}
132
+ {{{/array}}}
133
+ ```
134
+ - Inside a block rendered by `renderBlocks`, use `renderItem` against the passed item:
135
+ ```hbs
136
+ <article>
137
+ <h2>{{renderItem.name}}</h2>
138
+ {{{content}}}
139
+ </article>
140
+ ```
141
+ - Canonical direct lookup:
142
+ ```hbs
143
+ {{{#array {"field":"siteDoc","collection":{"path":"sites","canonicalLookup":{"key":"{orgId}:{siteId}"},"order":[]},"value":[]}}}}
144
+ ```
145
+ - For canonical-only fetches, `uniqueKey` and `limit` are not required.
146
+
121
147
  ### Firestore index + KV mirror requirements
122
148
  - Any Firestore compound query used by blocks (for example array contains + order) requires matching composite indexes in `firestore.indexes.json`.
123
149
  - Fast CMS filtering requires KV mirroring in Functions with both index and metadata coverage.
@@ -154,14 +180,17 @@ exports.onListingWritten = createKvMirrorHandlerFromFields({
154
180
  - Do reuse Edge components (`dashboard`, `editor`, `cms` blocks, auth widgets) before adding new ones.
155
181
  - Do keep Firestore paths, queries, and role checks consistent with `edgeGlobal` helpers (`isAdminGlobal`, `getRoleName`, etc.).
156
182
  - Do use the `edge-*.sh` scripts (like `edge-pull.sh` and `edge-components-update.sh`) to sync/update the `edge` subtree instead of manual edits.
183
+ - For CMS work, all approved code changes must stay inside `edge/**` unless the change specifically belongs in `functions/cms.js` or `functions/history.js` and that file was explicitly approved for this task. Do not add CMS-specific code in root-level `components/`, `composables/`, `pages/`, or other non-`edge` app locations.
184
+ - If the likely correct fix appears to be inside `edge/**`, `functions/cms.js`, or `functions/history.js`, ask for permission to edit that location. Do not avoid the correct fix by automatically routing around those files.
157
185
  - Don’t introduce TypeScript, Options API, raw Firebase SDK calls, or ad-hoc forms/tables when an Edge component exists.
158
186
  - Don’t edit code inside the `edge` folder (including `edge/components/cms/*`) unless absolutely required and you have asked for and received user permission for that specific edit every time; it is a shared repo.
159
187
  - If an `edge/*` change is unavoidable, keep it generic (no project-specific hacks) and call out the suggestion instead of making the edit when possible.
160
188
  - Don’t modify `storage.rules` or `firestore.rules`.
161
189
 
162
190
  ## Firebase Functions guidance
163
- - Review `functions/config.js`, `functions/edgeFirebase.js`, and `functions/cms.js` to mirror established patterns.
164
- - Only edit `functions/config.js`, `functions/edgeFirebase.js`, or `functions/cms.js` when absolutely required and only after asking for and receiving user permission each time.
191
+ - Review `functions/config.js`, `functions/edgeFirebase.js`, `functions/cms.js`, and `functions/history.js` to mirror established patterns.
192
+ - Only edit `functions/config.js`, `functions/edgeFirebase.js`, `functions/cms.js`, or `functions/history.js` when absolutely required and only after asking for and receiving user permission each time.
193
+ - When a bug or feature likely belongs in `functions/cms.js` or `functions/history.js`, ask to edit that file instead of automatically creating a workaround in a new function or another file.
165
194
  - When adding new cloud functions, create a new JS file under `functions/` and export handlers using the shared imports from `config.js`. Wire it up by requiring it in `functions/index.js` (same pattern as `stripe.js`), instead of modifying restricted files.
166
195
  - For every `onCall` function, always enforce both checks up front: `request.auth?.uid` must exist, and `request.data?.uid` must exactly match `request.auth.uid`. Throw `HttpsError('unauthenticated', ...)` when auth is missing and `HttpsError('permission-denied', ...)` when the uid does not match.
167
196
 
@@ -50,6 +50,9 @@ sync_edge_functions() {
50
50
 
51
51
  find "$edge_functions_dir" -type f | sort | while IFS= read -r src_file; do
52
52
  rel_path="${src_file#$edge_functions_dir/}"
53
+ if [ "$rel_path" = "index.js" ]; then
54
+ continue
55
+ fi
53
56
  dest_file="$local_functions_dir/$rel_path"
54
57
  dest_dir="$(dirname "$dest_file")"
55
58
 
@@ -59,6 +62,68 @@ sync_edge_functions() {
59
62
  done
60
63
  }
61
64
 
65
+ merge_edge_functions_index() {
66
+ edge_index="$SCRIPT_DIR/edge/functions/index.js"
67
+ local_index="$SCRIPT_DIR/functions/index.js"
68
+
69
+ if [ ! -f "$edge_index" ] || [ ! -f "$local_index" ]; then
70
+ return
71
+ fi
72
+
73
+ echo "==> Merging extra edge function exports into local functions/index.js"
74
+ EDGE_FUNCTIONS_INDEX_PATH="$edge_index" LOCAL_FUNCTIONS_INDEX_PATH="$local_index" node <<'EOF'
75
+ const fs = require('fs')
76
+
77
+ const edgePath = process.env.EDGE_FUNCTIONS_INDEX_PATH
78
+ const localPath = process.env.LOCAL_FUNCTIONS_INDEX_PATH
79
+ const startMarker = '// START EXTRA EDGE functions'
80
+ const endMarker = '// END EXTRA EDGE functions'
81
+
82
+ const readText = filePath => fs.readFileSync(filePath, 'utf8')
83
+
84
+ const extractMarkedBlock = (text) => {
85
+ const start = text.indexOf(startMarker)
86
+ const end = text.indexOf(endMarker)
87
+ if (start === -1 || end === -1 || end < start)
88
+ throw new Error(`Missing ${startMarker}/${endMarker} block in ${edgePath}`)
89
+
90
+ const endLine = text.indexOf('\n', end)
91
+ return endLine === -1 ? text.slice(start) : text.slice(start, endLine + 1)
92
+ }
93
+
94
+ const replaceMarkedBlock = (text, replacement) => {
95
+ const start = text.indexOf(startMarker)
96
+ const end = text.indexOf(endMarker)
97
+ const edgeFirebaseEndMarker = '// END @edge/firebase functions'
98
+
99
+ if (start === -1 || end === -1 || end < start) {
100
+ const edgeFirebaseEnd = text.indexOf(edgeFirebaseEndMarker)
101
+ if (edgeFirebaseEnd !== -1) {
102
+ const edgeFirebaseEndLine = text.indexOf('\n', edgeFirebaseEnd)
103
+ const insertAt = edgeFirebaseEndLine === -1 ? text.length : edgeFirebaseEndLine + 1
104
+ const before = text.slice(0, insertAt)
105
+ const after = text.slice(insertAt)
106
+ const joiner = before.endsWith('\n\n') ? '' : '\n'
107
+ return `${before}${joiner}${replacement}${after}`
108
+ }
109
+ const normalized = text.endsWith('\n') ? text : `${text}\n`
110
+ return `${normalized}\n${replacement}`
111
+ }
112
+
113
+ const endLine = text.indexOf('\n', end)
114
+ const after = endLine === -1 ? '' : text.slice(endLine + 1)
115
+ return `${text.slice(0, start)}${replacement}${after}`
116
+ }
117
+
118
+ const edgeText = readText(edgePath)
119
+ const localText = readText(localPath)
120
+ const edgeBlock = extractMarkedBlock(edgeText)
121
+ const mergedText = replaceMarkedBlock(localText, edgeBlock)
122
+
123
+ fs.writeFileSync(localPath, mergedText.endsWith('\n') ? mergedText : `${mergedText}\n`)
124
+ EOF
125
+ }
126
+
62
127
  merge_firestore_indexes() {
63
128
  edge_indexes="$SCRIPT_DIR/edge/root/firestore.indexes.json"
64
129
  local_indexes="$SCRIPT_DIR/firestore.indexes.json"
@@ -112,11 +177,66 @@ fs.writeFileSync(localPath, `${JSON.stringify(merged, null, 2)}\n`)
112
177
  EOF
113
178
  }
114
179
 
180
+ merge_history_config() {
181
+ edge_history_config="$SCRIPT_DIR/edge/root/history.config.json"
182
+ local_history_config="$SCRIPT_DIR/functions/history.config.json"
183
+
184
+ if [ ! -f "$edge_history_config" ]; then
185
+ return
186
+ fi
187
+
188
+ if [ ! -f "$local_history_config" ]; then
189
+ echo "==> Writing functions/history.config.json from edge/root"
190
+ mkdir -p "$(dirname "$local_history_config")"
191
+ cp "$edge_history_config" "$local_history_config"
192
+ return
193
+ fi
194
+
195
+ echo "==> Merging functions/history.config.json with local priority"
196
+ EDGE_HISTORY_CONFIG_PATH="$edge_history_config" LOCAL_HISTORY_CONFIG_PATH="$local_history_config" node <<'EOF'
197
+ const fs = require('fs')
198
+
199
+ const edgePath = process.env.EDGE_HISTORY_CONFIG_PATH
200
+ const localPath = process.env.LOCAL_HISTORY_CONFIG_PATH
201
+
202
+ const readJson = filePath => JSON.parse(fs.readFileSync(filePath, 'utf8'))
203
+
204
+ const isPlainObject = value => {
205
+ return !!value && typeof value === 'object' && !Array.isArray(value)
206
+ }
207
+
208
+ const mergeWithLocalPriority = (edgeValue, localValue) => {
209
+ if (localValue === undefined)
210
+ return edgeValue
211
+
212
+ if (Array.isArray(edgeValue) || Array.isArray(localValue))
213
+ return localValue
214
+
215
+ if (isPlainObject(edgeValue) && isPlainObject(localValue)) {
216
+ const merged = { ...edgeValue }
217
+ for (const key of Object.keys(localValue))
218
+ merged[key] = mergeWithLocalPriority(edgeValue?.[key], localValue[key])
219
+ return merged
220
+ }
221
+
222
+ return localValue
223
+ }
224
+
225
+ const edgeJson = readJson(edgePath)
226
+ const localJson = readJson(localPath)
227
+ const merged = mergeWithLocalPriority(edgeJson, localJson)
228
+
229
+ fs.writeFileSync(localPath, `${JSON.stringify(merged, null, 2)}\n`)
230
+ EOF
231
+ }
232
+
115
233
  echo "==> Updating edge subtree"
116
234
  "$SCRIPT_DIR/edge-pull.sh"
117
235
 
118
236
  sync_edge_functions
237
+ merge_edge_functions_index
119
238
  merge_firestore_indexes
239
+ merge_history_config
120
240
 
121
241
  echo "==> Removing @edgedev packages"
122
242
  pnpm remove @edgedev/template-engine @edgedev/firebase
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgedev/create-edge-app",
3
- "version": "1.2.35",
3
+ "version": "1.2.36",
4
4
  "description": "Create Edge Starter App",
5
5
  "bin": {
6
6
  "create-edge-app": "./bin/cli.js"