nuxt-content-assets 0.7.0 → 0.9.0-alpha

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.
@@ -0,0 +1,77 @@
1
+ import { useRuntimeConfig } from "#app";
2
+ const plugin = "[Content Assets]";
3
+ const logger = {
4
+ // eslint-disable-next-line no-console
5
+ log: (...args) => console.log(plugin, ...args),
6
+ // eslint-disable-next-line no-console
7
+ warn: (...args) => console.warn(plugin, ...args)
8
+ };
9
+ let ws;
10
+ export function useSocket(channel, callback) {
11
+ if (!window.WebSocket) {
12
+ logger.warn("Unable to hot-reload images, your browser does not support WebSocket");
13
+ return;
14
+ }
15
+ const onOpen = () => logger.log("WS connected!");
16
+ const onError = (e) => {
17
+ switch (e.code) {
18
+ case "ECONNREFUSED":
19
+ connect(true);
20
+ break;
21
+ default:
22
+ logger.warn("WS Error:", e);
23
+ break;
24
+ }
25
+ };
26
+ const onClose = (e) => {
27
+ if (e.code === 1e3 || e.code === 1005) {
28
+ logger.log("WS closed!");
29
+ } else {
30
+ connect(true);
31
+ }
32
+ };
33
+ const onMessage = (message) => {
34
+ try {
35
+ const data = JSON.parse(message.data);
36
+ if (channel === data.channel) {
37
+ return callback(data);
38
+ }
39
+ } catch (err) {
40
+ logger.warn("Error parsing message:", message.data);
41
+ }
42
+ };
43
+ const send = (data) => {
44
+ if (ws) {
45
+ ws.send(JSON.stringify({ channel, data }));
46
+ }
47
+ };
48
+ const connect = (retry = false) => {
49
+ if (retry) {
50
+ logger.log("WS reconnecting..");
51
+ setTimeout(connect, 1e3);
52
+ return;
53
+ }
54
+ if (ws) {
55
+ try {
56
+ ws.close();
57
+ } catch (err) {
58
+ }
59
+ ws = void 0;
60
+ }
61
+ const url = useRuntimeConfig().public.sockets?.wsUrl;
62
+ if (url) {
63
+ const wsUrl = `${url}ws`;
64
+ logger.log(`watching for image updates on ${wsUrl}`);
65
+ ws = new WebSocket(wsUrl);
66
+ ws.onopen = onOpen;
67
+ ws.onmessage = onMessage;
68
+ ws.onerror = onError;
69
+ ws.onclose = onClose;
70
+ }
71
+ };
72
+ connect();
73
+ return {
74
+ connect,
75
+ send
76
+ };
77
+ }
@@ -0,0 +1,23 @@
1
+ /// <reference types="ws" />
2
+ /// <reference types="node" />
3
+ import type { IncomingMessage } from 'http';
4
+ import { Nuxt } from '@nuxt/schema';
5
+ export type Callback = (data: any) => void;
6
+ export type Handler = {
7
+ channel: string;
8
+ callback: Callback;
9
+ };
10
+ /**
11
+ * WebSocket server useful for live content reload.
12
+ */
13
+ export declare function createWebSocket(): {
14
+ wss: import("ws").Server<import("ws").WebSocket>;
15
+ serve: (req: IncomingMessage, socket?: import("net").Socket, head?: any) => void;
16
+ broadcast: (data: any, channel?: string) => void;
17
+ onMessage: (channel: string, callback: Callback) => void;
18
+ close: () => Promise<unknown>;
19
+ };
20
+ export declare function useSocketServer(nuxt: Nuxt, channel: string, onMessage?: Callback): {
21
+ send(data: any): any;
22
+ onMessage(callback: Callback): any;
23
+ };
@@ -0,0 +1,78 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { listen } from "listhen";
3
+ export function createWebSocket() {
4
+ const wss = new WebSocketServer({ noServer: true });
5
+ const serve = (req, socket = req.socket, head = "") => wss.handleUpgrade(req, socket, head, (client) => wss.emit("connection", client, req));
6
+ const broadcast = (data, channel = "*") => {
7
+ data = JSON.stringify({ channel, data });
8
+ for (const client of wss.clients) {
9
+ try {
10
+ client.send(data);
11
+ } catch (err) {
12
+ }
13
+ }
14
+ };
15
+ const handlers = [];
16
+ const onMessage = (channel, callback) => {
17
+ handlers.push({ channel, callback });
18
+ };
19
+ wss.on("connection", (client) => {
20
+ client.addEventListener("message", (event) => {
21
+ try {
22
+ const { channel, data } = JSON.parse(event.data || "{}");
23
+ handlers.filter((handler) => handler.channel === channel || handler.channel === "*").forEach((handler) => handler.callback(data));
24
+ } catch (err) {
25
+ }
26
+ });
27
+ });
28
+ return {
29
+ wss,
30
+ serve,
31
+ broadcast,
32
+ onMessage,
33
+ close: () => {
34
+ wss.clients.forEach((client) => client.close());
35
+ return new Promise((resolve) => wss.close(resolve));
36
+ }
37
+ };
38
+ }
39
+ const ws = createWebSocket();
40
+ let initialized = false;
41
+ const defaults = {
42
+ port: {
43
+ port: 4001,
44
+ portRange: [4001, 4040]
45
+ },
46
+ hostname: "localhost",
47
+ showURL: false
48
+ };
49
+ export function useSocketServer(nuxt, channel, onMessage) {
50
+ nuxt.hook("nitro:init", async (nitro) => {
51
+ if (!initialized) {
52
+ initialized = true;
53
+ const { server, url } = await listen(() => "Nuxt Sockets", defaults);
54
+ server.on("upgrade", ws.serve);
55
+ nitro.options.runtimeConfig.public.sockets = {
56
+ wsUrl: url.replace("http", "ws")
57
+ };
58
+ nitro.hooks.hook("close", async () => {
59
+ await ws.close();
60
+ await server.close();
61
+ });
62
+ }
63
+ });
64
+ const instance = {
65
+ send(data) {
66
+ ws.broadcast(data, channel);
67
+ return this;
68
+ },
69
+ onMessage(callback) {
70
+ ws.onMessage(channel, callback);
71
+ return this;
72
+ }
73
+ };
74
+ if (onMessage) {
75
+ instance.onMessage(onMessage);
76
+ }
77
+ return instance;
78
+ }
@@ -1,10 +1,22 @@
1
- type GithubOptions = {
2
- repo: string;
3
- branch?: string;
4
- dir?: string;
5
- prefix?: string;
6
- ttl?: number;
7
- };
8
- export declare function getGithubAssets(key: string, source: GithubOptions, tempPath: string, extensions: string[]): Promise<string[]>;
9
- export declare function getFsAssets(path: string, extensions: string[]): string[];
10
- export {};
1
+ import { WatchEvent, Storage } from 'unstorage';
2
+ import { MountOptions } from '@nuxt/content';
3
+ /**
4
+ * Make a Storage instance
5
+ */
6
+ export declare function makeStorage(source: MountOptions | string, key?: string): Storage;
7
+ export interface SourceManager {
8
+ storage: Storage;
9
+ init: () => Promise<string[]>;
10
+ keys: () => Promise<string[]>;
11
+ }
12
+ /**
13
+ * Make a SourceManager instance
14
+ *
15
+ * Each Source Manager is responsible for mirroring source files to the public folder
16
+ *
17
+ * @param key
18
+ * @param source
19
+ * @param publicPath
20
+ * @param callback
21
+ */
22
+ export declare function makeSourceManager(key: string, source: MountOptions, publicPath: string, callback?: (event: WatchEvent, path: string) => void): SourceManager;
@@ -1,39 +1,92 @@
1
- import * as Fs from "fs";
2
1
  import * as Path from "path";
3
- import glob from "glob";
4
2
  import { createStorage } from "unstorage";
5
3
  import githubDriver from "unstorage/drivers/github";
6
- export async function getGithubAssets(key, source, tempPath, extensions) {
4
+ import fsDriver from "unstorage/drivers/fs";
5
+ import { warn, isAsset, toPath, removeFile, copyFile, writeBlob, writeFile, deKey } from "../utils/index.mjs";
6
+ export function makeStorage(source, key = "") {
7
7
  const storage = createStorage();
8
- storage.mount(key, githubDriver({
9
- repo: source.repo,
10
- branch: source.branch || "main",
11
- dir: source.dir || "/",
12
- ttl: source.ttl || 600
13
- }));
14
- const rx = new RegExp(`.${extensions.join("|")}$`);
15
- const keys = await storage.getKeys();
16
- const assetKeys = keys.filter((key2) => rx.test(key2));
17
- const assetItems = await Promise.all(assetKeys.map(async (id) => {
18
- const data = await storage.getItem(id);
19
- return { id, data };
20
- }));
21
- const prefix = source.prefix || "";
22
- const paths = [];
23
- for (const { id, data } of assetItems) {
24
- if (data) {
25
- const path = id.replaceAll(":", "/");
26
- const absPath = Path.join(tempPath, path.replace(key, `${key}/${prefix}`));
27
- const absFolder = Path.dirname(absPath);
28
- const buffer = data.constructor.name === "Blob" ? Buffer.from(await data.arrayBuffer()) : typeof data === "object" ? JSON.stringify(data, null, " ") : String(data);
29
- Fs.mkdirSync(absFolder, { recursive: true });
30
- Fs.writeFileSync(absPath, buffer);
31
- paths.push(absPath);
32
- }
8
+ const options = typeof source === "string" ? { driver: "fs", base: source } : source;
9
+ switch (options.driver) {
10
+ case "fs":
11
+ storage.mount(key, fsDriver({
12
+ ...options,
13
+ ignore: ["[^:]+?\\.md"]
14
+ }));
15
+ break;
16
+ case "github":
17
+ storage.mount(key, githubDriver({
18
+ branch: "main",
19
+ dir: "/",
20
+ ...options
21
+ }));
22
+ break;
33
23
  }
34
- return paths;
24
+ return storage;
35
25
  }
36
- export function getFsAssets(path, extensions) {
37
- const pattern = `${path}/**/*.{${extensions.join(",")}}`;
38
- return glob.globSync(pattern) || [];
26
+ export function makeSourceManager(key, source, publicPath, callback) {
27
+ async function onWatch(event, key2) {
28
+ if (isAsset(key2)) {
29
+ const path = event === "update" ? await copyItem(key2) : removeItem(key2);
30
+ if (callback) {
31
+ callback(event, path);
32
+ }
33
+ }
34
+ }
35
+ function getRelSrc(key2) {
36
+ return toPath(key2).replace(/\w+/, "").replace(source.prefix || "", "");
37
+ }
38
+ function getAbsSrc(key2) {
39
+ return Path.join(source.base, getRelSrc(key2));
40
+ }
41
+ function getRelTrg(key2) {
42
+ return Path.join(source.prefix || "", toPath(deKey(key2)));
43
+ }
44
+ function getAbsTrg(key2) {
45
+ return Path.join(publicPath, getRelTrg(key2));
46
+ }
47
+ function removeItem(key2) {
48
+ const absTrg = getAbsTrg(key2);
49
+ removeFile(absTrg);
50
+ return absTrg;
51
+ }
52
+ async function copyItem(key2) {
53
+ const absTrg = getAbsTrg(key2);
54
+ const driver = source.driver;
55
+ if (driver === "fs") {
56
+ const absSrc = getAbsSrc(key2);
57
+ copyFile(absSrc, absTrg);
58
+ } else if (driver === "github") {
59
+ try {
60
+ const data = await storage.getItem(key2);
61
+ if (data) {
62
+ data?.constructor.name === "Blob" ? await writeBlob(absTrg, data) : writeFile(absTrg, data);
63
+ } else {
64
+ warn(`No data for key "${key2}"`);
65
+ }
66
+ } catch (err) {
67
+ warn(err.message);
68
+ }
69
+ }
70
+ return absTrg;
71
+ }
72
+ async function getKeys() {
73
+ const keys = await storage.getKeys();
74
+ return keys.filter(isAsset);
75
+ }
76
+ async function init() {
77
+ const keys = await getKeys();
78
+ const paths = [];
79
+ for (const key2 of keys) {
80
+ const path = await copyItem(key2);
81
+ paths.push(path);
82
+ }
83
+ return paths;
84
+ }
85
+ const storage = makeStorage(source, key);
86
+ storage.watch(onWatch);
87
+ return {
88
+ storage,
89
+ init,
90
+ keys: getKeys
91
+ };
39
92
  }
@@ -3,14 +3,18 @@
3
3
  */
4
4
  export declare function isRelative(path: string): boolean;
5
5
  /**
6
- * Test path for image extension
6
+ * Test path or id for image extension
7
7
  */
8
8
  export declare function isImage(path: string): boolean;
9
9
  /**
10
- * Test path for asset extension
10
+ * Test path or id is markdown
11
+ */
12
+ export declare function isArticle(path: string): boolean;
13
+ /**
14
+ * Test path or id is asset
11
15
  */
12
16
  export declare function isAsset(path: string): boolean;
13
17
  /**
14
- * Test if value is a valid asset
18
+ * Test if value is a relative asset
15
19
  */
16
20
  export declare function isValidAsset(value?: string): boolean;
@@ -1,15 +1,18 @@
1
1
  import Path from "path";
2
- import { extensions, imageExtensions } from "../options.mjs";
2
+ import { extensions } from "../options.mjs";
3
3
  export function isRelative(path) {
4
4
  return !(path.startsWith("http") || Path.isAbsolute(path));
5
5
  }
6
6
  export function isImage(path) {
7
7
  const ext = Path.extname(path).substring(1);
8
- return imageExtensions.includes(ext);
8
+ return extensions.image.includes(ext);
9
+ }
10
+ export function isArticle(path) {
11
+ return /\.mdx?$/.test(path);
9
12
  }
10
13
  export function isAsset(path) {
11
- const ext = Path.extname(path).substring(1);
12
- return extensions.includes(ext);
14
+ const ext = Path.extname(path);
15
+ return !!ext && ext !== ".DS_Store" && !isArticle(path);
13
16
  }
14
17
  export function isValidAsset(value) {
15
18
  return typeof value === "string" && isAsset(value) && isRelative(value);
@@ -1,4 +1,7 @@
1
+ export declare function readFile(path: string, asJson?: boolean): any;
1
2
  export declare function writeFile(path: string, data: null | string | number | boolean | object): void;
3
+ export declare function writeBlob(path: string, data: object): Promise<void>;
2
4
  export declare function copyFile(src: string, trg: string): void;
5
+ export declare function removeFile(src: string): void;
3
6
  export declare function createFolder(path: string): void;
4
7
  export declare function removeFolder(path: string): void;
@@ -1,14 +1,26 @@
1
1
  import * as Path from "path";
2
2
  import * as Fs from "fs";
3
+ export function readFile(path, asJson = false) {
4
+ const text = Fs.readFileSync(path, { encoding: "utf8" });
5
+ return asJson ? JSON.parse(text) : text;
6
+ }
3
7
  export function writeFile(path, data) {
4
8
  const text = typeof data === "object" ? JSON.stringify(data, null, " ") : String(data);
5
9
  createFolder(Path.dirname(path));
6
10
  Fs.writeFileSync(path, text, { encoding: "utf8" });
7
11
  }
12
+ export async function writeBlob(path, data) {
13
+ const buffer = Buffer.from(await data.arrayBuffer());
14
+ createFolder(Path.dirname(path));
15
+ Fs.writeFileSync(path, buffer);
16
+ }
8
17
  export function copyFile(src, trg) {
9
18
  createFolder(Path.dirname(trg));
10
19
  Fs.copyFileSync(src, trg);
11
20
  }
21
+ export function removeFile(src) {
22
+ Fs.rmSync(src);
23
+ }
12
24
  export function createFolder(path) {
13
25
  Fs.mkdirSync(path, { recursive: true });
14
26
  }
@@ -2,3 +2,6 @@
2
2
  * Get matched words from a string
3
3
  */
4
4
  export declare function matchWords(value?: string): string[];
5
+ export declare function toPath(key: string): string;
6
+ export declare function toKey(path: string): any;
7
+ export declare function deKey(path: string): string;
@@ -1,3 +1,12 @@
1
1
  export function matchWords(value) {
2
2
  return typeof value === "string" ? value.match(/\w+/g) || [] : [];
3
3
  }
4
+ export function toPath(key) {
5
+ return key.replaceAll(":", "/");
6
+ }
7
+ export function toKey(path) {
8
+ return path.replaceAll("/", ":");
9
+ }
10
+ export function deKey(path) {
11
+ return path.replace(/^[^:]+:/, "");
12
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { defineNuxtPlugin } from "#imports";
2
+ import { useSocketClient } from "./services/sockets/client.mjs";
3
+ export default defineNuxtPlugin(async () => {
4
+ if (process.client) {
5
+ void useSocketClient("content-assets", ({ data }) => {
6
+ const { event, src } = data;
7
+ if (src) {
8
+ const isUpdate = event === "update";
9
+ document.querySelectorAll(`img[src^="${src}"]`).forEach((img) => {
10
+ img.style.opacity = isUpdate ? "1" : "0.2";
11
+ if (isUpdate) {
12
+ img.setAttribute("src", src);
13
+ }
14
+ });
15
+ }
16
+ });
17
+ }
18
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-content-assets",
3
- "version": "0.7.0",
3
+ "version": "0.9.0-alpha",
4
4
  "description": "Enable locally-located assets in Nuxt Content",
5
5
  "repository": "davestewart/nuxt-content-assets",
6
6
  "license": "MIT",
@@ -31,12 +31,16 @@
31
31
  "release:dry": "npm run lint && npm run test && npm run build && npm publish --dry-run"
32
32
  },
33
33
  "dependencies": {
34
+ "@davestewart/nuxt-sockets": "^0.1.0",
34
35
  "@nuxt/kit": "^3.3.2",
36
+ "debounce": "^1.2.1",
35
37
  "glob": "^9.3.2",
36
38
  "image-size": "^1.0.2",
39
+ "listhen": "^1.0.4",
37
40
  "ohash": "^1.0.0",
38
41
  "unist-util-visit": "^4.1.2",
39
- "unstorage": "^1.4.1"
42
+ "unstorage": "^1.4.1",
43
+ "ws": "^8.13.0"
40
44
  },
41
45
  "peerDependencies": {
42
46
  "@nuxt/content": "latest"
@@ -47,6 +51,8 @@
47
51
  "@nuxt/module-builder": "^0.2.1",
48
52
  "@nuxt/schema": "^3.3.2",
49
53
  "@nuxt/test-utils": "^3.3.2",
54
+ "@types/debounce": "^1.2.1",
55
+ "@types/ws": "^8.5.4",
50
56
  "changelogen": "^0.5.1",
51
57
  "eslint": "^8.36.0",
52
58
  "nuxt": "^3.3.2",