znode-plugin 0.1.10 → 0.1.11
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 +14 -5
- package/lib/next-imports.mjs +59 -2
- package/lib/znode-package.mjs +52 -5
- package/package.json +1 -1
- package/types/extension-api.d.ts +1 -0
- package/types/images.d.ts +40 -0
package/README.md
CHANGED
|
@@ -248,11 +248,20 @@ package. The store then renders your copy in its place; everything else stays th
|
|
|
248
248
|
npx znode eject ProductBasicInfo # or by its slot: npx znode eject product.basicInfo
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
It copies the store's file to `src/components/ProductBasicInfo.tsx`
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
251
|
+
It copies the store's file to `src/components/ProductBasicInfo.tsx` and registers it in `src/browser.tsx` under its
|
|
252
|
+
slot. On the way:
|
|
253
|
+
|
|
254
|
+
- **Store imports** become the store's `@znode/...` aliases, or the store barrel that re-exports them
|
|
255
|
+
(`../../stores/modal` -> `@znode/base-components/stores`).
|
|
256
|
+
- **npm packages** the copy needs are installed at the **store's exact version** (`installed (store versions):
|
|
257
|
+
zustand@4.5.7`); the ones the store shares (`next-intl`, `lodash`) are not installed: the store's copy is used, and
|
|
258
|
+
`link-store` gives you their types. `--no-install` only prints the `npm install` line.
|
|
259
|
+
- **Images** it imports from the store (`../../assets/no-image.png`) are copied into `src/images/`. The build ships them
|
|
260
|
+
with the package and turns the import into `{ src, width, height }`, as Next.js does, so `<Image src={noImage}>` works
|
|
261
|
+
unchanged. Your own code can import images the same way (`import logo from "../images/logo.png"`).
|
|
262
|
+
|
|
263
|
+
Edit the copy, then `npm run build` and `npm run dev`. The copy is yours: store fixes to the component no longer reach
|
|
264
|
+
it, so compare it with the store's file after a store upgrade. `npx znode eject` with no name lists what can be ejected.
|
|
256
265
|
|
|
257
266
|
**The component has no slot yet?** `eject` offers to create one in the **base store** (the store you linked), then ejects:
|
|
258
267
|
|
package/lib/next-imports.mjs
CHANGED
|
@@ -150,6 +150,40 @@ const VALUES = {
|
|
|
150
150
|
|
|
151
151
|
const FONT_MODULE = "next/font/google";
|
|
152
152
|
const VIRTUAL = "\0znode-next:";
|
|
153
|
+
|
|
154
|
+
// Image imports, as Next.js does them (import noImage from "./no-image.png"): the file goes into the package (dist; the
|
|
155
|
+
// store serves a package's files at /api/extensions/files/<file>) and the import becomes { src, width, height }, what
|
|
156
|
+
// next/image takes for a static import, so code written for the store works unchanged.
|
|
157
|
+
const IMAGE = /\.(png|jpe?g|gif|webp|avif|svg|ico)$/i;
|
|
158
|
+
const FILES_URL = "/api/extensions/files";
|
|
159
|
+
|
|
160
|
+
// the image's size from its header (png, gif, jpeg, svg); 0 when it cannot be read (webp, avif, ico: pass width and
|
|
161
|
+
// height to next/image yourself)
|
|
162
|
+
function imageSize(b, ext) {
|
|
163
|
+
try {
|
|
164
|
+
if (ext === ".png" && b.readUInt32BE(0) === 0x89504e47) return { width: b.readUInt32BE(16), height: b.readUInt32BE(20) };
|
|
165
|
+
if (ext === ".gif" && b.toString("ascii", 0, 3) === "GIF") return { width: b.readUInt16LE(6), height: b.readUInt16LE(8) };
|
|
166
|
+
if ((ext === ".jpg" || ext === ".jpeg") && b[0] === 0xff && b[1] === 0xd8) {
|
|
167
|
+
for (let i = 2; i + 9 < b.length; ) {
|
|
168
|
+
if (b[i] !== 0xff) break;
|
|
169
|
+
const marker = b[i + 1];
|
|
170
|
+
if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) return { width: b.readUInt16BE(i + 7), height: b.readUInt16BE(i + 5) };
|
|
171
|
+
i += 2 + b.readUInt16BE(i + 2);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (ext === ".svg") {
|
|
175
|
+
const text = b.toString("utf8");
|
|
176
|
+
const tag = /<svg\b[^>]*>/i.exec(text)?.[0] || "";
|
|
177
|
+
const width = /\bwidth=["']([\d.]+)/.exec(tag)?.[1];
|
|
178
|
+
const height = /\bheight=["']([\d.]+)/.exec(tag)?.[1];
|
|
179
|
+
const box = /\bviewBox=["'][\d.\s,-]*?([\d.]+)[\s,]+([\d.]+)["']/.exec(tag);
|
|
180
|
+
return { width: Number(width || box?.[1] || 0), height: Number(height || box?.[2] || 0) };
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// a header too short to read: no size
|
|
184
|
+
}
|
|
185
|
+
return { width: 0, height: 0 };
|
|
186
|
+
}
|
|
153
187
|
const CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
154
188
|
|
|
155
189
|
// the stand-in module: each name reads the store's module when used, never at load (a server part is also loaded where
|
|
@@ -321,11 +355,22 @@ async function transformFonts(code, id, context) {
|
|
|
321
355
|
* The rollup plugin. part: "server" | "browser"; prefix: "<name>-<version>"; outDir: dist; fonts: collects what
|
|
322
356
|
* next/font/google built (files, css), which the build adds to the package's assets and stylesheet.
|
|
323
357
|
*/
|
|
324
|
-
export function znodeNext({ part, prefix, outDir, fonts, store, storeImports = [] }) {
|
|
358
|
+
export function znodeNext({ part, prefix, outDir, fonts, store, storeImports = [], sourceRoot, buildRoot }) {
|
|
325
359
|
return {
|
|
326
360
|
name: "znode-next",
|
|
327
|
-
resolveId(source) {
|
|
361
|
+
resolveId(source, importer) {
|
|
328
362
|
if (MODULES[source]) return VIRTUAL + source;
|
|
363
|
+
// an image the package's own code imports: the build compiles src/ into .build/ (code only), so a relative image
|
|
364
|
+
// path is looked up next to the source file the compiled one came from
|
|
365
|
+
if (IMAGE.test(source) && importer && source.startsWith(".")) {
|
|
366
|
+
const beside = path.resolve(path.dirname(importer), source);
|
|
367
|
+
if (fs.existsSync(beside)) return beside;
|
|
368
|
+
if (sourceRoot && buildRoot) {
|
|
369
|
+
const inSource = path.resolve(sourceRoot, path.relative(buildRoot, path.dirname(importer)), source);
|
|
370
|
+
if (fs.existsSync(inSource)) return inSource;
|
|
371
|
+
}
|
|
372
|
+
this.error(`image "${source}" (imported by ${path.basename(importer)}) not found: put it under src/ (e.g. src/assets/) and import it from there`);
|
|
373
|
+
}
|
|
329
374
|
if (SHARED_PACKAGES.has(source) || source.startsWith("@znode/") || source.startsWith(STORE_FILE)) {
|
|
330
375
|
if (!store) this.error(`"${source}" is a store module: connect the store first (npm run link-store -- <store repo>), which writes tsconfig.store.json`);
|
|
331
376
|
// a server part imports from the store's server registry (an allow-list: agents, texts and store context, logger,
|
|
@@ -356,6 +401,18 @@ export function znodeNext({ part, prefix, outDir, fonts, store, storeImports = [
|
|
|
356
401
|
return null;
|
|
357
402
|
},
|
|
358
403
|
load(id) {
|
|
404
|
+
if (IMAGE.test(id) && fs.existsSync(id)) {
|
|
405
|
+
// one copy per image, named by the package and a hash of its content (all packages' files share the store's
|
|
406
|
+
// plugin folder); it goes into the package's assets through the fonts list, whose files the build ships
|
|
407
|
+
const content = fs.readFileSync(id);
|
|
408
|
+
const hash = crypto.createHash("sha1").update(content).digest("hex").slice(0, 8);
|
|
409
|
+
const ext = path.extname(id).toLowerCase();
|
|
410
|
+
const name = `${prefix}-${path.basename(id, path.extname(id)).replace(/[^a-zA-Z0-9_-]/g, "-")}-${hash}${ext}`;
|
|
411
|
+
fs.writeFileSync(path.join(outDir, name), content);
|
|
412
|
+
if (!fonts.some((font) => font.files?.includes(name))) fonts.push({ files: [name], css: "" });
|
|
413
|
+
const { width, height } = imageSize(content, ext);
|
|
414
|
+
return `export default ${JSON.stringify({ src: `${FILES_URL}/${name}`, width, height })};`;
|
|
415
|
+
}
|
|
359
416
|
if (!id.startsWith(VIRTUAL)) return null;
|
|
360
417
|
const name = id.slice(VIRTUAL.length);
|
|
361
418
|
if (name.startsWith(STORE)) return { code: storeStandIn(name.slice(STORE.length), part), syntheticNamedExports: "__store" };
|
package/lib/znode-package.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import fs from "node:fs";
|
|
|
50
50
|
import path from "node:path";
|
|
51
51
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
52
52
|
import { builtinModules, createRequire } from "node:module";
|
|
53
|
+
import { createHash } from "node:crypto";
|
|
53
54
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
54
55
|
import vm from "node:vm";
|
|
55
56
|
import { TEMPLATES } from "./templates.mjs";
|
|
@@ -313,7 +314,8 @@ function build(target) {
|
|
|
313
314
|
` input: ${JSON.stringify(compiled)},`,
|
|
314
315
|
` output: { file: ${JSON.stringify(path.join(out, target))}, format: "es", sourcemap: true },`,
|
|
315
316
|
` plugins: [`,
|
|
316
|
-
|
|
317
|
+
// sourceRoot / buildRoot: an image a compiled file imports is found next to its source file (src/ -> .build/)
|
|
318
|
+
` znodeNext({ part: ${JSON.stringify(part)}, prefix: ${JSON.stringify(prefix)}, outDir: ${JSON.stringify(out)}, fonts, store, storeImports, sourceRoot: ${JSON.stringify(path.dirname(path.join(dir, entry)))}, buildRoot: ${JSON.stringify(tmp)} }),`,
|
|
317
319
|
` { name: "fonts-out", writeBundle() { fs.writeFileSync(${JSON.stringify(fontsFile)}, JSON.stringify(fonts)); fs.writeFileSync(${JSON.stringify(storeImportsFile)}, JSON.stringify(storeImports)); } },`,
|
|
318
320
|
` ],`,
|
|
319
321
|
`};`,
|
|
@@ -329,9 +331,10 @@ function build(target) {
|
|
|
329
331
|
|
|
330
332
|
// styles: concatenated into one stylesheet, after the fonts next/font/google built; assets (fonts, images) keep their
|
|
331
333
|
// names, which the stylesheet refers to
|
|
332
|
-
|
|
334
|
+
// (the fonts list also carries the images the code imports, with no CSS: next-imports.mjs)
|
|
335
|
+
if (manifest.styles?.length || fonts.some((font) => font.css)) {
|
|
333
336
|
const css = [
|
|
334
|
-
...fonts.map((font) => font.css),
|
|
337
|
+
...fonts.map((font) => font.css).filter(Boolean),
|
|
335
338
|
...(manifest.styles || []).flatMap((pattern) => glob(dir, pattern)).map((file) => fs.readFileSync(file, "utf8")),
|
|
336
339
|
].join("\n");
|
|
337
340
|
files.styles = [`${prefix}.css`];
|
|
@@ -569,7 +572,10 @@ function create(kind, name, dir) {
|
|
|
569
572
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
570
573
|
fs.writeFileSync(full, typeof content === "string" ? content : JSON.stringify(content, null, 2) + "\n");
|
|
571
574
|
}
|
|
572
|
-
if (kind !== "content")
|
|
575
|
+
if (kind !== "content") {
|
|
576
|
+
// the API types, with the image-import types they reference
|
|
577
|
+
for (const file of ["extension-api.d.ts", "images.d.ts"]) fs.copyFileSync(path.join(HERE, "..", "types", file), path.join(target, "src", file));
|
|
578
|
+
}
|
|
573
579
|
console.log(`created ${kind} ${name} in ${target}`);
|
|
574
580
|
console.log(" next: fill in label, publisher and description in znode-package.json, then run its build-and-upload.sh");
|
|
575
581
|
}
|
|
@@ -919,6 +925,26 @@ function ejectableComponents(root) {
|
|
|
919
925
|
return found.sort((a, b) => a.name.localeCompare(b.name));
|
|
920
926
|
}
|
|
921
927
|
|
|
928
|
+
// a store image a copied file imports (../../assets/no-image.png): copied into the package's src/images (not src/assets,
|
|
929
|
+
// which a package ships as it is: an imported image is shipped by the build, which turns the import into
|
|
930
|
+
// { src, width, height }), and the import's new path from the copy
|
|
931
|
+
const STORE_ASSET = /\.(png|jpe?g|gif|webp|avif|svg|ico)$/i;
|
|
932
|
+
function copyStoreAsset(project, absolute, copyFile) {
|
|
933
|
+
if (!fs.existsSync(absolute)) return null;
|
|
934
|
+
const assets = path.join(project, "src", "images");
|
|
935
|
+
fs.mkdirSync(assets, { recursive: true });
|
|
936
|
+
const content = fs.readFileSync(absolute);
|
|
937
|
+
let local = path.join(assets, path.basename(absolute));
|
|
938
|
+
// another image with the same name already there: this one gets a hash in its name
|
|
939
|
+
if (fs.existsSync(local) && !fs.readFileSync(local).equals(content)) {
|
|
940
|
+
const ext = path.extname(absolute);
|
|
941
|
+
local = path.join(assets, `${path.basename(absolute, ext)}-${createHash("sha1").update(content).digest("hex").slice(0, 6)}${ext}`);
|
|
942
|
+
}
|
|
943
|
+
if (!fs.existsSync(local)) fs.writeFileSync(local, content);
|
|
944
|
+
const spec = path.relative(path.dirname(copyFile), local).split(path.sep).join("/");
|
|
945
|
+
return { spec: spec.startsWith(".") ? spec : `./${spec}`, file: path.relative(project, local).split(path.sep).join("/") };
|
|
946
|
+
}
|
|
947
|
+
|
|
922
948
|
// a store file that re-exports `target` (export * from / export { … } from), followed through nested barrels
|
|
923
949
|
function reExports(barrel, target, seen = new Set()) {
|
|
924
950
|
if (!barrel || seen.has(barrel) || !fs.existsSync(barrel)) return false;
|
|
@@ -1003,9 +1029,18 @@ function ejectRouteCopy(project, s, store) {
|
|
|
1003
1029
|
// relative imports become the store's aliases (the package lives elsewhere)
|
|
1004
1030
|
const warnings = [];
|
|
1005
1031
|
text = text.replace(/((?:from|import)\s*\(?\s*["'])(\.{1,2}\/[^"']+)(["'])/g, (whole, before, spec, after, offset, source) => {
|
|
1032
|
+
// a store image: copied into src/images, imported from there (the build ships it)
|
|
1033
|
+
if (STORE_ASSET.test(spec)) {
|
|
1034
|
+
const asset = copyStoreAsset(project, path.resolve(path.dirname(storeFile), spec), path.join(project, "src", "server", "pages", `${name}.tsx`));
|
|
1035
|
+
if (!asset) {
|
|
1036
|
+
warnings.push(`image ${spec}: not found in the store; add it under src/images and import it from there`);
|
|
1037
|
+
return whole;
|
|
1038
|
+
}
|
|
1039
|
+
return `${before}${asset.spec}${after}`;
|
|
1040
|
+
}
|
|
1006
1041
|
const alias = storeAlias(store, path.resolve(path.dirname(storeFile), spec), { statement: source.slice(source.lastIndexOf("import", offset), offset) });
|
|
1007
1042
|
if (!alias) {
|
|
1008
|
-
warnings.push(`import ${spec}: no store alias for it; change it by hand`);
|
|
1043
|
+
warnings.push(`import ${spec}: no store alias for it (nor a store barrel that re-exports its names); change it by hand`);
|
|
1009
1044
|
return whole;
|
|
1010
1045
|
}
|
|
1011
1046
|
return `${before}${alias}${after}`;
|
|
@@ -1189,6 +1224,8 @@ async function eject(name, force, { slot: slotArg, yes, noInstall } = {}) {
|
|
|
1189
1224
|
const out = path.join(project, "src", "components", `${c.name}.tsx`);
|
|
1190
1225
|
if (fs.existsSync(out) && !force) fail(`${path.relative(project, out)} exists: delete it or run znode eject ${c.name} --force`);
|
|
1191
1226
|
const warnings = [];
|
|
1227
|
+
// the store images the copy imports, copied into src/images
|
|
1228
|
+
const copiedAssets = new Set();
|
|
1192
1229
|
let text = fs.readFileSync(c.file, "utf8");
|
|
1193
1230
|
// relative imports become the store's aliases (the package lives elsewhere); a style file stays the store's
|
|
1194
1231
|
text = text.replace(/((?:from|import)\s*\(?\s*["'])(\.{1,2}\/[^"']+)(["'])/g, (whole, before, spec, after, offset, source) => {
|
|
@@ -1197,6 +1234,15 @@ async function eject(name, force, { slot: slotArg, yes, noInstall } = {}) {
|
|
|
1197
1234
|
warnings.push(`style import ${spec}: kept out of the copy; the store's own stylesheet is loaded where the store renders it, add your CSS to src/styles/`);
|
|
1198
1235
|
return `${before}${spec}${after}`;
|
|
1199
1236
|
}
|
|
1237
|
+
if (STORE_ASSET.test(spec)) {
|
|
1238
|
+
const asset = copyStoreAsset(project, absolute, out);
|
|
1239
|
+
if (!asset) {
|
|
1240
|
+
warnings.push(`image ${spec}: not found in the store; add it under src/images and import it from there`);
|
|
1241
|
+
return whole;
|
|
1242
|
+
}
|
|
1243
|
+
copiedAssets.add(asset.file);
|
|
1244
|
+
return `${before}${asset.spec}${after}`;
|
|
1245
|
+
}
|
|
1200
1246
|
const alias = storeAlias(store, absolute, { statement: source.slice(source.lastIndexOf("import", offset), offset) });
|
|
1201
1247
|
if (!alias) {
|
|
1202
1248
|
warnings.push(`import ${spec}: no store alias for it (nor a store barrel that re-exports its names); change it by hand`);
|
|
@@ -1217,6 +1263,7 @@ async function eject(name, force, { slot: slotArg, yes, noInstall } = {}) {
|
|
|
1217
1263
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
1218
1264
|
fs.writeFileSync(out, text);
|
|
1219
1265
|
console.log(`ejected ${c.name} -> ${path.relative(project, out)}`);
|
|
1266
|
+
if (copiedAssets.size) console.log(`copied the store's images it imports: ${[...copiedAssets].join(", ")} (shipped with the package)`);
|
|
1220
1267
|
|
|
1221
1268
|
// npm packages the copy imports: the store's shared ones are used from the store (types through link-store), the others
|
|
1222
1269
|
// are installed in this package at the store's version (bundled into the package when it builds)
|
package/package.json
CHANGED
package/types/extension-api.d.ts
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Image imports in a package, as in a Next.js app: import noImage from "./no-image.png". The build ships the file with the
|
|
2
|
+
// package and the import is { src, width, height } (what next/image takes for a static import; width and height are 0
|
|
3
|
+
// for webp, avif and ico: pass them to next/image yourself).
|
|
4
|
+
interface IZnodeStaticImage {
|
|
5
|
+
src: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
declare module "*.png" {
|
|
10
|
+
const image: IZnodeStaticImage;
|
|
11
|
+
export default image;
|
|
12
|
+
}
|
|
13
|
+
declare module "*.jpg" {
|
|
14
|
+
const image: IZnodeStaticImage;
|
|
15
|
+
export default image;
|
|
16
|
+
}
|
|
17
|
+
declare module "*.jpeg" {
|
|
18
|
+
const image: IZnodeStaticImage;
|
|
19
|
+
export default image;
|
|
20
|
+
}
|
|
21
|
+
declare module "*.gif" {
|
|
22
|
+
const image: IZnodeStaticImage;
|
|
23
|
+
export default image;
|
|
24
|
+
}
|
|
25
|
+
declare module "*.webp" {
|
|
26
|
+
const image: IZnodeStaticImage;
|
|
27
|
+
export default image;
|
|
28
|
+
}
|
|
29
|
+
declare module "*.avif" {
|
|
30
|
+
const image: IZnodeStaticImage;
|
|
31
|
+
export default image;
|
|
32
|
+
}
|
|
33
|
+
declare module "*.ico" {
|
|
34
|
+
const image: IZnodeStaticImage;
|
|
35
|
+
export default image;
|
|
36
|
+
}
|
|
37
|
+
declare module "*.svg" {
|
|
38
|
+
const image: IZnodeStaticImage;
|
|
39
|
+
export default image;
|
|
40
|
+
}
|