@palettelab/cli 0.3.48 → 0.3.50

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.
@@ -0,0 +1,426 @@
1
+ "use strict"
2
+
3
+ /**
4
+ * pltt services — OS broker developer tools.
5
+ *
6
+ * pltt services list — show every service/event the org's installed
7
+ * apps make available; useful for "what can I
8
+ * consume?".
9
+ * pltt services pull — read consumes block from palette-plugin.json,
10
+ * fetch JSON Schemas from the platform, and
11
+ * generate typed clients into:
12
+ * .palette/types/services.d.ts
13
+ * .palette/types/services.ts (proxy)
14
+ * .palette/python/services.py (pydantic)
15
+ * pltt services scaffold <method>
16
+ * — emit a provider stub (schemas + handler) in
17
+ * the current plugin under ./broker/.
18
+ * pltt services add <target>
19
+ * — add a consumed service/event target to
20
+ * palette-plugin.json.
21
+ */
22
+
23
+ const fs = require("fs")
24
+ const path = require("path")
25
+ const { parseFlags, resolveEnvironment } = require("../environments")
26
+
27
+ function readManifest(cwd) {
28
+ const p = path.join(cwd, "palette-plugin.json")
29
+ if (!fs.existsSync(p)) {
30
+ throw new Error("no palette-plugin.json found in current directory")
31
+ }
32
+ return JSON.parse(fs.readFileSync(p, "utf8"))
33
+ }
34
+
35
+ function writeManifest(cwd, manifest) {
36
+ const p = path.join(cwd, "palette-plugin.json")
37
+ fs.writeFileSync(p, JSON.stringify(manifest, null, 2) + "\n")
38
+ }
39
+
40
+ function ensureDir(p) {
41
+ fs.mkdirSync(p, { recursive: true })
42
+ }
43
+
44
+ async function authFetch(env, path, init = {}) {
45
+ const url = `${env.url}${path}`
46
+ const headers = { "Content-Type": "application/json", ...(init.headers || {}) }
47
+ if (env.token) headers.Authorization = `Bearer ${env.token}`
48
+ const res = await fetch(url, { ...init, headers })
49
+ if (!res.ok) {
50
+ const text = await res.text()
51
+ throw new Error(`${init.method || "GET"} ${url} → ${res.status}: ${text}`)
52
+ }
53
+ return res.json()
54
+ }
55
+
56
+ // ---- list --------------------------------------------------------------------
57
+
58
+ async function runList(argv, { cwd }) {
59
+ const { flags } = parseFlags(argv)
60
+ const env = resolveEnvironment({ cwd, flags })
61
+ const catalog = await authFetch(env, "/api/v1/os-broker/catalog")
62
+ if (argv.includes("--json")) {
63
+ process.stdout.write(JSON.stringify(catalog, null, 2) + "\n")
64
+ return
65
+ }
66
+ const byNs = {}
67
+ for (const svc of catalog.services || []) {
68
+ const key = `${svc.namespace}/${svc.version}`
69
+ byNs[key] = byNs[key] || { services: [], events: [], providers: new Set() }
70
+ byNs[key].services.push(svc)
71
+ byNs[key].providers.add(svc.provider_app_id)
72
+ }
73
+ for (const evt of catalog.events || []) {
74
+ const key = `${evt.namespace}/${evt.version}`
75
+ byNs[key] = byNs[key] || { services: [], events: [], providers: new Set() }
76
+ byNs[key].events.push(evt)
77
+ byNs[key].providers.add(evt.provider_app_id)
78
+ }
79
+ const keys = Object.keys(byNs).sort()
80
+ if (keys.length === 0) {
81
+ console.log("[pltt] no services or events registered for the current org.")
82
+ return
83
+ }
84
+ for (const key of keys) {
85
+ const entry = byNs[key]
86
+ console.log(`\n${key} (provider: ${Array.from(entry.providers).join(", ")})`)
87
+ for (const svc of entry.services) {
88
+ const scope = svc.scope ? ` [scope: ${svc.scope}]` : ""
89
+ console.log(` service ${key}#${svc.method}${scope}`)
90
+ if (svc.description) console.log(` ${svc.description}`)
91
+ }
92
+ for (const evt of entry.events) {
93
+ console.log(` event ${key}#${evt.topic}`)
94
+ if (evt.description) console.log(` ${evt.description}`)
95
+ }
96
+ }
97
+ }
98
+
99
+ // ---- pull (codegen) ----------------------------------------------------------
100
+
101
+ function consumeTargets(manifest) {
102
+ const consumes = manifest.consumes || {}
103
+ const normalize = (entry) => {
104
+ if (typeof entry === "string") return entry
105
+ if (entry && typeof entry === "object" && typeof entry.target === "string") return entry.target
106
+ return null
107
+ }
108
+ const services = (consumes.services || []).map(normalize).filter(Boolean)
109
+ const events = (consumes.events || []).map(normalize).filter(Boolean)
110
+ return { services, events }
111
+ }
112
+
113
+ function targetFromConsumeEntry(entry) {
114
+ if (typeof entry === "string") return entry
115
+ if (entry && typeof entry === "object" && typeof entry.target === "string") return entry.target
116
+ return null
117
+ }
118
+
119
+ function brokerTargetLooksValid(target) {
120
+ return typeof target === "string" && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#[A-Za-z0-9_.-]+$/.test(target)
121
+ }
122
+
123
+ function valueAfter(argv, name) {
124
+ const index = argv.indexOf(name)
125
+ if (index === -1) return null
126
+ return argv[index + 1] || null
127
+ }
128
+
129
+ function jsonSchemaToTs(schema, indent = "") {
130
+ if (!schema || typeof schema !== "object") return "any"
131
+ if (schema.type === "string") return "string"
132
+ if (schema.type === "number" || schema.type === "integer") return "number"
133
+ if (schema.type === "boolean") return "boolean"
134
+ if (schema.type === "array") return `${jsonSchemaToTs(schema.items)}[]`
135
+ if (schema.type === "object" || schema.properties) {
136
+ const props = schema.properties || {}
137
+ const required = new Set(schema.required || [])
138
+ const lines = []
139
+ for (const [name, prop] of Object.entries(props)) {
140
+ const opt = required.has(name) ? "" : "?"
141
+ lines.push(`${indent} ${JSON.stringify(name)}${opt}: ${jsonSchemaToTs(prop, indent + " ")}`)
142
+ }
143
+ return `{\n${lines.join("\n")}\n${indent}}`
144
+ }
145
+ return "any"
146
+ }
147
+
148
+ function jsonSchemaToPydantic(name, schema) {
149
+ if (!schema || typeof schema !== "object") {
150
+ return { code: "", ref: "dict" }
151
+ }
152
+ if (schema.type === "object" || schema.properties) {
153
+ const props = schema.properties || {}
154
+ const required = new Set(schema.required || [])
155
+ const fieldLines = []
156
+ for (const [field, prop] of Object.entries(props)) {
157
+ const opt = required.has(field) ? "" : " | None = None"
158
+ fieldLines.push(` ${field}: ${pythonScalar(prop)}${opt}`)
159
+ }
160
+ const code =
161
+ `class ${name}(BaseModel):\n` +
162
+ (fieldLines.length ? fieldLines.join("\n") : " pass") +
163
+ "\n"
164
+ return { code, ref: name }
165
+ }
166
+ return { code: "", ref: pythonScalar(schema) }
167
+ }
168
+
169
+ function pythonScalar(schema) {
170
+ if (!schema) return "Any"
171
+ if (schema.type === "string") return "str"
172
+ if (schema.type === "integer") return "int"
173
+ if (schema.type === "number") return "float"
174
+ if (schema.type === "boolean") return "bool"
175
+ if (schema.type === "array") return `list[${pythonScalar(schema.items)}]`
176
+ return "Any"
177
+ }
178
+
179
+ function safeIdent(s) {
180
+ return s.replace(/[^A-Za-z0-9_]/g, "_")
181
+ }
182
+
183
+ function tsHeader() {
184
+ return (
185
+ "// AUTOGENERATED by `pltt services pull` — do not edit by hand.\n" +
186
+ "// Re-run after adding to consumes.services / consumes.events.\n\n" +
187
+ 'import { palette } from "@palettelab/sdk"\n\n'
188
+ )
189
+ }
190
+
191
+ function pyHeader() {
192
+ return (
193
+ '"""AUTOGENERATED by `pltt services pull` — do not edit by hand."""\n\n' +
194
+ "from __future__ import annotations\n\n" +
195
+ "from typing import Any\n\n" +
196
+ "from pydantic import BaseModel\n" +
197
+ "from palette_sdk import services as _services\n\n"
198
+ )
199
+ }
200
+
201
+ async function runPull(argv, { cwd }) {
202
+ const manifest = readManifest(cwd)
203
+ const { services, events } = consumeTargets(manifest)
204
+ const targets = [...new Set([...services, ...events])]
205
+ if (targets.length === 0) {
206
+ console.log("[pltt] palette-plugin.json declares no consumes.services or consumes.events — nothing to pull.")
207
+ return
208
+ }
209
+ const { flags } = parseFlags(argv)
210
+ const env = resolveEnvironment({ cwd, flags })
211
+ const targetsParam = encodeURIComponent(targets.join(","))
212
+ const schemas = await authFetch(env, `/api/v1/os-broker/schemas?targets=${targetsParam}`)
213
+
214
+ const byTarget = new Map(schemas.map((s) => [s.target, s]))
215
+ const missing = targets.filter((t) => !byTarget.has(t))
216
+ if (missing.length) {
217
+ console.warn(`[pltt] warning: no schema returned for: ${missing.join(", ")}`)
218
+ }
219
+
220
+ // ---- TypeScript output
221
+ const tsLines = [tsHeader()]
222
+ const tsProxies = {}
223
+ for (const target of targets) {
224
+ const entry = byTarget.get(target)
225
+ if (!entry) continue
226
+ const proxyKey = `${entry.namespace}/${entry.version}`
227
+ tsProxies[proxyKey] = tsProxies[proxyKey] || []
228
+ if (entry.kind === "service") {
229
+ const inT = entry.input_schema ? jsonSchemaToTs(entry.input_schema) : "Record<string, unknown>"
230
+ const outT = entry.output_schema ? jsonSchemaToTs(entry.output_schema) : "unknown"
231
+ tsProxies[proxyKey].push({ kind: "service", method: entry.method, inT, outT, scope: entry.scope })
232
+ } else if (entry.kind === "event") {
233
+ const payT = entry.payload_schema ? jsonSchemaToTs(entry.payload_schema) : "Record<string, unknown>"
234
+ tsProxies[proxyKey].push({ kind: "event", topic: entry.topic, payT })
235
+ }
236
+ }
237
+ for (const [ns, items] of Object.entries(tsProxies)) {
238
+ const safe = safeIdent(ns)
239
+ tsLines.push(`// ----- ${ns} -----`)
240
+ const services = items.filter((i) => i.kind === "service")
241
+ const events = items.filter((i) => i.kind === "event")
242
+ if (services.length) {
243
+ tsLines.push(`export const ${safe} = {`)
244
+ for (const svc of services) {
245
+ const comment = svc.scope ? ` /** scope: ${svc.scope} */` : ""
246
+ if (comment) tsLines.push(comment)
247
+ tsLines.push(` async ${JSON.stringify(svc.method)}(input: ${svc.inT}): Promise<${svc.outT}> {`)
248
+ tsLines.push(` return (await palette.broker.call(${JSON.stringify(ns + "#" + svc.method)}, input)) as ${svc.outT}`)
249
+ tsLines.push(` },`)
250
+ }
251
+ tsLines.push(`}\n`)
252
+ }
253
+ if (events.length) {
254
+ tsLines.push(`export const ${safe}Events = {`)
255
+ for (const evt of events) {
256
+ tsLines.push(` on${safeIdent(evt.topic).replace(/^(.)/, (m) => m.toUpperCase())}(handler: (payload: ${evt.payT}) => void) {`)
257
+ tsLines.push(` return palette.events.on(${JSON.stringify(ns + "#" + evt.topic)}, (payload) => handler(payload as ${evt.payT}))`)
258
+ tsLines.push(` },`)
259
+ }
260
+ tsLines.push(`}\n`)
261
+ }
262
+ }
263
+ const tsDir = path.join(cwd, ".palette", "types")
264
+ ensureDir(tsDir)
265
+ fs.writeFileSync(path.join(tsDir, "services.ts"), tsLines.join("\n"))
266
+
267
+ // ---- Python output
268
+ const pyParts = [pyHeader()]
269
+ for (const target of targets) {
270
+ const entry = byTarget.get(target)
271
+ if (!entry) continue
272
+ const safeNs = safeIdent(`${entry.namespace}_${entry.version}`)
273
+ const safeMethod = safeIdent(entry.method || entry.topic)
274
+ if (entry.kind === "service") {
275
+ const { code: inCode, ref: inRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Input`, entry.input_schema)
276
+ const { code: outCode, ref: outRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Output`, entry.output_schema)
277
+ if (inCode) pyParts.push(inCode)
278
+ if (outCode) pyParts.push(outCode)
279
+ pyParts.push(
280
+ `async def ${safeNs}_${safeMethod}(ctx, payload: ${inRef}) -> ${outRef}:\n` +
281
+ ` """Call ${entry.namespace}/${entry.version}#${entry.method}.\n\n` +
282
+ (entry.scope ? ` Scope: ${entry.scope}\n """\n` : ` """\n`) +
283
+ ` return await _services.services(ctx).call(${JSON.stringify(target)}, payload if isinstance(payload, dict) else payload.model_dump())\n`,
284
+ )
285
+ } else if (entry.kind === "event") {
286
+ const { code: payCode, ref: payRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Payload`, entry.payload_schema)
287
+ if (payCode) pyParts.push(payCode)
288
+ pyParts.push(
289
+ `def on_${safeNs}_${safeMethod}(handler):\n` +
290
+ ` """Subscribe to ${target}. Returns the underlying SDK subscribe call."""\n` +
291
+ ` from palette_sdk.events import subscribe_event\n` +
292
+ ` subscribe_event(${JSON.stringify(target)}, handler)\n`,
293
+ )
294
+ }
295
+ }
296
+ const pyDir = path.join(cwd, ".palette", "python")
297
+ ensureDir(pyDir)
298
+ fs.writeFileSync(path.join(pyDir, "services.py"), pyParts.join("\n"))
299
+
300
+ console.log(`[pltt] generated:`)
301
+ console.log(` ${path.relative(cwd, path.join(tsDir, "services.ts"))}`)
302
+ console.log(` ${path.relative(cwd, path.join(pyDir, "services.py"))}`)
303
+ if (missing.length) process.exit(2)
304
+ }
305
+
306
+ // ---- scaffold provider stub --------------------------------------------------
307
+
308
+ async function runScaffold(argv, { cwd }) {
309
+ const positional = argv.filter((a) => !a.startsWith("-"))
310
+ const method = positional[0]
311
+ if (!method) {
312
+ console.error("[pltt] usage: pltt services scaffold <method>")
313
+ process.exit(1)
314
+ }
315
+ const manifest = readManifest(cwd)
316
+ const namespace = (manifest.provides && manifest.provides.namespace) || manifest.id
317
+ const provides = manifest.provides || {}
318
+ provides.namespace = namespace
319
+ provides.services = provides.services || []
320
+ // Ensure a default service slot.
321
+ let svc = provides.services.find((s) => (s.id || "") === method) || null
322
+ if (!svc) {
323
+ svc = { id: method, version: "v1", methods: [] }
324
+ provides.services.push(svc)
325
+ }
326
+ svc.methods = svc.methods || []
327
+ if (!svc.methods.find((m) => m && m.name === method)) {
328
+ svc.methods.push({
329
+ name: method,
330
+ scope: `${namespace}.${method}`,
331
+ input_schema: `schemas/${method}.in.json`,
332
+ output_schema: `schemas/${method}.out.json`,
333
+ })
334
+ }
335
+ manifest.provides = provides
336
+ writeManifest(cwd, manifest)
337
+
338
+ const schemasDir = path.join(cwd, "schemas")
339
+ ensureDir(schemasDir)
340
+ for (const suffix of ["in", "out"]) {
341
+ const p = path.join(schemasDir, `${method}.${suffix}.json`)
342
+ if (!fs.existsSync(p)) {
343
+ fs.writeFileSync(
344
+ p,
345
+ JSON.stringify(
346
+ {
347
+ $schema: "https://json-schema.org/draft/2020-12/schema",
348
+ type: "object",
349
+ properties: {},
350
+ required: [],
351
+ },
352
+ null,
353
+ 2,
354
+ ) + "\n",
355
+ )
356
+ }
357
+ }
358
+
359
+ const brokerDir = path.join(cwd, "broker")
360
+ ensureDir(brokerDir)
361
+ const handlerPath = path.join(brokerDir, `${safeIdent(method)}.py`)
362
+ if (!fs.existsSync(handlerPath)) {
363
+ fs.writeFileSync(
364
+ handlerPath,
365
+ `from palette_sdk import service, PluginContext\n\n` +
366
+ `@service(${JSON.stringify(method)}, scope=${JSON.stringify(namespace + "." + method)})\n` +
367
+ `async def ${safeIdent(method)}(ctx: PluginContext, payload: dict) -> dict:\n` +
368
+ ` """Provider handler for ${namespace}/v1#${method}.\n\n` +
369
+ ` The OS broker validates payload against schemas/${method}.in.json and the\n` +
370
+ ` response against schemas/${method}.out.json before routing.\n """\n` +
371
+ ` return {}\n`,
372
+ )
373
+ }
374
+ console.log(`[pltt] scaffolded ${namespace}/v1#${method}`)
375
+ console.log(` manifest: palette-plugin.json (updated)`)
376
+ console.log(` schemas: schemas/${method}.in.json, schemas/${method}.out.json`)
377
+ console.log(` handler: ${path.relative(cwd, handlerPath)}`)
378
+ console.log(` import the handler from your backend entry to register it.`)
379
+ }
380
+
381
+ // ---- add consumed target -----------------------------------------------------
382
+
383
+ async function runAdd(argv, { cwd }) {
384
+ const target = argv.find((arg) => !arg.startsWith("-"))
385
+ if (!target || !brokerTargetLooksValid(target)) {
386
+ console.error("[pltt] usage: pltt services add <namespace/version#name> [--event] [--optional] [--reason <text>]")
387
+ process.exit(1)
388
+ }
389
+ const manifest = readManifest(cwd)
390
+ manifest.consumes = manifest.consumes || {}
391
+ const bucket = argv.includes("--event") || valueAfter(argv, "--type") === "event" ? "events" : "services"
392
+ manifest.consumes[bucket] = manifest.consumes[bucket] || []
393
+ const existing = new Set(manifest.consumes[bucket].map(targetFromConsumeEntry).filter(Boolean))
394
+ if (!existing.has(target)) {
395
+ const reason = valueAfter(argv, "--reason")
396
+ const optional = argv.includes("--optional")
397
+ const entry = reason || optional ? { target, required: !optional, ...(reason ? { reason } : {}) } : target
398
+ manifest.consumes[bucket].push(entry)
399
+ writeManifest(cwd, manifest)
400
+ console.log(`[pltt] added ${target} to consumes.${bucket}`)
401
+ } else {
402
+ console.log(`[pltt] ${target} is already declared in consumes.${bucket}`)
403
+ }
404
+ }
405
+
406
+ // ---- dispatch ----------------------------------------------------------------
407
+
408
+ async function run(argv, ctx) {
409
+ const sub = argv[0]
410
+ const rest = argv.slice(1)
411
+ if (sub === "list") return runList(rest, ctx)
412
+ if (sub === "pull") return runPull(rest, ctx)
413
+ if (sub === "scaffold") return runScaffold(rest, ctx)
414
+ if (sub === "add") return runAdd(rest, ctx)
415
+ console.log("pltt services <command>\n")
416
+ console.log(" list Show services/events available to the current org.")
417
+ console.log(" pull Generate typed TS+Python clients from consumes block.")
418
+ console.log(" scaffold Add a new provider method + schemas + handler stub.")
419
+ console.log(" add Add a consumed service/event target to palette-plugin.json.")
420
+ if (sub && !["help", "--help", "-h"].includes(sub)) {
421
+ console.error(`\n[pltt] unknown subcommand: ${sub}`)
422
+ process.exit(1)
423
+ }
424
+ }
425
+
426
+ module.exports = run
@@ -5,7 +5,7 @@ const path = require("path")
5
5
  const { spawnSync } = require("child_process")
6
6
  const { loadManifest, validateManifest, KNOWN_PERMISSIONS } = require("../manifest")
7
7
  const { bundleFrontend, bundleBackend } = require("../bundler")
8
- const { declaredSecrets, loadLocalEnv } = require("../secrets")
8
+ const { declaredSecrets, loadLocalEnv, loadLocalEnvDetails } = require("../secrets")
9
9
  const buildCommand = require("./build")
10
10
 
11
11
  const DEFAULT_FRONTEND_BUNDLE_LIMIT = 15 * 1024 * 1024
@@ -664,31 +664,32 @@ function checkDeclaredSecrets(cwd, manifest, out) {
664
664
  out.ok("no manifest secrets declared")
665
665
  return 0
666
666
  }
667
- const localValues = loadLocalEnv(cwd, { apply: false })
667
+ const localEnv = loadLocalEnvDetails(cwd, { apply: false })
668
+ const localValues = localEnv.values
668
669
  let failures = 0
669
670
  for (const [name, spec] of Object.entries(declared)) {
670
671
  if (spec.scope.includes("dev") && spec.required && !localValues[name]) {
671
672
  out.warn(
672
- `required dev secret ${name} is missing from .palette/.env.local`,
673
- "Run pltt secrets init and fill the local value before pltt dev.",
673
+ `required dev secret ${name} is missing from local .env files`,
674
+ "Add it to .env, .env.local, or an environment-specific .env file.",
674
675
  { secret: name, scope: spec.scope },
675
676
  )
676
677
  }
677
678
  if (spec.scope.includes("plugin") && spec.required && !localValues[name] && !process.env[name]) {
678
679
  out.warn(
679
680
  `required plugin secret ${name} has no local value`,
680
- "Set it before publish with an env var, --secrets-file, or pltt secrets set.",
681
+ "Add it to .env/.env.local, set an env var, pass --secrets-file, or run pltt secrets set.",
681
682
  { secret: name, scope: spec.scope },
682
683
  )
683
684
  }
684
685
  }
685
- if (!fs.existsSync(path.join(cwd, ".palette", ".env.local"))) {
686
+ if (!localEnv.files.length) {
686
687
  out.warn(
687
- ".palette/.env.local is missing",
688
- "Run pltt secrets init to create local developer env files.",
688
+ "no local .env files found",
689
+ "Create .env or .env.local when your app needs local environment values.",
689
690
  )
690
691
  } else {
691
- out.ok(".palette/.env.local is present")
692
+ out.ok(`local env files present: ${localEnv.files.join(", ")}`)
692
693
  }
693
694
  out.ok(`manifest declares ${names.length} secret(s)`, { secrets: names })
694
695
  return failures
@@ -757,14 +758,20 @@ async function run(args, { cwd }) {
757
758
  }
758
759
  if ((manifest.permissions || []).length) out.ok("declared permissions are known")
759
760
 
760
- if (manifest.provides || manifest.requires) {
761
+ if (manifest.provides || manifest.requires || manifest.consumes) {
761
762
  const providedServices = manifest.provides?.services?.map((item) => item.id).filter(Boolean) || []
762
763
  const requiredServices = manifest.requires?.services?.map((item) => item.id).filter(Boolean) || []
763
764
  const requiredApps = manifest.requires?.apps?.map((item) => item.id).filter(Boolean) || []
765
+ const consumedServices =
766
+ manifest.consumes?.services?.map((item) => (typeof item === "string" ? item : item.target)).filter(Boolean) || []
767
+ const consumedEvents =
768
+ manifest.consumes?.events?.map((item) => (typeof item === "string" ? item : item.target)).filter(Boolean) || []
764
769
  out.ok("app-to-app contracts are valid", {
765
770
  provides_services: providedServices,
766
771
  requires_services: requiredServices,
767
772
  requires_apps: requiredApps,
773
+ consumes_services: consumedServices,
774
+ consumes_events: consumedEvents,
768
775
  })
769
776
  }
770
777
 
@@ -786,7 +793,10 @@ async function run(args, { cwd }) {
786
793
 
787
794
  if (manifest.frontend?.entry) {
788
795
  try {
789
- const frontend = await bundleFrontend(cwd, manifest.frontend.entry, manifest.frontend)
796
+ const frontend = await bundleFrontend(cwd, manifest.frontend.entry, {
797
+ ...manifest.frontend,
798
+ pluginId: manifest.id,
799
+ })
790
800
  out.ok(`frontend bundles successfully (${frontend.length} bytes)`, { bytes: frontend.length })
791
801
  failures += checkBundleSize("frontend", frontend.length, out)
792
802
  failures += checkFrontendSecretLeaks(cwd, frontend, manifest, out)
@@ -0,0 +1,124 @@
1
+ "use strict"
2
+
3
+ const postcss = require("postcss")
4
+ const selectorParser = require("postcss-selector-parser")
5
+
6
+ function cssString(value) {
7
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')
8
+ }
9
+
10
+ function cssIdentifier(value) {
11
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, "-")
12
+ }
13
+
14
+ function rootSelector(pluginId) {
15
+ return `[data-palette-plugin-root="${cssString(pluginId)}"]`
16
+ }
17
+
18
+ function isKeyframesRule(rule) {
19
+ let parent = rule.parent
20
+ while (parent) {
21
+ if (parent.type === "atrule" && /keyframes$/i.test(parent.name)) return true
22
+ parent = parent.parent
23
+ }
24
+ return false
25
+ }
26
+
27
+ function isRootLike(node) {
28
+ return (
29
+ (node.type === "tag" && /^(html|body)$/i.test(node.value)) ||
30
+ (node.type === "pseudo" && node.value === ":root")
31
+ )
32
+ }
33
+
34
+ function removeLeadingCombinators(selector) {
35
+ while (selector.nodes[0]?.type === "combinator") {
36
+ selector.nodes[0].remove()
37
+ }
38
+ }
39
+
40
+ function replaceRootLikeSelectors(selector, rootNode) {
41
+ let replaced = false
42
+ selector.walk((node) => {
43
+ if (!isRootLike(node)) return
44
+ node.replaceWith(rootNode.clone())
45
+ replaced = true
46
+ })
47
+ return replaced
48
+ }
49
+
50
+ function prefixSelector(selector, rootNode) {
51
+ if (!selector.nodes.length) return
52
+ if (replaceRootLikeSelectors(selector, rootNode)) {
53
+ removeLeadingCombinators(selector)
54
+ return
55
+ }
56
+ selector.prepend(selectorParser.combinator({ value: " " }))
57
+ selector.prepend(rootNode.clone())
58
+ }
59
+
60
+ function scopeSelectors(selector, pluginId) {
61
+ const rootNode = selectorParser.attribute({
62
+ attribute: "data-palette-plugin-root",
63
+ operator: "=",
64
+ quoteMark: '"',
65
+ value: String(pluginId),
66
+ })
67
+
68
+ return selectorParser((selectors) => {
69
+ selectors.each((sel) => prefixSelector(sel, rootNode))
70
+ }).processSync(selector)
71
+ }
72
+
73
+ function renameKeyframes(root, pluginId) {
74
+ const prefix = `palette-${cssIdentifier(pluginId)}-`
75
+ const names = new Map()
76
+
77
+ root.walkAtRules((atRule) => {
78
+ if (!/keyframes$/i.test(atRule.name)) return
79
+ const current = atRule.params.trim()
80
+ if (!current || /^["']/.test(current)) return
81
+ const next = `${prefix}${current}`
82
+ names.set(current, next)
83
+ atRule.params = next
84
+ })
85
+
86
+ if (names.size === 0) return
87
+
88
+ const namePattern = new RegExp(
89
+ `\\b(${Array.from(names.keys()).map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`,
90
+ "g",
91
+ )
92
+ root.walkDecls((decl) => {
93
+ if (!/^animation(-name)?$/i.test(decl.prop)) return
94
+ decl.value = decl.value.replace(namePattern, (match) => names.get(match) || match)
95
+ })
96
+ }
97
+
98
+ function scopePluginCss(css, pluginId) {
99
+ if (!css || !String(css).trim()) return ""
100
+ const root = postcss.parse(css)
101
+
102
+ renameKeyframes(root, pluginId)
103
+ root.walkRules((rule) => {
104
+ if (isKeyframesRule(rule)) return
105
+ rule.selector = scopeSelectors(rule.selector, pluginId)
106
+ })
107
+
108
+ return root.toString()
109
+ }
110
+
111
+ function appendCssExport(js, css) {
112
+ if (!css || !css.trim()) return js
113
+ const exportLine = `export const __palettePluginCss = ${JSON.stringify(css)};\n`
114
+ const sourceMapPattern = /\n?\/\/# sourceMappingURL=data:application\/json[^]*$/m
115
+ const match = js.match(sourceMapPattern)
116
+ if (!match) return `${js}\n${exportLine}`
117
+ return `${js.slice(0, match.index)}\n${exportLine}${match[0]}`
118
+ }
119
+
120
+ module.exports = {
121
+ appendCssExport,
122
+ rootSelector,
123
+ scopePluginCss,
124
+ }