@palettelab/cli 0.3.27 → 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.
- package/README.md +29 -0
- package/backend-sdk/palette_sdk/manifest.py +2 -0
- package/lib/bundler.js +238 -21
- package/lib/cli.js +1 -1
- package/lib/commands/dev.js +1 -1
- package/lib/commands/doctor.js +1 -1
- package/lib/commands/init.js +20 -5
- package/lib/commands/package.js +1 -1
- package/lib/commands/publish.js +1 -1
- package/lib/commands/test.js +7 -3
- package/lib/dev-simulator.js +4 -0
- package/lib/manifest.js +9 -1
- package/package.json +1 -1
- package/template-fallback/package.json +1 -1
- package/template-fallback/palette-plugin.json +1 -1
- package/template-fallback/templates/dashboard/package.json +1 -1
- package/template-fallback/templates/dashboard/palette-plugin.json +1 -1
- package/template-fallback/templates/database/package.json +1 -1
- package/template-fallback/templates/database/palette-plugin.json +1 -1
- package/template-fallback/templates/external-service/package.json +1 -1
- package/template-fallback/templates/external-service/palette-plugin.json +1 -1
- package/template-fallback/templates/frontend-only/package.json +1 -1
- package/template-fallback/templates/frontend-only/palette-plugin.json +1 -1
- package/template-fallback/templates/next/README.md +40 -0
- package/template-fallback/templates/next/frontend/next.config.ts +8 -0
- package/template-fallback/templates/next/frontend/src/index.tsx +30 -0
- package/template-fallback/templates/next/frontend/src/translations.ts +14 -0
- package/template-fallback/templates/next/frontend/tsconfig.json +15 -0
- package/template-fallback/templates/next/package.json +13 -0
- 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.
|
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) {
|
|
@@ -132,28 +323,54 @@ async function bundleBackend(pluginDir) {
|
|
|
132
323
|
pluginDir = path.resolve(pluginDir)
|
|
133
324
|
const { spawnSync } = require("child_process")
|
|
134
325
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-bundle-"))
|
|
326
|
+
const stage = path.join(tmp, "stage")
|
|
135
327
|
const outPath = path.join(tmp, "backend.tar.gz")
|
|
136
328
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
329
|
+
try {
|
|
330
|
+
fs.mkdirSync(stage, { recursive: true })
|
|
331
|
+
const copy = (from, to) => {
|
|
332
|
+
fs.cpSync(from, to, {
|
|
333
|
+
recursive: true,
|
|
334
|
+
filter(src) {
|
|
335
|
+
const parts = src.split(path.sep)
|
|
336
|
+
return !parts.includes("__pycache__") && !parts.includes(".venv")
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
}
|
|
143
340
|
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
341
|
+
const backendDir = path.join(pluginDir, "backend")
|
|
342
|
+
if (fs.existsSync(backendDir)) copy(backendDir, path.join(stage, "backend"))
|
|
343
|
+
for (const metadataFile of ["package.json", "pyproject.toml", "palette-plugin.json"]) {
|
|
344
|
+
const src = path.join(pluginDir, metadataFile)
|
|
345
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(stage, metadataFile))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const sdkDir = path.resolve(__dirname, "..", "backend-sdk", "palette_sdk")
|
|
349
|
+
const targetSdkDir = path.join(stage, "backend", "palette_sdk")
|
|
350
|
+
if (fs.existsSync(sdkDir) && fs.existsSync(path.join(stage, "backend"))) {
|
|
351
|
+
copy(sdkDir, targetSdkDir)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const includes = fs.readdirSync(stage)
|
|
355
|
+
const result = spawnSync("tar", ["-czf", outPath, ...includes], {
|
|
356
|
+
cwd: stage,
|
|
357
|
+
stdio: "pipe",
|
|
358
|
+
})
|
|
359
|
+
if (result.status !== 0) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`tar failed (exit ${result.status}): ${result.stderr?.toString() || "unknown error"}`,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
return fs.readFileSync(outPath)
|
|
365
|
+
} finally {
|
|
366
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
153
367
|
}
|
|
154
|
-
const buf = fs.readFileSync(outPath)
|
|
155
|
-
fs.rmSync(tmp, { recursive: true, force: true })
|
|
156
|
-
return buf
|
|
157
368
|
}
|
|
158
369
|
|
|
159
|
-
module.exports = {
|
|
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)")
|
package/lib/commands/dev.js
CHANGED
|
@@ -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: ${
|
package/lib/commands/doctor.js
CHANGED
|
@@ -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(
|
package/lib/commands/init.js
CHANGED
|
@@ -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
|
|
114
|
-
const
|
|
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.
|
|
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 ${
|
|
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`)
|
package/lib/commands/package.js
CHANGED
|
@@ -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
|
|
package/lib/commands/publish.js
CHANGED
|
@@ -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")
|
package/lib/commands/test.js
CHANGED
|
@@ -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:
|
|
503
|
-
{ re:
|
|
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)
|
package/lib/dev-simulator.js
CHANGED
|
@@ -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
|
@@ -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.
|
|
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,
|
|
@@ -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.
|
|
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,
|
|
@@ -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.
|
|
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,
|
|
@@ -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.
|
|
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,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,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
|
+
}
|