@kosdev-code/kos-asset-manager 0.0.1-next.20
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 +189 -0
- package/bin/kos-assets.mjs +123 -0
- package/bin/lib/declare-assets.mjs +404 -0
- package/index.d.ts +10 -0
- package/index.js +45476 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @kosdev-code/kos-asset-manager
|
|
2
|
+
|
|
3
|
+
Normalized access to images, video and other media that applications declare
|
|
4
|
+
in their kab descriptors and the asset manager application serves.
|
|
5
|
+
|
|
6
|
+
## Declaring assets in a ui project
|
|
7
|
+
|
|
8
|
+
Put files under `src/assets/media` in the project. Everything below it is
|
|
9
|
+
declared automatically, keyed by its path without the extension, so
|
|
10
|
+
`src/assets/media/brand/logo-primary.svg` is the key `brand/logo-primary`.
|
|
11
|
+
|
|
12
|
+
A key derived from a path breaks when the file moves, so pin the ones a ui
|
|
13
|
+
depends on. Bindings live in the project's `.kos.json`, take a path relative
|
|
14
|
+
to the media directory, and may capture with `{name}` or span folders with
|
|
15
|
+
`**`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"kondra": {
|
|
20
|
+
"ui": {
|
|
21
|
+
"assets": {
|
|
22
|
+
"keys": {
|
|
23
|
+
"brand/logo-primary": "brand/logos/primary.svg",
|
|
24
|
+
"beverages/{name}": "drinks/{name}.svg",
|
|
25
|
+
"video/globe": "**/globe.mp4"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Bindings can also sit beside the files they name, in a `.kosassets` file in
|
|
34
|
+
any folder under the media directory. Its sources are relative to that
|
|
35
|
+
folder while its keys stay absolute, so renaming or moving the folder
|
|
36
|
+
carries the rules with it and no key changes:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
# src/assets/media/brand/.kosassets
|
|
40
|
+
brand/logo-primary = logos/primary.svg
|
|
41
|
+
brand/{name} = *.svg
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A file takes the first key that claims it: `.kos.json`, then the nearest
|
|
45
|
+
`.kosassets` walking up, then its own path. A binding that matches no file
|
|
46
|
+
is reported during the build.
|
|
47
|
+
|
|
48
|
+
Renaming a key is a breaking change for whoever uses it, so the old key can
|
|
49
|
+
be kept alive alongside the new one:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
"entries": { "brand/logo-primary": { "aliases": ["brand/kos-logo"] } }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Lookups try the key first and then the aliases, which gives consumers time
|
|
56
|
+
to catch up before the alias is dropped.
|
|
57
|
+
|
|
58
|
+
Add the target that generates the declaration:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npx kos-assets init apps/my-app
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This adds an `assets` target that runs after `descriptor`, and makes `zip`
|
|
65
|
+
depend on it so the declaration is in the descriptor before the kab is built.
|
|
66
|
+
To wire it by hand instead, add to the project's `project.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
"assets": {
|
|
70
|
+
"command": "kos-assets build apps/my-app",
|
|
71
|
+
"dependsOn": ["descriptor"],
|
|
72
|
+
"inputs": [
|
|
73
|
+
"{projectRoot}/src/assets/media/**/*",
|
|
74
|
+
"{projectRoot}/.kos.json"
|
|
75
|
+
],
|
|
76
|
+
"outputs": ["{workspaceRoot}/dist/{projectRoot}/descriptor.json"]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
and add `"assets"` to the `zip` target's `dependsOn`. The inputs and outputs
|
|
81
|
+
matter: the declaration is written into a build output, so without them a
|
|
82
|
+
restored build can be paired with a freshly scanned declaration and the kab
|
|
83
|
+
ships assets it declares but does not contain.
|
|
84
|
+
|
|
85
|
+
Video gets its thumbnail from an image that shares its key: drop
|
|
86
|
+
`video/pour-loop.mp4` and `video/pour-loop-poster.png` in the same folder and
|
|
87
|
+
they are paired automatically (`-poster` or `-still`). Name one explicitly
|
|
88
|
+
when the convention doesn't fit:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
"entries": { "video/pour-loop": { "poster": "brand/hero-still" } }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The poster is an ordinary asset, so it is measured, preloaded and decoded
|
|
95
|
+
like any other image. `wrapAsset('video/pour-loop').poster` is its url.
|
|
96
|
+
|
|
97
|
+
Video is measured from its container for mp4, mov and m4v; anything else
|
|
98
|
+
takes the dimensions of its poster, so a video reserves its space either
|
|
99
|
+
way.
|
|
100
|
+
|
|
101
|
+
Groups, tags and eager-loading are authored in the project's `.kos.json`.
|
|
102
|
+
Groups and tags take patterns rather than lists of files -- `*` matches
|
|
103
|
+
within a path segment and `**` across them -- and are expanded into concrete
|
|
104
|
+
keys when the declaration is generated:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"kondra": {
|
|
109
|
+
"ui": {
|
|
110
|
+
"assets": {
|
|
111
|
+
"tags": { "beverage-logo": ["beverages/**"] },
|
|
112
|
+
"groups": { "boot": ["brand/logo-primary", "beverages/**"] },
|
|
113
|
+
"entries": { "brand/logo-primary": { "preload": true } }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A pattern that matches nothing is reported during the build.
|
|
121
|
+
|
|
122
|
+
## Using assets in a ui
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { initAssets, wrapAsset, resolveAsset, preloadAssets } from '@kosdev-code/kos-asset-manager';
|
|
126
|
+
|
|
127
|
+
// once while starting, naming the applications whose assets this ui uses
|
|
128
|
+
await initAssets({ contexts: ['my.app.id'], preload: ['boot'] });
|
|
129
|
+
|
|
130
|
+
const logo = wrapAsset('brand/logo-primary');
|
|
131
|
+
<img src={logo.src} alt="" />
|
|
132
|
+
|
|
133
|
+
// anywhere a url is expected
|
|
134
|
+
element.style.backgroundImage = `url(${resolveAsset('brand/logo-primary')})`;
|
|
135
|
+
|
|
136
|
+
// ready assets ahead of a screen that needs them
|
|
137
|
+
await preloadAssets('checkout');
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Sets of assets are selected rather than named, so adding a file to a folder
|
|
141
|
+
is all it takes to include it:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
await preloadAssets({ prefix: 'beverages/' }); // a folder
|
|
145
|
+
await preloadAssets({ tag: 'beverage-logo' }); // a tag, across folders
|
|
146
|
+
await preloadAssets({ type: 'video/' }); // by mime type
|
|
147
|
+
|
|
148
|
+
listAssets({ tag: 'beverage-logo' }).map((asset) => asset.src);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Selector fields combine, so `{ prefix: 'brand/', type: 'image/svg+xml' }` is
|
|
152
|
+
the svg files in that folder.
|
|
153
|
+
|
|
154
|
+
## Preloading during bootstrap
|
|
155
|
+
|
|
156
|
+
Preloading readies an image by fetching *and decoding* it, and holds the
|
|
157
|
+
decoded image so the first paint has nothing left to do. Assets are served
|
|
158
|
+
from the device over loopback, so the decode is the part that costs time --
|
|
159
|
+
which is what makes images pop into view on lower powered hardware. Paying
|
|
160
|
+
for it during a bootstrap sequence trades a couple of seconds at startup for
|
|
161
|
+
screens that render immediately afterwards.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
await preloadAssets({ prefix: 'beverages/' }, {
|
|
165
|
+
concurrency: 4,
|
|
166
|
+
onProgress: ({ done, total }) => setBootProgress(done / total),
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Assets are readied a few at a time, because a wide fan out on a slow device
|
|
171
|
+
only thrashes. A single asset that cannot be readied is reported through
|
|
172
|
+
`onProgress` with `ok: false` and never fails the sequence, so one bad file
|
|
173
|
+
cannot stop a device from starting.
|
|
174
|
+
|
|
175
|
+
Images are measured when the declaration is generated, so the space they
|
|
176
|
+
take can be reserved and nothing reflows when they arrive:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
const logo = wrapAsset('brand/logo-primary');
|
|
180
|
+
<img src={logo.src} width={logo.width} height={logo.height} alt="" />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`{ hold: true }` additionally keeps the bytes as an object url, which is
|
|
184
|
+
worth it only when an asset must survive the content going away.
|
|
185
|
+
`releaseAssets()` drops everything preloading is holding.
|
|
186
|
+
|
|
187
|
+
Only the contexts named in `initAssets` are requested. Keys resolve in the
|
|
188
|
+
default context (the first one declared) unless they name another, as in
|
|
189
|
+
`other.app.id:brand/logo-primary`.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Declares the ui assets of a kos project.
|
|
4
|
+
*
|
|
5
|
+
* kos-assets build <projectRoot> [--out <descriptor.json>]
|
|
6
|
+
* kos-assets init <projectRoot>
|
|
7
|
+
*
|
|
8
|
+
* `build` scans a fixed directory in the project and merges a
|
|
9
|
+
* kondra.ui.assets block into the descriptor the kab is built from. `init`
|
|
10
|
+
* adds the target that runs it to the project.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { join, resolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
SOURCE_DIR,
|
|
17
|
+
buildAssets,
|
|
18
|
+
readJson,
|
|
19
|
+
writeJson,
|
|
20
|
+
} from './lib/declare-assets.mjs';
|
|
21
|
+
|
|
22
|
+
const fail = (message) => {
|
|
23
|
+
console.error(`kos-assets: ${message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const build = (projectRoot, out) => {
|
|
28
|
+
const descriptorPath = resolve(
|
|
29
|
+
out ?? join('dist', projectRoot, 'descriptor.json')
|
|
30
|
+
);
|
|
31
|
+
if (!existsSync(descriptorPath)) {
|
|
32
|
+
fail(
|
|
33
|
+
`no descriptor at ${descriptorPath} — run the descriptor target first, or pass --out`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const assets = buildAssets(resolve(projectRoot));
|
|
38
|
+
if (!assets) {
|
|
39
|
+
console.log(`kos-assets: no assets in ${join(projectRoot, ...SOURCE_DIR)}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const descriptor = readJson(descriptorPath);
|
|
44
|
+
descriptor.kondra = {
|
|
45
|
+
...descriptor.kondra,
|
|
46
|
+
ui: { ...descriptor.kondra?.ui, assets },
|
|
47
|
+
};
|
|
48
|
+
writeJson(descriptorPath, descriptor);
|
|
49
|
+
console.log(
|
|
50
|
+
`kos-assets: declared ${Object.keys(assets.entries).length} assets in ${descriptorPath}`
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Add the assets target to a project and make the archive depend on it, so
|
|
56
|
+
* the block is in the descriptor before the kab is zipped.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* How the target should invoke this tool. A project that consumes the
|
|
60
|
+
* published package gets it from node_modules; the workspace that owns the
|
|
61
|
+
* source runs it from there, since nothing links a bin for a local lib.
|
|
62
|
+
*/
|
|
63
|
+
const invocation = () => {
|
|
64
|
+
const local = join(
|
|
65
|
+
'libs',
|
|
66
|
+
'kos-asset-manager-models',
|
|
67
|
+
'bin',
|
|
68
|
+
'kos-assets.mjs'
|
|
69
|
+
);
|
|
70
|
+
if (existsSync(resolve(local))) {
|
|
71
|
+
return `node ${local}`;
|
|
72
|
+
}
|
|
73
|
+
return 'npx kos-assets';
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const init = (projectRoot) => {
|
|
77
|
+
const path = resolve(projectRoot, 'project.json');
|
|
78
|
+
if (!existsSync(path)) {
|
|
79
|
+
fail(`no project.json in ${projectRoot}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const project = readJson(path);
|
|
83
|
+
project.targets = project.targets ?? {};
|
|
84
|
+
// the declaration is written into a build output, so nx needs to know what
|
|
85
|
+
// it reads and what it produces — otherwise a restored build can be paired
|
|
86
|
+
// with a freshly scanned declaration and the kab ships a mismatch
|
|
87
|
+
project.targets.assets = {
|
|
88
|
+
command: `${invocation()} build ${projectRoot}`,
|
|
89
|
+
dependsOn: ['descriptor'],
|
|
90
|
+
inputs: [
|
|
91
|
+
`{projectRoot}/${SOURCE_DIR.join('/')}/**/*`,
|
|
92
|
+
'{projectRoot}/.kos.json',
|
|
93
|
+
],
|
|
94
|
+
outputs: ['{workspaceRoot}/dist/{projectRoot}/descriptor.json'],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const zip = project.targets.zip;
|
|
98
|
+
if (zip && !(zip.dependsOn ?? []).includes('assets')) {
|
|
99
|
+
zip.dependsOn = [...(zip.dependsOn ?? []), 'assets'];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
writeJson(path, project);
|
|
103
|
+
console.log(`kos-assets: added the assets target to ${path}`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const [command, projectRoot, ...rest] = process.argv.slice(2);
|
|
107
|
+
if (!command || !projectRoot) {
|
|
108
|
+
fail('usage: kos-assets <build|init> <projectRoot> [--out <descriptor.json>]');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const outIndex = rest.indexOf('--out');
|
|
112
|
+
const out = outIndex === -1 ? undefined : rest[outIndex + 1];
|
|
113
|
+
|
|
114
|
+
switch (command) {
|
|
115
|
+
case 'build':
|
|
116
|
+
build(projectRoot, out);
|
|
117
|
+
break;
|
|
118
|
+
case 'init':
|
|
119
|
+
init(projectRoot);
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
fail(`unknown command: ${command}`);
|
|
123
|
+
}
|