nuxt-content-assets 0.6.0 → 0.7.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
@@ -47,7 +47,13 @@ That's it!
47
47
 
48
48
  ## Demo
49
49
 
50
- To run the demo online, go to:
50
+ To view the demo locally, run:
51
+
52
+ ```
53
+ npm run dev
54
+ ```
55
+
56
+ To view the demo online, visit:
51
57
 
52
58
  - https://stackblitz.com/github/davestewart/nuxt-content-assets?file=demo%2Fapp.vue
53
59
 
@@ -55,12 +61,6 @@ You can browse the demo files in:
55
61
 
56
62
  - https://github.com/davestewart/nuxt-content-assets/tree/main/demo
57
63
 
58
- To run the demo locally, clone the application and from the root, run:
59
-
60
- ```
61
- npm run demo
62
- ```
63
-
64
64
  ## Setup
65
65
 
66
66
  Install the dependency:
@@ -97,7 +97,7 @@ Relative paths are defined by anything not starting with a slash or `http`, for
97
97
 
98
98
  ```
99
99
  image.jpg
100
- images/featured.png
100
+ assets/featured.png
101
101
  ../assets/cv.pdf
102
102
  ```
103
103
 
@@ -119,20 +119,20 @@ However, you can use relative paths in frontmatter:
119
119
  ---
120
120
  title: Portfolio Item 1
121
121
  images:
122
- - images/image-1.jpg
123
- - images/image-2.jpg
124
- - images/image-3.jpg
122
+ - assets/image-1.jpg
123
+ - assets/image-2.jpg
124
+ - assets/image-3.jpg
125
125
  ---
126
126
  ```
127
127
 
128
128
  Then pass these to components like so:
129
129
 
130
130
  ```markdown
131
- ::gallery{:images="images"}
131
+ ::gallery{:data="images"}
132
132
  ::
133
133
  ```
134
134
 
135
- > Note: to pass size hints in frontmatter, set the `imageSize` configuration option to `'url'`
135
+ > Note: to pass size hints in frontmatter, set the `imageSize` configuration [option](#image-size) to `'url'`
136
136
 
137
137
  See the [Demo](demo/content/recipes/index.md) for an example.
138
138
 
@@ -156,7 +156,7 @@ The module can prevent content jumps by optionally writing image size informatio
156
156
  <img src="/image.jpg?width=640&height=480" width="640" height="480" style="aspect-ratio:640/480">
157
157
  ```
158
158
 
159
- If you use custom [ProseImg](https://content.nuxtjs.org/api/components/prose) components, you can pass these values to your own markup:
159
+ If you use [ProseImg](https://content.nuxtjs.org/api/components/prose) components, you can pass these values to your own markup:
160
160
 
161
161
  ```vue
162
162
  <template>
@@ -172,7 +172,7 @@ export default {
172
172
  </script>
173
173
  ```
174
174
 
175
- See the [configuration](#image-size) section to add this, and the [Demo](demo/components/_content/ProseImg.vue) for an example.
175
+ See the [configuration](#image-size) section to add this, and the [Demo](demo/components/temp/ProseImg.vue) for an example.
176
176
 
177
177
  ### Build
178
178
 
@@ -196,7 +196,7 @@ export default defineNuxtConfig({
196
196
  // where to generate and serve the assets from
197
197
  output: 'assets/content/[path]/[file]',
198
198
 
199
- // add additional extensions
199
+ // include additional extensions
200
200
  additionalExtensions: 'html',
201
201
 
202
202
  // completely replace supported extensions
@@ -216,38 +216,43 @@ export default defineNuxtConfig({
216
216
  The output path can be customised using a template string:
217
217
 
218
218
  ```
219
- assets/content/[name]-[hash].[ext]
219
+ assets/[name]-[hash].[ext]
220
220
  ```
221
221
 
222
- The first part of the path should be public root-relative folder:
222
+ The first part of the path is where you want content assets to be served from:
223
223
 
224
224
  ```
225
- assets/img/content
225
+ assets/
226
226
  ```
227
227
 
228
- The optional second part of the path indicates the relative location of each image:
228
+ The optional second part of the path indicates the relative location of each asset.
229
229
 
230
- | Token | Description | Example |
231
- |-------------|--------------------------------------------|--------------------|
232
- | `[folder]` | The relative folder of the file | `posts/2023-01-01` |
233
- | `[file]` | The full filename of the file | `featured.jpg` |
234
- | `[name]` | The name of the file without the extension | `featured` |
235
- | `[hash]` | A hash of the absolute source path | `9M00N4l9A0` |
236
- | `[extname]` | The full extension with the dot | `.jpg` |
237
- | `[ext]` | The extension without the dot | `jpg` |
230
+ The table below shows replacements for the asset `content/posts/2023-01-01/featured.jpg`:
231
+
232
+ | Token | Description | Example |
233
+ |-------------|----------------------------------------------------------------------------------------------------|----------------------------|
234
+ | `[key]` | The config key of the source (see [sources](https://content.nuxtjs.org/api/configuration#sources)) | `content` |
235
+ | `[path]` | The relative path of the source | `content/posts/2023-01-01` |
236
+ | `[folder]` | The relative path of the file's folder | `posts/2023-01-01` |
237
+ | `[file]` | The full filename of the file | `featured.jpg` |
238
+ | `[name]` | The name of the file without the extension | `featured` |
239
+ | `[hash]` | A hash of the absolute source path | `9M00N4l9A0` |
240
+ | `[extname]` | The full extension with the dot | `.jpg` |
241
+ | `[ext]` | The extension without the dot | `jpg` |
238
242
 
239
243
  For example:
240
244
 
241
- | Template | Output |
242
- |--------------------------------------|----------------------------------------------------|
243
- | `assets/img/content/[folder]/[file]` | `assets/img/content/posts/2023-01-01/featured.jpg` |
244
- | `assets/img/[name]-[hash].[ext]` | `assets/img/featured-9M00N4l9A0.jpg` |
245
- | `content/[hash].[ext]` | `content/9M00N4l9A0.jpg` |
245
+ | Template | Output |
246
+ |------------------------------|------------------------------------------------|
247
+ | `assets/[path]/[file]` | `assets/content/posts/2023-01-01/featured.jpg` |
248
+ | `assets/[folder]/[file]` | `assets/posts/2023-01-01/featured.jpg` |
249
+ | `assets/[name]-[hash].[ext]` | `assets/featured-9M00N4l9A0.jpg` |
250
+ | `assets/[hash].[ext]` | `assets/9M00N4l9A0.jpg` |
246
251
 
247
252
  Note that the module defaults to:
248
253
 
249
254
  ```
250
- /assets/content/[folder]/[file]
255
+ /assets/[path]/[file]
251
256
  ```
252
257
 
253
258
  ### Extensions
@@ -323,30 +328,51 @@ If you want to see what the module does as it runs, set `debug` to true:
323
328
 
324
329
  Should you wish to develop the project, the scripts are:
325
330
 
331
+ Develop the module itself:
332
+
326
333
  ```bash
327
334
  # install dependencies
328
335
  npm install
329
336
 
330
- # generate type stubs
331
- npm run dev:prepare
332
-
333
- # develop with the demo
337
+ # develop (runs using the demo)
334
338
  npm run dev
335
339
 
336
- # build the demo
337
- npm run dev:build
338
-
339
340
  # run eslint
340
341
  npm run lint
341
342
 
342
343
  # run vitest
343
344
  npm run test
344
345
  npm run test:watch
346
+ ```
347
+
348
+ Build and check the demo:
349
+
350
+ ```bash
351
+ # generate demo type stubs
352
+ npm run demo:prepare
345
353
 
354
+ # generate the demo output
355
+ npm run demo:generate
356
+
357
+ # serve the demo output
358
+ npm run demo:serve
359
+
360
+ # build the demo
361
+ npm run demo:build
362
+ ```
363
+
364
+ Make a new release:
365
+
366
+ ```bash
346
367
  # release new version
347
368
  npm run release
369
+
370
+ # dry run the release
371
+ npm run release:dry
348
372
  ```
349
373
 
374
+ Make sure to edit changelog and update `package.json` version first!
375
+
350
376
  <!-- Badges -->
351
377
  [npm-version-src]: https://img.shields.io/npm/v/nuxt-content-assets/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
352
378
  [npm-version-href]: https://npmjs.com/package/nuxt-content-assets
package/dist/module.json CHANGED
@@ -4,5 +4,5 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.6.0"
7
+ "version": "0.7.0"
8
8
  }
package/dist/module.mjs CHANGED
@@ -1,18 +1,20 @@
1
- import { createResolver, defineNuxtModule, addTemplate } from '@nuxt/kit';
2
- import getImageSize from 'image-size';
3
- import glob from 'glob';
4
1
  import * as Fs from 'fs';
5
2
  import * as Path from 'path';
6
3
  import Path__default from 'path';
4
+ import { createResolver, defineNuxtModule, addTemplate } from '@nuxt/kit';
5
+ import getImageSize from 'image-size';
7
6
  import { hash } from 'ohash';
7
+ import glob from 'glob';
8
+ import { createStorage } from 'unstorage';
9
+ import githubDriver from 'unstorage/drivers/github';
8
10
 
9
11
  function matchWords(value) {
10
12
  return typeof value === "string" ? value.match(/\w+/g) || [] : [];
11
13
  }
12
14
 
13
15
  const defaults = {
14
- assetsDir: "assets/content",
15
- assetsPattern: "[folder]/[file]"
16
+ assetsDir: "/assets/",
17
+ assetsPattern: "[path]/[file]"
16
18
  };
17
19
  const imageExtensions = matchWords("png jpg jpeg gif svg webp ico");
18
20
  const mediaExtensions = matchWords("mp3 m4a wav mp4 mov webm ogg avi flv avchd");
@@ -28,40 +30,57 @@ function isImage(path) {
28
30
  return imageExtensions.includes(ext);
29
31
  }
30
32
 
31
- function getSources(sources) {
32
- return Object.keys(sources).reduce((output, key) => {
33
- const source = sources[key];
34
- if (source) {
35
- const { driver, base } = source;
36
- if (driver === "fs") {
37
- output[key] = base;
38
- }
39
- }
40
- return output;
41
- }, {});
42
- }
43
-
44
33
  const moduleName = "nuxt-content-assets";
45
34
  const moduleKey = "content-assets";
46
35
 
47
36
  function log(...data) {
48
37
  console.info(`[${moduleKey}]`, ...data);
49
38
  }
39
+ function warn(...data) {
40
+ console.warn(`[${moduleKey}]`, ...data);
41
+ }
42
+ function list(message, items) {
43
+ log(`${message}:
44
+
45
+ ${items.map((item) => ` - ${item}`).join("\n")}
46
+ `);
47
+ }
48
+
49
+ function writeFile(path, data) {
50
+ const text = typeof data === "object" ? JSON.stringify(data, null, " ") : String(data);
51
+ createFolder(Path.dirname(path));
52
+ Fs.writeFileSync(path, text, { encoding: "utf8" });
53
+ }
54
+ function copyFile(src, trg) {
55
+ createFolder(Path.dirname(trg));
56
+ Fs.copyFileSync(src, trg);
57
+ }
58
+ function createFolder(path) {
59
+ Fs.mkdirSync(path, { recursive: true });
60
+ }
61
+ function removeFolder(path) {
62
+ const isDownstream = path.startsWith(Path.resolve());
63
+ if (isDownstream) {
64
+ Fs.rmSync(path, { recursive: true, force: true });
65
+ }
66
+ }
50
67
 
51
68
  const replacers = {
52
- folder: (src, dir) => Path__default.dirname(src.replace(dir, "")),
69
+ key: (src) => Path__default.dirname(src).split("/").filter((e) => e).shift() || "",
70
+ path: (src) => Path__default.dirname(src),
71
+ folder: (src) => Path__default.dirname(src).replace(/[^/]+\//, ""),
53
72
  file: (src) => Path__default.basename(src),
54
73
  name: (src) => Path__default.basename(src, Path__default.extname(src)),
55
74
  extname: (src) => Path__default.extname(src),
56
75
  ext: (src) => Path__default.extname(src).substring(1),
57
76
  hash: (src) => hash({ src })
58
77
  };
59
- function interpolatePattern(pattern, src, dir, warn = false) {
78
+ function interpolatePattern(pattern, src, warn = false) {
60
79
  return Path__default.join(pattern.replace(/\[\w+]/g, (match) => {
61
80
  const name = match.substring(1, match.length - 1);
62
81
  const fn = replacers[name];
63
82
  if (fn) {
64
- return fn(src, dir);
83
+ return fn(src);
65
84
  }
66
85
  if (warn) {
67
86
  log(`Unknown output token ${match}`, true);
@@ -70,6 +89,69 @@ function interpolatePattern(pattern, src, dir, warn = false) {
70
89
  }));
71
90
  }
72
91
 
92
+ function getAssetConfig(srcDir, srcAbs, pattern, hints) {
93
+ let width = void 0;
94
+ let height = void 0;
95
+ let ratio = void 0;
96
+ let query = void 0;
97
+ if (hints.length && isImage(srcAbs)) {
98
+ try {
99
+ const size = getImageSize(srcAbs);
100
+ if (hints.includes("style")) {
101
+ ratio = `${size.width}/${size.height}`;
102
+ }
103
+ if (hints.includes("attrs")) {
104
+ width = size.width;
105
+ height = size.height;
106
+ }
107
+ if (hints.includes("url")) {
108
+ query = `?width=${width}&height=${height}`;
109
+ }
110
+ } catch (err) {
111
+ warn(`could not read image "${srcAbs}`);
112
+ }
113
+ }
114
+ const srcRel = Path.basename(srcDir) + srcAbs.substring(srcDir.length);
115
+ const srcAttr = interpolatePattern(pattern, srcRel);
116
+ const id = srcRel.replaceAll("/", ":");
117
+ return { id, srcRel, srcAttr, width, height, ratio, query };
118
+ }
119
+
120
+ async function getGithubAssets(key, source, tempPath, extensions) {
121
+ const storage = createStorage();
122
+ storage.mount(key, githubDriver({
123
+ repo: source.repo,
124
+ branch: source.branch || "main",
125
+ dir: source.dir || "/",
126
+ ttl: source.ttl || 600
127
+ }));
128
+ const rx = new RegExp(`.${extensions.join("|")}$`);
129
+ const keys = await storage.getKeys();
130
+ const assetKeys = keys.filter((key2) => rx.test(key2));
131
+ const assetItems = await Promise.all(assetKeys.map(async (id) => {
132
+ const data = await storage.getItem(id);
133
+ return { id, data };
134
+ }));
135
+ const prefix = source.prefix || "";
136
+ const paths = [];
137
+ for (const { id, data } of assetItems) {
138
+ if (data) {
139
+ const path = id.replaceAll(":", "/");
140
+ const absPath = Path.join(tempPath, path.replace(key, `${key}/${prefix}`));
141
+ const absFolder = Path.dirname(absPath);
142
+ const buffer = data.constructor.name === "Blob" ? Buffer.from(await data.arrayBuffer()) : typeof data === "object" ? JSON.stringify(data, null, " ") : String(data);
143
+ Fs.mkdirSync(absFolder, { recursive: true });
144
+ Fs.writeFileSync(absPath, buffer);
145
+ paths.push(absPath);
146
+ }
147
+ }
148
+ return paths;
149
+ }
150
+ function getFsAssets(path, extensions) {
151
+ const pattern = `${path}/**/*.{${extensions.join(",")}}`;
152
+ return glob.globSync(pattern) || [];
153
+ }
154
+
73
155
  const resolve = createResolver(import.meta.url).resolve;
74
156
  const module = defineNuxtModule({
75
157
  meta: {
@@ -86,97 +168,134 @@ const module = defineNuxtModule({
86
168
  imageSize: "",
87
169
  debug: false
88
170
  },
89
- setup(options, nuxt) {
90
- var _a;
171
+ async setup(options, nuxt) {
172
+ var _a, _b;
91
173
  const pluginPath = resolve("./runtime/plugin");
92
174
  const buildPath = nuxt.options.buildDir;
93
- const cachePath = Path.resolve(buildPath, "content-assets");
175
+ const cachePath = Path.join(buildPath, "content-assets");
176
+ const publicPath = Path.join(cachePath, "public");
177
+ const tempPath = Path.resolve("node_modules/.nuxt-content-assets");
178
+ const dump = (name, data) => {
179
+ const path = `${cachePath}/debug/${name}.json`;
180
+ log(`Dumping "${Path.relative("", path)}"`);
181
+ writeFile(path, data);
182
+ };
94
183
  if (options.debug) {
95
184
  log("Removing cache folders...");
96
185
  }
97
- Fs.rmSync(Path.join(buildPath, "content-cache"), { recursive: true, force: true });
98
- Fs.rmSync(cachePath, { recursive: true, force: true });
99
- const sources = nuxt.options._layers.map((layer) => layer.config?.content?.sources).reduce((output2, sources2) => {
100
- if (sources2) {
101
- Object.assign(output2, getSources(sources2));
102
- }
103
- return output2;
104
- }, {});
105
- if (Object.keys(sources).length === 0 || !sources.content) {
106
- const content = nuxt.options.srcDir + "/content";
107
- if (Fs.existsSync(content)) {
108
- sources.content = content;
109
- }
186
+ removeFolder(Path.join(buildPath, "content-cache"));
187
+ removeFolder(cachePath);
188
+ removeFolder(tempPath);
189
+ (_a = nuxt.options).content || (_a.content = {});
190
+ if (nuxt.options.content) {
191
+ (_b = nuxt.options.content).ignores || (_b.ignores = []);
110
192
  }
111
193
  const output = options.output || defaults.assetsDir;
112
194
  const matches = output.match(/([^[]+)(.*)?/);
113
- const assetsDir = matches ? matches[1] : defaults.assetsDir;
195
+ const assetsDir = matches ? matches[1].replace(/^\/*/, "/").replace(/\/*$/, "") : defaults.assetsDir;
114
196
  const assetsPattern = (matches ? matches[2] : "") || defaults.assetsPattern;
115
- interpolatePattern(assetsPattern, "", "", true);
197
+ interpolatePattern(assetsPattern, "", true);
116
198
  const imageFlags = matchWords(options.imageSize);
117
199
  if (options.extensions?.trim()) {
118
200
  extensions.splice(0, extensions.length, ...matchWords(options.extensions));
119
201
  } else if (options.additionalExtensions) {
120
202
  extensions.push(...matchWords(options.additionalExtensions));
121
203
  }
122
- function getAssetConfig(pattern, src, dir) {
123
- let width = void 0;
124
- let height = void 0;
125
- let ratio = void 0;
126
- let query = void 0;
127
- if (imageFlags.length && isImage(src)) {
128
- const size = getImageSize(src);
129
- if (imageFlags.includes("style")) {
130
- ratio = `${size.width}/${size.height}`;
131
- }
132
- if (imageFlags.includes("attrs")) {
133
- width = size.width;
134
- height = size.height;
135
- }
136
- if (imageFlags.includes("url")) {
137
- query = `?width=${width}&height=${height}`;
138
- }
139
- }
140
- const id = Path.join(Path.basename(dir), Path.relative(dir, src)).replaceAll("/", ":");
141
- const file = interpolatePattern(pattern, src, dir);
142
- const trg = Path.join(cachePath, assetsDir, file);
143
- const rel = Path.join("/", assetsDir, file);
144
- return { id, file, trg, rel, width, height, ratio, query };
204
+ if (options.debug) {
205
+ log("Preparing sources...");
145
206
  }
146
- const publicFolder = Path.join(cachePath, assetsDir);
147
- const sourceFolders = Object.values(sources);
148
207
  const assets = {};
149
- if (nuxt.options.content) {
150
- (_a = nuxt.options.content).ignores || (_a.ignores = []);
208
+ const sources = nuxt.options._layers.map((layer) => layer.config?.content?.sources).reduce((output2, sources2) => {
209
+ if (sources2) {
210
+ Object.assign(output2, sources2);
211
+ }
212
+ return output2;
213
+ }, {});
214
+ if (Object.keys(sources).length === 0 || !sources.content) {
215
+ const content = nuxt.options.srcDir + "/content";
216
+ if (Fs.existsSync(content)) {
217
+ sources.content = {
218
+ driver: "fs",
219
+ base: content
220
+ };
221
+ }
222
+ }
223
+ for (const [key, source] of Object.entries(sources)) {
224
+ const { driver } = source;
225
+ let srcDir = "";
226
+ let paths = [];
227
+ switch (driver) {
228
+ case "fs":
229
+ paths = getFsAssets(source.base, extensions);
230
+ srcDir = source.base;
231
+ break;
232
+ case "github":
233
+ paths = await getGithubAssets(key, source, tempPath, extensions);
234
+ srcDir = Path.join(tempPath, key);
235
+ break;
236
+ }
237
+ if (options.debug) {
238
+ log(`Prepared ${paths.length} paths for source "${key}"`);
239
+ }
240
+ if (paths.length) {
241
+ paths.forEach((src) => {
242
+ const {
243
+ id,
244
+ srcRel,
245
+ srcAttr,
246
+ width,
247
+ height,
248
+ ratio,
249
+ query
250
+ } = getAssetConfig(srcDir, src, assetsPattern, imageFlags);
251
+ nuxt.options.content.ignores.push(id);
252
+ const trg = Path.join(publicPath, assetsDir, srcAttr);
253
+ assets[srcRel] = {
254
+ src,
255
+ trg,
256
+ config: {
257
+ srcAttr: Path.join(assetsDir, srcAttr),
258
+ width,
259
+ height,
260
+ ratio,
261
+ query
262
+ }
263
+ };
264
+ });
265
+ }
151
266
  }
152
- sourceFolders.forEach((folder) => {
153
- const pattern = `${folder}/**/*.{${extensions.join(",")}}`;
154
- const paths = glob.globSync(pattern);
155
- paths.forEach((src) => {
156
- const config = getAssetConfig(assetsPattern, src, folder);
157
- nuxt.options.content.ignores.push(config.id);
158
- assets[src] = config;
159
- });
160
- });
161
267
  nuxt.hook("build:before", function() {
162
- Fs.mkdirSync(publicFolder, { recursive: true });
163
268
  if (options.debug) {
164
- const paths = Object.keys(assets).map((key) => " - " + assets[key].id.replaceAll(":", "/"));
165
- log(`Copying ${Object.keys(assets).length} assets:
166
-
167
- ${paths.join("\n")}
168
- `);
269
+ dump("assets", assets);
270
+ }
271
+ if (options.debug) {
272
+ log(`Copying ${Object.keys(assets).length} assets...`);
273
+ }
274
+ const copied = [];
275
+ const failed = [];
276
+ for (const [key, { src, trg }] of Object.entries(assets)) {
277
+ if (Fs.existsSync(src)) {
278
+ copyFile(src, trg);
279
+ copied.push(key);
280
+ } else {
281
+ failed.push(key);
282
+ }
283
+ }
284
+ if (options.debug) {
285
+ if (copied.length) {
286
+ list("Copied", copied);
287
+ }
288
+ if (failed.length) {
289
+ list("Failed to copy", failed);
290
+ }
169
291
  }
170
- Object.keys(assets).forEach((src) => {
171
- const { trg } = assets[src];
172
- const trgFolder = Path.dirname(trg);
173
- Fs.mkdirSync(trgFolder, { recursive: true });
174
- Fs.copyFileSync(src, trg);
175
- });
176
292
  });
293
+ const nitroAssets = Object.entries(assets).reduce((output2, [key, value]) => {
294
+ output2[key] = value.config;
295
+ return output2;
296
+ }, {});
177
297
  const virtualConfig = [
178
- `export const assets = ${JSON.stringify(assets)}`,
179
- `export const sources = ${JSON.stringify(sources)}`
298
+ `export const assets = ${JSON.stringify(nitroAssets, null, " ")}`
180
299
  ].join("\n");
181
300
  nuxt.options.alias[`#${moduleName}`] = addTemplate({
182
301
  filename: `${moduleName}.mjs`,
@@ -189,7 +308,7 @@ ${paths.join("\n")}
189
308
  config.virtual[`#${moduleName}`] = virtualConfig;
190
309
  config.publicAssets || (config.publicAssets = []);
191
310
  config.publicAssets.push({
192
- dir: cachePath
311
+ dir: publicPath
193
312
  // maxAge: 60 * 60 * 24 * 365 // 1 year
194
313
  });
195
314
  });
@@ -0,0 +1,2 @@
1
+ export declare const moduleName = "nuxt-content-assets";
2
+ export declare const moduleKey = "content-assets";
@@ -0,0 +1,2 @@
1
+ export const moduleName = "nuxt-content-assets";
2
+ export const moduleKey = "content-assets";
@@ -1,7 +1,7 @@
1
- import { matchWords } from "./utils/config.mjs";
1
+ import { matchWords } from "./utils/string.mjs";
2
2
  export const defaults = {
3
- assetsDir: "assets/content",
4
- assetsPattern: "[folder]/[file]"
3
+ assetsDir: "/assets/",
4
+ assetsPattern: "[path]/[file]"
5
5
  };
6
6
  export const imageExtensions = matchWords("png jpg jpeg gif svg webp ico");
7
7
  export const mediaExtensions = matchWords("mp3 m4a wav mp4 mov webm ogg avi flv avchd");
@@ -1,2 +1,3 @@
1
- declare const _default: any;
2
- export default _default;
1
+ import type { NitroAppPlugin } from 'nitropack';
2
+ declare const plugin: NitroAppPlugin;
3
+ export default plugin;
@@ -2,49 +2,42 @@ import Path from "path";
2
2
  import { visit } from "unist-util-visit";
3
3
  import { isValidAsset, walk } from "./utils/index.mjs";
4
4
  import { tags } from "./options.mjs";
5
- import { assets, sources } from "#nuxt-content-assets";
6
- function getDocPath(id) {
7
- const parts = id.split(":");
8
- const key = parts.shift();
9
- const relPath = parts.join("/");
10
- const absBase = sources[key];
11
- return Path.join(absBase, relPath);
5
+ import { assets } from "#nuxt-content-assets";
6
+ function getAsset(srcDoc, relAsset) {
7
+ const srcAsset = Path.join(Path.dirname(srcDoc), relAsset);
8
+ return assets[srcAsset] || {};
12
9
  }
13
- function getAsset(absDoc, relAsset) {
14
- const absAsset = Path.join(Path.dirname(absDoc), relAsset);
15
- return assets[absAsset] || {};
16
- }
17
- export default defineNitroPlugin(async (nitroApp) => {
18
- nitroApp.hooks.hook("content:file:afterParse", async (file) => {
10
+ const plugin = async (nitro) => {
11
+ nitro.hooks.hook("content:file:afterParse", async (file) => {
19
12
  if (file._id.endsWith(".md")) {
20
- const absDoc = getDocPath(file._id);
13
+ const srcDoc = file._id.split(":").join("/");
21
14
  const filter = (value, key) => !(String(key).startsWith("_") || key === "body");
22
15
  walk(file, (value, parent, key) => {
23
16
  if (isValidAsset(value)) {
24
- const { rel, query } = getAsset(absDoc, value);
25
- if (rel) {
26
- parent[key] = rel + (query || "");
17
+ const { srcAttr, query } = getAsset(srcDoc, value);
18
+ if (srcAttr) {
19
+ parent[key] = srcAttr + (query || "");
27
20
  }
28
21
  }
29
22
  }, filter);
30
23
  visit(file.body, (n) => tags.includes(n.tag), (node) => {
31
24
  if (node.props.src) {
32
- const { rel, width, height, ratio } = getAsset(absDoc, node.props.src);
33
- if (rel) {
34
- node.props.src = rel;
35
- }
36
- if (width && height) {
37
- node.props.width = width;
38
- node.props.height = height;
39
- }
40
- if (ratio) {
41
- node.props.style = `aspect-ratio:${ratio}`;
25
+ const { srcAttr, width, height, ratio } = getAsset(srcDoc, node.props.src);
26
+ if (srcAttr) {
27
+ node.props.src = srcAttr;
28
+ if (width && height) {
29
+ node.props.width = width;
30
+ node.props.height = height;
31
+ }
32
+ if (ratio) {
33
+ node.props.style = `aspect-ratio:${ratio}`;
34
+ }
42
35
  }
43
36
  } else if (node.tag === "a") {
44
37
  if (node.props.href) {
45
- const { rel } = getAsset(absDoc, node.props.href);
46
- if (rel) {
47
- node.props.href = rel;
38
+ const { srcAttr } = getAsset(srcDoc, node.props.href);
39
+ if (srcAttr) {
40
+ node.props.href = srcAttr;
48
41
  node.props.target = "_blank";
49
42
  }
50
43
  }
@@ -52,4 +45,5 @@ export default defineNitroPlugin(async (nitroApp) => {
52
45
  });
53
46
  }
54
47
  });
55
- });
48
+ };
49
+ export default plugin;
@@ -0,0 +1,18 @@
1
+ export type AssetConfig = {
2
+ id: string;
3
+ srcRel: string;
4
+ srcAttr: string;
5
+ width?: number;
6
+ height?: number;
7
+ ratio?: string;
8
+ query?: string;
9
+ };
10
+ /**
11
+ * Get config for asset
12
+ *
13
+ * @param srcDir The absolute path to the asset's source folder
14
+ * @param srcAbs The absolute path to the asset itself
15
+ * @param pattern The user-defined pattern to create the public src attribute
16
+ * @param hints A list of named image size hints, i.e. 'style', 'attrs', etc
17
+ */
18
+ export declare function getAssetConfig(srcDir: string, srcAbs: string, pattern: string, hints: string[]): AssetConfig;
@@ -0,0 +1,31 @@
1
+ import * as Path from "path";
2
+ import getImageSize from "image-size";
3
+ import { isImage, warn } from "../utils/index.mjs";
4
+ import { interpolatePattern } from "./paths.mjs";
5
+ export function getAssetConfig(srcDir, srcAbs, pattern, hints) {
6
+ let width = void 0;
7
+ let height = void 0;
8
+ let ratio = void 0;
9
+ let query = void 0;
10
+ if (hints.length && isImage(srcAbs)) {
11
+ try {
12
+ const size = getImageSize(srcAbs);
13
+ if (hints.includes("style")) {
14
+ ratio = `${size.width}/${size.height}`;
15
+ }
16
+ if (hints.includes("attrs")) {
17
+ width = size.width;
18
+ height = size.height;
19
+ }
20
+ if (hints.includes("url")) {
21
+ query = `?width=${width}&height=${height}`;
22
+ }
23
+ } catch (err) {
24
+ warn(`could not read image "${srcAbs}`);
25
+ }
26
+ }
27
+ const srcRel = Path.basename(srcDir) + srcAbs.substring(srcDir.length);
28
+ const srcAttr = interpolatePattern(pattern, srcRel);
29
+ const id = srcRel.replaceAll("/", ":");
30
+ return { id, srcRel, srcAttr, width, height, ratio, query };
31
+ }
@@ -0,0 +1,3 @@
1
+ export * from './assets';
2
+ export * from './sources';
3
+ export * from './paths';
@@ -0,0 +1,3 @@
1
+ export * from "./assets.mjs";
2
+ export * from "./sources.mjs";
3
+ export * from "./paths.mjs";
@@ -2,8 +2,7 @@
2
2
  * Interpolate assets path pattern
3
3
  *
4
4
  * @param pattern A path pattern with tokens
5
- * @param src The absolute path to a src asset
6
- * @param dir The absolute path to its containing folder
5
+ * @param src The relative path to a src asset
7
6
  * @param warn An optional flag to warn for unknown tokens
8
7
  */
9
- export declare function interpolatePattern(pattern: string, src: string, dir: string, warn?: boolean): string;
8
+ export declare function interpolatePattern(pattern: string, src: string, warn?: boolean): string;
@@ -1,20 +1,22 @@
1
1
  import Path from "path";
2
2
  import { hash } from "ohash";
3
- import { log } from "./debug.mjs";
3
+ import { log } from "../utils/index.mjs";
4
4
  const replacers = {
5
- folder: (src, dir) => Path.dirname(src.replace(dir, "")),
5
+ key: (src) => Path.dirname(src).split("/").filter((e) => e).shift() || "",
6
+ path: (src) => Path.dirname(src),
7
+ folder: (src) => Path.dirname(src).replace(/[^/]+\//, ""),
6
8
  file: (src) => Path.basename(src),
7
9
  name: (src) => Path.basename(src, Path.extname(src)),
8
10
  extname: (src) => Path.extname(src),
9
11
  ext: (src) => Path.extname(src).substring(1),
10
12
  hash: (src) => hash({ src })
11
13
  };
12
- export function interpolatePattern(pattern, src, dir, warn = false) {
14
+ export function interpolatePattern(pattern, src, warn = false) {
13
15
  return Path.join(pattern.replace(/\[\w+]/g, (match) => {
14
16
  const name = match.substring(1, match.length - 1);
15
17
  const fn = replacers[name];
16
18
  if (fn) {
17
- return fn(src, dir);
19
+ return fn(src);
18
20
  }
19
21
  if (warn) {
20
22
  log(`Unknown output token ${match}`, true);
@@ -0,0 +1,10 @@
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 {};
@@ -0,0 +1,39 @@
1
+ import * as Fs from "fs";
2
+ import * as Path from "path";
3
+ import glob from "glob";
4
+ import { createStorage } from "unstorage";
5
+ import githubDriver from "unstorage/drivers/github";
6
+ export async function getGithubAssets(key, source, tempPath, extensions) {
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
+ }
33
+ }
34
+ return paths;
35
+ }
36
+ export function getFsAssets(path, extensions) {
37
+ const pattern = `${path}/**/*.{${extensions.join(",")}}`;
38
+ return glob.globSync(pattern) || [];
39
+ }
@@ -1,3 +1,3 @@
1
1
  export declare function log(...data: any[]): void;
2
2
  export declare function warn(...data: any[]): void;
3
- export declare function dump(name: string, data: any): void;
3
+ export declare function list(message: string, items: string[]): void;
@@ -1,16 +1,13 @@
1
- import Path from "path";
2
- import Fs from "fs";
3
- import { moduleKey } from "../../config";
1
+ import { moduleKey } from "../config.mjs";
4
2
  export function log(...data) {
5
3
  console.info(`[${moduleKey}]`, ...data);
6
4
  }
7
5
  export function warn(...data) {
8
6
  console.warn(`[${moduleKey}]`, ...data);
9
7
  }
10
- export function dump(name, data) {
11
- const path = `debug/${name}.json`;
12
- const folder = Path.dirname(path);
13
- log(`Dumping "${path}"`);
14
- Fs.mkdirSync(folder, { recursive: true });
15
- Fs.writeFileSync(path, JSON.stringify(data, null, " "), { encoding: "utf8" });
8
+ export function list(message, items) {
9
+ log(`${message}:
10
+
11
+ ${items.map((item) => ` - ${item}`).join("\n")}
12
+ `);
16
13
  }
@@ -0,0 +1,4 @@
1
+ export declare function writeFile(path: string, data: null | string | number | boolean | object): void;
2
+ export declare function copyFile(src: string, trg: string): void;
3
+ export declare function createFolder(path: string): void;
4
+ export declare function removeFolder(path: string): void;
@@ -0,0 +1,20 @@
1
+ import * as Path from "path";
2
+ import * as Fs from "fs";
3
+ export function writeFile(path, data) {
4
+ const text = typeof data === "object" ? JSON.stringify(data, null, " ") : String(data);
5
+ createFolder(Path.dirname(path));
6
+ Fs.writeFileSync(path, text, { encoding: "utf8" });
7
+ }
8
+ export function copyFile(src, trg) {
9
+ createFolder(Path.dirname(trg));
10
+ Fs.copyFileSync(src, trg);
11
+ }
12
+ export function createFolder(path) {
13
+ Fs.mkdirSync(path, { recursive: true });
14
+ }
15
+ export function removeFolder(path) {
16
+ const isDownstream = path.startsWith(Path.resolve());
17
+ if (isDownstream) {
18
+ Fs.rmSync(path, { recursive: true, force: true });
19
+ }
20
+ }
@@ -1,6 +1,5 @@
1
- export * from './assets';
2
- export * from './config';
3
- export * from './content';
1
+ export * from './assert';
4
2
  export * from './debug';
3
+ export * from './fs';
4
+ export * from './string';
5
5
  export * from './object';
6
- export * from './paths';
@@ -1,6 +1,5 @@
1
- export * from "./assets.mjs";
2
- export * from "./config.mjs";
3
- export * from "./content.mjs";
1
+ export * from "./assert.mjs";
4
2
  export * from "./debug.mjs";
3
+ export * from "./fs.mjs";
4
+ export * from "./string.mjs";
5
5
  export * from "./object.mjs";
6
- export * from "./paths.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-content-assets",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Enable locally-located assets in Nuxt Content",
5
5
  "repository": "davestewart/nuxt-content-assets",
6
6
  "license": "MIT",
@@ -19,22 +19,24 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "dev": "nuxi dev demo",
22
- "dev:build": "nuxi build demo",
23
- "dev:prepare": "nuxt-module-build --stub && nuxi prepare demo",
24
22
  "build": "nuxt-module-build",
25
- "release": "npm run lint && npm run test && npm run build && npm publish && git push --follow-tags",
26
- "release:dry": "npm run lint && npm run test && npm run build && npm publish --dry-run",
27
- "release:old": "npm run lint && npm run test && npm run build && changelogen --release && npm publish && git push --follow-tags",
28
23
  "lint": "eslint .",
29
24
  "test": "vitest run",
30
- "test:watch": "vitest watch"
25
+ "test:watch": "vitest watch",
26
+ "demo:prepare": "nuxt-module-build --stub && nuxi prepare demo",
27
+ "demo:generate": "nuxt generate demo",
28
+ "demo:serve": "npx serve demo/dist",
29
+ "demo:build": "nuxi build demo",
30
+ "release": "npm run lint && npm run test && npm run build && npm publish && git push --follow-tags",
31
+ "release:dry": "npm run lint && npm run test && npm run build && npm publish --dry-run"
31
32
  },
32
33
  "dependencies": {
33
34
  "@nuxt/kit": "^3.3.2",
34
35
  "glob": "^9.3.2",
35
36
  "image-size": "^1.0.2",
36
37
  "ohash": "^1.0.0",
37
- "unist-util-visit": "^4.1.2"
38
+ "unist-util-visit": "^4.1.2",
39
+ "unstorage": "^1.4.1"
38
40
  },
39
41
  "peerDependencies": {
40
42
  "@nuxt/content": "latest"
@@ -49,5 +51,8 @@
49
51
  "eslint": "^8.36.0",
50
52
  "nuxt": "^3.3.2",
51
53
  "vitest": "^0.29.7"
54
+ },
55
+ "engines": {
56
+ "node": ">=14.0.0"
52
57
  }
53
58
  }
@@ -1,7 +0,0 @@
1
- import type { MountOptions } from '@nuxt/content';
2
- /**
3
- * Get content source folders
4
- *
5
- * @param sources
6
- */
7
- export declare function getSources(sources: Record<string, MountOptions>): Record<string, string>;
@@ -1,12 +0,0 @@
1
- export function getSources(sources) {
2
- return Object.keys(sources).reduce((output, key) => {
3
- const source = sources[key];
4
- if (source) {
5
- const { driver, base } = source;
6
- if (driver === "fs") {
7
- output[key] = base;
8
- }
9
- }
10
- return output;
11
- }, {});
12
- }
File without changes
File without changes
File without changes
File without changes