@avocadostudio-ai/site-sdk 0.1.0 → 0.2.1

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.
Files changed (50) hide show
  1. package/README.md +212 -2
  2. package/dist/cli/register.js +23 -1
  3. package/dist/create-site-page.d.ts +38 -8
  4. package/dist/create-site-page.js +59 -8
  5. package/dist/draft-common.d.ts +32 -0
  6. package/dist/draft-common.js +58 -0
  7. package/dist/draft-context-core.js +39 -6
  8. package/dist/draft-context-core.test.d.ts +10 -0
  9. package/dist/draft-context-core.test.js +146 -0
  10. package/dist/draft-fetch.d.ts +9 -10
  11. package/dist/draft-fetch.js +71 -5
  12. package/dist/draft-fetch.test.d.ts +1 -0
  13. package/dist/draft-fetch.test.js +87 -0
  14. package/dist/editor-cors.d.ts +12 -0
  15. package/dist/editor-cors.js +31 -6
  16. package/dist/editor-cors.test.d.ts +1 -0
  17. package/dist/editor-cors.test.js +66 -0
  18. package/dist/editor-manifest.d.ts +2 -3
  19. package/dist/editor-manifest.js +12 -64
  20. package/dist/editor-matcher.d.ts +27 -0
  21. package/dist/editor-matcher.js +34 -0
  22. package/dist/editor-query.js +7 -1
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/integration-check.js +11 -1
  26. package/dist/manifest-utils.d.ts +13 -0
  27. package/dist/manifest-utils.js +30 -3
  28. package/dist/manifest-utils.test.d.ts +1 -0
  29. package/dist/manifest-utils.test.js +72 -0
  30. package/dist/middleware.d.ts +21 -19
  31. package/dist/middleware.js +19 -22
  32. package/dist/next-config.test.d.ts +1 -0
  33. package/dist/next-config.test.js +355 -0
  34. package/dist/page-metadata.d.ts +66 -0
  35. package/dist/page-metadata.js +110 -0
  36. package/dist/page-metadata.test.d.ts +1 -0
  37. package/dist/page-metadata.test.js +105 -0
  38. package/dist/proxy.d.ts +95 -0
  39. package/dist/proxy.js +76 -0
  40. package/dist/proxy.test.d.ts +1 -0
  41. package/dist/proxy.test.js +123 -0
  42. package/dist/publish/field-diff.d.ts +191 -0
  43. package/dist/publish/field-diff.js +252 -0
  44. package/dist/publish/field-diff.test.d.ts +1 -0
  45. package/dist/publish/field-diff.test.js +286 -0
  46. package/dist/server/orchestrator.d.ts +1 -117
  47. package/dist/server/orchestrator.js +14 -733
  48. package/next-config.d.ts +78 -0
  49. package/next-config.mjs +468 -0
  50. package/package.json +63 -19
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Types for `next-config.mjs`.
3
+ *
4
+ * The implementation is plain ESM on purpose — it is imported by `next.config`,
5
+ * before any of Next's loaders exist — so its types cannot be inferred and have
6
+ * to be declared here. A `next.config.ts` under `noImplicitAny` fails without
7
+ * this file.
8
+ */
9
+
10
+ /** The linked Avocado packages that still ship TypeScript entry points. */
11
+ export function linkedAvocadoPackages(from?: string): string[]
12
+
13
+ /** A `next/image` remote pattern, structurally matching Next's own type. */
14
+ export type AvocadoImagePattern = {
15
+ protocol: "http" | "https"
16
+ hostname: string
17
+ port?: string
18
+ pathname?: string
19
+ }
20
+
21
+ /**
22
+ * The image hosts Avocado itself can produce URLs for — stock search, the image
23
+ * models' blob storage, and the placeholder host. `withAvocado` merges these
24
+ * into `images.remotePatterns`; exported so an app that opts out with
25
+ * `{ images: false }` can still reference the list.
26
+ */
27
+ export const AVOCADO_IMAGE_HOSTS: AvocadoImagePattern[]
28
+
29
+ /**
30
+ * The packages that must stay external to the server build — native binaries
31
+ * (`better-sqlite3`, `sharp`) and the provider SDKs `orchestrator-core` reaches
32
+ * through `await import(...)`. `withAvocado` applies these; exported so an app
33
+ * that opts out with `{ serverExternals: false }` can still reference the list.
34
+ */
35
+ export const AVOCADO_SERVER_EXTERNALS: string[]
36
+
37
+ /**
38
+ * Wrap a Next config so `transpilePackages` covers every linked Avocado package,
39
+ * `images.remotePatterns` covers every host Avocado can serve an image from,
40
+ * Avocado's native and provider dependencies stay external to the server build,
41
+ * and a `trailingSlash: true` site stops redirecting the editor's API calls into
42
+ * a failed CORS preflight. Additive, and never throws.
43
+ */
44
+ export function withAvocado<
45
+ T extends {
46
+ transpilePackages?: string[]
47
+ images?: { remotePatterns?: unknown[] }
48
+ serverExternalPackages?: string[]
49
+ trailingSlash?: boolean
50
+ skipTrailingSlashRedirect?: boolean
51
+ /** `null` is in Next's own type for this field, so the constraint admits it. */
52
+ webpack?: ((config: any, context: any) => any) | null
53
+ }
54
+ >(
55
+ config?: T,
56
+ options?: {
57
+ cwd?: string
58
+ silent?: boolean
59
+ /** Set false to manage `images.remotePatterns` yourself. Defaults to true. */
60
+ images?: boolean
61
+ /**
62
+ * Set false to manage `serverExternalPackages` and the server `externals`
63
+ * yourself. Defaults to true.
64
+ */
65
+ serverExternals?: boolean
66
+ /**
67
+ * Set false to keep Next's trailing-slash redirect on a `trailingSlash: true`
68
+ * site — at the cost of the editor, whose API calls cannot survive a 308 on
69
+ * their CORS preflight. Defaults to true, and pairs with
70
+ * `createEditorProxy({ trailingSlash: true })`.
71
+ */
72
+ trailingSlash?: boolean
73
+ /** Environment to read the orchestrator origin from. Defaults to `process.env`. */
74
+ env?: Record<string, string | undefined>
75
+ }
76
+ ): T
77
+
78
+ export default withAvocado
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Next.js config helper.
3
+ *
4
+ * Plain ESM, deliberately: it is imported *by* `next.config`, which Next loads
5
+ * before any of its own loaders exist. Everything else in this package ships
6
+ * TypeScript source in a workspace checkout, and a config that imported that
7
+ * would fail to parse — which is the very failure this file exists to prevent.
8
+ *
9
+ * ## What it prevents
10
+ *
11
+ * Avocado's packages point `main` at `src/index.ts` so a monorepo checkout
12
+ * needs no build step. Webpack has no idea what to do with that, so every
13
+ * linked package must be named in `transpilePackages` — and when one is
14
+ * missing, the error names a file inside `node_modules` and says nothing about
15
+ * the config the integrator has to change:
16
+ *
17
+ * Module parse failed: Unexpected token (27:7)
18
+ * > type InlineToken,
19
+ *
20
+ * Nothing catches it earlier. A type-check does not compile a Next app, and a
21
+ * test suite does not boot one, so the app is green everywhere except in a
22
+ * browser. It also breaks *retroactively*: adding a package to Avocado, or
23
+ * re-exporting one from another, silently breaks every consumer that pinned the
24
+ * old list by hand.
25
+ *
26
+ * So the list is not maintained by hand. This reads what is actually linked and
27
+ * adds whatever ships TypeScript, keeping anything the app already declared.
28
+ *
29
+ * ## What else it prevents
30
+ *
31
+ * Avocado writes image URLs its host never chose. A generated image comes back
32
+ * from Unsplash or from an image model's blob storage, and `next/image` refuses
33
+ * any host not listed in `images.remotePatterns` — so the first image a user
34
+ * generates 500s, on a config line nothing in the integration docs mentioned.
35
+ * Four apps in this repo carried the same hand-copied host list to work around
36
+ * that. It belongs here, next to the other thing an integrator cannot be
37
+ * expected to know.
38
+ *
39
+ * ## And the third thing
40
+ *
41
+ * `orchestrator-core` reaches native binaries (`better-sqlite3`, `sharp`) and
42
+ * heavyweight provider SDKs, some of them through `await import(...)` because
43
+ * they are optional peers. A bundler must be told to leave all of them alone:
44
+ *
45
+ * - A native `.node` binary that gets bundled crashes when it is loaded, not
46
+ * when it is built, so the failure surfaces on the first request.
47
+ * - Turbopack statically resolves dynamic imports, so an optional peer that is
48
+ * *deliberately absent* fails the build with `Module not found`. "An optional
49
+ * peer is genuinely skipped" holds at install time; it does not hold at bundle
50
+ * time.
51
+ *
52
+ * `serverExternalPackages` alone is not enough, and this is the part nobody
53
+ * guesses: `transpilePackages` overrides server externals for a transitive
54
+ * dependency, so `sharp` reached through the transpiled `orchestrator-core` got
55
+ * bundled anyway. The two options interact, this helper sets both, and it is the
56
+ * only place that knows it has to.
57
+ *
58
+ * ## And the fourth
59
+ *
60
+ * `trailingSlash: true` makes Next answer `/api/editor/blocks` with a 308 to
61
+ * `/api/editor/blocks/`. `fetch` follows that; a CORS preflight does not — a
62
+ * browser treats a redirect on `OPTIONS` as a network failure — so on a site
63
+ * with trailing slashes every editor API call fails before it is sent, and
64
+ * nothing in the error says why. `skipTrailingSlashRedirect` is the only way
65
+ * out, because the redirect runs before middleware and cannot be intercepted.
66
+ * Turning it off is safe only because the SDK's own proxy puts the redirect back
67
+ * for page routes — see `createEditorProxy({ trailingSlash: true })`, which is
68
+ * the other half of this and is not optional.
69
+ */
70
+
71
+ import { existsSync, readdirSync, readFileSync } from "node:fs"
72
+ import { dirname, join, resolve } from "node:path"
73
+
74
+ /** Scopes whose packages may ship TypeScript source in a linked checkout. */
75
+ const SCOPES = ["@avocadostudio-ai", "@ai-site-editor"]
76
+
77
+ function readJson(file) {
78
+ try {
79
+ return JSON.parse(readFileSync(file, "utf8"))
80
+ } catch {
81
+ return null
82
+ }
83
+ }
84
+
85
+ /**
86
+ * An entry point that has to be compiled, as opposed to merely described.
87
+ *
88
+ * `.d.ts` ends in `.ts` and is not TypeScript that anything compiles — it is
89
+ * the type description of JavaScript that is already built. Every published
90
+ * package sets `types: "dist/index.d.ts"`, so a naive `/\.tsx?$/` matched all
91
+ * of them and this whole helper inverted on a registry install: it added the
92
+ * published packages to `transpilePackages`, and `transpilePackages` is
93
+ * precisely what drags `orchestrator-core` into the bundle and fails the build
94
+ * on an optional peer. The bug it exists to prevent was the bug it caused.
95
+ *
96
+ * The declaration test has to run against every string, not just `types` — an
97
+ * `exports` map carries its own `types` condition.
98
+ */
99
+ const DECLARATION_FILE = /\.d\.[cm]?tsx?$/
100
+ const TYPESCRIPT_FILE = /\.[cm]?tsx?$/
101
+
102
+ function isCompilableEntry(entry) {
103
+ return TYPESCRIPT_FILE.test(entry) && !DECLARATION_FILE.test(entry)
104
+ }
105
+
106
+ /** Does any entry point in this manifest resolve to TypeScript source? */
107
+ function shipsTypeScript(pkg) {
108
+ const seen = []
109
+ const walk = (value) => {
110
+ if (typeof value === "string") seen.push(value)
111
+ else if (value && typeof value === "object") Object.values(value).forEach(walk)
112
+ }
113
+ walk(pkg.main)
114
+ walk(pkg.types)
115
+ walk(pkg.exports)
116
+ return seen.some(isCompilableEntry)
117
+ }
118
+
119
+ /** Every `node_modules` directory from `start` up to the filesystem root. */
120
+ function nodeModulesDirs(start) {
121
+ const dirs = []
122
+ let current = resolve(start)
123
+ for (;;) {
124
+ const candidate = join(current, "node_modules")
125
+ if (existsSync(candidate)) dirs.push(candidate)
126
+ const parent = dirname(current)
127
+ if (parent === current) return dirs
128
+ current = parent
129
+ }
130
+ }
131
+
132
+ /**
133
+ * The linked Avocado packages that have to be transpiled with the app.
134
+ *
135
+ * Resolved from what is on disk rather than from a list in this file, so a
136
+ * package added to Avocado tomorrow is picked up without anyone editing a
137
+ * config. A package installed from a registry resolves to built JavaScript and
138
+ * is correctly left out.
139
+ */
140
+ export function linkedAvocadoPackages(from = process.cwd()) {
141
+ const found = new Set()
142
+ for (const nodeModules of nodeModulesDirs(from)) {
143
+ for (const scope of SCOPES) {
144
+ const scopeDir = join(nodeModules, scope)
145
+ if (!existsSync(scopeDir)) continue
146
+ let entries
147
+ try {
148
+ entries = readdirSync(scopeDir)
149
+ } catch {
150
+ continue
151
+ }
152
+ for (const name of entries) {
153
+ const full = `${scope}/${name}`
154
+ if (found.has(full)) continue
155
+ const pkg = readJson(join(scopeDir, name, "package.json"))
156
+ if (pkg && shipsTypeScript(pkg)) found.add(full)
157
+ }
158
+ }
159
+ }
160
+ return [...found].sort()
161
+ }
162
+
163
+ /**
164
+ * Hosts Avocado itself can put in an image URL.
165
+ *
166
+ * Unsplash for stock search, the image models' own blob storage for generated
167
+ * images, `placehold.co` for the placeholder a block renders before an image is
168
+ * chosen. A host the *site's own CMS* serves from (`cdn.sanity.io`,
169
+ * `images.ctfassets.net`, …) is not here — that is the app's to declare,
170
+ * because only the app knows which CMS it uses.
171
+ */
172
+ export const AVOCADO_IMAGE_HOSTS = [
173
+ { protocol: "https", hostname: "images.unsplash.com" },
174
+ { protocol: "https", hostname: "plus.unsplash.com" },
175
+ { protocol: "https", hostname: "oaidalleapiprodscus.blob.core.windows.net" },
176
+ { protocol: "https", hostname: "generativelanguage.googleapis.com" },
177
+ { protocol: "https", hostname: "placehold.co" },
178
+ ]
179
+
180
+ /** `{protocol}://{hostname}` — the identity two patterns are deduped on. */
181
+ function patternKey(pattern) {
182
+ return `${pattern.protocol ?? "https"}://${pattern.hostname}${pattern.port ? `:${pattern.port}` : ""}${pattern.pathname ?? ""}`
183
+ }
184
+
185
+ /**
186
+ * The orchestrator's own origin, when it is somewhere `next/image` would need
187
+ * permission to reach.
188
+ *
189
+ * Uploaded and locally-cached images are served by the orchestrator, so its
190
+ * host has to be allowed too — and unlike the list above it is per-deployment,
191
+ * which is exactly why hand-maintained config kept getting it wrong.
192
+ */
193
+ function orchestratorImagePattern(env) {
194
+ const configured = (env.NEXT_PUBLIC_ORCHESTRATOR_URL ?? env.ORCHESTRATOR_URL ?? "").trim()
195
+ /*
196
+ * The same default `draft-fetch.ts` uses — but only outside production. A
197
+ * build that never names its orchestrator is a local one, and allowing
198
+ * `localhost` through the image optimizer of a deployed site is a wider hole
199
+ * than the convenience is worth.
200
+ */
201
+ const raw = configured || (env.NODE_ENV === "production" ? "" : "http://localhost:4200")
202
+ if (!raw) return null
203
+ try {
204
+ const url = new URL(raw)
205
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null
206
+ return {
207
+ protocol: url.protocol.replace(":", ""),
208
+ hostname: url.hostname,
209
+ ...(url.port ? { port: url.port } : {}),
210
+ }
211
+ } catch {
212
+ return null
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Merge Avocado's image hosts into a Next config's `images.remotePatterns`.
218
+ *
219
+ * Additive, like the transpile list: whatever the app declared stays, in its
220
+ * own order, and a host it already covers is not duplicated.
221
+ */
222
+ function withAvocadoImages(config, env) {
223
+ const images = config.images ?? {}
224
+ const declared = Array.isArray(images.remotePatterns) ? images.remotePatterns : []
225
+ const seen = new Set(declared.map(patternKey))
226
+
227
+ const additions = []
228
+ for (const pattern of [...AVOCADO_IMAGE_HOSTS, orchestratorImagePattern(env)]) {
229
+ if (!pattern) continue
230
+ const key = patternKey(pattern)
231
+ if (seen.has(key)) continue
232
+ seen.add(key)
233
+ additions.push(pattern)
234
+ }
235
+ if (additions.length === 0) return config
236
+
237
+ return { ...config, images: { ...images, remotePatterns: [...declared, ...additions] } }
238
+ }
239
+
240
+ /**
241
+ * Packages a bundler must not bundle into the server build.
242
+ *
243
+ * Two kinds, and they fail differently:
244
+ *
245
+ * - **Native binaries** — `better-sqlite3` (the orchestrator's state) and
246
+ * `sharp` (image processing). Bundling one produces a build that succeeds and
247
+ * a server that dies loading the `.node` file.
248
+ * - **Provider SDKs** — reached from `orchestrator-core`, several of them via
249
+ * `await import(...)` so that a site which does not use a provider need not
250
+ * install it. Turbopack resolves those statically and fails the build over a
251
+ * package that was never meant to be there.
252
+ *
253
+ * Naming a package that is not installed is a no-op, which is what makes one
254
+ * list correct for every consumer: a site with no Gemini key still lists
255
+ * `@google/genai`, and nothing looks for it.
256
+ */
257
+ export const AVOCADO_SERVER_EXTERNALS = [
258
+ "better-sqlite3",
259
+ "sharp",
260
+ "@anthropic-ai/sdk",
261
+ "@anthropic-ai/claude-agent-sdk",
262
+ "openai",
263
+ "@google/genai",
264
+ "googleapis",
265
+ "@modelcontextprotocol/sdk",
266
+ ]
267
+
268
+ /**
269
+ * Stop Next issuing the trailing-slash redirect, so the editor's API calls can
270
+ * reach the site at all.
271
+ *
272
+ * Only for an app that asked for `trailingSlash: true`; every other config is
273
+ * returned untouched. An app that already stated a `skipTrailingSlashRedirect`
274
+ * of its own — either value — keeps it, because a site that has thought about
275
+ * this has thought about it harder than a helper can.
276
+ *
277
+ * This half alone is a regression: it stops `/about` redirecting to `/about/`,
278
+ * which for a site that has published slashed URLs for years is an SEO change
279
+ * nobody asked for. `createEditorProxy({ trailingSlash: true })` re-issues that
280
+ * 308 for page routes, and the pair is the fix. They are documented together
281
+ * and neither is useful without the other.
282
+ */
283
+ function withAvocadoTrailingSlash(config) {
284
+ if (config.trailingSlash !== true) return config
285
+ if (config.skipTrailingSlashRedirect !== undefined) return config
286
+ return { ...config, skipTrailingSlashRedirect: true }
287
+ }
288
+
289
+ /**
290
+ * Mark every entry in `AVOCADO_SERVER_EXTERNALS` external to the server build,
291
+ * both ways it has to be said.
292
+ *
293
+ * `serverExternalPackages` is the documented knob and is not sufficient on its
294
+ * own: `transpilePackages` — which the same helper is busy populating — takes
295
+ * precedence for a *transitive* dependency, so a native binary imported by a
296
+ * transpiled Avocado package is bundled despite being listed. The webpack
297
+ * `externals` entry is what actually holds, and it is scoped to `isServer` so a
298
+ * client bundle is untouched.
299
+ *
300
+ * Both halves are additive: an app's own externals and its own `webpack` hook
301
+ * run first and keep whatever they did.
302
+ *
303
+ * The hook is attached even for an app that had none, and on Next 16 that is not
304
+ * cosmetic. Turbopack is the default there, and a config carrying a `webpack`
305
+ * key with no `turbopack` key is a **build error**, not a warning:
306
+ *
307
+ * ERROR: This build is using Turbopack, with a `webpack` config and no
308
+ * `turbopack` config. This may be a mistake.
309
+ *
310
+ * Next's own message names the remedy — an empty `turbopack` config — so that is
311
+ * what goes in whenever we are the ones adding the hook and the app declared no
312
+ * Turbopack config of its own. It is inert on Next 15 and it never overwrites an
313
+ * app's own `turbopack` key. Under Turbopack the webpack hook is never called
314
+ * and `serverExternalPackages` carries the fix; under webpack the hook is what
315
+ * holds. Declaring both is the only way to be right on both.
316
+ *
317
+ * An app that would rather manage all of this itself opts out with
318
+ * `{ serverExternals: false }`, which attaches nothing.
319
+ */
320
+ function withAvocadoServerExternals(config, extra = []) {
321
+ const externals = [...AVOCADO_SERVER_EXTERNALS, ...extra]
322
+ const declared = Array.isArray(config.serverExternalPackages) ? config.serverExternalPackages : []
323
+ const missing = externals.filter((name) => !declared.includes(name))
324
+
325
+ const appWebpack = typeof config.webpack === "function" ? config.webpack : null
326
+
327
+ return {
328
+ ...config,
329
+ ...(missing.length > 0 ? { serverExternalPackages: [...declared, ...missing] } : {}),
330
+ ...(config.turbopack === undefined ? { turbopack: {} } : {}),
331
+ webpack(webpackConfig, context) {
332
+ const result = appWebpack ? appWebpack(webpackConfig, context) : webpackConfig
333
+ if (!context?.isServer) return result
334
+
335
+ const existing = result.externals ?? []
336
+ result.externals = [
337
+ ...(Array.isArray(existing) ? existing : [existing]),
338
+ /*
339
+ * A function rather than a name list so that a deep import
340
+ * (`sharp/lib/...`) is externalised along with the package root.
341
+ * Calling back with no arguments means "not external" and hands the
342
+ * request to the next resolver, which is why this composes.
343
+ */
344
+ ({ request }, callback) => {
345
+ if (!request) return callback()
346
+ for (const name of externals) {
347
+ if (request === name || request.startsWith(`${name}/`)) {
348
+ return callback(null, `commonjs ${request}`)
349
+ }
350
+ }
351
+ callback()
352
+ },
353
+ ]
354
+ return result
355
+ },
356
+ }
357
+ }
358
+
359
+ /**
360
+ * `orchestrator-core`, when it arrived built rather than linked.
361
+ *
362
+ * Naming its *dependencies* external is not enough. Turbopack resolves a
363
+ * dynamic import statically, so as long as it walks into
364
+ * `orchestrator-core/dist` at all it meets `await import("googleapis")` and
365
+ * fails the build over an optional peer the site deliberately never installed —
366
+ * `serverExternalPackages` notwithstanding, because that governs what is
367
+ * bundled, not what is traversed. Externalising the package itself is what
368
+ * stops the walk, and it is the one fix that appears in no list and no
369
+ * document; it was found by building a registry install on Next 16.
370
+ *
371
+ * Only when it is *not* linked. A workspace checkout points `main` at
372
+ * `src/index.ts`, and externalising that hands Node a TypeScript file to
373
+ * require at runtime — trading a build error for a crash on the first request.
374
+ */
375
+ const ORCHESTRATOR_CORE = "@avocadostudio-ai/orchestrator-core"
376
+
377
+ function builtOrchestratorCore(from, linked) {
378
+ if (linked.includes(ORCHESTRATOR_CORE)) return []
379
+ for (const nodeModules of nodeModulesDirs(from)) {
380
+ if (existsSync(join(nodeModules, ORCHESTRATOR_CORE, "package.json"))) return [ORCHESTRATOR_CORE]
381
+ }
382
+ return []
383
+ }
384
+
385
+ /**
386
+ * Wrap a Next config so its `transpilePackages` covers every linked Avocado
387
+ * package.
388
+ *
389
+ * Also merges Avocado's own image hosts into `images.remotePatterns`, since a
390
+ * generated image comes from a host the site never chose, marks Avocado's
391
+ * native and provider dependencies external to the server build, since neither
392
+ * survives being bundled, and — on a site that sets `trailingSlash: true` —
393
+ * turns off the redirect that would otherwise make every editor API call fail
394
+ * its CORS preflight. Pass `{ images: false }`, `{ serverExternals: false }` or
395
+ * `{ trailingSlash: false }` to manage any of them yourself.
396
+ *
397
+ * `trailingSlash` is the one that needs a second step: the SDK's proxy has to
398
+ * re-issue the redirect it turns off. See `withAvocadoTrailingSlash`.
399
+ *
400
+ * Additive and total: whatever the app already listed is kept in the order it
401
+ * wrote it, unrelated entries included, its own `webpack` hook still runs and
402
+ * still wins, and any failure to read the filesystem leaves the config exactly
403
+ * as it was. Never throws — a helper that can break `next.config` is worse than
404
+ * the bug it fixes.
405
+ */
406
+ export function withAvocado(config = {}, options = {}) {
407
+ const {
408
+ cwd = process.cwd(),
409
+ silent = false,
410
+ images = true,
411
+ serverExternals = true,
412
+ trailingSlash = true,
413
+ env = process.env,
414
+ } = options
415
+
416
+ /*
417
+ * Resolved first because the externals depend on it: whether
418
+ * `orchestrator-core` has to be external is exactly the question of whether
419
+ * it is linked. A filesystem this cannot read leaves both lists empty, which
420
+ * is the same "never throw" contract as before — a helper that can break
421
+ * `next.config` is worse than the bug it fixes.
422
+ */
423
+ let linked = null
424
+ try {
425
+ linked = linkedAvocadoPackages(cwd)
426
+ } catch {
427
+ linked = null
428
+ }
429
+
430
+ /*
431
+ * Applied before the `transpilePackages` derivation below, which has two
432
+ * early returns of its own — a config that already lists every linked package
433
+ * still needs its externals.
434
+ */
435
+ const base = trailingSlash ? withAvocadoTrailingSlash(config) : config
436
+ const withImages = images ? withAvocadoImages(base, env) : base
437
+ const result = serverExternals
438
+ ? withAvocadoServerExternals(withImages, linked === null ? [] : builtOrchestratorCore(cwd, linked))
439
+ : withImages
440
+
441
+ if (linked === null) return result
442
+
443
+ const declared = Array.isArray(result.transpilePackages) ? result.transpilePackages : []
444
+ const missing = linked.filter((name) => !declared.includes(name))
445
+ if (missing.length === 0) return result
446
+
447
+ if (!silent) {
448
+ /*
449
+ * Best-effort only. Next runs `next.config` inside its own reporter, and on
450
+ * Next 16 neither stdout nor stderr written during that window reaches the
451
+ * terminal (measured, not assumed). So this is not the mechanism that keeps
452
+ * an integrator informed — the auto-fix is, and the comment they write next
453
+ * to their own `withAvocado(...)` call is. It stays because it does surface
454
+ * on hosts that do not capture the config run, and it costs nothing.
455
+ */
456
+ try {
457
+ process.stderr.write(
458
+ `[avocado] transpilePackages: added ${missing.join(", ")} — linked from source, so webpack has to compile them.\n`
459
+ )
460
+ } catch {
461
+ // A host with no writable stderr is not a reason to fail the build.
462
+ }
463
+ }
464
+
465
+ return { ...result, transpilePackages: [...declared, ...missing] }
466
+ }
467
+
468
+ export default withAvocado