@pantheon-systems/p1-next-sdk 0.6.0 → 0.8.0
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/bin/lib/cli.js +171 -0
- package/bin/lib/detect.js +179 -0
- package/bin/lib/fs-ops.js +32 -0
- package/bin/lib/git.js +60 -0
- package/bin/lib/messages.js +65 -0
- package/bin/lib/transform.js +155 -0
- package/bin/p1-migrate.js +5 -0
- package/dist/editor-paths.d.ts +10 -0
- package/dist/editor-paths.d.ts.map +1 -0
- package/dist/editor-paths.js +38 -0
- package/dist/editor-paths.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pages-handler.d.ts +43 -13
- package/dist/pages-handler.d.ts.map +1 -1
- package/dist/pages-handler.js +31 -23
- package/dist/pages-handler.js.map +1 -1
- package/package.json +9 -7
package/bin/lib/cli.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codemod orchestration.
|
|
3
|
+
*
|
|
4
|
+
* `migrate()` is pure-ish and testable (takes a dir, returns a result, throws
|
|
5
|
+
* BailError on unrecognized input). `runCLI()` wraps it with argv parsing and
|
|
6
|
+
* process exit codes for the bin entrypoint.
|
|
7
|
+
*
|
|
8
|
+
* All transforms run before any write, so a bail leaves the tree untouched —
|
|
9
|
+
* the app is never left half-migrated.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join, relative } from "node:path";
|
|
13
|
+
import { detectApp, assertSuiteVersions, isRouteSpecial } from "./detect.js";
|
|
14
|
+
import { assertCleanTree } from "./git.js";
|
|
15
|
+
import * as fsops from "./fs-ops.js";
|
|
16
|
+
import * as msg from "./messages.js";
|
|
17
|
+
import {
|
|
18
|
+
BailError,
|
|
19
|
+
rewriteEditorClient,
|
|
20
|
+
splitPageFile,
|
|
21
|
+
buildLayoutFile,
|
|
22
|
+
} from "./transform.js";
|
|
23
|
+
|
|
24
|
+
const show = (dir, target) => relative(dir, target) || target;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The codemod's only irreversible act is removing the old catch-all, so when it
|
|
28
|
+
* holds anything else the bail has to be worth reading: name every file, and
|
|
29
|
+
* separate the ones that just need moving from the ones whose destination is a
|
|
30
|
+
* real decision.
|
|
31
|
+
*/
|
|
32
|
+
function extraFilesMessage({ catchAll, extras, p1Dir }, dir) {
|
|
33
|
+
const special = extras.filter(isRouteSpecial);
|
|
34
|
+
const plain = extras.filter((entry) => !isRouteSpecial(entry));
|
|
35
|
+
const group = show(dir, join(p1Dir, "(editor)"));
|
|
36
|
+
const lines = [
|
|
37
|
+
`${show(dir, catchAll)} contains files this codemod does not know how to move:`,
|
|
38
|
+
];
|
|
39
|
+
if (plain.length > 0) {
|
|
40
|
+
lines.push(
|
|
41
|
+
"",
|
|
42
|
+
...plain.map((entry) => ` ${entry}`),
|
|
43
|
+
`Move these into ${join(group, "[[...p1]]")}/ and add one ../ to each parent-relative import.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (special.length > 0) {
|
|
47
|
+
lines.push(
|
|
48
|
+
"",
|
|
49
|
+
...special.map((entry) => ` ${entry}`),
|
|
50
|
+
"These wrap the route segment. The editor now renders from " +
|
|
51
|
+
`${join(group, "layout.tsx")}, one level up, so they likely belong beside it in ${group}/.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
lines.push(
|
|
55
|
+
"",
|
|
56
|
+
"Move them, then re-run — the page split, import depth, and layout are still handled for you.",
|
|
57
|
+
);
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function partialMessage({ catchAll, p1Dir }, dir) {
|
|
62
|
+
return (
|
|
63
|
+
`Both ${show(dir, join(p1Dir, "(editor)"))} and ${show(dir, catchAll)} exist, so an ` +
|
|
64
|
+
"earlier run was interrupted before it removed the old route. Check which files you " +
|
|
65
|
+
`want to keep, delete ${show(dir, catchAll)}, then re-run.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function migrate(opts = {}) {
|
|
70
|
+
const dir = opts.dir ?? process.cwd();
|
|
71
|
+
const force = opts.force ?? false;
|
|
72
|
+
const dryRun = opts.dryRun ?? false;
|
|
73
|
+
|
|
74
|
+
const app = detectApp(dir);
|
|
75
|
+
if (app.status === "already-migrated") {
|
|
76
|
+
msg.alreadyMigrated();
|
|
77
|
+
return { changed: false };
|
|
78
|
+
}
|
|
79
|
+
if (app.status === "not-found") {
|
|
80
|
+
throw new BailError(
|
|
81
|
+
`Could not find app/p1/[[...p1]]/page.tsx under ${dir}. Run this from your project root.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (app.status === "partial") {
|
|
85
|
+
throw new BailError(partialMessage(app, dir));
|
|
86
|
+
}
|
|
87
|
+
if (app.status === "extra-files") {
|
|
88
|
+
throw new BailError(extraFilesMessage(app, dir));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Runs on --dry-run too: a plan the installed suite cannot render is not a
|
|
92
|
+
// plan worth previewing.
|
|
93
|
+
if (assertSuiteVersions(dir).status === "unverified") msg.versionsUnverified();
|
|
94
|
+
|
|
95
|
+
if (!force && !dryRun && assertCleanTree(dir).status === "no-repo") msg.noGitRepo();
|
|
96
|
+
|
|
97
|
+
const { catchAll, p1Dir } = app;
|
|
98
|
+
const editorGroup = join(p1Dir, "(editor)");
|
|
99
|
+
const newCatchAll = join(editorGroup, "[[...p1]]");
|
|
100
|
+
|
|
101
|
+
// Transform everything up front — any bail happens before we touch disk.
|
|
102
|
+
const newEditorClient = rewriteEditorClient(fsops.read(join(catchAll, "editor-client.tsx")));
|
|
103
|
+
const { p1Pages, page } = splitPageFile(fsops.read(join(catchAll, "page.tsx")));
|
|
104
|
+
const layout = buildLayoutFile();
|
|
105
|
+
|
|
106
|
+
const writes = [
|
|
107
|
+
[join(newCatchAll, "editor-client.tsx"), newEditorClient],
|
|
108
|
+
[join(newCatchAll, "p1-pages.tsx"), p1Pages],
|
|
109
|
+
[join(newCatchAll, "page.tsx"), page],
|
|
110
|
+
[join(editorGroup, "layout.tsx"), layout],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const [path] of writes) fsops.assertWithin(dir, path);
|
|
114
|
+
fsops.assertWithin(dir, catchAll);
|
|
115
|
+
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
msg.dryRunPlan(writes.map(([path]) => path), catchAll);
|
|
118
|
+
return { changed: false, dryRun: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fsops.ensureDir(newCatchAll);
|
|
122
|
+
for (const [path, content] of writes) fsops.write(path, content);
|
|
123
|
+
fsops.removeDir(catchAll);
|
|
124
|
+
|
|
125
|
+
msg.success(writes.map(([path]) => path), catchAll);
|
|
126
|
+
return { changed: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Strict on purpose: a silently-ignored `--dryrun` would run the real migration,
|
|
131
|
+
* and a `--dir` given with a space would migrate the current directory instead
|
|
132
|
+
* of the one the user named.
|
|
133
|
+
*/
|
|
134
|
+
export function parseArgs(argv) {
|
|
135
|
+
const opts = { dir: process.cwd(), force: false, dryRun: false, help: false };
|
|
136
|
+
for (const arg of argv) {
|
|
137
|
+
if (arg === "--force" || arg === "-f") opts.force = true;
|
|
138
|
+
else if (arg === "--dry-run") opts.dryRun = true;
|
|
139
|
+
else if (arg === "--help" || arg === "-h") opts.help = true;
|
|
140
|
+
else if (arg.startsWith("--dir=")) opts.dir = arg.slice("--dir=".length);
|
|
141
|
+
else {
|
|
142
|
+
throw new BailError(
|
|
143
|
+
`Unrecognized argument: ${arg}. Run with --help to see the supported options.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return opts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function runCLI(argv = process.argv.slice(2)) {
|
|
151
|
+
let opts;
|
|
152
|
+
try {
|
|
153
|
+
opts = parseArgs(argv);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (err instanceof BailError) msg.bail(err.message);
|
|
156
|
+
else msg.unexpected(err);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (opts.help) {
|
|
161
|
+
msg.help();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await migrate(opts);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err instanceof BailError) msg.bail(err.message);
|
|
168
|
+
else msg.unexpected(err);
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify a consumer project's editor route so the codemod knows whether to
|
|
3
|
+
* run, skip (already migrated), or bail (unrecognized), and verify the installed
|
|
4
|
+
* package suite is new enough for the shape this codemod writes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { BailError } from "./transform.js";
|
|
10
|
+
|
|
11
|
+
/** The only two files the codemod knows how to transform and carry across. */
|
|
12
|
+
const MOVABLE = ["page.tsx", "editor-client.tsx"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* App Router files whose behavior depends on where they sit. The editor moves
|
|
16
|
+
* up into `(editor)/layout.tsx`, so these stop wrapping it even when relocated
|
|
17
|
+
* faithfully — the destination is a judgment call the codemod should not make.
|
|
18
|
+
*/
|
|
19
|
+
const ROUTE_SPECIAL = new Set([
|
|
20
|
+
"layout",
|
|
21
|
+
"template",
|
|
22
|
+
"error",
|
|
23
|
+
"global-error",
|
|
24
|
+
"loading",
|
|
25
|
+
"not-found",
|
|
26
|
+
"default",
|
|
27
|
+
"route",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function isRouteSpecial(entry) {
|
|
31
|
+
return ROUTE_SPECIAL.has(entry.replace(/\.(tsx|ts|jsx|js)$/, ""));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extraEntries(catchAll) {
|
|
35
|
+
return readdirSync(catchAll, { withFileTypes: true })
|
|
36
|
+
.filter((entry) => !MOVABLE.includes(entry.name))
|
|
37
|
+
.map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
|
|
38
|
+
.sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function detectApp(dir) {
|
|
42
|
+
const p1Dir = [join(dir, "app", "p1"), join(dir, "src", "app", "p1")].find((p) =>
|
|
43
|
+
existsSync(p),
|
|
44
|
+
);
|
|
45
|
+
if (!p1Dir) return { status: "not-found" };
|
|
46
|
+
|
|
47
|
+
const catchAll = join(p1Dir, "[[...p1]]");
|
|
48
|
+
|
|
49
|
+
if (existsSync(join(p1Dir, "(editor)"))) {
|
|
50
|
+
// Both trees present means a previous run died between the writes and the
|
|
51
|
+
// cleanup; calling that "already migrated" would strand the old route.
|
|
52
|
+
if (existsSync(catchAll)) return { status: "partial", p1Dir, catchAll };
|
|
53
|
+
return { status: "already-migrated", p1Dir };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
existsSync(join(catchAll, "page.tsx")) &&
|
|
58
|
+
existsSync(join(catchAll, "editor-client.tsx"))
|
|
59
|
+
) {
|
|
60
|
+
const extras = extraEntries(catchAll);
|
|
61
|
+
if (extras.length > 0) return { status: "extra-files", p1Dir, catchAll, extras };
|
|
62
|
+
return { status: "legacy", p1Dir, catchAll };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { status: "not-found" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The release that moved the editor from `pages.Page` to `pages.Layout`. The
|
|
70
|
+
* codemod writes routes that call `Layout`, so anything older would be
|
|
71
|
+
* restructured to import an export that does not exist yet.
|
|
72
|
+
*/
|
|
73
|
+
export const MIN_SUITE_VERSION = "0.8.0";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The lockstep-versioned packages a consumer app actually installs.
|
|
77
|
+
* `create-p1-starter-kit` is in the same `fixed` group but only ever scaffolds,
|
|
78
|
+
* so it is never present in the tree being migrated.
|
|
79
|
+
*/
|
|
80
|
+
const SUITE = [
|
|
81
|
+
"@pantheon-systems/p1-next-sdk",
|
|
82
|
+
"@pantheon-systems/puck-css",
|
|
83
|
+
"@pantheon-systems/css-client",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function readVersion(packageJsonPath) {
|
|
87
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf-8")).version ?? null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function manifestPath(...segments) {
|
|
96
|
+
return join(...segments, "package.json");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Numeric release triple, ignoring any prerelease tag. Null when unparseable. */
|
|
100
|
+
function parseVersion(version) {
|
|
101
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(version));
|
|
102
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isOlderThan(version, floor) {
|
|
106
|
+
for (let i = 0; i < 3; i++) {
|
|
107
|
+
if (version[i] !== floor[i]) return version[i] < floor[i];
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Verify the installed suite is consistent and new enough.
|
|
114
|
+
*
|
|
115
|
+
* Reads the installed tree rather than the consumer's declared ranges: a
|
|
116
|
+
* pre-1.0 caret is pinned to its minor, and an exact-pinned internal dep is
|
|
117
|
+
* satisfied by a nested private copy, so ranges cannot reveal either a stale
|
|
118
|
+
* install or a duplicated package. When nothing is resolvable we cannot verify
|
|
119
|
+
* anything — proceed rather than block, matching the clean-tree check.
|
|
120
|
+
*
|
|
121
|
+
* Only root-level packages are checked. Under pnpm's isolated node_modules just
|
|
122
|
+
* the app's direct dependencies are linked at the root, so a suite package
|
|
123
|
+
* missing from there is transitive, not broken — and a genuinely absent one
|
|
124
|
+
* fails loudly at build time anyway.
|
|
125
|
+
*/
|
|
126
|
+
export function assertSuiteVersions(dir) {
|
|
127
|
+
const modules = join(dir, "node_modules");
|
|
128
|
+
const resolved = SUITE.map((pkg) => ({
|
|
129
|
+
pkg,
|
|
130
|
+
version: readVersion(manifestPath(modules, pkg)),
|
|
131
|
+
})).filter((entry) => entry.version !== null);
|
|
132
|
+
|
|
133
|
+
if (resolved.length === 0) return { status: "unverified" };
|
|
134
|
+
|
|
135
|
+
const duplicates = [];
|
|
136
|
+
for (const { pkg: owner } of resolved) {
|
|
137
|
+
for (const { pkg: nested, version: root } of resolved) {
|
|
138
|
+
if (owner === nested) continue;
|
|
139
|
+
const version = readVersion(manifestPath(modules, owner, "node_modules", nested));
|
|
140
|
+
if (version !== null && version !== root) {
|
|
141
|
+
duplicates.push(`${nested}@${version} nested under ${owner}, and ${nested}@${root} at the root`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (duplicates.length > 0) {
|
|
146
|
+
throw new BailError(
|
|
147
|
+
`Your install has more than one copy of a P1 package: ${duplicates.join("; ")}. ` +
|
|
148
|
+
"Two copies mean two React contexts and the editor will misbehave at runtime. " +
|
|
149
|
+
"Upgrade every @pantheon-systems/* dependency in your package.json to the same " +
|
|
150
|
+
"version, reinstall, then re-run.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const versions = [...new Set(resolved.map((entry) => entry.version))];
|
|
155
|
+
if (versions.length > 1) {
|
|
156
|
+
throw new BailError(
|
|
157
|
+
"Installed P1 packages are on different versions " +
|
|
158
|
+
`(${resolved.map((e) => `${e.pkg}@${e.version}`).join(", ")}). ` +
|
|
159
|
+
"They are released in lockstep and must match. Upgrade them together, then re-run.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const [version] = versions;
|
|
164
|
+
const parsed = parseVersion(version);
|
|
165
|
+
if (parsed === null) return { status: "unverified" };
|
|
166
|
+
|
|
167
|
+
if (isOlderThan(parsed, parseVersion(MIN_SUITE_VERSION))) {
|
|
168
|
+
throw new BailError(
|
|
169
|
+
`Installed P1 packages are at ${version}, but the persistent (editor) layout ` +
|
|
170
|
+
`needs ${MIN_SUITE_VERSION} or newer. This codemod runs at the latest published ` +
|
|
171
|
+
"version because npx fetches it from the registry, so it can restructure routes " +
|
|
172
|
+
`your installed version cannot render. Note a "^${version}" range will not resolve ` +
|
|
173
|
+
`${MIN_SUITE_VERSION} — pre-1.0 carets are pinned to their minor — so upgrade ` +
|
|
174
|
+
"explicitly, reinstall, then re-run.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { status: "ok", version };
|
|
179
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem helpers for the codemod, with a path-traversal guard so a write
|
|
3
|
+
* can never land outside the project being migrated.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { resolve, sep } from "node:path";
|
|
8
|
+
import { BailError } from "./transform.js";
|
|
9
|
+
|
|
10
|
+
export function read(path) {
|
|
11
|
+
return readFileSync(path, "utf-8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function assertWithin(root, target) {
|
|
15
|
+
const r = resolve(root);
|
|
16
|
+
const t = resolve(target);
|
|
17
|
+
if (t !== r && !t.startsWith(r + sep)) {
|
|
18
|
+
throw new BailError(`Refusing to touch a path outside the project: ${target}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ensureDir(path) {
|
|
23
|
+
mkdirSync(path, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function write(path, content) {
|
|
27
|
+
writeFileSync(path, content);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function removeDir(path) {
|
|
31
|
+
rmSync(path, { recursive: true, force: true });
|
|
32
|
+
}
|
package/bin/lib/git.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file-moving codemod needs a clean tree so the user can review its diff and
|
|
3
|
+
* roll back. Not being a git repo is a state we can report and proceed from;
|
|
4
|
+
* git being present but unable to answer is not — that is an unverified tree
|
|
5
|
+
* wearing the same mask, and the caller would delete files on the strength of
|
|
6
|
+
* a check that never ran.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { BailError } from "./transform.js";
|
|
11
|
+
|
|
12
|
+
function runGit(args, dir) {
|
|
13
|
+
return execFileSync("git", args, {
|
|
14
|
+
cwd: dir,
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function describeFailure(err) {
|
|
21
|
+
const stderr = typeof err?.stderr === "string" ? err.stderr.trim() : "";
|
|
22
|
+
return stderr || (err instanceof Error ? err.message : String(err));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function unverified(err) {
|
|
26
|
+
return new BailError(
|
|
27
|
+
`git could not verify the working tree (${describeFailure(err)}). ` +
|
|
28
|
+
"Fix that, or re-run with --force to migrate without a clean-tree check.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @returns `{ status: "clean" }` when the target subtree has no pending changes,
|
|
34
|
+
* or `{ status: "no-repo" }` when there is no repository to check. Throws
|
|
35
|
+
* BailError when the tree is dirty or git could not answer.
|
|
36
|
+
*/
|
|
37
|
+
export function assertCleanTree(dir, run = runGit) {
|
|
38
|
+
try {
|
|
39
|
+
run(["rev-parse", "--git-dir"], dir);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (/not a git repository/i.test(describeFailure(err))) return { status: "no-repo" };
|
|
42
|
+
throw unverified(err);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let out;
|
|
46
|
+
try {
|
|
47
|
+
// Scoped to the target: `git status` is repo-wide by default, so an
|
|
48
|
+
// unrelated dirty file elsewhere in a monorepo would block a clean subtree.
|
|
49
|
+
out = run(["status", "--porcelain", "--", "."], dir);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw unverified(err);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (out.trim()) {
|
|
55
|
+
throw new BailError(
|
|
56
|
+
"Working tree is not clean. Commit or stash your changes first, or re-run with --force.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return { status: "clean" };
|
|
60
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All console output for the codemod, kept in one place. Plain text — no color
|
|
3
|
+
* dependency — so the SDK's runtime deps stay unchanged.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MIN_SUITE_VERSION } from "./detect.js";
|
|
7
|
+
|
|
8
|
+
const TAG = "[p1-migrate]";
|
|
9
|
+
|
|
10
|
+
export function help() {
|
|
11
|
+
console.log(
|
|
12
|
+
[
|
|
13
|
+
"p1-migrate — migrate a P1 app to the persistent (editor) layout",
|
|
14
|
+
"",
|
|
15
|
+
"Usage: npx @pantheon-systems/p1-next-sdk p1-migrate [options]",
|
|
16
|
+
"",
|
|
17
|
+
"Options:",
|
|
18
|
+
" --dir=<path> Project directory to migrate (default: current directory)",
|
|
19
|
+
" --dry-run Show what would change without writing anything",
|
|
20
|
+
" --force, -f Skip the clean-git-tree check",
|
|
21
|
+
" --help, -h Show this help",
|
|
22
|
+
].join("\n"),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function alreadyMigrated() {
|
|
27
|
+
console.log(`${TAG} Already on the (editor) layout — nothing to do.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function noGitRepo() {
|
|
31
|
+
console.log(
|
|
32
|
+
`${TAG} Not a git repository — skipping the clean-tree check. This rewrites and ` +
|
|
33
|
+
"removes files with no way to roll them back.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function versionsUnverified() {
|
|
38
|
+
console.log(
|
|
39
|
+
`${TAG} Could not read installed @pantheon-systems/* versions — skipping the version check.`,
|
|
40
|
+
);
|
|
41
|
+
console.log(
|
|
42
|
+
`${TAG} Make sure your dependencies are installed and on ${MIN_SUITE_VERSION} or newer.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function dryRunPlan(writePaths, oldDir) {
|
|
47
|
+
console.log(`${TAG} Dry run — no files written. Planned changes:`);
|
|
48
|
+
for (const p of writePaths) console.log(` write ${p}`);
|
|
49
|
+
console.log(` remove ${oldDir}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function success(writePaths, oldDir) {
|
|
53
|
+
console.log(`${TAG} Migrated to the persistent (editor) layout:`);
|
|
54
|
+
for (const p of writePaths) console.log(` wrote ${p}`);
|
|
55
|
+
console.log(` removed ${oldDir}`);
|
|
56
|
+
console.log(`${TAG} Review the diff and commit when it looks right.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function bail(message) {
|
|
60
|
+
console.error(`${TAG} Could not migrate automatically: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function unexpected(err) {
|
|
64
|
+
console.error(`${TAG} Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure string transforms for the p1-migrate codemod.
|
|
3
|
+
*
|
|
4
|
+
* No filesystem, no side effects — every function takes source in and returns
|
|
5
|
+
* source out (or throws BailError when the input isn't the shape we recognize).
|
|
6
|
+
* The moves and depth rule are mechanical; the two localized edits validate
|
|
7
|
+
* their target and bail rather than guess, so a diverged app is never left
|
|
8
|
+
* half-migrated.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class BailError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "BailError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Moving a file one directory deeper (into the `(editor)` group) adds a real
|
|
20
|
+
* on-disk segment the URL never sees, so every relative import that already
|
|
21
|
+
* points at a parent gains one more `../`. Sibling (`./`) and bare package
|
|
22
|
+
* specifiers are untouched.
|
|
23
|
+
*/
|
|
24
|
+
export function deepenRelativeImports(source) {
|
|
25
|
+
return source.replace(
|
|
26
|
+
/(\bfrom\s+|\bimport\s+|\bimport\(\s*|\brequire\(\s*)(['"])(\.\.\/)/g,
|
|
27
|
+
(_match, prefix, quote, dots) => `${prefix}${quote}../${dots}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add `name` to the named-import list for `moduleSpecifier`. Idempotent when
|
|
33
|
+
* the name is already imported; bails when the module isn't imported at all.
|
|
34
|
+
*/
|
|
35
|
+
export function addNamedImport(source, moduleSpecifier, name, position = "append") {
|
|
36
|
+
const escaped = moduleSpecifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37
|
+
const re = new RegExp(`(import\\s*\\{)([^}]*)(\\}\\s*from\\s*['"])${escaped}(['"])`);
|
|
38
|
+
const match = source.match(re);
|
|
39
|
+
if (!match) {
|
|
40
|
+
throw new BailError(
|
|
41
|
+
`Expected an import from "${moduleSpecifier}" to add "${name}" to; migrate this file by hand.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const names = match[2].split(",").map((s) => s.trim()).filter(Boolean);
|
|
45
|
+
if (names.includes(name)) return source;
|
|
46
|
+
const next = position === "prepend" ? [name, ...names] : [...names, name];
|
|
47
|
+
return source.replace(re, `$1 ${next.join(", ")} $3${moduleSpecifier}$4`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const LEGACY_SIGNATURE =
|
|
51
|
+
/export function EditorClientWrapper\(\{\s*path\s*\}\s*:\s*\{\s*path\s*:\s*string\s*\}\s*\)\s*\{/;
|
|
52
|
+
const MIGRATED_SIGNATURE = /export function EditorClientWrapper\(\s*\)/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Drop the `{ path }` prop from EditorClientWrapper and derive the path from
|
|
56
|
+
* the URL instead — matching how the persistent layout renders it with no props.
|
|
57
|
+
*/
|
|
58
|
+
export function rewriteWrapperSignature(source) {
|
|
59
|
+
if (LEGACY_SIGNATURE.test(source)) {
|
|
60
|
+
return source.replace(
|
|
61
|
+
LEGACY_SIGNATURE,
|
|
62
|
+
"export function EditorClientWrapper() {\n" +
|
|
63
|
+
" // Rendered from the persistent (editor) layout, so this survives page\n" +
|
|
64
|
+
" // switches; the edited page is derived from the URL instead of route params.\n" +
|
|
65
|
+
" const pathname = usePathname();\n" +
|
|
66
|
+
" const path = editorPagePathFromUrlPath(pathname);",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (MIGRATED_SIGNATURE.test(source)) return source;
|
|
70
|
+
throw new BailError(
|
|
71
|
+
"EditorClientWrapper has an unrecognized signature; migrate this file by hand.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Full editor-client transform: deepen imports, add the two named imports, rewrite the signature. */
|
|
76
|
+
export function rewriteEditorClient(source) {
|
|
77
|
+
let out = deepenRelativeImports(source);
|
|
78
|
+
out = addNamedImport(out, "next/navigation", "usePathname", "prepend");
|
|
79
|
+
out = addNamedImport(out, "@pantheon-systems/p1-next-sdk", "editorPagePathFromUrlPath", "append");
|
|
80
|
+
out = rewriteWrapperSignature(out);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The thin page.tsx that re-exports from the shared p1-pages module. */
|
|
85
|
+
export function buildNewPageFile() {
|
|
86
|
+
return (
|
|
87
|
+
'import { pages } from "./p1-pages";\n' +
|
|
88
|
+
"\n" +
|
|
89
|
+
"export default pages.Page;\n" +
|
|
90
|
+
"export const generateMetadata = pages.generateMetadata;\n" +
|
|
91
|
+
'export const dynamic = "force-dynamic";\n'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The `(editor)/layout.tsx` that renders the persistent editor. */
|
|
96
|
+
export function buildLayoutFile() {
|
|
97
|
+
return (
|
|
98
|
+
'import "@puckeditor/core/puck.css";\n' +
|
|
99
|
+
'import { pages } from "./[[...p1]]/p1-pages";\n' +
|
|
100
|
+
"\n" +
|
|
101
|
+
"// The editor renders from this layout, NOT the page. The (editor) group is a\n" +
|
|
102
|
+
"// static segment, so this layout survives navigation between /p1/<pageA> and\n" +
|
|
103
|
+
"// /p1/<pageB> — a layout inside [[...p1]] would remount on every switch, since\n" +
|
|
104
|
+
"// Next keys segment cache nodes by param value.\n" +
|
|
105
|
+
"//\n" +
|
|
106
|
+
"// Scoping the layout to the (editor) group (instead of app/p1/layout.tsx) is\n" +
|
|
107
|
+
"// what keeps the editor off sibling routes: /p1/merge and future pages like\n" +
|
|
108
|
+
"// /p1/settings live outside the group and never render the editor. Add such\n" +
|
|
109
|
+
"// pages as siblings of (editor), not inside it.\n" +
|
|
110
|
+
"export default pages.Layout;\n"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The re-exports that belong to the route file, not the shared factory module. */
|
|
115
|
+
const PAGE_LEVEL_EXPORTS = [
|
|
116
|
+
/^export default pages\.Page;\n/m,
|
|
117
|
+
/^export const generateMetadata = pages\.generateMetadata;\n/m,
|
|
118
|
+
/^export const dynamic = ["']force-dynamic["'];\n/m,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Split the old catch-all page.tsx into the shared factory module (p1-pages.tsx)
|
|
123
|
+
* and the thin re-export page.tsx. Bails when the file isn't the recognized
|
|
124
|
+
* createP1Pages editor page.
|
|
125
|
+
*/
|
|
126
|
+
export function splitPageFile(source) {
|
|
127
|
+
if (!source.includes("createP1Pages(") || !source.includes("pages.Page")) {
|
|
128
|
+
throw new BailError(
|
|
129
|
+
"page.tsx is not the recognized createP1Pages editor page; migrate it by hand.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
let p1Pages = deepenRelativeImports(source);
|
|
133
|
+
// The puck.css side-effect moves to layout.tsx.
|
|
134
|
+
p1Pages = p1Pages.replace(/^import ["']@puckeditor\/core\/puck\.css["'];\n/m, "");
|
|
135
|
+
|
|
136
|
+
// Export the factory result so both layout.tsx and page.tsx can consume it.
|
|
137
|
+
p1Pages = p1Pages.replace(/\bconst pages = createP1Pages\(/, "export const pages = createP1Pages(");
|
|
138
|
+
if (!/\bexport const pages = createP1Pages\(/.test(p1Pages)) {
|
|
139
|
+
throw new BailError(
|
|
140
|
+
"Could not find `const pages = createP1Pages(` to export from page.tsx; migrate this file by hand.",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The page-level re-exports move to the thin page.tsx. Stripped individually
|
|
145
|
+
// so a reordered file does not leave dead exports behind in p1-pages.tsx.
|
|
146
|
+
for (const re of PAGE_LEVEL_EXPORTS) p1Pages = p1Pages.replace(re, "");
|
|
147
|
+
p1Pages = p1Pages.replace(/\n+$/, "\n");
|
|
148
|
+
if (/^export default pages\.Page;$/m.test(p1Pages)) {
|
|
149
|
+
throw new BailError(
|
|
150
|
+
"Could not remove the page-level exports from page.tsx; migrate this file by hand.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { p1Pages, page: buildNewPageFile() };
|
|
155
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor URL → page path mapping, shared by the server page handler and the
|
|
3
|
+
* client-side editor. Pure and client-safe: the editor renders from a
|
|
4
|
+
* persistent layout (so it survives route-param changes) and derives its page
|
|
5
|
+
* path from usePathname() with these helpers, which must agree with the
|
|
6
|
+
* server's parsing in pages-handler.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseEditorSegments(segments: string[]): string;
|
|
9
|
+
export declare function editorPagePathFromUrlPath(pathname: string, basePath?: string): string;
|
|
10
|
+
//# sourceMappingURL=editor-paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-paths.d.ts","sourceRoot":"","sources":["../src/editor-paths.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAc9D;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAQ,GACf,MAAM,CAgBR"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor URL → page path mapping, shared by the server page handler and the
|
|
3
|
+
* client-side editor. Pure and client-safe: the editor renders from a
|
|
4
|
+
* persistent layout (so it survives route-param changes) and derives its page
|
|
5
|
+
* path from usePathname() with these helpers, which must agree with the
|
|
6
|
+
* server's parsing in pages-handler.
|
|
7
|
+
*/
|
|
8
|
+
import { pagePathFromCatchAllSegments } from "@pantheon-systems/puck-css/routes";
|
|
9
|
+
export function parseEditorSegments(segments) {
|
|
10
|
+
if (segments.length === 0)
|
|
11
|
+
return "/";
|
|
12
|
+
const command = segments[0];
|
|
13
|
+
// /p1/api/... is handled by the route handler, not the page
|
|
14
|
+
if (command === "api")
|
|
15
|
+
return "/";
|
|
16
|
+
// /p1/edit/... -> editor for the path
|
|
17
|
+
if (command === "edit") {
|
|
18
|
+
return pagePathFromCatchAllSegments(segments.slice(1));
|
|
19
|
+
}
|
|
20
|
+
// /p1/... (anything else) -> editor for that path
|
|
21
|
+
return pagePathFromCatchAllSegments(segments);
|
|
22
|
+
}
|
|
23
|
+
export function editorPagePathFromUrlPath(pathname, basePath = "/p1") {
|
|
24
|
+
if (pathname !== basePath && !pathname.startsWith(`${basePath}/`)) {
|
|
25
|
+
// Falling back silently would make the editor load and edit the root
|
|
26
|
+
// document while the URL points somewhere else entirely.
|
|
27
|
+
console.warn(`[p1-next-sdk] "${pathname}" is outside the editor base path "${basePath}"; ` +
|
|
28
|
+
`falling back to the root page. If the editor is not mounted at ${basePath}, ` +
|
|
29
|
+
`pass the correct basePath to editorPagePathFromUrlPath.`);
|
|
30
|
+
return "/";
|
|
31
|
+
}
|
|
32
|
+
const segments = pathname
|
|
33
|
+
.slice(basePath.length)
|
|
34
|
+
.split("/")
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
return parseEditorSegments(segments);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=editor-paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-paths.js","sourceRoot":"","sources":["../src/editor-paths.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAEjF,MAAM,UAAU,mBAAmB,CAAC,QAAkB;IACpD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACtC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAE5B,4DAA4D;IAC5D,IAAI,OAAO,KAAK,KAAK;QAAE,OAAO,GAAG,CAAC;IAElC,sCAAsC;IACtC,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,OAAO,4BAA4B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,kDAAkD;IAClD,OAAO,4BAA4B,CAAC,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,QAAgB,EAChB,QAAQ,GAAG,KAAK;IAEhB,IAAI,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,CAAC,EAAE,CAAC;QAClE,qEAAqE;QACrE,yDAAyD;QACzD,OAAO,CAAC,IAAI,CACV,kBAAkB,QAAQ,sCAAsC,QAAQ,KAAK;YAC3E,kEAAkE,QAAQ,IAAI;YAC9E,yDAAyD,CAC5D,CAAC;QACF,OAAO,GAAG,CAAC;IACb,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ;SACtB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;SACtB,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,OAAO,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AACvC,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EACL,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EACL,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC"}
|
package/dist/pages-handler.d.ts
CHANGED
|
@@ -1,30 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* P1 Next SDK page handler — provides page components for `/p1/[...p1]`.
|
|
2
|
+
* P1 Next SDK page handler — provides page components for `/p1/[[...p1]]`.
|
|
3
|
+
*
|
|
4
|
+
* The editor renders from `Layout`, not `Page`: Next.js keys segment cache
|
|
5
|
+
* nodes by their param values, so everything inside `[[...p1]]` — page AND
|
|
6
|
+
* any layout placed there — remounts when the param changes, tearing down
|
|
7
|
+
* the whole editor (providers, auth, Puck and its canvas iframe) on every
|
|
8
|
+
* document switch. `Layout` must therefore be mounted at a static segment,
|
|
9
|
+
* which persists; the editor follows the URL client-side from there (see
|
|
10
|
+
* editor-paths.ts).
|
|
11
|
+
*
|
|
12
|
+
* Mount that layout in an `(editor)` route group rather than directly at
|
|
13
|
+
* `app/p1/layout.tsx`. A layout at `/p1` wraps EVERY route under it, so
|
|
14
|
+
* sibling routes with their own pages (e.g. /p1/merge, /p1/settings) would
|
|
15
|
+
* render the editor on top of themselves. Scoping the layout to the group
|
|
16
|
+
* means only the catch-all page gets the editor; siblings placed outside the
|
|
17
|
+
* group stay editor-free by construction. Route groups add no URL segment, so
|
|
18
|
+
* /p1 and its subpaths are unchanged.
|
|
3
19
|
*
|
|
4
20
|
* Usage:
|
|
5
|
-
* // app/p1/[...p1]/
|
|
6
|
-
* import { createP1Pages } from "@pantheon-systems/p1-next-sdk";
|
|
7
|
-
*
|
|
8
|
-
*
|
|
21
|
+
* // app/p1/(editor)/[[...p1]]/p1-pages.tsx (shared module)
|
|
22
|
+
* import { createP1Pages } from "@pantheon-systems/p1-next-sdk/server";
|
|
23
|
+
* export const pages = createP1Pages({ config, EditorClient });
|
|
24
|
+
*
|
|
25
|
+
* // app/p1/(editor)/layout.tsx <- static segment scoped to the group
|
|
26
|
+
* export default pages.Layout;
|
|
27
|
+
*
|
|
28
|
+
* // app/p1/(editor)/[[...p1]]/page.tsx
|
|
9
29
|
* export default pages.Page;
|
|
10
30
|
* export const generateMetadata = pages.generateMetadata;
|
|
11
31
|
* export const dynamic = "force-dynamic";
|
|
32
|
+
*
|
|
33
|
+
* // app/p1/merge/page.tsx <- sibling OUTSIDE the group, no editor
|
|
34
|
+
* // app/p1/settings/page.tsx <- future siblings: same, editor-free
|
|
35
|
+
*
|
|
36
|
+
* The editor must be mounted at `/p1`: the client derives the edited page
|
|
37
|
+
* from the URL via editorPagePathFromUrlPath, whose basePath defaults to
|
|
38
|
+
* "/p1". A different mount point needs that basePath passed through in the
|
|
39
|
+
* EditorClient implementation, or every URL falls back to the root page.
|
|
12
40
|
*/
|
|
13
41
|
import type { Config } from "@puckeditor/core";
|
|
14
42
|
import type { Metadata } from "next";
|
|
15
43
|
import { type P1DataConfig } from "@pantheon-systems/puck-css/server";
|
|
16
44
|
export type P1PagesConfig = P1DataConfig & {
|
|
17
45
|
config: Config;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
/**
|
|
47
|
+
* React component to render the editor. Rendered from the persistent
|
|
48
|
+
* layout with no props — it derives the page path from the URL (see
|
|
49
|
+
* editorPagePathFromUrlPath) and handles its own data loading and auth
|
|
50
|
+
* via P1App.
|
|
51
|
+
*/
|
|
52
|
+
EditorClient: React.ComponentType;
|
|
22
53
|
};
|
|
23
54
|
export declare function createP1Pages(opts: P1PagesConfig): {
|
|
24
|
-
Page: (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}>;
|
|
55
|
+
Page: () => null;
|
|
56
|
+
Layout: ({ children }: {
|
|
57
|
+
children: React.ReactNode;
|
|
28
58
|
}) => Promise<import("react/jsx-runtime").JSX.Element>;
|
|
29
59
|
generateMetadata: ({ params, }: {
|
|
30
60
|
params: Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages-handler.d.ts","sourceRoot":"","sources":["../src/pages-handler.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pages-handler.d.ts","sourceRoot":"","sources":["../src/pages-handler.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAErC,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,mCAAmC,CAAC;AAI3C,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IACzC,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,YAAY,EAAE,KAAK,CAAC,aAAa,CAAC;CACnC,CAAC;AAEF,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa;;2BAyBX;QAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;KAAE;oCAX9D;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAC;KACpC,KAAG,OAAO,CAAC,QAAQ,CAAC;EAwCtB"}
|
package/dist/pages-handler.js
CHANGED
|
@@ -1,35 +1,43 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
if (path.length === 0)
|
|
5
|
-
return { pagePath: "/" };
|
|
6
|
-
const command = path[0];
|
|
7
|
-
// /p1/api/... is handled by the route handler, not the page
|
|
8
|
-
if (command === "api")
|
|
9
|
-
return { pagePath: "/" };
|
|
10
|
-
// /p1/edit/... -> editor for the path
|
|
11
|
-
if (command === "edit") {
|
|
12
|
-
const rest = path.slice(1);
|
|
13
|
-
return { pagePath: pagePathFromCatchAllSegments(rest) };
|
|
14
|
-
}
|
|
15
|
-
// /p1/... (anything else) -> editor for that path
|
|
16
|
-
return { pagePath: pagePathFromCatchAllSegments(path) };
|
|
17
|
-
}
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ensureInitialized, } from "@pantheon-systems/puck-css/server";
|
|
3
|
+
import { parseEditorSegments } from "./editor-paths";
|
|
18
4
|
export function createP1Pages(opts) {
|
|
19
5
|
const { EditorClient } = opts;
|
|
20
6
|
const initPromise = ensureInitialized(opts);
|
|
7
|
+
// A parent layout always renders before its child page within a request, so
|
|
8
|
+
// if the editor is mounted correctly Layout flips this before Page reads it.
|
|
9
|
+
// A legacy page-only app (no `(editor)/layout.tsx`) renders Page alone, and
|
|
10
|
+
// the flag is still false — the dev-only nudge below fires. Per-process,
|
|
11
|
+
// best-effort: it can't run in production and never affects output.
|
|
12
|
+
let layoutRendered = false;
|
|
13
|
+
let warnedMissingLayout = false;
|
|
21
14
|
async function generateMetadata({ params, }) {
|
|
22
15
|
await initPromise;
|
|
23
16
|
const { p1 = [] } = await params;
|
|
24
|
-
const
|
|
17
|
+
const pagePath = parseEditorSegments(p1);
|
|
25
18
|
return { title: "P1 Editor: " + pagePath };
|
|
26
19
|
}
|
|
27
|
-
|
|
20
|
+
// Persists across route-param changes — the editor lives here. Sets the flag
|
|
21
|
+
// synchronously (before any await) so Page can observe it in the same request.
|
|
22
|
+
async function Layout({ children }) {
|
|
23
|
+
layoutRendered = true;
|
|
28
24
|
await initPromise;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsx(EditorClient, {}), children] }));
|
|
26
|
+
}
|
|
27
|
+
// Remounts per navigation; must stay empty so nothing is lost when it does.
|
|
28
|
+
function Page() {
|
|
29
|
+
if (process.env.NODE_ENV !== "production" &&
|
|
30
|
+
!layoutRendered &&
|
|
31
|
+
!warnedMissingLayout) {
|
|
32
|
+
warnedMissingLayout = true;
|
|
33
|
+
console.warn("[p1-next-sdk] The P1 editor now renders from pages.Layout, but this " +
|
|
34
|
+
"app rendered pages.Page without it — the editor will be empty. Mount " +
|
|
35
|
+
"it at app/p1/(editor)/layout.tsx, or run: " +
|
|
36
|
+
"npx @pantheon-systems/p1-next-sdk p1-migrate — see " +
|
|
37
|
+
"docs/MIGRATION-EDITOR-LAYOUT.md");
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
32
40
|
}
|
|
33
|
-
return { Page, generateMetadata };
|
|
41
|
+
return { Page, Layout, generateMetadata };
|
|
34
42
|
}
|
|
35
43
|
//# sourceMappingURL=pages-handler.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages-handler.js","sourceRoot":"","sources":["../src/pages-handler.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"pages-handler.js","sourceRoot":"","sources":["../src/pages-handler.tsx"],"names":[],"mappings":";AA4CA,OAAO,EACL,iBAAiB,GAElB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAarD,MAAM,UAAU,aAAa,CAAC,IAAmB;IAC/C,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAC9B,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAE5C,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,yEAAyE;IACzE,oEAAoE;IACpE,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,IAAI,mBAAmB,GAAG,KAAK,CAAC;IAEhC,KAAK,UAAU,gBAAgB,CAAC,EAC9B,MAAM,GAGP;QACC,MAAM,WAAW,CAAC;QAClB,MAAM,EAAE,EAAE,GAAG,EAAE,EAAE,GAAG,MAAM,MAAM,CAAC;QACjC,MAAM,QAAQ,GAAG,mBAAmB,CAAC,EAAE,CAAC,CAAC;QACzC,OAAO,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,EAAE,CAAC;IAC7C,CAAC;IAED,6EAA6E;IAC7E,+EAA+E;IAC/E,KAAK,UAAU,MAAM,CAAC,EAAE,QAAQ,EAAiC;QAC/D,cAAc,GAAG,IAAI,CAAC;QACtB,MAAM,WAAW,CAAC;QAClB,OAAO,CACL,8BACE,KAAC,YAAY,KAAG,EACf,QAAQ,IACR,CACJ,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,SAAS,IAAI;QACX,IACE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;YACrC,CAAC,cAAc;YACf,CAAC,mBAAmB,EACpB,CAAC;YACD,mBAAmB,GAAG,IAAI,CAAC;YAC3B,OAAO,CAAC,IAAI,CACV,sEAAsE;gBACpE,uEAAuE;gBACvE,4CAA4C;gBAC5C,qDAAqD;gBACrD,iCAAiC,CACpC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;AAC5C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantheon-systems/p1-next-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Next.js SDK for P1 editor integration",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "./dist/index.js",
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"p1-migrate": "./bin/p1-migrate.js"
|
|
20
|
+
},
|
|
18
21
|
"exports": {
|
|
19
22
|
".": {
|
|
20
23
|
"import": "./dist/index.js",
|
|
@@ -26,20 +29,19 @@
|
|
|
26
29
|
}
|
|
27
30
|
},
|
|
28
31
|
"files": [
|
|
29
|
-
"dist"
|
|
32
|
+
"dist",
|
|
33
|
+
"bin"
|
|
30
34
|
],
|
|
31
35
|
"dependencies": {
|
|
32
36
|
"server-only": "^0.0.1",
|
|
33
|
-
"@pantheon-systems/css-client": "0.
|
|
34
|
-
"@pantheon-systems/puck-css": "0.
|
|
37
|
+
"@pantheon-systems/css-client": "0.8.0",
|
|
38
|
+
"@pantheon-systems/puck-css": "0.8.0"
|
|
35
39
|
},
|
|
36
40
|
"peerDependencies": {
|
|
37
41
|
"@puckeditor/core": ">=0.21.0",
|
|
38
42
|
"next": ">=14.0.0",
|
|
39
43
|
"react": ">=18.0.0",
|
|
40
|
-
"react-dom": ">=18.0.0"
|
|
41
|
-
"@pantheon-systems/css-client": "0.6.0",
|
|
42
|
-
"@pantheon-systems/puck-css": "0.6.0"
|
|
44
|
+
"react-dom": ">=18.0.0"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@testing-library/dom": "^10.4.1",
|