create-ampless 1.0.0-alpha.131 → 1.0.0-alpha.135
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/dist/index.js +148 -1
- package/dist/templates/_shared/app/(admin)/admin/_editor-bootstrap.tsx +5 -13
- package/dist/templates/_shared/app/(admin)/admin/preview/route.tsx +3 -1
- package/dist/templates/_shared/docs/plugin-author-guide.ja.md +60 -9
- package/dist/templates/_shared/docs/plugin-author-guide.md +86 -17
- package/dist/templates/_shared/package.json +14 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2156,6 +2156,116 @@ function detectIndent(jsonStr) {
|
|
|
2156
2156
|
if (!m) return 2;
|
|
2157
2157
|
return m[1].length;
|
|
2158
2158
|
}
|
|
2159
|
+
async function bumpUserAmplessPlugins(destDir, projectPkg, templatePkg) {
|
|
2160
|
+
const result = { bumped: [], warnings: [] };
|
|
2161
|
+
const deps = projectPkg.dependencies ?? {};
|
|
2162
|
+
const templateDeps = templatePkg.dependencies ?? {};
|
|
2163
|
+
for (const pkgName of Object.keys(deps)) {
|
|
2164
|
+
if (!pkgName.startsWith("@ampless/")) continue;
|
|
2165
|
+
if (pkgName in templateDeps) continue;
|
|
2166
|
+
let latest;
|
|
2167
|
+
try {
|
|
2168
|
+
const { stdout } = await execa4("npm", ["view", `${pkgName}@alpha`, "version"], {
|
|
2169
|
+
cwd: destDir,
|
|
2170
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2171
|
+
});
|
|
2172
|
+
latest = stdout.trim();
|
|
2173
|
+
} catch (e) {
|
|
2174
|
+
result.warnings.push(
|
|
2175
|
+
`${pkgName}: failed to resolve @alpha version (${e instanceof Error ? e.message : String(e)}). Leaving the existing pin in package.json.`
|
|
2176
|
+
);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
if (!latest) {
|
|
2180
|
+
result.warnings.push(`${pkgName}: npm view returned empty version. Skipping bump.`);
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
const target = `^${latest}`;
|
|
2184
|
+
const current = deps[pkgName];
|
|
2185
|
+
if (current === target) continue;
|
|
2186
|
+
deps[pkgName] = target;
|
|
2187
|
+
result.bumped.push({ name: pkgName, from: current, to: target });
|
|
2188
|
+
}
|
|
2189
|
+
return result;
|
|
2190
|
+
}
|
|
2191
|
+
function djb2Hash(s) {
|
|
2192
|
+
let h = 5381;
|
|
2193
|
+
for (let i = 0; i < s.length; i++) {
|
|
2194
|
+
h = h * 33 ^ s.charCodeAt(i) | 0;
|
|
2195
|
+
}
|
|
2196
|
+
return (h >>> 0).toString(16);
|
|
2197
|
+
}
|
|
2198
|
+
async function regenerateEditorBootstrap(destDir, projectPkg) {
|
|
2199
|
+
const targetPath = join3(destDir, "app", "(admin)", "admin", "_editor-bootstrap.tsx");
|
|
2200
|
+
const installedPlugins = [];
|
|
2201
|
+
const warnings = [];
|
|
2202
|
+
const deps = projectPkg.dependencies ?? {};
|
|
2203
|
+
for (const pkgName of Object.keys(deps)) {
|
|
2204
|
+
const pkgJsonPath = join3(destDir, "node_modules", pkgName, "package.json");
|
|
2205
|
+
if (!existsSync5(pkgJsonPath)) continue;
|
|
2206
|
+
let mf;
|
|
2207
|
+
try {
|
|
2208
|
+
mf = JSON.parse(await readFile3(pkgJsonPath, "utf-8"));
|
|
2209
|
+
} catch (e) {
|
|
2210
|
+
warnings.push(`${pkgName}: failed to read package.json (${e instanceof Error ? e.message : String(e)}). Skipping editor wiring.`);
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
const ep = mf?.amplessPlugin?.editorExports;
|
|
2214
|
+
if (typeof ep !== "string") continue;
|
|
2215
|
+
if (!ep.startsWith("./") || ep.includes("..") || ep.startsWith("/") || /[?#]/.test(ep)) {
|
|
2216
|
+
warnings.push(`${pkgName}: amplessPlugin.editorExports="${ep}" is invalid (must be a ./-prefixed relative subpath without .. or query/fragment). Skipping editor wiring.`);
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
if (!mf?.exports || typeof mf.exports[ep] === "undefined") {
|
|
2220
|
+
warnings.push(`${pkgName}: amplessPlugin.editorExports="${ep}" is not declared in package.json#exports. Skipping editor wiring.`);
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
installedPlugins.push({ packageName: pkgName, editorExports: ep });
|
|
2224
|
+
}
|
|
2225
|
+
installedPlugins.sort((a, b) => a.packageName.localeCompare(b.packageName));
|
|
2226
|
+
const lines = [];
|
|
2227
|
+
lines.push("'use client'", "");
|
|
2228
|
+
lines.push("// AUTO-GENERATED by `npm run update-ampless`. Do not edit \u2014 your changes");
|
|
2229
|
+
lines.push("// will be overwritten on the next run. To enable / disable a plugin's");
|
|
2230
|
+
lines.push("// editor extension, add / remove the corresponding @ampless/plugin-...");
|
|
2231
|
+
lines.push("// entry in your project's package.json (and register it in cms.config.ts");
|
|
2232
|
+
lines.push("// for runtime).", "");
|
|
2233
|
+
lines.push("import { installAdminEditorExtensions, installAdminTiptapNodeMarkdown } from '@ampless/admin/editor'");
|
|
2234
|
+
const identifiers = [];
|
|
2235
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2236
|
+
for (const p3 of installedPlugins) {
|
|
2237
|
+
let ident = "__" + p3.packageName.replace(/^@/, "").replace(/[^a-zA-Z0-9]+/g, "_") + "_editor";
|
|
2238
|
+
if (seen.has(ident) && seen.get(ident) !== p3.packageName) {
|
|
2239
|
+
const suffix = djb2Hash(p3.packageName).slice(0, 8);
|
|
2240
|
+
const orig = ident;
|
|
2241
|
+
ident = `${ident}_${suffix}`;
|
|
2242
|
+
warnings.push(`editor-bootstrap codegen: sanitised identifier "${orig}" collides between "${seen.get(orig)}" and "${p3.packageName}". Using "${ident}" for the latter to disambiguate.`);
|
|
2243
|
+
}
|
|
2244
|
+
seen.set(ident, p3.packageName);
|
|
2245
|
+
const importSubpath = p3.editorExports.startsWith("./") ? `${p3.packageName}/${p3.editorExports.slice(2)}` : `${p3.packageName}/${p3.editorExports}`;
|
|
2246
|
+
lines.push(`import * as ${ident} from '${importSubpath}'`);
|
|
2247
|
+
identifiers.push(ident);
|
|
2248
|
+
}
|
|
2249
|
+
lines.push("");
|
|
2250
|
+
lines.push("export function EditorBootstrap({ children }: { children: React.ReactNode }) {");
|
|
2251
|
+
if (identifiers.length === 0) {
|
|
2252
|
+
lines.push(" installAdminEditorExtensions([])");
|
|
2253
|
+
lines.push(" installAdminTiptapNodeMarkdown([])");
|
|
2254
|
+
} else {
|
|
2255
|
+
lines.push(" installAdminEditorExtensions([");
|
|
2256
|
+
for (const id of identifiers) lines.push(` ${id}.editorExtension,`);
|
|
2257
|
+
lines.push(" ])");
|
|
2258
|
+
lines.push(" installAdminTiptapNodeMarkdown([");
|
|
2259
|
+
for (const id of identifiers) lines.push(` ${id}.tiptapNodeToMarkdown ?? {},`);
|
|
2260
|
+
lines.push(" ])");
|
|
2261
|
+
}
|
|
2262
|
+
lines.push(" return <>{children}</>");
|
|
2263
|
+
lines.push("}");
|
|
2264
|
+
lines.push("");
|
|
2265
|
+
await mkdir2(dirname(targetPath), { recursive: true });
|
|
2266
|
+
await writeFile3(targetPath, lines.join("\n"), "utf-8");
|
|
2267
|
+
return { path: targetPath, pluginCount: installedPlugins.length, warnings };
|
|
2268
|
+
}
|
|
2159
2269
|
async function runUpgradeIn(destDir, sharedDir, opts = {}) {
|
|
2160
2270
|
const derivedRoot = opts.templatesRoot ?? join3(sharedDir, "..");
|
|
2161
2271
|
const themeSyncEnabled = existsSync5(join3(derivedRoot, "_shared"));
|
|
@@ -2269,11 +2379,46 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
|
|
|
2269
2379
|
projectPkg["scripts"] = projectScripts;
|
|
2270
2380
|
}
|
|
2271
2381
|
await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
|
|
2382
|
+
const templatePkgForBump = templatePkg;
|
|
2383
|
+
const projectPkgForBump = projectPkg;
|
|
2384
|
+
const userPluginBump = await bumpUserAmplessPlugins(destDir, projectPkgForBump, templatePkgForBump);
|
|
2385
|
+
if (userPluginBump.bumped.length > 0) {
|
|
2386
|
+
await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
|
|
2387
|
+
}
|
|
2272
2388
|
if (!opts.noInstall) {
|
|
2273
2389
|
const usePnpm = existsSync5(join3(destDir, "pnpm-lock.yaml"));
|
|
2274
2390
|
const pm = usePnpm ? "pnpm" : "npm";
|
|
2275
2391
|
await execa4(pm, ["install"], { cwd: destDir, stdio: "inherit" });
|
|
2276
2392
|
}
|
|
2393
|
+
const codegenProjectPkg = JSON.parse(
|
|
2394
|
+
await readFile3(join3(destDir, "package.json"), "utf-8")
|
|
2395
|
+
);
|
|
2396
|
+
const bootstrapResult = await regenerateEditorBootstrap(destDir, codegenProjectPkg);
|
|
2397
|
+
if (opts.noInstall) {
|
|
2398
|
+
bootstrapResult.warnings.push(
|
|
2399
|
+
"_editor-bootstrap.tsx was generated without running npm install \u2014 node_modules may be stale. Run `npm run update-ampless` (without --no-install) to pick up newly added plugin editor extensions."
|
|
2400
|
+
);
|
|
2401
|
+
if (userPluginBump.bumped.length > 0) {
|
|
2402
|
+
userPluginBump.warnings.push(
|
|
2403
|
+
`package.json was updated with ${userPluginBump.bumped.length} bumped @ampless/* plugin version(s) but npm install was skipped \u2014 run \`npm install\` (or \`npm run update-ampless\` without --no-install) to apply the new versions.`
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
for (const w of bootstrapResult.warnings) {
|
|
2408
|
+
log3.warn(w);
|
|
2409
|
+
}
|
|
2410
|
+
log3.info(
|
|
2411
|
+
`editor: ${pc3.cyan(`auto-wired ${bootstrapResult.pluginCount} editor extension(s) in _editor-bootstrap.tsx`)}`
|
|
2412
|
+
);
|
|
2413
|
+
if (userPluginBump.bumped.length > 0) {
|
|
2414
|
+
log3.info(`plugins: ${pc3.cyan(`bumped ${userPluginBump.bumped.length} user-installed @ampless/* plugin(s) to latest @alpha:`)}`);
|
|
2415
|
+
for (const { name, from, to } of userPluginBump.bumped) {
|
|
2416
|
+
log3.info(` ${name}: ${from} \u2192 ${to}`);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
for (const w of userPluginBump.warnings) {
|
|
2420
|
+
log3.warn(w);
|
|
2421
|
+
}
|
|
2277
2422
|
return {
|
|
2278
2423
|
added: replaceNew,
|
|
2279
2424
|
updated: replaceUpdate,
|
|
@@ -2283,7 +2428,9 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
|
|
|
2283
2428
|
themesPreserved: themeResult.preserved,
|
|
2284
2429
|
themesQuarantined: themeResult.quarantined,
|
|
2285
2430
|
packageJsonMerged: true,
|
|
2286
|
-
obsoleteRemoved: obsoleteFiles
|
|
2431
|
+
obsoleteRemoved: obsoleteFiles,
|
|
2432
|
+
editorExtensionsWired: bootstrapResult.pluginCount,
|
|
2433
|
+
userPluginsBumped: userPluginBump.bumped.length
|
|
2287
2434
|
};
|
|
2288
2435
|
}
|
|
2289
2436
|
async function runUpgrade(args) {
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Wired into the admin layout via:
|
|
9
|
-
// createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
|
|
3
|
+
// Placeholder for fresh scaffolds before `update-ampless` runs.
|
|
4
|
+
// `npm run update-ampless` rewrites this file with auto-wired plugin
|
|
5
|
+
// editor extensions based on the project's installed `@ampless/plugin-*`
|
|
6
|
+
// packages.
|
|
10
7
|
|
|
11
8
|
import { installAdminEditorExtensions } from '@ampless/admin/editor'
|
|
12
|
-
// import { youtubeEditor } from '@ampless/plugin-youtube/editor'
|
|
13
|
-
// import { tweetEditor } from '@ampless/plugin-x-embed/editor'
|
|
14
9
|
|
|
15
10
|
export function EditorBootstrap({ children }: { children: React.ReactNode }) {
|
|
16
|
-
installAdminEditorExtensions([
|
|
17
|
-
// youtubeEditor.extension,
|
|
18
|
-
// tweetEditor.extension,
|
|
19
|
-
])
|
|
11
|
+
installAdminEditorExtensions([])
|
|
20
12
|
return <>{children}</>
|
|
21
13
|
}
|
|
@@ -6,7 +6,9 @@ import { admin } from '@/lib/admin'
|
|
|
6
6
|
* POST a draft Post to this endpoint while the preview tab is open; we
|
|
7
7
|
* render the body + page-level scripts via `ampless.renderBody` /
|
|
8
8
|
* `publicPostScriptsForPage` and return a fully-rendered HTML string
|
|
9
|
-
* that the admin shows in an iframe (`sandbox="allow-scripts"
|
|
9
|
+
* that the admin shows in an iframe (`sandbox="allow-scripts allow-same-origin"`,
|
|
10
|
+
* v1 trust boundary expansion — admin preview content / plugin script are
|
|
11
|
+
* explicitly treated as trusted; see the iframe comment in post-form.tsx).
|
|
10
12
|
*
|
|
11
13
|
* Why a Route Handler instead of a Server Action: Next.js 15+
|
|
12
14
|
* refuses to compile Client Components that reach `react-dom/server`
|
|
@@ -777,25 +777,67 @@ runtime は `src` のホスト allowlist を強制しない。CSP はサイト
|
|
|
777
777
|
|
|
778
778
|
## 6d. Admin エディタ拡張の配線 (Phase 7)
|
|
779
779
|
|
|
780
|
-
admin エディタに tiptap Node 拡張を提供するプラグインは、別途 `./editor` subpath の client-side
|
|
780
|
+
admin エディタに tiptap Node 拡張を提供するプラグインは、別途 `./editor` subpath の client-side エントリを出荷する。
|
|
781
|
+
`app/(admin)/admin/_editor-bootstrap.tsx` は `npm run update-ampless` によって**自動生成**される — 手動で編集してはならない。
|
|
782
|
+
|
|
783
|
+
### プラグイン著者向け
|
|
784
|
+
|
|
785
|
+
`package.json#amplessPlugin` に `editorExports` を宣言する:
|
|
786
|
+
|
|
787
|
+
```jsonc
|
|
788
|
+
// packages/plugin-youtube/package.json
|
|
789
|
+
"amplessPlugin": {
|
|
790
|
+
"apiVersion": 1,
|
|
791
|
+
"name": "youtube",
|
|
792
|
+
"trustLevel": "trusted",
|
|
793
|
+
"capabilities": ["contentFields"],
|
|
794
|
+
"editorExports": "./editor" // ← editor module のある subpath
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
その subpath から `editorExtension` を named export する:
|
|
799
|
+
|
|
800
|
+
```ts
|
|
801
|
+
// packages/plugin-youtube/src/editor.tsx
|
|
802
|
+
export const editorExtension = AmplessYoutubeNode // tiptap Node または Extension
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
`package.json#exports` にも同 subpath を宣言すること(codegen がこれを検証する — 未宣言の場合は warning を出してそのプラグインをスキップする):
|
|
806
|
+
|
|
807
|
+
```jsonc
|
|
808
|
+
"exports": {
|
|
809
|
+
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
|
|
810
|
+
"./editor": { "import": "./dist/editor.js", "types": "./dist/editor.d.ts" }
|
|
811
|
+
}
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### プラグインユーザー(サイトエンジニア)向け
|
|
815
|
+
|
|
816
|
+
1. `npm i @ampless/plugin-youtube@alpha` — plugin を dependency として追加する。
|
|
817
|
+
2. `cms.config.ts` に登録してサーバーサイド renderer を有効化する。
|
|
818
|
+
3. `npm run update-ampless` — インストール済み plugin の manifest から `_editor-bootstrap.tsx` を自動再生成する。
|
|
819
|
+
|
|
820
|
+
生成されたファイルはリポジトリにコミットされ、以下のような内容になる:
|
|
781
821
|
|
|
782
822
|
```tsx
|
|
783
|
-
//
|
|
823
|
+
// app/(admin)/admin/_editor-bootstrap.tsx (AUTO-GENERATED — 編集不可)
|
|
784
824
|
'use client'
|
|
825
|
+
// AUTO-GENERATED by `npm run update-ampless`. Do not edit — your changes
|
|
826
|
+
// will be overwritten on the next run.
|
|
785
827
|
import { installAdminEditorExtensions } from '@ampless/admin/editor'
|
|
786
|
-
import {
|
|
787
|
-
import {
|
|
828
|
+
import { editorExtension as __ampless_plugin_x_embed_editor } from '@ampless/plugin-x-embed/editor'
|
|
829
|
+
import { editorExtension as __ampless_plugin_youtube_editor } from '@ampless/plugin-youtube/editor'
|
|
788
830
|
|
|
789
831
|
export function EditorBootstrap({ children }: { children: React.ReactNode }) {
|
|
790
832
|
installAdminEditorExtensions([
|
|
791
|
-
|
|
792
|
-
|
|
833
|
+
__ampless_plugin_x_embed_editor,
|
|
834
|
+
__ampless_plugin_youtube_editor,
|
|
793
835
|
])
|
|
794
836
|
return <>{children}</>
|
|
795
837
|
}
|
|
796
838
|
```
|
|
797
839
|
|
|
798
|
-
|
|
840
|
+
このファイルは admin layout に渡される(テンプレートに既に組み込まれているので追加の作業は不要):
|
|
799
841
|
|
|
800
842
|
```tsx
|
|
801
843
|
// templates/_shared/app/(admin)/admin/layout.tsx
|
|
@@ -806,9 +848,16 @@ export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
|
|
|
806
848
|
|
|
807
849
|
`installAdminEditorExtensions` は idempotent で、render 時に client component 内で呼ばれる。admin の `<TiptapEditor>` は毎回 render 時に登録済みリストを built-in extensions の末尾に spread する。
|
|
808
850
|
|
|
851
|
+
### active source と完全無効化
|
|
852
|
+
|
|
853
|
+
**エディタ配線の active source は `package.json#dependencies`**(= `node_modules`)であり、`cms.config.ts` ではない。
|
|
854
|
+
|
|
855
|
+
- plugin を `cms.config.ts` から外しても `package.json` に dep が残っていれば、editor の paste rule は**有効のまま**になる。editor は tiptap document にそのノード型を挿入できるが、公開 renderer はレンダリングしない(不整合状態 — 避けること)。
|
|
856
|
+
- 完全に無効化するには: `cms.config.ts` から削除 **かつ** `npm uninstall @ampless/plugin-...` を実行し、`npm run update-ampless` で bootstrap ファイルを再生成する。
|
|
857
|
+
|
|
809
858
|
### エディタプレビューのパイプライン
|
|
810
859
|
|
|
811
|
-
admin の edit / new post フォームは preview ペインを `<iframe sandbox="allow-scripts">` で表示する。`srcDoc` はテンプレート側 preview Route Handler が返す HTML。テンプレート scaffold は handler を `app/(admin)/admin/preview/route.tsx` に同梱する:
|
|
860
|
+
admin の edit / new post フォームは preview ペインを `<iframe sandbox="allow-scripts allow-same-origin">` で表示する。`srcDoc` はテンプレート側 preview Route Handler が返す HTML。テンプレート scaffold は handler を `app/(admin)/admin/preview/route.tsx` に同梱する:
|
|
812
861
|
|
|
813
862
|
```tsx
|
|
814
863
|
// templates/_shared/app/(admin)/admin/preview/route.tsx
|
|
@@ -862,7 +911,9 @@ export default createEditPostPage(admin, { previewEndpoint: '/cms/admin/preview'
|
|
|
862
911
|
|
|
863
912
|
Server Action ではなく Route Handler を採用した理由: `'use server'` モジュール内部で `react-dom/server` 由来の rendering を行うと、Next.js 15+ が edit-post page の compile に失敗する。build 時に Client Component から Server Action モジュール経由で import graph を辿るため、その経路上で `react-dom/server` に到達した時点で「You're importing a component that imports react-dom/server」チェックが発火する。Route Handler に切り出すことで rendering を import graph から完全に切り離せる — `<PostForm>` は plain な HTTP endpoint を fetch するだけで、bundler は form から handler 側へ踏み込まない。さらに handler 自身が `admin.isEditor()` を明示的にチェックすることで、将来 `(admin)` route-group gate が誤設定された場合の安全側のフェールセーフになる。`react-dom/server` の import 自体を dynamic にしているのは Next.js 16 の Turbopack が、Route Handler を含む app router build 経路で reach できる top-level の `react-dom/server` static import を一律で flag するため。request 時に解決することで build-time import-graph walker から外しつつ、runtime では同じ Node.js subpath からロードする。
|
|
864
913
|
|
|
865
|
-
iframe
|
|
914
|
+
**Preview iframe sandbox — v1 trust boundary 拡張:** iframe は `sandbox="allow-scripts allow-same-origin"` を使う。`srcDoc` とともに使用すると iframe は admin の origin を継承し、サードパーティ embed widget(YouTube SDK、x.com `widgets.js`)が動作できるようになる — opaque-origin(`allow-scripts` のみ)の iframe では動作を拒否する(非 HttpOnly storage / cache へのアクセスと real-origin requests が必要なため)。同時に same-origin により preview スクリプトは admin の auth state / 非 HttpOnly storage / DOM へのアクセスと authenticated same-origin XHR / fetch の発行が可能になる。
|
|
915
|
+
|
|
916
|
+
これは単なる sandbox 緩和ではなく、**v1 の明示的な設計判断**です: **ampless v1 は admin preview content / plugin script を全部 trusted とみなす**。エンジニアは npm install 前に plugin を審査するし(カスタマイズベース CMS モデル)、body content はこのサイトの trusted editor が作成する。`<PostHistoryPanel>` では別の editor が作成した過去 revision(revision author ≠ preview viewer)をプレビューするケースも通常発生するが、v1 ではそれも明示的に trust ring 内と位置づける。より安全な代替案(別 origin preview route + CSP / COEP / COOP)は v2.0+ でリアルなプラグインマーケットプレイスが必要になった場合に再検討する。
|
|
866
917
|
|
|
867
918
|
---
|
|
868
919
|
|
|
@@ -1026,26 +1026,72 @@ middleware. This is documented in each plugin's README.
|
|
|
1026
1026
|
## 6d. Admin editor extension wiring (Phase 7)
|
|
1027
1027
|
|
|
1028
1028
|
Plugins that contribute tiptap Node extensions to the admin editor
|
|
1029
|
-
ship a separate client-side entry under the `./editor` subpath.
|
|
1030
|
-
|
|
1029
|
+
ship a separate client-side entry under the `./editor` subpath.
|
|
1030
|
+
`app/(admin)/admin/_editor-bootstrap.tsx` is **auto-generated** by
|
|
1031
|
+
`npm run update-ampless` — you should not edit it by hand.
|
|
1032
|
+
|
|
1033
|
+
### For plugin authors
|
|
1034
|
+
|
|
1035
|
+
Declare `editorExports` in `package.json#amplessPlugin`:
|
|
1036
|
+
|
|
1037
|
+
```jsonc
|
|
1038
|
+
// packages/plugin-youtube/package.json
|
|
1039
|
+
"amplessPlugin": {
|
|
1040
|
+
"apiVersion": 1,
|
|
1041
|
+
"name": "youtube",
|
|
1042
|
+
"trustLevel": "trusted",
|
|
1043
|
+
"capabilities": ["contentFields"],
|
|
1044
|
+
"editorExports": "./editor" // ← subpath where the editor module lives
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
Export `editorExtension` as a named symbol from that subpath:
|
|
1049
|
+
|
|
1050
|
+
```ts
|
|
1051
|
+
// packages/plugin-youtube/src/editor.tsx
|
|
1052
|
+
export const editorExtension = AmplessYoutubeNode // tiptap Node or Extension
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
The subpath must be declared in `package.json#exports` as well (the
|
|
1056
|
+
codegen validates this — a missing `exports` entry triggers a warning
|
|
1057
|
+
and the plugin is skipped):
|
|
1058
|
+
|
|
1059
|
+
```jsonc
|
|
1060
|
+
"exports": {
|
|
1061
|
+
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
|
|
1062
|
+
"./editor": { "import": "./dist/editor.js", "types": "./dist/editor.d.ts" }
|
|
1063
|
+
}
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### For plugin users (site engineers)
|
|
1067
|
+
|
|
1068
|
+
1. `npm i @ampless/plugin-youtube@alpha` — add the plugin as a dependency.
|
|
1069
|
+
2. Register it in `cms.config.ts` for the server-side renderer.
|
|
1070
|
+
3. `npm run update-ampless` — regenerates `_editor-bootstrap.tsx`
|
|
1071
|
+
automatically from the installed plugin manifests.
|
|
1072
|
+
|
|
1073
|
+
The generated file is committed to your repo and looks like:
|
|
1031
1074
|
|
|
1032
1075
|
```tsx
|
|
1033
|
-
//
|
|
1076
|
+
// app/(admin)/admin/_editor-bootstrap.tsx (AUTO-GENERATED — do not edit)
|
|
1034
1077
|
'use client'
|
|
1078
|
+
// AUTO-GENERATED by `npm run update-ampless`. Do not edit — your changes
|
|
1079
|
+
// will be overwritten on the next run.
|
|
1035
1080
|
import { installAdminEditorExtensions } from '@ampless/admin/editor'
|
|
1036
|
-
import {
|
|
1037
|
-
import {
|
|
1081
|
+
import { editorExtension as __ampless_plugin_x_embed_editor } from '@ampless/plugin-x-embed/editor'
|
|
1082
|
+
import { editorExtension as __ampless_plugin_youtube_editor } from '@ampless/plugin-youtube/editor'
|
|
1038
1083
|
|
|
1039
1084
|
export function EditorBootstrap({ children }: { children: React.ReactNode }) {
|
|
1040
1085
|
installAdminEditorExtensions([
|
|
1041
|
-
|
|
1042
|
-
|
|
1086
|
+
__ampless_plugin_x_embed_editor,
|
|
1087
|
+
__ampless_plugin_youtube_editor,
|
|
1043
1088
|
])
|
|
1044
1089
|
return <>{children}</>
|
|
1045
1090
|
}
|
|
1046
1091
|
```
|
|
1047
1092
|
|
|
1048
|
-
|
|
1093
|
+
The file is then wired into the admin layout (already in the template — no
|
|
1094
|
+
extra work required):
|
|
1049
1095
|
|
|
1050
1096
|
```tsx
|
|
1051
1097
|
// templates/_shared/app/(admin)/admin/layout.tsx
|
|
@@ -1058,10 +1104,23 @@ export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
|
|
|
1058
1104
|
inside a client component. The admin's `<TiptapEditor>` spreads the
|
|
1059
1105
|
registered list onto its built-in extensions on every render.
|
|
1060
1106
|
|
|
1107
|
+
### Active source and full disable
|
|
1108
|
+
|
|
1109
|
+
**The active source for editor wiring is `package.json#dependencies`**
|
|
1110
|
+
(= `node_modules`), not `cms.config.ts`. This means:
|
|
1111
|
+
|
|
1112
|
+
- Removing a plugin from `cms.config.ts` but keeping it in
|
|
1113
|
+
`package.json` leaves the editor paste rule active. The editor can
|
|
1114
|
+
still insert the node type into the tiptap document, but the public
|
|
1115
|
+
renderer won't render it (inconsistent state — avoid).
|
|
1116
|
+
- To fully disable a plugin's editor extension: remove it from
|
|
1117
|
+
`cms.config.ts` **and** run `npm uninstall @ampless/plugin-...`,
|
|
1118
|
+
then `npm run update-ampless` to regenerate the bootstrap file.
|
|
1119
|
+
|
|
1061
1120
|
### Editor preview pipeline
|
|
1062
1121
|
|
|
1063
1122
|
The admin's edit / new post forms render the preview pane in an
|
|
1064
|
-
`<iframe sandbox="allow-scripts">` whose `srcDoc` is the HTML returned
|
|
1123
|
+
`<iframe sandbox="allow-scripts allow-same-origin">` whose `srcDoc` is the HTML returned
|
|
1065
1124
|
by the template's preview Route Handler. The template scaffold ships
|
|
1066
1125
|
the handler at `app/(admin)/admin/preview/route.tsx`:
|
|
1067
1126
|
|
|
@@ -1136,14 +1195,24 @@ included; deferring it to request time keeps the module outside the
|
|
|
1136
1195
|
build-time import-graph walker while still loading it from the same
|
|
1137
1196
|
Node.js subpath at runtime.
|
|
1138
1197
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1198
|
+
**Preview iframe sandbox — v1 trust boundary expansion:** The iframe uses
|
|
1199
|
+
`sandbox="allow-scripts allow-same-origin"`. With `srcDoc`, this gives
|
|
1200
|
+
the iframe the admin's origin, which 3rd-party embed widgets (YouTube SDK,
|
|
1201
|
+
x.com `widgets.js`) require — they refuse to initialise in an opaque-origin
|
|
1202
|
+
(`allow-scripts`-only) iframe because they need access to non-HttpOnly
|
|
1203
|
+
storage / cache and real-origin requests. Same-origin also gives the
|
|
1204
|
+
preview script access to the admin's auth state / non-HttpOnly storage /
|
|
1205
|
+
DOM and lets it issue authenticated same-origin XHR / fetch.
|
|
1206
|
+
|
|
1207
|
+
This is an explicit v1 design decision, not a no-op sandbox relax:
|
|
1208
|
+
**ampless v1 treats admin preview content / plugin script as trusted**.
|
|
1209
|
+
The engineer audits plugins before npm-installing them (customization-based
|
|
1210
|
+
CMS model), and body content is produced by trusted editors of this site.
|
|
1211
|
+
`<PostHistoryPanel>` can also surface a past revision authored by a
|
|
1212
|
+
different editor (revision author ≠ preview viewer) — v1 explicitly puts
|
|
1213
|
+
both inside the trust ring. The safer alternative — separate-origin preview
|
|
1214
|
+
route + CSP / COEP / COOP — is parked for v2.0+ if/when a real plugin
|
|
1215
|
+
marketplace lands.
|
|
1147
1216
|
|
|
1148
1217
|
---
|
|
1149
1218
|
|
|
@@ -26,21 +26,21 @@
|
|
|
26
26
|
"@tiptap/pm": "^3.23.6",
|
|
27
27
|
"@tiptap/react": "^3.23.6",
|
|
28
28
|
"@tiptap/starter-kit": "^3.23.6",
|
|
29
|
-
"@ampless/plugin-analytics-ga4": "^0.2.0-alpha.
|
|
30
|
-
"@ampless/plugin-cookie-consent": "^0.1.0-alpha.
|
|
31
|
-
"@ampless/plugin-gtm": "^0.2.0-alpha.
|
|
32
|
-
"@ampless/plugin-og-image": "^0.2.0-alpha.
|
|
33
|
-
"@ampless/plugin-plausible": "^0.2.0-alpha.
|
|
34
|
-
"@ampless/plugin-reading-time": "^0.1.0-alpha.
|
|
35
|
-
"@ampless/plugin-rss": "^0.2.0-alpha.
|
|
36
|
-
"@ampless/plugin-schema-jsonld": "^0.1.1-alpha.
|
|
37
|
-
"@ampless/plugin-seo": "^0.2.0-alpha.
|
|
38
|
-
"@ampless/plugin-webhook": "^0.2.0-alpha.
|
|
39
|
-
"@ampless/admin": "^1.0.0-alpha.
|
|
40
|
-
"@ampless/backend": "^1.0.0-alpha.
|
|
41
|
-
"@ampless/runtime": "^1.0.0-alpha.
|
|
29
|
+
"@ampless/plugin-analytics-ga4": "^0.2.0-alpha.29",
|
|
30
|
+
"@ampless/plugin-cookie-consent": "^0.1.0-alpha.20",
|
|
31
|
+
"@ampless/plugin-gtm": "^0.2.0-alpha.28",
|
|
32
|
+
"@ampless/plugin-og-image": "^0.2.0-alpha.45",
|
|
33
|
+
"@ampless/plugin-plausible": "^0.2.0-alpha.28",
|
|
34
|
+
"@ampless/plugin-reading-time": "^0.1.0-alpha.18",
|
|
35
|
+
"@ampless/plugin-rss": "^0.2.0-alpha.45",
|
|
36
|
+
"@ampless/plugin-schema-jsonld": "^0.1.1-alpha.24",
|
|
37
|
+
"@ampless/plugin-seo": "^0.2.0-alpha.45",
|
|
38
|
+
"@ampless/plugin-webhook": "^0.2.0-alpha.46",
|
|
39
|
+
"@ampless/admin": "^1.0.0-alpha.78",
|
|
40
|
+
"@ampless/backend": "^1.0.0-alpha.67",
|
|
41
|
+
"@ampless/runtime": "^1.0.0-alpha.54",
|
|
42
42
|
"@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
|
|
43
|
-
"ampless": "^1.0.0-alpha.
|
|
43
|
+
"ampless": "^1.0.0-alpha.45",
|
|
44
44
|
"aws-amplify": "^6.17.0",
|
|
45
45
|
"class-variance-authority": "^0.7.1",
|
|
46
46
|
"clsx": "^2.1.1",
|