@kosdev-code/kos-asset-manager 0.0.1-next.30 → 0.0.1-next.32

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
@@ -184,6 +184,55 @@ tell what the root is, rather than guessing one. The target it writes then
184
184
  scans the right folder and follows the project's own `descriptor` target
185
185
  wherever that writes.
186
186
 
187
+ ## Content in its own section
188
+
189
+ Content usually ships inside an application's section, and then it belongs to
190
+ that application: the kab's assets join the application's context and are
191
+ addressed by its id. Content that ships in a section of its own has no
192
+ application to take a name from, so it names itself:
193
+
194
+ ```json
195
+ {
196
+ "kondra": {
197
+ "ui": {
198
+ "assets": {
199
+ "context": "tccc.brandset",
200
+ "mediaSrc": "assets/logos",
201
+ "mediaRoot": "/logos"
202
+ }
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ That name is what a ui asks for and addresses it by, exactly like an
209
+ application id:
210
+
211
+ ```tsx
212
+ <KosAssetProvider contexts={['my.app.id', 'tccc.brandset']} gate>
213
+ ```
214
+
215
+ ```ts
216
+ wrapAsset('tccc.brandset:coke/pour');
217
+ ```
218
+
219
+ Content in its own section that names nothing belongs to its manifest
220
+ section, under the section's name. Two kabs naming the same context
221
+ contribute to the same one.
222
+
223
+ Shipping separately is also what lets content replace what an application
224
+ declares. Within one context the first declaration of a key wins and the rest
225
+ are ignored, so a brandset packaged alongside an application can only add to
226
+ it. As its own context it is a separate set of keys, and a ui chooses which
227
+ one wins by the order it lists them in `contexts`.
228
+
229
+ Everything you author under `kondra.ui.assets` carries through to the
230
+ declaration, with what the build derives landing on top. The exception is the
231
+ handful of settings that drive the build rather than describe the result —
232
+ `keys`, `tags`, `mediaSrc`, `mediaRoot`, and the pattern form of `groups` and
233
+ `entries` — which are expanded into what the device reads and not shipped as
234
+ written.
235
+
187
236
  ## Installing
188
237
 
189
238
  ```sh
@@ -275,6 +324,38 @@ branching on an optional asset without the warning `wrapAsset` logs.
275
324
  A key resolves in the contexts named at the provider, first match winning,
276
325
  unless it names one: `other.app.id:brand/logo-primary`.
277
326
 
327
+ ## Loading data in a model
328
+
329
+ Json is declared like any other asset, which makes it fetchable by key:
330
+
331
+ ```ts
332
+ import { loadAsset } from '@kosdev-code/kos-asset-manager';
333
+
334
+ @kosModel({ modelTypeId: MODEL_TYPE })
335
+ export class MenuModelImpl {
336
+ async load(): Promise<void> {
337
+ const menu = await loadAsset<MenuDto>('config/menu');
338
+ if (!menu) return;
339
+ this.reconcile(menu.items);
340
+ }
341
+ }
342
+ ```
343
+
344
+ Data usually has to be parsed and normalised into a model's own shape
345
+ anyway, so this is deliberately a plain async call rather than something a
346
+ component reads. The point is the key: the file can be renamed, moved, or
347
+ moved into a different kab without the model changing.
348
+
349
+ Undefined comes back when the key isn't declared, the fetch fails, or the
350
+ content isn't valid json, each logged with the key — so a model handles it
351
+ the way it already handles a service returning nothing. `loadAssetText(key)`
352
+ is the same without the parse.
353
+
354
+ Json files are declared and keyed like anything else, but they don't count
355
+ as media when `kos-assets init` works out where a project keeps its media —
356
+ a manifest sitting beside a folder of images is what marks the images as the
357
+ media, not the whole folder.
358
+
278
359
  ## Selecting sets
279
360
 
280
361
  Sets of assets are selected rather than named, so adding a file to a folder
@@ -45,6 +45,35 @@ const TYPES = {
45
45
  '.webm': 'video/webm',
46
46
  '.webp': 'image/webp',
47
47
  '.woff2': 'font/woff2',
48
+ '.json': 'application/json',
49
+ };
50
+
51
+ /**
52
+ * Data assets are declared like any other, but they are not what makes a
53
+ * directory a project's media directory -- a brandset keeps its manifest
54
+ * beside its images, and it is the manifest sitting there that tells you the
55
+ * images are the media rather than the whole folder. So layout detection
56
+ * looks past them.
57
+ */
58
+ const DATA_TYPES = new Set(['.json']);
59
+
60
+ /**
61
+ * Authored to drive the build rather than to be read on the device. Patterns
62
+ * and partial entries are expanded into the real thing, so passing them
63
+ * through would ship a `groups` of patterns the device would read as keys.
64
+ */
65
+ const BUILD_INPUTS = new Set([
66
+ 'keys',
67
+ 'tags',
68
+ 'entries',
69
+ 'groups',
70
+ 'mediaSrc',
71
+ 'mediaRoot',
72
+ ]);
73
+
74
+ const isMedia = (name) => {
75
+ const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
76
+ return Boolean(TYPES[ext]) && !DATA_TYPES.has(ext);
48
77
  };
49
78
 
50
79
  /**
@@ -280,8 +309,7 @@ const scanMedia = (dir, prefix = '') =>
280
309
  if (item.name.startsWith('.') || SKIP_DIRS.has(item.name)) return [];
281
310
  const rel = prefix ? `${prefix}/${item.name}` : item.name;
282
311
  if (item.isDirectory()) return scanMedia(join(dir, item.name), rel);
283
- const ext = item.name.slice(item.name.lastIndexOf('.')).toLowerCase();
284
- return TYPES[ext] ? [rel] : [];
312
+ return isMedia(item.name) ? [rel] : [];
285
313
  });
286
314
 
287
315
  /** The deepest directory every one of these paths sits under. */
@@ -319,7 +347,7 @@ const climbToMedia = (projectRoot, dir) => {
319
347
  const onlyMedia = entries.every((item) =>
320
348
  item.isDirectory()
321
349
  ? holdsMedia(resolve(projectRoot, ...parent, item.name))
322
- : TYPES[item.name.slice(item.name.lastIndexOf('.')).toLowerCase()]
350
+ : isMedia(item.name)
323
351
  );
324
352
  if (!onlyMedia) {
325
353
  break;
@@ -512,7 +540,16 @@ const buildAssets = (projectRoot) => {
512
540
  );
513
541
  }
514
542
 
543
+ // what a project authors under kondra.ui.assets carries through to the
544
+ // declaration, and what the build derives lands on top of it -- so a field
545
+ // the device reads, like the context a brandset belongs to, is authored
546
+ // once and needs nothing here to know about it
547
+ const authored = Object.fromEntries(
548
+ Object.entries(declared).filter(([key]) => !BUILD_INPUTS.has(key))
549
+ );
550
+
515
551
  return {
552
+ ...authored,
516
553
  root: mediaRoot(declared),
517
554
  entries,
518
555
  ...(Object.keys(groups).length > 0 ? { groups } : {}),
@@ -524,6 +561,9 @@ export {
524
561
  SOURCE_DIR,
525
562
  ASSETS_ROOT,
526
563
  TYPES,
564
+ DATA_TYPES,
565
+ BUILD_INPUTS,
566
+ isMedia,
527
567
  mediaSource,
528
568
  mediaRoot,
529
569
  scanMedia,
package/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { registerAssetModels } from './registration';
5
5
  export { contextNames } from './runtime/assets';
6
6
  export { KosAssetProvider, KosAssetContext, useAssetManager, useKosAssetContext, } from './runtime/asset-provider';
7
7
  export { resolveAsset, wrapAsset, hasAsset, listAssets } from './runtime/resolve';
8
+ export { loadAsset, loadAssetText } from './runtime/data';
8
9
  export { preloadAssets, preloadDeclared, releaseAssets, } from './runtime/preload';
9
10
  export type { AssetSelector, PreloadOptions, PreloadProgress, ResolvedAsset, } from './runtime/types';
10
11
  //# sourceMappingURL=index.d.ts.map