nuxt-content-assets 0.9.0-alpha → 0.10.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/README.md CHANGED
@@ -40,10 +40,10 @@ I loved being in the mountains.
40
40
 
41
41
  Almost as much as being in the sea!
42
42
 
43
- <video src="media/seaside.mp4"></video>
43
+ :video{src="media/seaside.mp4"}
44
44
  ```
45
45
 
46
- The module supports a variety of [common tags](#how-it-works) and has additional goodies such as [image sizing](#image-sizing) and [live-reload](#live-reload).
46
+ At build time the module [collates and serves](#how-it-works) assets and content together.
47
47
 
48
48
  ## Demo
49
49
 
@@ -82,8 +82,6 @@ export default defineNuxtConfig({
82
82
 
83
83
  Run the dev server or build and local assets should now be served alongside markdown content.
84
84
 
85
- See the [How it works](#how-it-works) section for more information.
86
-
87
85
  ## Usage
88
86
 
89
87
  ### Overview
@@ -91,15 +89,24 @@ See the [How it works](#how-it-works) section for more information.
91
89
  Use relative paths anywhere within your documents:
92
90
 
93
91
  ```mdx
92
+ Images
94
93
  ![image](image.jpg)
95
- <video src="media/video.mp4" />
94
+
95
+ Links
96
+ [link](docs/article.txt)
97
+
98
+ Elements / components
99
+ :video{src="media/video.mp4"}
100
+
101
+ HTML
102
+ <iframe src="media/example.html" />
96
103
  ```
97
104
 
98
- Relative paths can be defined in frontmatter, as long as they are the only value:
105
+ Relative paths can be defined in frontmatter as long as they are the only value:
99
106
 
100
107
  ```mdx
101
108
  ---
102
- title: Portfolio Item 1
109
+ title: Portfolio
103
110
  images:
104
111
  - assets/image-1.jpg
105
112
  - assets/image-2.jpg
@@ -110,27 +117,32 @@ images:
110
117
  These values can then be passed to components:
111
118
 
112
119
  ```markdown
113
- ::gallery{:data="images"}
114
- ::
120
+ :image-gallery{:data="images"}
115
121
  ```
116
122
 
117
- See the [Demo](demo/content/recipes/index.md) for a component example.
123
+ See the Demo for [markup](demo/content/advanced/gallery.md) and [component](demo/components/content/ContentGallery.vue) examples.
118
124
 
119
125
  ### Live reload
120
126
 
121
- From version `0.9.0-alpha` assets are watched and live-reloaded!
127
+ In development, the module watches for asset additions, moves and deletes, and will update the browser live.
122
128
 
123
- Any additions, moves or deletes, or modifications to image content will be updated in the browser automatically.
129
+ If you delete an asset, it will be greyed out in the browser until you replace the file or modify the path to it.
130
+
131
+ If you edit an image, video, embed or iframe source, the content will update immediately, which is useful if you're looking to get that design just right!
124
132
 
125
133
  ### Image sizing
126
134
 
127
- The module can prevent content jumps by optionally writing image size information to generated `<img>` tags:
135
+ You can [configure](#image-size) the module to add image size attributes to generated `<img>` tags:
128
136
 
129
137
  ```html
130
- <img src="/image.jpg?width=640&height=480" width="640" height="480" style="aspect-ratio:640/480">
138
+ <img src="/image.jpg"
139
+ style="aspect-ratio:640/480"
140
+ width="640"
141
+ height="480"
142
+ >
131
143
  ```
132
144
 
133
- If you use [ProseImg](https://content.nuxtjs.org/api/components/prose) components, you can hook into these values via the `$attrs` property:
145
+ If you use [ProseImg](https://content.nuxtjs.org/api/components/prose) components, you can [hook into these values](demo/components/temp/ProseImg.vue) via the `$attrs` property:
134
146
 
135
147
  ```vue
136
148
  <template>
@@ -146,51 +158,32 @@ export default {
146
158
  </script>
147
159
  ```
148
160
 
149
- For more information see the [configuration](#image-size) section and [Demo](demo/components/temp/ProseImg.vue) for an example.
150
-
151
- ## How it works
152
-
153
- Nuxt Content Assets works by serving a _copy_ of your assets using [Nitro](https://nitro.unjs.io/guide/assets#custom-server-assets).
154
-
155
- When Nuxt builds, the following happens:
156
-
157
- - all content sources are scanned for valid assets
158
- - found assets are copied to a temporary build folder
159
- - relative paths in markdown are rewritten to point at this folder
160
- - metadata such as image size is written to a lookup file
161
- - finally, Nitro serves the folder for public access
162
-
163
- Note only specific tags and attributes are targeted in the parsing phase for rewriting:
161
+ If you pass [frontmatter](demo/content/advanced/gallery.md) to [custom components](demo/components/content/ContentImage.vue) set the `'url'` configuration option to encode size in the URL:
164
162
 
165
- ```html
166
- <a href="...">
167
- <img src="...">
168
- <video src="...">
169
- <audio src="...">
170
- <source src="...">
171
- <embed src="...">
172
- <iframe src="...">
163
+ ```
164
+ :image-gallery={:data="images"}
173
165
  ```
174
166
 
175
167
  ## Configuration
176
168
 
177
- You can configure the module like so:
169
+ The module can be configured in your Nuxt configuration file:
178
170
 
179
171
  ```ts
180
172
  // nuxt.config.ts
181
173
  export default defineNuxtConfig({
182
- 'content-assets': {
183
- // use aspect-ratio rather than attributes
174
+ contentAssets: {
175
+ // inject image sizes into the rendered html
184
176
  imageSize: 'style',
185
177
 
186
- // print debug messages to the console
178
+ // treat these extensions as content
179
+ contentExtensions: 'mdx? csv ya?ml json',
180
+
181
+ // output debug messages
187
182
  debug: true,
188
183
  }
189
184
  })
190
185
  ```
191
186
 
192
- Note that from version `0.9.0-alpha` the `output` location is no longer configurable; images are copied relative to their original locations.
193
-
194
187
  ### Image size
195
188
 
196
189
  You can add one or more image size hints to the generated images:
@@ -203,11 +196,11 @@ You can add one or more image size hints to the generated images:
203
196
 
204
197
  Pick from the following switches:
205
198
 
206
- | Switch | What it does |
207
- |---------|------------------------------------------------------------------------------|
208
- | `style` | Adds `style="aspect-ratio:..."` to any `<img>` tag |
209
- | `attrs` | Adds `width` and `height` attributes to any `<img>` tag |
210
- | `url` | Adds the `?width=...&height=...` query string to image frontmatter variables |
199
+ | Switch | What it does |
200
+ | ------- | ------------------------------------------------------------ |
201
+ | `style` | Adds `style="aspect-ratio:..."` to any `<img>` tag |
202
+ | `attrs` | Adds `width` and `height` attributes to any `<img>` tag |
203
+ | `url` | Adds a `?width=...&height=...` query string to image paths in frontmatter |
211
204
 
212
205
  Note: if you add `attrs` only, include the following CSS in your app:
213
206
 
@@ -218,6 +211,18 @@ img {
218
211
  }
219
212
  ```
220
213
 
214
+ ### Content extensions
215
+
216
+ This setting tells Nuxt Content to ignore anything that is **not** one of these file extensions:
217
+
218
+ ```
219
+ mdx? csv ya?ml json
220
+ ```
221
+
222
+ This way, you can use any **other** file type as an asset, without needing to explicitly configure extensions.
223
+
224
+ Generally, you shouldn't need to touch this setting.
225
+
221
226
  ### Debug
222
227
 
223
228
  If you want to see what the module does as it runs, set `debug` to true:
@@ -228,6 +233,16 @@ If you want to see what the module does as it runs, set `debug` to true:
228
233
  }
229
234
  ```
230
235
 
236
+ ## How it works
237
+
238
+ When Nuxt builds, the module scans all content sources for assets, copies them to an accessible public assets folder, and indexes path and image metadata.
239
+
240
+ After Nuxt Content has run the parsed content is traversed, and both element attributes and frontmatter properties are checked to see if they resolve to the indexed asset paths.
241
+
242
+ If they do, then the attribute or property is rewritten with the absolute path. If the asset is an image, then the element or path is optionally updated with size attributes or size query string.
243
+
244
+ Finally, Nitro serves the site, and any requests made to the transformed asset paths should be picked up and the *copied* asset served by the browser.
245
+
231
246
  ## Development
232
247
 
233
248
  Should you wish to develop the project, the scripts are:
package/dist/module.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  interface ModuleOptions {
4
- output?: string;
5
- imageSize?: string;
4
+ imageSize?: string | string[];
5
+ contentExtensions: string | string[];
6
6
  debug?: boolean;
7
7
  }
8
8
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
package/dist/module.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "nuxt-content-assets",
3
- "configKey": "content-assets",
3
+ "configKey": "contentAssets",
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.9.0-alpha"
7
+ "version": "0.10.0"
8
8
  }
package/dist/module.mjs CHANGED
@@ -1,18 +1,21 @@
1
1
  import * as Fs from 'fs';
2
2
  import * as Path from 'path';
3
3
  import Path__default from 'path';
4
- import { createResolver, defineNuxtModule, addPlugin } from '@nuxt/kit';
4
+ import { useNuxt, createResolver, defineNuxtModule, addPlugin } from '@nuxt/kit';
5
5
  import debounce from 'debounce';
6
6
  import getImageSize from 'image-size';
7
7
  import { createStorage } from 'unstorage';
8
8
  import githubDriver from 'unstorage/drivers/github';
9
9
  import fsDriver from 'unstorage/drivers/fs';
10
- import { hash } from 'ohash';
11
- import { WebSocketServer } from 'ws';
10
+ import 'ohash';
12
11
  import { listen } from 'listhen';
12
+ import { WebSocketServer, WebSocket } from 'ws';
13
13
 
14
- function matchWords(value) {
15
- return typeof value === "string" ? value.match(/\w+/g) || [] : [];
14
+ function matchTokens(value) {
15
+ const tokens = typeof value === "string" ? value.match(/[^\s,|]+/g) || [] : Array.isArray(value) ? value.filter((value2) => typeof value2 === "string").reduce((output, input) => {
16
+ return [...output, ...matchTokens(input)];
17
+ }, []) : [];
18
+ return Array.from(new Set(tokens));
16
19
  }
17
20
  function toPath(key) {
18
21
  return key.replaceAll(":", "/");
@@ -21,14 +24,23 @@ function deKey(path) {
21
24
  return path.replace(/^[^:]+:/, "");
22
25
  }
23
26
 
24
- const defaults$1 = {
25
- assetsDir: "/assets/",
26
- assetsPattern: "[path]/[file]"
27
+ const defaults = {
28
+ // inject image size into the rendered html
29
+ imageSize: "attrs",
30
+ // treat these extensions as content
31
+ contentExtensions: "mdx? csv ya?ml json",
32
+ // output debug messages
33
+ debug: false
27
34
  };
28
35
  const extensions = {
29
- image: matchWords("png jpg jpeg gif svg webp ico"),
30
- media: matchWords("mp3 m4a wav mp4 mov webm ogg avi flv avchd")
36
+ // used to get image size
37
+ image: matchTokens("png jpg jpeg gif svg webp ico"),
38
+ // unused for now
39
+ media: matchTokens("mp3 m4a wav mp4 mov webm ogg avi flv avchd")
31
40
  };
41
+ function getIgnores(extensions2) {
42
+ return `^((?!(${matchTokens(extensions2).join("|")})).)*$`;
43
+ }
32
44
 
33
45
  function isImage(path) {
34
46
  const ext = Path__default.extname(path).substring(1);
@@ -43,7 +55,7 @@ function isAsset(path) {
43
55
  }
44
56
 
45
57
  const moduleName = "nuxt-content-assets";
46
- const moduleKey = "content-assets";
58
+ const moduleKey = "contentAssets";
47
59
 
48
60
  function log(...data) {
49
61
  console.info(`[${moduleKey}]`, ...data);
@@ -85,7 +97,17 @@ function removeFolder(path) {
85
97
  }
86
98
  }
87
99
 
88
- function getAssetConfig(srcDir, srcAbs, pattern, hints) {
100
+ function getAssetPaths(srcDir, srcAbs) {
101
+ const srcRel = Path.relative(srcDir, srcAbs);
102
+ const srcAttr = "/" + srcRel;
103
+ const id = srcRel.replaceAll("/", ":");
104
+ return {
105
+ id,
106
+ srcRel,
107
+ srcAttr
108
+ };
109
+ }
110
+ function getAssetSizes(srcAbs, hints) {
89
111
  let width = void 0;
90
112
  let height = void 0;
91
113
  let ratio = void 0;
@@ -107,10 +129,12 @@ function getAssetConfig(srcDir, srcAbs, pattern, hints) {
107
129
  warn(`could not read image "${srcAbs}`);
108
130
  }
109
131
  }
110
- const srcRel = Path.relative(srcDir, srcAbs);
111
- const srcAttr = "/" + srcRel;
112
- const id = srcRel.replaceAll("/", ":");
113
- return { id, srcRel, srcAttr, width, height, ratio, query };
132
+ return {
133
+ width,
134
+ height,
135
+ ratio,
136
+ query
137
+ };
114
138
  }
115
139
 
116
140
  function makeStorage(source, key = "") {
@@ -201,81 +225,80 @@ function makeSourceManager(key, source, publicPath, callback) {
201
225
  };
202
226
  }
203
227
 
204
- const replacers = {
205
- key: (src) => Path__default.dirname(src).split("/").filter((e) => e).shift() || "",
206
- path: (src) => Path__default.dirname(src),
207
- folder: (src) => Path__default.dirname(src).replace(/[^/]+\//, ""),
208
- file: (src) => Path__default.basename(src),
209
- name: (src) => Path__default.basename(src, Path__default.extname(src)),
210
- extname: (src) => Path__default.extname(src),
211
- ext: (src) => Path__default.extname(src).substring(1),
212
- hash: (src) => hash({ src })
213
- };
214
- function interpolatePattern(pattern, src, warn = false) {
215
- return Path__default.join(pattern.replace(/\[\w+]/g, (match) => {
216
- const name = match.substring(1, match.length - 1);
217
- const fn = replacers[name];
218
- if (fn) {
219
- return fn(src);
220
- }
221
- if (warn) {
222
- log(`Unknown output token ${match}`, true);
223
- }
224
- return match;
225
- }));
226
- }
227
-
228
228
  function createWebSocket() {
229
229
  const wss = new WebSocketServer({ noServer: true });
230
230
  const serve = (req, socket = req.socket, head = "") => wss.handleUpgrade(req, socket, head, (client) => wss.emit("connection", client, req));
231
- const broadcast = (data, channel = "*") => {
232
- data = JSON.stringify({ channel, data });
231
+ const broadcast = (data) => {
232
+ data = JSON.stringify(data);
233
233
  for (const client of wss.clients) {
234
- try {
234
+ if (client.readyState === WebSocket.OPEN) {
235
235
  client.send(data);
236
- } catch (err) {
237
236
  }
238
237
  }
239
238
  };
240
239
  const handlers = [];
241
- const onMessage = (channel, callback) => {
242
- handlers.push({ channel, callback });
240
+ const addHandler = (callback) => {
241
+ handlers.push(callback);
243
242
  };
244
- wss.on("connection", (client) => {
245
- client.addEventListener("message", (event) => {
243
+ wss.on("connection", (socket) => {
244
+ socket.addEventListener("message", (event) => {
245
+ let data;
246
246
  try {
247
- const { channel, data } = JSON.parse(event.data || "{}");
248
- handlers.filter((handler) => handler.channel === channel || handler.channel === "*").forEach((handler) => handler.callback(data));
247
+ data = JSON.parse(event.data || "{}");
249
248
  } catch (err) {
250
249
  }
250
+ if (data) {
251
+ handlers.forEach((callback) => callback(data));
252
+ }
251
253
  });
252
254
  });
253
255
  return {
254
256
  wss,
255
257
  serve,
256
258
  broadcast,
257
- onMessage,
259
+ addHandler,
258
260
  close: () => {
259
261
  wss.clients.forEach((client) => client.close());
260
262
  return new Promise((resolve) => wss.close(resolve));
261
263
  }
262
264
  };
263
265
  }
266
+
267
+ function isObject(data) {
268
+ return data && typeof data === "object" && !Array.isArray(data);
269
+ }
270
+ function makeChannelBroker(ws2) {
271
+ const handlers = [];
272
+ const broadcast = (channel, data) => {
273
+ ws2.broadcast({ channel, data });
274
+ };
275
+ const addHandler = (channel, callback) => {
276
+ handlers.push({ channel, callback });
277
+ };
278
+ ws2.addHandler(function(message) {
279
+ if (isObject(message)) {
280
+ const { channel } = message;
281
+ handlers.filter((handler) => handler.channel === channel || handler.channel === "*").forEach((handler) => handler.callback(message));
282
+ }
283
+ });
284
+ return {
285
+ broadcast,
286
+ addHandler
287
+ };
288
+ }
264
289
  const ws = createWebSocket();
265
- let initialized = false;
266
- const defaults = {
267
- port: {
268
- port: 4001,
269
- portRange: [4001, 4040]
270
- },
271
- hostname: "localhost",
272
- showURL: false
273
- };
274
- function useSocketServer(nuxt, channel, onMessage) {
290
+ const broker = makeChannelBroker(ws);
291
+ async function setupSocketServer(channel, handler) {
292
+ const nuxt = useNuxt();
275
293
  nuxt.hook("nitro:init", async (nitro) => {
276
- if (!initialized) {
277
- initialized = true;
278
- const { server, url } = await listen(() => "Nuxt Sockets", defaults);
294
+ if (!nuxt._socketServer) {
295
+ const defaults = nuxt.options.runtimeConfig.content.watch.ws;
296
+ const { server, url } = await listen(() => "Nuxt Content Assets", {
297
+ port: defaults.port.port + 1,
298
+ hostname: defaults.hostname,
299
+ showURL: false
300
+ });
301
+ nuxt._socketServer = server;
279
302
  server.on("upgrade", ws.serve);
280
303
  nitro.options.runtimeConfig.public.sockets = {
281
304
  wsUrl: url.replace("http", "ws")
@@ -288,16 +311,16 @@ function useSocketServer(nuxt, channel, onMessage) {
288
311
  });
289
312
  const instance = {
290
313
  send(data) {
291
- ws.broadcast(data, channel);
314
+ broker.broadcast(channel, data);
292
315
  return this;
293
316
  },
294
- onMessage(callback) {
295
- ws.onMessage(channel, callback);
317
+ addHandler(callback) {
318
+ broker.addHandler(channel, callback);
296
319
  return this;
297
320
  }
298
321
  };
299
- if (onMessage) {
300
- instance.onMessage(onMessage);
322
+ if (handler) {
323
+ instance.addHandler(handler);
301
324
  }
302
325
  return instance;
303
326
  }
@@ -311,11 +334,7 @@ const module = defineNuxtModule({
311
334
  nuxt: "^3.0.0"
312
335
  }
313
336
  },
314
- defaults: {
315
- output: `${defaults$1.assetsDir}/${defaults$1.assetsPattern}`,
316
- imageSize: "",
317
- debug: false
318
- },
337
+ defaults,
319
338
  async setup(options, nuxt) {
320
339
  var _a, _b;
321
340
  const pluginPath = resolve("./runtime/plugin");
@@ -332,17 +351,14 @@ const module = defineNuxtModule({
332
351
  if (nuxt.options.content) {
333
352
  (_b = nuxt.options.content).ignores || (_b.ignores = []);
334
353
  }
335
- nuxt.options.content?.ignores.push("^((?!(md|json|yaml|csv)).)*$");
336
- const output = options.output || defaults$1.assetsDir;
337
- const matches = output.match(/([^[]+)(.*)?/);
338
- const assetsPattern = (matches ? matches[2] : "") || defaults$1.assetsPattern;
339
- interpolatePattern(assetsPattern, "", true);
340
- const imageFlags = matchWords(options.imageSize);
341
- const sources = nuxt.options._layers.map((layer) => layer.config?.content?.sources).reduce((output2, sources2) => {
354
+ const ignores = getIgnores(options.contentExtensions);
355
+ nuxt.options.content?.ignores.push(ignores);
356
+ const imageFlags = matchTokens(options.imageSize);
357
+ const sources = nuxt.options._layers.map((layer) => layer.config?.content?.sources).reduce((output, sources2) => {
342
358
  if (sources2) {
343
- Object.assign(output2, sources2);
359
+ Object.assign(output, sources2);
344
360
  }
345
- return output2;
361
+ return output;
346
362
  }, {});
347
363
  if (Object.keys(sources).length === 0 || !sources.content) {
348
364
  const content = nuxt.options.srcDir + "/content";
@@ -353,23 +369,9 @@ const module = defineNuxtModule({
353
369
  };
354
370
  }
355
371
  }
356
- addPlugin(resolve("./runtime/watcher"));
357
- const socket = nuxt.options.dev ? useSocketServer(nuxt, "content-assets") : null;
358
- function removeAsset(src) {
359
- const srcRel = Path.relative(publicPath, src);
360
- delete assets[srcRel];
361
- saveAssets();
362
- return "/" + srcRel;
363
- }
364
372
  function updateAsset(src) {
365
- const {
366
- srcRel,
367
- srcAttr,
368
- width,
369
- height,
370
- ratio,
371
- query
372
- } = getAssetConfig(publicPath, src, assetsPattern, imageFlags);
373
+ const { srcRel, srcAttr } = getAssetPaths(publicPath, src);
374
+ const { width, height, ratio, query } = getAssetSizes(src, imageFlags);
373
375
  assets[srcRel] = {
374
376
  srcRel,
375
377
  srcAttr,
@@ -381,22 +383,30 @@ const module = defineNuxtModule({
381
383
  saveAssets();
382
384
  return srcAttr;
383
385
  }
384
- function watchAsset(event, absTrg) {
385
- const srcAttr = event === "update" ? updateAsset(absTrg) : removeAsset(absTrg);
386
- if (socket) {
387
- socket.send({ event: "update", src: srcAttr });
388
- }
386
+ function removeAsset(src) {
387
+ const { srcRel, srcAttr } = getAssetPaths(publicPath, src);
388
+ delete assets[srcRel];
389
+ saveAssets();
390
+ return srcAttr;
389
391
  }
390
392
  const saveAssets = debounce(() => {
391
393
  writeFile(indexPath, assets);
392
394
  }, 50);
393
395
  const assets = {};
396
+ function onAssetChange(event, absTrg) {
397
+ const src = event === "update" ? updateAsset(absTrg) : removeAsset(absTrg);
398
+ if (socket) {
399
+ socket.send({ event, src });
400
+ }
401
+ }
402
+ addPlugin(resolve("./runtime/sockets/plugin"));
403
+ const socket = nuxt.options.dev ? await setupSocketServer("content-assets") : null;
394
404
  const managers = {};
395
405
  for (const [key, source] of Object.entries(sources)) {
396
406
  if (options.debug) {
397
407
  log(`Creating source "${key}"`);
398
408
  }
399
- managers[key] = makeSourceManager(key, source, publicPath, watchAsset);
409
+ managers[key] = makeSourceManager(key, source, publicPath, onAssetChange);
400
410
  }
401
411
  nuxt.hook("build:before", async function() {
402
412
  for (const [key, manager] of Object.entries(managers)) {
@@ -408,7 +418,6 @@ const module = defineNuxtModule({
408
418
  }
409
419
  });
410
420
  const virtualConfig = [
411
- // `export const assets = ${JSON.stringify(assets, null, ' ')}`,
412
421
  `export const cachePath = '${cachePath}'`
413
422
  ].join("\n");
414
423
  nuxt.hook("nitro:config", async (config) => {
@@ -1,2 +1,2 @@
1
1
  export declare const moduleName = "nuxt-content-assets";
2
- export declare const moduleKey = "content-assets";
2
+ export declare const moduleKey = "contentAssets";
@@ -1,2 +1,2 @@
1
1
  export const moduleName = "nuxt-content-assets";
2
- export const moduleKey = "content-assets";
2
+ export const moduleKey = "contentAssets";
@@ -1,9 +1,10 @@
1
1
  export declare const defaults: {
2
- assetsDir: string;
3
- assetsPattern: string;
2
+ imageSize: string;
3
+ contentExtensions: string;
4
+ debug: boolean;
4
5
  };
5
- export declare const tags: string[];
6
6
  export declare const extensions: {
7
7
  image: string[];
8
8
  media: string[];
9
9
  };
10
+ export declare function getIgnores(extensions: string | string[]): string;
@@ -1,10 +1,18 @@
1
- import { matchWords } from "./utils/string.mjs";
1
+ import { matchTokens } from "./utils/string.mjs";
2
2
  export const defaults = {
3
- assetsDir: "/assets/",
4
- assetsPattern: "[path]/[file]"
3
+ // inject image size into the rendered html
4
+ imageSize: "attrs",
5
+ // treat these extensions as content
6
+ contentExtensions: "mdx? csv ya?ml json",
7
+ // output debug messages
8
+ debug: false
5
9
  };
6
- export const tags = ["img", "video", "audio", "source", "embed", "iframe", "a"];
7
10
  export const extensions = {
8
- image: matchWords("png jpg jpeg gif svg webp ico"),
9
- media: matchWords("mp3 m4a wav mp4 mov webm ogg avi flv avchd")
11
+ // used to get image size
12
+ image: matchTokens("png jpg jpeg gif svg webp ico"),
13
+ // unused for now
14
+ media: matchTokens("mp3 m4a wav mp4 mov webm ogg avi flv avchd")
10
15
  };
16
+ export function getIgnores(extensions2) {
17
+ return `^((?!(${matchTokens(extensions2).join("|")})).)*$`;
18
+ }
@@ -1,7 +1,6 @@
1
1
  import Path from "path";
2
2
  import { visit } from "unist-util-visit";
3
3
  import { deKey, isValidAsset, toPath, walk } from "./utils/index.mjs";
4
- import { tags } from "./options.mjs";
5
4
  import { cachePath } from "#nuxt-content-assets";
6
5
  import { makeStorage } from "./services/index.mjs";
7
6
  async function updateAssets() {
@@ -32,11 +31,14 @@ const plugin = async (nitro) => {
32
31
  }
33
32
  }
34
33
  }, filter);
35
- visit(file.body, (n) => tags.includes(n.tag), (node) => {
36
- if (node.props.src) {
37
- const { srcAttr, width, height, ratio } = getAsset(srcDoc, node.props.src);
34
+ visit(file.body, (node) => node.type === "element", (node) => {
35
+ for (const [prop, value] of Object.entries(node.props)) {
36
+ if (typeof value !== "string") {
37
+ return;
38
+ }
39
+ const { srcAttr, width, height, ratio } = getAsset(srcDoc, value);
38
40
  if (srcAttr) {
39
- node.props.src = srcAttr;
41
+ node.props[prop] = srcAttr;
40
42
  if (width && height) {
41
43
  node.props.width = width;
42
44
  node.props.height = height;
@@ -44,12 +46,7 @@ const plugin = async (nitro) => {
44
46
  if (ratio) {
45
47
  node.props.style = `aspect-ratio:${ratio}`;
46
48
  }
47
- }
48
- } else if (node.tag === "a") {
49
- if (node.props.href) {
50
- const { srcAttr } = getAsset(srcDoc, node.props.href);
51
- if (srcAttr) {
52
- node.props.href = srcAttr;
49
+ if (node.tag === "a" && !node.props.target) {
53
50
  node.props.target = "_blank";
54
51
  }
55
52
  }
@@ -8,11 +8,25 @@ export type AssetConfig = {
8
8
  query?: string;
9
9
  };
10
10
  /**
11
- * Get config for asset
11
+ * Parse asset paths from absolute path
12
12
  *
13
13
  * @param srcDir The absolute path to the asset's source folder
14
14
  * @param srcAbs The absolute path to the asset itself
15
- * @param pattern The user-defined pattern to create the public src attribute
15
+ */
16
+ export declare function getAssetPaths(srcDir: string, srcAbs: string): {
17
+ id: any;
18
+ srcRel: string;
19
+ srcAttr: string;
20
+ };
21
+ /**
22
+ * Get asset image sizes
23
+ *
24
+ * @param srcAbs The absolute path to the asset itself
16
25
  * @param hints A list of named image size hints, i.e. 'style', 'attrs', etc
17
26
  */
18
- export declare function getAssetConfig(srcDir: string, srcAbs: string, pattern: string, hints: string[]): AssetConfig;
27
+ export declare function getAssetSizes(srcAbs: string, hints: string[]): {
28
+ width: number | undefined;
29
+ height: number | undefined;
30
+ ratio: string | undefined;
31
+ query: string | undefined;
32
+ };
@@ -1,7 +1,17 @@
1
1
  import * as Path from "path";
2
2
  import getImageSize from "image-size";
3
3
  import { isImage, warn } from "../utils/index.mjs";
4
- export function getAssetConfig(srcDir, srcAbs, pattern, hints) {
4
+ export function getAssetPaths(srcDir, srcAbs) {
5
+ const srcRel = Path.relative(srcDir, srcAbs);
6
+ const srcAttr = "/" + srcRel;
7
+ const id = srcRel.replaceAll("/", ":");
8
+ return {
9
+ id,
10
+ srcRel,
11
+ srcAttr
12
+ };
13
+ }
14
+ export function getAssetSizes(srcAbs, hints) {
5
15
  let width = void 0;
6
16
  let height = void 0;
7
17
  let ratio = void 0;
@@ -23,8 +33,10 @@ export function getAssetConfig(srcDir, srcAbs, pattern, hints) {
23
33
  warn(`could not read image "${srcAbs}`);
24
34
  }
25
35
  }
26
- const srcRel = Path.relative(srcDir, srcAbs);
27
- const srcAttr = "/" + srcRel;
28
- const id = srcRel.replaceAll("/", ":");
29
- return { id, srcRel, srcAttr, width, height, ratio, query };
36
+ return {
37
+ width,
38
+ height,
39
+ ratio,
40
+ query
41
+ };
30
42
  }
@@ -0,0 +1,2 @@
1
+ import { Callback, SocketInstance } from '../../types';
2
+ export declare function useSockets(channel: string, callback?: Callback): Promise<SocketInstance | null>;
@@ -0,0 +1,12 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ export function useSockets(channel, callback) {
3
+ const url = useRuntimeConfig().public.sockets?.wsUrl;
4
+ return new Promise(function(resolve) {
5
+ if (process.client && url) {
6
+ return import("./setup").then(({ setupSocketClient }) => {
7
+ return resolve(setupSocketClient(channel, callback));
8
+ });
9
+ }
10
+ resolve(null);
11
+ });
12
+ }
@@ -0,0 +1,5 @@
1
+ import { Callback } from '../../types';
2
+ export declare function createWebSocket(): {
3
+ send: (data: any) => void;
4
+ addHandler(callback: Callback): void;
5
+ } | null;
@@ -7,10 +7,10 @@ const logger = {
7
7
  warn: (...args) => console.warn(plugin, ...args)
8
8
  };
9
9
  let ws;
10
- export function useSocket(channel, callback) {
10
+ export function createWebSocket() {
11
11
  if (!window.WebSocket) {
12
- logger.warn("Unable to hot-reload images, your browser does not support WebSocket");
13
- return;
12
+ logger.warn("Your browser does not support WebSocket");
13
+ return null;
14
14
  }
15
15
  const onOpen = () => logger.log("WS connected!");
16
16
  const onError = (e) => {
@@ -19,30 +19,31 @@ export function useSocket(channel, callback) {
19
19
  connect(true);
20
20
  break;
21
21
  default:
22
- logger.warn("WS Error:", e);
22
+ logger.warn("Socket error:", e);
23
23
  break;
24
24
  }
25
25
  };
26
26
  const onClose = (e) => {
27
27
  if (e.code === 1e3 || e.code === 1005) {
28
- logger.log("WS closed!");
28
+ logger.log("Socket closed");
29
29
  } else {
30
30
  connect(true);
31
31
  }
32
32
  };
33
+ const handlers = [];
33
34
  const onMessage = (message) => {
35
+ let data;
34
36
  try {
35
- const data = JSON.parse(message.data);
36
- if (channel === data.channel) {
37
- return callback(data);
38
- }
37
+ data = JSON.parse(message.data);
39
38
  } catch (err) {
40
39
  logger.warn("Error parsing message:", message.data);
40
+ return;
41
41
  }
42
+ handlers.forEach((handler) => handler(data));
42
43
  };
43
44
  const send = (data) => {
44
45
  if (ws) {
45
- ws.send(JSON.stringify({ channel, data }));
46
+ ws.send(JSON.stringify(data));
46
47
  }
47
48
  };
48
49
  const connect = (retry = false) => {
@@ -61,7 +62,7 @@ export function useSocket(channel, callback) {
61
62
  const url = useRuntimeConfig().public.sockets?.wsUrl;
62
63
  if (url) {
63
64
  const wsUrl = `${url}ws`;
64
- logger.log(`watching for image updates on ${wsUrl}`);
65
+ logger.log(`Running on ${wsUrl}`);
65
66
  ws = new WebSocket(wsUrl);
66
67
  ws.onopen = onOpen;
67
68
  ws.onmessage = onMessage;
@@ -69,9 +70,15 @@ export function useSocket(channel, callback) {
69
70
  ws.onclose = onClose;
70
71
  }
71
72
  };
72
- connect();
73
+ if (!ws) {
74
+ connect();
75
+ }
73
76
  return {
74
- connect,
75
- send
77
+ send,
78
+ addHandler(callback) {
79
+ if (typeof callback === "function") {
80
+ handlers.push(callback);
81
+ }
82
+ }
76
83
  };
77
84
  }
@@ -0,0 +1,21 @@
1
+ import { defineNuxtPlugin } from "#imports";
2
+ import { useSockets } from "./composable.mjs";
3
+ export default defineNuxtPlugin(async () => {
4
+ if (process.client) {
5
+ const sockets = await useSockets("content-assets");
6
+ if (sockets) {
7
+ sockets.addHandler(({ data }) => {
8
+ const { event, src } = data;
9
+ if (src) {
10
+ const isUpdate = event === "update";
11
+ document.querySelectorAll(`:is(img, video, source, embed, iframe):where([src^="${src}"])`).forEach((el) => {
12
+ el.style.opacity = isUpdate ? "1" : "0.2";
13
+ if (isUpdate) {
14
+ el.setAttribute("src", `${src}?${(/* @__PURE__ */ new Date()).getTime()}`);
15
+ }
16
+ });
17
+ }
18
+ });
19
+ }
20
+ }
21
+ });
@@ -0,0 +1,2 @@
1
+ import { Callback, SocketInstance } from '../../types';
2
+ export declare function setupSocketClient(channel: string, callback?: Callback): SocketInstance | null;
@@ -0,0 +1,26 @@
1
+ import { createWebSocket } from "./factory.mjs";
2
+ const client = createWebSocket();
3
+ export function setupSocketClient(channel, callback) {
4
+ const instance = {
5
+ addHandler(callback2) {
6
+ if (client && typeof callback2 === "function") {
7
+ client.addHandler((data) => {
8
+ if (data.channel === channel) {
9
+ return callback2(data);
10
+ }
11
+ });
12
+ }
13
+ return this;
14
+ },
15
+ send(data) {
16
+ if (client) {
17
+ client.send({ channel, data });
18
+ }
19
+ return this;
20
+ }
21
+ };
22
+ if (callback) {
23
+ instance.addHandler(callback);
24
+ }
25
+ return instance;
26
+ }
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Get matched words from a string
2
+ * Get matched tokens (words, expressions) from a string or an array of possible strings
3
+ *
4
+ * Tokens may be separated by space, comma or pipe
3
5
  */
4
- export declare function matchWords(value?: string): string[];
6
+ export declare function matchTokens(value?: string | unknown[]): string[];
5
7
  export declare function toPath(key: string): string;
6
8
  export declare function toKey(path: string): any;
7
9
  export declare function deKey(path: string): string;
@@ -1,5 +1,8 @@
1
- export function matchWords(value) {
2
- return typeof value === "string" ? value.match(/\w+/g) || [] : [];
1
+ export function matchTokens(value) {
2
+ const tokens = typeof value === "string" ? value.match(/[^\s,|]+/g) || [] : Array.isArray(value) ? value.filter((value2) => typeof value2 === "string").reduce((output, input) => {
3
+ return [...output, ...matchTokens(input)];
4
+ }, []) : [];
5
+ return Array.from(new Set(tokens));
3
6
  }
4
7
  export function toPath(key) {
5
8
  return key.replaceAll(":", "/");
package/dist/types.d.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  import { ModuleOptions } from './module'
3
3
 
4
4
  declare module '@nuxt/schema' {
5
- interface NuxtConfig { ['content-assets']?: Partial<ModuleOptions> }
6
- interface NuxtOptions { ['content-assets']?: ModuleOptions }
5
+ interface NuxtConfig { ['contentAssets']?: Partial<ModuleOptions> }
6
+ interface NuxtOptions { ['contentAssets']?: ModuleOptions }
7
7
  }
8
8
 
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-content-assets",
3
- "version": "0.9.0-alpha",
3
+ "version": "0.10.0",
4
4
  "description": "Enable locally-located assets in Nuxt Content",
5
5
  "repository": "davestewart/nuxt-content-assets",
6
6
  "license": "MIT",
@@ -31,7 +31,6 @@
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",
35
34
  "@nuxt/kit": "^3.3.2",
36
35
  "debounce": "^1.2.1",
37
36
  "glob": "^9.3.2",
@@ -1,2 +0,0 @@
1
- import { Callback } from '../../utils';
2
- export declare function useSocketClient(channel: string, onMessage: Callback): void;
@@ -1,9 +0,0 @@
1
- import { useRuntimeConfig } from "#imports";
2
- export function useSocketClient(channel, onMessage) {
3
- const url = useRuntimeConfig().public.sockets?.wsUrl;
4
- if (process.client && url) {
5
- import("./composable").then(({ useSocket }) => {
6
- useSocket(channel, onMessage);
7
- });
8
- }
9
- }
@@ -1,4 +0,0 @@
1
- export declare function useSocket(channel: string, callback: (data: any) => void): {
2
- connect: (retry?: boolean) => void;
3
- send: (data: any) => void;
4
- } | undefined;
@@ -1,23 +0,0 @@
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
- };
@@ -1,78 +0,0 @@
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,18 +0,0 @@
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
- });