@palettelab/cli 0.3.28 → 0.3.29

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 (30) hide show
  1. package/README.md +29 -0
  2. package/backend-sdk/palette_sdk/manifest.py +2 -0
  3. package/lib/bundler.js +200 -3
  4. package/lib/cli.js +1 -1
  5. package/lib/commands/dev.js +1 -1
  6. package/lib/commands/doctor.js +1 -1
  7. package/lib/commands/init.js +20 -5
  8. package/lib/commands/package.js +1 -1
  9. package/lib/commands/publish.js +1 -1
  10. package/lib/commands/test.js +7 -3
  11. package/lib/dev-simulator.js +4 -0
  12. package/lib/manifest.js +9 -1
  13. package/package.json +1 -1
  14. package/template-fallback/package.json +1 -1
  15. package/template-fallback/palette-plugin.json +1 -1
  16. package/template-fallback/templates/dashboard/package.json +1 -1
  17. package/template-fallback/templates/dashboard/palette-plugin.json +1 -1
  18. package/template-fallback/templates/database/package.json +1 -1
  19. package/template-fallback/templates/database/palette-plugin.json +1 -1
  20. package/template-fallback/templates/external-service/package.json +1 -1
  21. package/template-fallback/templates/external-service/palette-plugin.json +1 -1
  22. package/template-fallback/templates/frontend-only/package.json +1 -1
  23. package/template-fallback/templates/frontend-only/palette-plugin.json +1 -1
  24. package/template-fallback/templates/next/README.md +40 -0
  25. package/template-fallback/templates/next/frontend/next.config.ts +8 -0
  26. package/template-fallback/templates/next/frontend/src/index.tsx +30 -0
  27. package/template-fallback/templates/next/frontend/src/translations.ts +14 -0
  28. package/template-fallback/templates/next/frontend/tsconfig.json +15 -0
  29. package/template-fallback/templates/next/package.json +13 -0
  30. package/template-fallback/templates/next/palette-plugin.json +30 -0
package/README.md CHANGED
@@ -341,6 +341,7 @@ Scaffold a new plugin directory from the official template.
341
341
  ```bash
342
342
  pltt init data-explorer
343
343
  pltt init crm-dashboard --template dashboard
344
+ pltt init next-panel --template next
344
345
  cd data-explorer
345
346
  ```
346
347
 
@@ -349,11 +350,39 @@ Creates `data-explorer/` with a valid `palette-plugin.json`, a frontend React en
349
350
  Templates:
350
351
 
351
352
  - `dashboard`
353
+ - `next`
352
354
  - `agent-tool`
353
355
  - `external-service`
354
356
  - `database`
355
357
  - `frontend-only`
356
358
 
359
+ ### Next-Compatible Frontend Config
360
+
361
+ Palette native apps publish as a single React module loaded by the OS. They do
362
+ not run a standalone Next server. When an app needs Next-style frontend config,
363
+ set the manifest to Next-compatible native mode:
364
+
365
+ ```json
366
+ {
367
+ "frontend": {
368
+ "entry": "./frontend/src/index.tsx",
369
+ "sandbox": true,
370
+ "framework": "next",
371
+ "config": "./frontend/next.config.ts"
372
+ }
373
+ }
374
+ ```
375
+
376
+ Put the config file at `frontend/next.config.ts` unless you set a custom
377
+ `frontend.config` path. `pltt dev`, `pltt test`, `pltt package`, and
378
+ `pltt publish` load `next.config.ts/js/mjs/cjs`, apply `env` values plus
379
+ `NEXT_PUBLIC_*` environment variables to the native bundle, and honor path
380
+ aliases from `frontend/tsconfig.json`.
381
+
382
+ Supported today: `env`, `NEXT_PUBLIC_*`, and TypeScript path aliases. Full Next server features
383
+ such as API routes, server components, Next image optimization, middleware, and
384
+ multi-file static export are outside this native module mode.
385
+
357
386
  ### `pltt dev`
358
387
 
359
388
  Run a no-Docker local SDK simulator with your plugin mounted live. Run this from inside your plugin directory.
@@ -29,6 +29,8 @@ class ToolEntry(BaseModel):
29
29
  class FrontendEntry(BaseModel):
30
30
  entry: str
31
31
  sandbox: bool = True
32
+ framework: Literal["react", "next"] = "react"
33
+ config: str | None = None
32
34
 
33
35
 
34
36
  class BackendEntry(BaseModel):
package/lib/bundler.js CHANGED
@@ -4,6 +4,13 @@ const path = require("path")
4
4
  const fs = require("fs")
5
5
  const os = require("os")
6
6
 
7
+ const NEXT_CONFIG_NAMES = [
8
+ "frontend/next.config.ts",
9
+ "frontend/next.config.mjs",
10
+ "frontend/next.config.js",
11
+ "frontend/next.config.cjs",
12
+ ]
13
+
7
14
  // esbuild is declared as a dependency in package.json; installed via npm install.
8
15
  // We require it lazily so `pltt init` / `pltt dev` / `pltt build` do
9
16
  // not pay the load cost.
@@ -18,6 +25,184 @@ function loadEsbuild() {
18
25
  }
19
26
  }
20
27
 
28
+ function frontendFramework(frontend = {}) {
29
+ return frontend.framework || "react"
30
+ }
31
+
32
+ function resolveNextConfigPath(pluginDir, frontend = {}) {
33
+ if (frontend.config) {
34
+ const explicit = path.resolve(pluginDir, frontend.config)
35
+ if (!fs.existsSync(explicit)) {
36
+ throw new Error(`frontend.config not found: ${frontend.config}`)
37
+ }
38
+ return explicit
39
+ }
40
+
41
+ for (const candidate of NEXT_CONFIG_NAMES) {
42
+ const abs = path.resolve(pluginDir, candidate)
43
+ if (fs.existsSync(abs)) return abs
44
+ }
45
+ return null
46
+ }
47
+
48
+ function requireBundledConfig(absConfig) {
49
+ const esbuild = loadEsbuild()
50
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-next-config-"))
51
+ const outfile = path.join(tmp, "next.config.cjs")
52
+ try {
53
+ esbuild.buildSync({
54
+ entryPoints: [absConfig],
55
+ bundle: true,
56
+ platform: "node",
57
+ target: ["node18"],
58
+ format: "cjs",
59
+ outfile,
60
+ logLevel: "silent",
61
+ external: ["next"],
62
+ })
63
+ delete require.cache[require.resolve(outfile)]
64
+ const mod = require(outfile)
65
+ return mod.default || mod
66
+ } finally {
67
+ fs.rmSync(tmp, { recursive: true, force: true })
68
+ }
69
+ }
70
+
71
+ function loadNextConfig(pluginDir, frontend = {}) {
72
+ if (frontendFramework(frontend) !== "next") return { config: {}, configPath: null }
73
+
74
+ const configPath = resolveNextConfigPath(pluginDir, frontend)
75
+ if (!configPath) {
76
+ return { config: {}, configPath: null }
77
+ }
78
+
79
+ const loaded = requireBundledConfig(configPath)
80
+ const config =
81
+ typeof loaded === "function"
82
+ ? loaded("phase-production-build", { defaultConfig: {} })
83
+ : loaded
84
+ if (config && typeof config.then === "function") {
85
+ throw new Error("async next.config is not supported by Palette native Next mode")
86
+ }
87
+ if (config && (typeof config !== "object" || Array.isArray(config))) {
88
+ throw new Error("next.config must export an object or a function returning an object")
89
+ }
90
+ return { config: config || {}, configPath }
91
+ }
92
+
93
+ function readJson(absPath) {
94
+ try {
95
+ return JSON.parse(fs.readFileSync(absPath, "utf8"))
96
+ } catch {
97
+ return null
98
+ }
99
+ }
100
+
101
+ function resolveTsconfig(pluginDir, frontend = {}) {
102
+ const entryDir = frontend.entry ? path.dirname(path.resolve(pluginDir, frontend.entry)) : path.resolve(pluginDir, "frontend")
103
+ const candidates = [
104
+ path.join(entryDir, "tsconfig.json"),
105
+ path.join(pluginDir, "frontend", "tsconfig.json"),
106
+ path.join(pluginDir, "tsconfig.json"),
107
+ ]
108
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null
109
+ }
110
+
111
+ function makeTsconfigPathsPlugin(pluginDir, frontend = {}) {
112
+ const tsconfigPath = resolveTsconfig(pluginDir, frontend)
113
+ if (!tsconfigPath) return null
114
+
115
+ const tsconfig = readJson(tsconfigPath)
116
+ const compilerOptions = tsconfig?.compilerOptions || {}
117
+ const paths = compilerOptions.paths || {}
118
+ const baseUrl = path.resolve(path.dirname(tsconfigPath), compilerOptions.baseUrl || ".")
119
+ const mappings = []
120
+
121
+ for (const [pattern, targets] of Object.entries(paths)) {
122
+ if (!Array.isArray(targets) || targets.length === 0) continue
123
+ const starIndex = pattern.indexOf("*")
124
+ mappings.push({
125
+ pattern,
126
+ prefix: starIndex >= 0 ? pattern.slice(0, starIndex) : pattern,
127
+ suffix: starIndex >= 0 ? pattern.slice(starIndex + 1) : "",
128
+ hasStar: starIndex >= 0,
129
+ targets,
130
+ })
131
+ }
132
+
133
+ if (mappings.length === 0) return null
134
+
135
+ const extensions = ["", ".tsx", ".ts", ".jsx", ".js", ".mjs", ".json"]
136
+ const resolveTarget = (target) => {
137
+ for (const ext of extensions) {
138
+ const candidate = path.resolve(baseUrl, `${target}${ext}`)
139
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate
140
+ }
141
+ const indexCandidates = ["index.tsx", "index.ts", "index.jsx", "index.js", "index.mjs"]
142
+ for (const indexFile of indexCandidates) {
143
+ const candidate = path.resolve(baseUrl, target, indexFile)
144
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate
145
+ }
146
+ return null
147
+ }
148
+
149
+ return {
150
+ name: "palette-tsconfig-paths",
151
+ setup(build) {
152
+ build.onResolve({ filter: /^[^./].*/ }, (args) => {
153
+ for (const mapping of mappings) {
154
+ let starValue = ""
155
+ if (mapping.hasStar) {
156
+ if (!args.path.startsWith(mapping.prefix) || !args.path.endsWith(mapping.suffix)) continue
157
+ starValue = args.path.slice(mapping.prefix.length, args.path.length - mapping.suffix.length)
158
+ } else if (args.path !== mapping.pattern) {
159
+ continue
160
+ }
161
+
162
+ for (const target of mapping.targets) {
163
+ const resolved = resolveTarget(String(target).replace("*", starValue))
164
+ if (resolved) return { path: resolved }
165
+ }
166
+ }
167
+ return null
168
+ })
169
+ },
170
+ }
171
+ }
172
+
173
+ function frontendBuildConfig(pluginDir, frontend = {}) {
174
+ const framework = frontendFramework(frontend)
175
+ const define = {
176
+ "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production"),
177
+ }
178
+ const plugins = []
179
+ const tsconfigPaths = framework === "next" ? makeTsconfigPathsPlugin(pluginDir, frontend) : null
180
+ if (tsconfigPaths) plugins.push(tsconfigPaths)
181
+
182
+ let nextConfigPath = null
183
+ if (framework === "next") {
184
+ const { config, configPath } = loadNextConfig(pluginDir, frontend)
185
+ nextConfigPath = configPath
186
+ for (const [key, value] of Object.entries(process.env)) {
187
+ if (key.startsWith("NEXT_PUBLIC_")) {
188
+ define[`process.env.${key}`] = JSON.stringify(value)
189
+ }
190
+ }
191
+ for (const [key, value] of Object.entries(config.env || {})) {
192
+ if (/^[A-Z0-9_]+$/i.test(key)) {
193
+ define[`process.env.${key}`] = JSON.stringify(value)
194
+ }
195
+ }
196
+ define["process.env.NEXT_RUNTIME"] = JSON.stringify("browser")
197
+ }
198
+
199
+ return { define, plugins, framework, nextConfigPath }
200
+ }
201
+
202
+ function mergePlugins(...pluginGroups) {
203
+ return pluginGroups.flat().filter(Boolean)
204
+ }
205
+
21
206
  /**
22
207
  * Bundle the plugin's frontend entry into a single ESM file.
23
208
  *
@@ -26,13 +211,14 @@ function loadEsbuild() {
26
211
  *
27
212
  * Returns the bundle as a Buffer.
28
213
  */
29
- async function bundleFrontend(pluginDir, entry) {
214
+ async function bundleFrontend(pluginDir, entry, frontend = {}) {
30
215
  pluginDir = path.resolve(pluginDir)
31
216
  const esbuild = loadEsbuild()
32
217
  const absEntry = path.resolve(pluginDir, entry)
33
218
  if (!fs.existsSync(absEntry)) {
34
219
  throw new Error(`frontend entry not found: ${entry}`)
35
220
  }
221
+ const buildConfig = frontendBuildConfig(pluginDir, { ...frontend, entry })
36
222
 
37
223
  const result = await esbuild.build({
38
224
  entryPoints: [absEntry],
@@ -43,6 +229,7 @@ async function bundleFrontend(pluginDir, entry) {
43
229
  write: false,
44
230
  jsx: "automatic",
45
231
  loader: { ".ts": "tsx", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
232
+ define: buildConfig.define,
46
233
  external: [
47
234
  "react",
48
235
  "react-dom",
@@ -55,6 +242,7 @@ async function bundleFrontend(pluginDir, entry) {
55
242
  sourcemap: "inline",
56
243
  logLevel: "silent",
57
244
  absWorkingDir: pluginDir,
245
+ plugins: buildConfig.plugins,
58
246
  })
59
247
 
60
248
  if (!result.outputFiles || result.outputFiles.length === 0) {
@@ -63,7 +251,7 @@ async function bundleFrontend(pluginDir, entry) {
63
251
  return Buffer.from(result.outputFiles[0].contents)
64
252
  }
65
253
 
66
- async function watchFrontend(pluginDir, entry, outfile) {
254
+ async function watchFrontend(pluginDir, entry, outfile, frontend = {}) {
67
255
  pluginDir = path.resolve(pluginDir)
68
256
  outfile = path.resolve(outfile)
69
257
  const esbuild = loadEsbuild()
@@ -71,6 +259,7 @@ async function watchFrontend(pluginDir, entry, outfile) {
71
259
  if (!fs.existsSync(absEntry)) {
72
260
  throw new Error(`frontend entry not found: ${entry}`)
73
261
  }
262
+ const buildConfig = frontendBuildConfig(pluginDir, { ...frontend, entry })
74
263
 
75
264
  fs.mkdirSync(path.dirname(outfile), { recursive: true })
76
265
 
@@ -83,6 +272,7 @@ async function watchFrontend(pluginDir, entry, outfile) {
83
272
  outfile,
84
273
  jsx: "automatic",
85
274
  loader: { ".ts": "tsx", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
275
+ define: buildConfig.define,
86
276
  external: [
87
277
  "react",
88
278
  "react-dom",
@@ -96,6 +286,7 @@ async function watchFrontend(pluginDir, entry, outfile) {
96
286
  logLevel: "silent",
97
287
  absWorkingDir: pluginDir,
98
288
  plugins: [
289
+ ...buildConfig.plugins,
99
290
  {
100
291
  name: "palette-dev-watch-logger",
101
292
  setup(build) {
@@ -176,4 +367,10 @@ async function bundleBackend(pluginDir) {
176
367
  }
177
368
  }
178
369
 
179
- module.exports = { bundleFrontend, bundleBackend, watchFrontend }
370
+ module.exports = {
371
+ bundleFrontend,
372
+ bundleBackend,
373
+ watchFrontend,
374
+ frontendBuildConfig,
375
+ mergePlugins,
376
+ }
package/lib/cli.js CHANGED
@@ -64,7 +64,7 @@ function printHelp() {
64
64
  console.log(" --token <token> Publish token for pltt login")
65
65
  console.log(" --no-default Do not make this environment the default")
66
66
  console.log("\nInit flags:")
67
- console.log(" --template <name> One of: dashboard, agent-tool, external-service, database, frontend-only")
67
+ console.log(" --template <name> One of: dashboard, agent-tool, external-service, database, frontend-only, next")
68
68
  console.log("\nLogs flags:")
69
69
  console.log(" --tail <n> Tail last n events (default 50)")
70
70
  console.log(" -f, --follow Stream events (poll every 3s)")
@@ -132,7 +132,7 @@ async function run(args, { cwd }) {
132
132
  if (manifest.frontend?.entry) {
133
133
  console.log(`[pltt] bundling frontend ${frontendEntry} → .palette/dist/frontend.mjs`)
134
134
  try {
135
- frontendWatcher = await watchFrontend(cwd, frontendEntry, frontendBundle)
135
+ frontendWatcher = await watchFrontend(cwd, frontendEntry, frontendBundle, manifest.frontend)
136
136
  } catch (err) {
137
137
  console.error(
138
138
  `[pltt] could not start frontend bundler: ${
@@ -122,7 +122,7 @@ async function run(args, { cwd }) {
122
122
 
123
123
  if (manifest.frontend?.entry) {
124
124
  try {
125
- const bundle = await bundleFrontend(cwd, manifest.frontend.entry)
125
+ const bundle = await bundleFrontend(cwd, manifest.frontend.entry, manifest.frontend)
126
126
  ok(`frontend bundles successfully (${bundle.length} bytes)`)
127
127
  } catch (err) {
128
128
  failures += fail(
@@ -9,7 +9,7 @@ const DEFAULT_TEMPLATE_REPO = "palette-lab/plugin-template"
9
9
  const TEMPLATE_REPO = process.env.PALETTE_TEMPLATE_REPO || DEFAULT_TEMPLATE_REPO
10
10
  const TEMPLATE_REF = process.env.PALETTE_TEMPLATE_REF || "main"
11
11
 
12
- const KNOWN_TEMPLATES = ["frontend-only", "dashboard", "agent-tool", "external-service", "database"]
12
+ const KNOWN_TEMPLATES = ["frontend-only", "next", "dashboard", "agent-tool", "external-service", "database"]
13
13
 
14
14
  function toSlug(name) {
15
15
  return name
@@ -96,6 +96,14 @@ function getOpt(args, name) {
96
96
  return null
97
97
  }
98
98
 
99
+ function hasPathSegment(value) {
100
+ return value.includes("/") || value.includes("\\")
101
+ }
102
+
103
+ function targetName(input) {
104
+ return path.basename(input.replace(/[\\/]+$/, ""))
105
+ }
106
+
99
107
  async function run(args, { cwd }) {
100
108
  const positional = args.filter((a) => !a.startsWith("-"))
101
109
  const name = positional[0]
@@ -110,11 +118,18 @@ async function run(args, { cwd }) {
110
118
  console.error(`[pltt] templates: ${KNOWN_TEMPLATES.join(", ")}`)
111
119
  process.exit(1)
112
120
  }
113
- const slug = toSlug(name)
114
- const displayName = name
121
+ const targetBase = targetName(name)
122
+ const slug = toSlug(targetBase)
123
+ if (!slug) {
124
+ console.error(`[pltt] invalid plugin name: ${name}`)
125
+ process.exit(1)
126
+ }
127
+ const displayName = targetBase
115
128
  .replace(/[-_]+/g, " ")
116
129
  .replace(/\b\w/g, (c) => c.toUpperCase())
117
- const destDir = path.join(cwd, slug)
130
+ const destDir = path.isAbsolute(name) || hasPathSegment(name)
131
+ ? path.resolve(cwd, name)
132
+ : path.join(cwd, slug)
118
133
  if (fs.existsSync(destDir)) {
119
134
  console.error(`[pltt] directory already exists: ${destDir}`)
120
135
  process.exit(1)
@@ -144,7 +159,7 @@ async function run(args, { cwd }) {
144
159
 
145
160
  console.log(`[pltt] created ${destDir}`)
146
161
  console.log("[pltt] next steps:")
147
- console.log(` cd ${slug}`)
162
+ console.log(` cd ${path.relative(cwd, destDir) || "."}`)
148
163
  console.log(" npm install")
149
164
  console.log(` npx @palettelab/cli dev`)
150
165
  console.log(` # or, after global install: pltt dev`)
@@ -24,7 +24,7 @@ async function run(argv, { cwd }) {
24
24
  fs.mkdirSync(distDir, { recursive: true })
25
25
 
26
26
  const frontend = manifest.frontend
27
- ? await bundleFrontend(cwd, manifest.frontend.entry || "./frontend/src/index.tsx")
27
+ ? await bundleFrontend(cwd, manifest.frontend.entry || "./frontend/src/index.tsx", manifest.frontend)
28
28
  : null
29
29
  const backend = manifest.backend ? await bundleBackend(cwd) : null
30
30
 
@@ -125,7 +125,7 @@ async function run(argv, { cwd }) {
125
125
  let frontend = null
126
126
  if (manifest.frontend?.entry) {
127
127
  log("[pltt] bundling frontend")
128
- frontend = await bundleFrontend(cwd, manifest.frontend.entry)
128
+ frontend = await bundleFrontend(cwd, manifest.frontend.entry, manifest.frontend)
129
129
  log(`[pltt] ${frontend.length} bytes`)
130
130
  } else {
131
131
  log("[pltt] no frontend declared")
@@ -498,9 +498,13 @@ function scanForbiddenImports(cwd, manifest, out) {
498
498
  const roots = []
499
499
  if (manifest.frontend?.entry) roots.push(path.resolve(cwd, "frontend"))
500
500
  if (manifest.backend?.entry) roots.push(path.resolve(cwd, "backend"))
501
+ const allowLocalNextAlias = manifest.frontend?.framework === "next"
502
+ const frontendImportPrefix = allowLocalNextAlias
503
+ ? "(?:app/|backend/|frontend/)"
504
+ : "(?:@/|app/|backend/|frontend/)"
501
505
  const forbidden = [
502
- { re: /from\s+["'](?:@\/|app\/|backend\/|frontend\/)/, reason: "frontend imports platform source" },
503
- { re: /import\s+["'](?:@\/|app\/|backend\/|frontend\/)/, reason: "frontend imports platform source" },
506
+ { re: new RegExp(`from\\s+["']${frontendImportPrefix}`), reason: "frontend imports platform source" },
507
+ { re: new RegExp(`import\\s+["']${frontendImportPrefix}`), reason: "frontend imports platform source" },
504
508
  { re: /^\s*(?:from|import)\s+app(?:\.|\s)/m, reason: "backend imports platform app source" },
505
509
  ]
506
510
  const issues = []
@@ -655,7 +659,7 @@ async function run(args, { cwd }) {
655
659
 
656
660
  if (manifest.frontend?.entry) {
657
661
  try {
658
- const frontend = await bundleFrontend(cwd, manifest.frontend.entry)
662
+ const frontend = await bundleFrontend(cwd, manifest.frontend.entry, manifest.frontend)
659
663
  out.ok(`frontend bundles successfully (${frontend.length} bytes)`, { bytes: frontend.length })
660
664
  failures += checkBundleSize("frontend", frontend.length, out)
661
665
  failures += sandboxBridgeSmoke(cwd, manifest, out)
@@ -6,6 +6,7 @@ const path = require("path")
6
6
  const { spawn, spawnSync } = require("child_process")
7
7
 
8
8
  const { loadManifest } = require("./manifest")
9
+ const { frontendBuildConfig } = require("./bundler")
9
10
 
10
11
  function loadEsbuild() {
11
12
  try {
@@ -360,6 +361,7 @@ async function startFrontend(cwd, devDir, manifest, frontendPort, backendPort) {
360
361
  const generatedEntry = path.join(devDir, "simulator-entry.jsx")
361
362
  const bundlePath = path.join(devDir, "simulator.js")
362
363
  fs.writeFileSync(generatedEntry, simulatorEntrySource(absEntry, manifest, backendPort))
364
+ const buildConfig = frontendBuildConfig(cwd, { ...(manifest.frontend || {}), entry })
363
365
 
364
366
  const esbuild = loadEsbuild()
365
367
  const ctx = await esbuild.context({
@@ -371,10 +373,12 @@ async function startFrontend(cwd, devDir, manifest, frontendPort, backendPort) {
371
373
  outfile: bundlePath,
372
374
  jsx: "automatic",
373
375
  loader: { ".ts": "tsx", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
376
+ define: buildConfig.define,
374
377
  absWorkingDir: cwd,
375
378
  sourcemap: "inline",
376
379
  logLevel: "silent",
377
380
  plugins: [
381
+ ...buildConfig.plugins,
378
382
  {
379
383
  name: "palette-simulator-watch",
380
384
  setup(build) {
package/lib/manifest.js CHANGED
@@ -127,11 +127,19 @@ function validateManifest(m) {
127
127
  if (m.frontend !== undefined) {
128
128
  if (!isObject(m.frontend)) errors.push("frontend must be an object")
129
129
  else {
130
- unknownKeys(m.frontend, new Set(["entry", "sandbox"]), "frontend", errors)
130
+ unknownKeys(m.frontend, new Set(["entry", "sandbox", "framework", "config"]), "frontend", errors)
131
131
  if (!m.frontend.entry || typeof m.frontend.entry !== "string") {
132
132
  errors.push("frontend.entry is required when frontend is set")
133
133
  }
134
134
  requireBoolean(m.frontend, "sandbox", "frontend", errors)
135
+ requireString(m.frontend, "framework", "frontend", errors)
136
+ requireString(m.frontend, "config", "frontend", errors)
137
+ if (m.frontend.framework !== undefined && !["react", "next"].includes(m.frontend.framework)) {
138
+ errors.push('frontend.framework must be "react" or "next"')
139
+ }
140
+ if (m.frontend.config !== undefined && m.frontend.framework !== "next") {
141
+ errors.push('frontend.config is only supported when frontend.framework is "next"')
142
+ }
135
143
  }
136
144
  }
137
145
  if (m.backend !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "description": "A Palette platform plugin",
6
6
  "dependencies": {
7
- "@palettelab/sdk": "^0.1.11"
7
+ "@palettelab/sdk": "^0.1.12"
8
8
  },
9
9
  "devDependencies": {
10
10
  "typescript": "^5.0.0",
@@ -13,7 +13,7 @@
13
13
  "text": "#fff"
14
14
  },
15
15
  "sdk": {
16
- "frontend": "^0.1.11",
16
+ "frontend": "^0.1.12",
17
17
  "backend": "^0.1.0"
18
18
  },
19
19
  "platform": {
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.11",
6
+ "@palettelab/sdk": "^0.1.12",
7
7
  "react": "^19.0.0"
8
8
  }
9
9
  }
@@ -9,7 +9,7 @@
9
9
  "description": "A widget that exposes a dashboard data source and renders a chart from it.",
10
10
  "icon": "ChartBar",
11
11
  "gradient": { "bg": "linear-gradient(135deg, #06B6D4, #6366F1)", "text": "#fff" },
12
- "sdk": { "frontend": "^0.1.11", "backend": "^0.1.0" },
12
+ "sdk": { "frontend": "^0.1.12", "backend": "^0.1.0" },
13
13
  "platform": { "min_version": "0.1.0" },
14
14
  "capabilities": {
15
15
  "frontend": true,
@@ -2,5 +2,5 @@
2
2
  "name": "my-db-plugin",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
- "dependencies": { "@palettelab/sdk": "^0.1.11", "react": "^19.0.0" }
5
+ "dependencies": { "@palettelab/sdk": "^0.1.12", "react": "^19.0.0" }
6
6
  }
@@ -9,7 +9,7 @@
9
9
  "description": "Stores notes per organization with RLS-enforced isolation.",
10
10
  "icon": "Database",
11
11
  "gradient": { "bg": "linear-gradient(135deg, #8B5CF6, #EC4899)", "text": "#fff" },
12
- "sdk": { "frontend": "^0.1.11", "backend": "^0.1.0" },
12
+ "sdk": { "frontend": "^0.1.12", "backend": "^0.1.0" },
13
13
  "platform": { "min_version": "0.1.0" },
14
14
  "capabilities": {
15
15
  "frontend": true,
@@ -2,5 +2,5 @@
2
2
  "name": "my-external-svc",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
- "dependencies": { "@palettelab/sdk": "^0.1.11", "react": "^19.0.0" }
5
+ "dependencies": { "@palettelab/sdk": "^0.1.12", "react": "^19.0.0" }
6
6
  }
@@ -9,7 +9,7 @@
9
9
  "description": "Demonstrates declared external_network access and a scoped per-org config token.",
10
10
  "icon": "CloudArrowUp",
11
11
  "gradient": { "bg": "linear-gradient(135deg, #10B981, #06B6D4)", "text": "#fff" },
12
- "sdk": { "frontend": "^0.1.11", "backend": "^0.1.0" },
12
+ "sdk": { "frontend": "^0.1.12", "backend": "^0.1.0" },
13
13
  "platform": { "min_version": "0.1.0" },
14
14
  "capabilities": {
15
15
  "frontend": true,
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.11",
6
+ "@palettelab/sdk": "^0.1.12",
7
7
  "react": "^19.0.0"
8
8
  }
9
9
  }
@@ -9,7 +9,7 @@
9
9
  "description": "A frontend-only plugin — renders inside the platform iframe sandbox with no backend.",
10
10
  "icon": "Puzzle",
11
11
  "gradient": { "bg": "linear-gradient(135deg, #6366F1, #8B5CF6)", "text": "#fff" },
12
- "sdk": { "frontend": "^0.1.11" },
12
+ "sdk": { "frontend": "^0.1.12" },
13
13
  "platform": { "min_version": "0.1.0" },
14
14
  "capabilities": {
15
15
  "frontend": true,
@@ -0,0 +1,40 @@
1
+ # Next-Compatible Palette Plugin
2
+
3
+ This template keeps config at `frontend/next.config.ts`.
4
+
5
+ Palette native apps still publish as a single React module, so `pltt` does not
6
+ run a full Next server. In `frontend.framework: "next"` mode the CLI reads the
7
+ Next config for supported native-bundle settings.
8
+
9
+ ## Files
10
+
11
+ ```text
12
+ frontend/
13
+ ├── next.config.ts
14
+ ├── tsconfig.json
15
+ └── src/
16
+ ├── index.tsx
17
+ └── translations.ts
18
+ ```
19
+
20
+ The manifest enables this mode:
21
+
22
+ ```json
23
+ {
24
+ "frontend": {
25
+ "entry": "./frontend/src/index.tsx",
26
+ "sandbox": true,
27
+ "framework": "next",
28
+ "config": "./frontend/next.config.ts"
29
+ }
30
+ }
31
+ ```
32
+
33
+ `pltt dev`, `pltt test`, `pltt package`, and `pltt publish` load
34
+ `next.config.ts/js/mjs/cjs`, apply `env` values plus `NEXT_PUBLIC_*`
35
+ environment variables to the native bundle, and honor aliases from
36
+ `frontend/tsconfig.json`.
37
+
38
+ Supported today: `env`, `NEXT_PUBLIC_*`, and TypeScript path aliases. Full Next
39
+ server features such as API routes, server components, middleware, image
40
+ optimization, and static export hosting are outside this native module mode.
@@ -0,0 +1,8 @@
1
+ const nextConfig = {
2
+ env: {
3
+ NEXT_PUBLIC_PLUGIN_FRAMEWORK: "next",
4
+ },
5
+ transpilePackages: ["@palettelab/sdk"],
6
+ }
7
+
8
+ export default nextConfig
@@ -0,0 +1,30 @@
1
+ "use client"
2
+
3
+ import { usePluginTranslations } from "@palettelab/sdk"
4
+ import type { PluginComponentProps } from "@palettelab/sdk"
5
+ import { translations } from "@/translations"
6
+
7
+ export default function NextCompatiblePlugin(_props: PluginComponentProps) {
8
+ const { t, language, setLanguage } = usePluginTranslations(translations)
9
+ const framework = process.env.NEXT_PUBLIC_PLUGIN_FRAMEWORK || "next"
10
+
11
+ return (
12
+ <main className="p-6 space-y-4">
13
+ <div className="flex items-start justify-between gap-3">
14
+ <div>
15
+ <h1 className="text-2xl font-bold">{t("title")}</h1>
16
+ <p className="mt-2 text-muted-foreground">{t("body")}</p>
17
+ </div>
18
+ <button
19
+ className="px-3 py-1.5 text-sm border rounded-md hover:bg-muted"
20
+ onClick={() => setLanguage(language === "ko" ? "en" : "ko")}
21
+ >
22
+ {language === "ko" ? "EN" : "KO"}
23
+ </button>
24
+ </div>
25
+ <p className="text-sm text-muted-foreground">
26
+ {t("framework", { framework })}
27
+ </p>
28
+ </main>
29
+ )
30
+ }
@@ -0,0 +1,14 @@
1
+ import type { TranslationResources } from "@palettelab/sdk"
2
+
3
+ export const translations = {
4
+ en: {
5
+ title: "Next-Compatible App",
6
+ body: "This app reads frontend/next.config.ts and follows the Palette OS language.",
7
+ framework: "Framework: {{framework}}",
8
+ },
9
+ ko: {
10
+ title: "Next 호환 앱",
11
+ body: "이 앱은 frontend/next.config.ts를 읽고 Palette OS 언어를 따릅니다.",
12
+ framework: "프레임워크: {{framework}}",
13
+ },
14
+ } satisfies TranslationResources
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["src/*"]
6
+ },
7
+ "jsx": "react-jsx",
8
+ "module": "esnext",
9
+ "moduleResolution": "bundler",
10
+ "target": "es2022",
11
+ "strict": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.tsx", "next.config.ts"]
15
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "my-next-plugin",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@palettelab/sdk": "^0.1.12",
7
+ "react": "^19.0.0"
8
+ },
9
+ "devDependencies": {
10
+ "typescript": "^5.0.0",
11
+ "@types/react": "^19.0.0"
12
+ }
13
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "manifest_version": "1",
3
+ "id": "my-next-plugin",
4
+ "name": "My Next Plugin",
5
+ "version": "1.0.0",
6
+ "developer": "Your Team",
7
+ "category": "Productivity",
8
+ "tagline": "A native Palette app with Next-compatible frontend config",
9
+ "description": "Uses frontend.framework=next so pltt reads frontend/next.config.ts while still publishing a native Palette module.",
10
+ "icon": "Puzzle",
11
+ "gradient": { "bg": "linear-gradient(135deg, #0F766E, #2563EB)", "text": "#fff" },
12
+ "sdk": { "frontend": "^0.1.12" },
13
+ "platform": { "min_version": "0.1.0" },
14
+ "capabilities": {
15
+ "frontend": true,
16
+ "backend": false,
17
+ "database": false,
18
+ "webhooks": false,
19
+ "scheduled_jobs": false,
20
+ "file_uploads": false,
21
+ "external_network": []
22
+ },
23
+ "frontend": {
24
+ "entry": "./frontend/src/index.tsx",
25
+ "sandbox": true,
26
+ "framework": "next",
27
+ "config": "./frontend/next.config.ts"
28
+ },
29
+ "permissions": []
30
+ }