znode-plugin 0.1.4 → 0.1.6
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 +4 -1
- package/lib/next-imports.mjs +105 -41
- package/lib/znode-package.mjs +1083 -680
- package/package.json +1 -1
- package/types/extension-api.d.ts +3 -0
package/README.md
CHANGED
|
@@ -218,7 +218,7 @@ export default function setup(host: IBrowserHost): IBrowserPlugin {
|
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
**Slots:** `layout.header`, `layout.footer`, `header.search`, `product.card`, `product.image`, `product.variants`,
|
|
221
|
-
`product.addToCart`, `product.layout`, `productList.layout`, `addToCart.notification`, `login.form`,
|
|
221
|
+
`product.addToCart`, `product.layout`, `product.basicInfo`, `productList.layout`, `addToCart.notification`, `login.form`,
|
|
222
222
|
`checkout.address`, `checkout.shippingOptions`. Server slots (in `server.tsx`): `layout.root`, `login.layout`, `login.page`.
|
|
223
223
|
|
|
224
224
|
### Add a page-builder widget
|
|
@@ -291,6 +291,9 @@ Run them with `npm run <name>` inside a project (the scripts call `znode … .`)
|
|
|
291
291
|
| `npm run link-store -- <store repo>` | `znode link-store <repo> .` | connect the store's types |
|
|
292
292
|
| | `npx znode install <zip> [pluginsDir]` | install a released zip into a store's plugin folder |
|
|
293
293
|
| | `npx znode create <theme\|plugin\|widget\|content> <name>` | a minimal starter of another kind |
|
|
294
|
+
| | `npx znode eject` | list the store components a package can take over (each has a slot that hands the component over) |
|
|
295
|
+
| | `npx znode add-slot <Component> <area.name> [--dry-run] [--file <store file>]` | add a slot around a store component that has none, in the **linked store's source** (not the theme, never `.next`): a `Runtime<Component>` wrapper in `packages/extensions/src/slots/RuntimeAddedSlots.tsx`, the component's export wrapped with `withRuntimeWrapper`, the slot added to the store's slot list and docs, the module registry regenerated. Refuses server components (`async`, server-only imports): a slot is filled in the browser. Nothing is committed: review the diff in the store, commit it there, rebuild the webstore, then `npx znode eject <Component>`. `add slot` (with a space) works too |
|
|
296
|
+
| | `npx znode eject <Name> [--force]` | `<Name>` is the component or its slot (`ProductBasicInfo` or `product.basicInfo`). Copy the store's component into `src/components/<Name>.tsx` (imports turned into store aliases, runtime wrapper removed) and register it in `src/browser.tsx` under its slot; prints any npm package the copy needs. The copy is yours: store fixes no longer reach it |
|
|
294
297
|
|
|
295
298
|
The plugin folder is taken from: the command's argument, else the `RUNTIME_PLUGINS_DIR` environment variable, else
|
|
296
299
|
`znode.local.json`.
|
package/lib/next-imports.mjs
CHANGED
|
@@ -56,36 +56,88 @@ const MODULES = {
|
|
|
56
56
|
// the server-only functions a browser part may not need, and the hooks a server part cannot call, stay importable:
|
|
57
57
|
// the store's module for that side decides (a hook in a server part fails as it would in a Next.js server component)
|
|
58
58
|
|
|
59
|
-
// The store's own modules a browser part may import by the store's path
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
59
|
+
// The store's own modules a browser part may import by the store's path (Generated Store Module Registry): any
|
|
60
|
+
// @znode/... import the package's tsconfig.store.json resolves to a store file the store's registry has (its
|
|
61
|
+
// packages/extensions/store-module-registry.json, generated by scripts/gen-store-module-registry.mjs from the store's
|
|
62
|
+
// source: its client modules), and the npm packages the store shares (lodash, next-intl). Each becomes a stand-in that
|
|
63
|
+
// reads the store's module, which the store loads before the package's bundle (the build lists the keys in the
|
|
64
|
+
// package's znode-package.json as storeImports). No list here: what the store has is what a package can use.
|
|
65
|
+
const SHARED_PACKAGES = new Set(["lodash", "next-intl"]);
|
|
66
|
+
const STORE = "store:";
|
|
67
|
+
|
|
68
|
+
const STORE_FILE = "\0znode-store-file:";
|
|
69
|
+
const asFile = (base) =>
|
|
70
|
+
[base, `${base}.ts`, `${base}.tsx`, path.join(base, "index.ts"), path.join(base, "index.tsx")].find((file) => fs.existsSync(file) && fs.statSync(file).isFile());
|
|
71
|
+
const keyOfFile = (file, store) => path.relative(store.root, file).split(path.sep).join("/").replace(/\.(tsx?|jsx?)$/, "").replace(/\/index$/, "");
|
|
72
|
+
|
|
73
|
+
// an import, with the package's tsconfig.store.json paths (or relative to a store file), to the store's file
|
|
74
|
+
function storeFile(source, store, from) {
|
|
75
|
+
if (from && source.startsWith(".")) return asFile(path.resolve(path.dirname(from), source));
|
|
76
|
+
for (const [alias, [target]] of Object.entries(store.paths)) {
|
|
77
|
+
if (alias.endsWith("/*")) {
|
|
78
|
+
const prefix = alias.slice(0, -1);
|
|
79
|
+
if (source.startsWith(prefix)) return asFile(path.join(store.root, target.replace("*", source.slice(prefix.length))));
|
|
80
|
+
} else if (source === alias) return asFile(path.join(store.root, target));
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// that file's registry key (its path from the store root without extension or /index, as the store's generator writes it)
|
|
86
|
+
function storeKey(source, store) {
|
|
87
|
+
if (SHARED_PACKAGES.has(source)) return source;
|
|
88
|
+
const file = storeFile(source, store);
|
|
89
|
+
return file ? keyOfFile(file, store) : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// the store file that defines an exported name, following the barrels (export * from, export { a as b } from): a
|
|
93
|
+
// theme's import { CustomImage } from ".../common/image" is loaded from common/image/CustomImage.tsx alone, not the whole
|
|
94
|
+
// barrel, whose other files may be server-only (the store's own bundler drops those for a static named import)
|
|
95
|
+
function definingFile(file, name, store, seen = new Set()) {
|
|
96
|
+
if (!file || seen.has(`${file}#${name}`)) return null;
|
|
97
|
+
seen.add(`${file}#${name}`);
|
|
98
|
+
const source = fs.readFileSync(file, "utf8");
|
|
99
|
+
const escaped = name.replace(/\$/g, "\\$");
|
|
100
|
+
if (name === "default" ? /export\s+default\b/.test(source) : new RegExp(`export\\s+(?:declare\\s+)?(?:async\\s+)?(?:const|let|var|function\\*?|class|enum)\\s+${escaped}\\b`).test(source)) return file;
|
|
101
|
+
for (const match of source.matchAll(/export\s*\{([^}]*)\}\s*(?:from\s*["']([^"']+)["'])?/g)) {
|
|
102
|
+
for (const part of match[1].split(",").map((item) => item.trim()).filter(Boolean)) {
|
|
103
|
+
const [inner, outer = inner] = part.replace(/^type\s+/, "").split(/\s+as\s+/).map((item) => item.trim());
|
|
104
|
+
if (outer !== name) continue;
|
|
105
|
+
return match[2] ? definingFile(storeFile(match[2], store, file), inner, store, seen) : file;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const match of source.matchAll(/export\s*\*\s*from\s*["']([^"']+)["']/g)) {
|
|
109
|
+
const found = definingFile(storeFile(match[1], store, file), name, store, seen);
|
|
110
|
+
if (found) return found;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// a package module's named imports from the store, split by the store file that defines each name
|
|
116
|
+
function splitStoreImports(code, store) {
|
|
117
|
+
return code.replace(/import\s*\{([^}]*)\}\s*from\s*["'](@znode\/[^"']+)["'];?/g, (whole, names, source) => {
|
|
118
|
+
const barrel = storeFile(source, store);
|
|
119
|
+
if (!barrel) return whole;
|
|
120
|
+
const byFile = new Map();
|
|
121
|
+
for (const part of names.split(",").map((item) => item.trim()).filter(Boolean)) {
|
|
122
|
+
const [name] = part.split(/\s+as\s+/).map((item) => item.trim());
|
|
123
|
+
const file = definingFile(barrel, name, store) || barrel;
|
|
124
|
+
if (!byFile.has(file)) byFile.set(file, []);
|
|
125
|
+
byFile.get(file).push(part);
|
|
126
|
+
}
|
|
127
|
+
return [...byFile].map(([file, parts]) => `import { ${parts.join(", ")} } from ${JSON.stringify(STORE_FILE + keyOfFile(file, store))};`).join("\n");
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// the stand-in for a store module: the store's module itself (loaded before the bundle), its names as rollup's
|
|
132
|
+
// synthetic named exports; a name missing on the module is read from its default (an npm CommonJS package)
|
|
133
|
+
function storeStandIn(key) {
|
|
134
|
+
return [
|
|
135
|
+
`const loaded = (globalThis.__znodeNextClient || {})[${JSON.stringify(key)}];`,
|
|
136
|
+
`if (!loaded) throw new Error(${JSON.stringify(`"${key}" was not loaded by the store: the package needs a store with the Generated Store Module Registry`)});`,
|
|
137
|
+
`export const __store = new Proxy(loaded, { get: (module, name) => (name in module ? module[name] : module.default?.[name]) });`,
|
|
138
|
+
`export default loaded.default;`,
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
89
141
|
|
|
90
142
|
// react's values, the same in every React 19 build and never read from the store at load
|
|
91
143
|
const VALUES = {
|
|
@@ -267,30 +319,42 @@ async function transformFonts(code, id, context) {
|
|
|
267
319
|
* The rollup plugin. part: "server" | "browser"; prefix: "<name>-<version>"; outDir: dist; fonts: collects what
|
|
268
320
|
* next/font/google built (files, css), which the build adds to the package's assets and stylesheet.
|
|
269
321
|
*/
|
|
270
|
-
export function znodeNext({ part, prefix, outDir, fonts }) {
|
|
322
|
+
export function znodeNext({ part, prefix, outDir, fonts, store, storeImports = [] }) {
|
|
271
323
|
return {
|
|
272
324
|
name: "znode-next",
|
|
273
325
|
resolveId(source) {
|
|
274
|
-
if (STORE_MODULES[source] && part === "server") {
|
|
275
|
-
this.error(`"${source}" is a store module for browser parts (src/browser.tsx), not server parts`);
|
|
276
|
-
}
|
|
277
326
|
if (MODULES[source]) return VIRTUAL + source;
|
|
327
|
+
if (SHARED_PACKAGES.has(source) || source.startsWith("@znode/") || source.startsWith(STORE_FILE)) {
|
|
328
|
+
if (part === "server") this.error(`"${source}" is a store module for browser parts (src/browser.tsx), not server parts`);
|
|
329
|
+
if (!store) this.error(`"${source}" is a store module: connect the store first (npm run link-store -- <store repo>), which writes tsconfig.store.json`);
|
|
330
|
+
if (!store.registry) this.error(`"${source}": the store at ${store.root} has no packages/extensions/store-module-registry.json (run node scripts/gen-store-module-registry.mjs there, or update the store)`);
|
|
331
|
+
// a named import was split to the file that defines it (transform); a default or namespace import is the module
|
|
332
|
+
const key = source.startsWith(STORE_FILE) ? source.slice(STORE_FILE.length) : storeKey(source, store);
|
|
333
|
+
if (!key) this.error(`"${source}" is not a store module (no file for it under the store's tsconfig paths)`);
|
|
334
|
+
if (!store.registry.has(key)) {
|
|
335
|
+
this.error(`"${source}" (${key}) is not in the store's module registry: it is server-only, or the store's registry is older than this file (regenerate it: node scripts/gen-store-module-registry.mjs in the store repo)`);
|
|
336
|
+
}
|
|
337
|
+
if (!storeImports.includes(key)) storeImports.push(key);
|
|
338
|
+
return VIRTUAL + STORE + key;
|
|
339
|
+
}
|
|
278
340
|
if (source === FONT_MODULE) return null;
|
|
279
341
|
if (source === "next" || source.startsWith("next/")) {
|
|
280
|
-
this.error(`"${source}" is not available to packages. Supported: ${[...Object.keys(MODULES), FONT_MODULE].
|
|
281
|
-
}
|
|
282
|
-
if (source.startsWith("@znode/")) {
|
|
283
|
-
this.error(`"${source}" is not available to packages. Store components a package can import: ${Object.keys(STORE_MODULES).join(", ")}`);
|
|
342
|
+
this.error(`"${source}" is not available to packages. Supported: ${[...Object.keys(MODULES), FONT_MODULE].join(", ")}`);
|
|
284
343
|
}
|
|
285
344
|
return null;
|
|
286
345
|
},
|
|
287
346
|
load(id) {
|
|
288
|
-
|
|
347
|
+
if (!id.startsWith(VIRTUAL)) return null;
|
|
348
|
+
const name = id.slice(VIRTUAL.length);
|
|
349
|
+
if (name.startsWith(STORE)) return { code: storeStandIn(name.slice(STORE.length)), syntheticNamedExports: "__store" };
|
|
350
|
+
return standIn(name, part);
|
|
289
351
|
},
|
|
290
352
|
async transform(code, id) {
|
|
291
353
|
if (id.startsWith(VIRTUAL)) return null;
|
|
292
|
-
const
|
|
293
|
-
|
|
354
|
+
const split = part === "browser" && store && /from\s*["']@znode\//.test(code) ? splitStoreImports(code, store) : code;
|
|
355
|
+
const output = await transformFonts(split, id, { prefix, outDir, fonts });
|
|
356
|
+
if (output === null) return split === code ? null : { code: split, map: null };
|
|
357
|
+
return { code: output, map: null };
|
|
294
358
|
},
|
|
295
359
|
};
|
|
296
360
|
}
|