@artbucket/brand 0.0.0-stage → 2.1.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/LICENSE +21 -0
- package/README.md +20 -2
- package/index.d.ts +52 -0
- package/index.js +152 -0
- package/package.json +25 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artbucket contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @artbucket/brand
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One release of a brand from [Artbucket](https://github.com/pwnera/artbucket), read at build time, and
|
|
4
|
+
the manifest of what the site used. MIT.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @artbucket/brand
|
|
8
|
+
npx artbucket brand export .brand --brand acme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { loadBrand } from "@artbucket/brand";
|
|
13
|
+
|
|
14
|
+
const brand = await loadBrand(".brand");
|
|
15
|
+
brand.value("color.primary");
|
|
16
|
+
brand.rules("color.*");
|
|
17
|
+
await brand.copyFiles("dist");
|
|
18
|
+
await brand.writeManifest("dist"); // dist/artbucket.site.json: lineage, the use check, rebuilds
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Docs: https://docs.artbucket.io/developers/kit
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** A rule as `artbucket brand export` writes it into brand.json. */
|
|
2
|
+
export type Rule = {
|
|
3
|
+
key: string;
|
|
4
|
+
type: string;
|
|
5
|
+
value: unknown;
|
|
6
|
+
context: string | null;
|
|
7
|
+
label: string | null;
|
|
8
|
+
usage: string | null;
|
|
9
|
+
spec: Record<string, unknown> | null;
|
|
10
|
+
assets: { id: string; mime?: string | null; file?: string; rendition?: string | null }[];
|
|
11
|
+
};
|
|
12
|
+
export type Release = { number: number; publishedAt: string; note?: string | null };
|
|
13
|
+
/** artbucket.site.json: what a build used, read by Artbucket as it deploys. */
|
|
14
|
+
export type Manifest = { brand: string; release: number | null; rules: string[]; assets: string[] };
|
|
15
|
+
|
|
16
|
+
/** The release exported into `dir` (.brand by default). */
|
|
17
|
+
export declare function loadBrand(dir?: string): Promise<Brand>;
|
|
18
|
+
|
|
19
|
+
export declare class Brand {
|
|
20
|
+
constructor(data: unknown, dir?: string);
|
|
21
|
+
readonly slug: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/** The release this is, or null when the brand had none yet. */
|
|
24
|
+
readonly release: Release | null;
|
|
25
|
+
/** The Artbucket it came from. */
|
|
26
|
+
readonly server: string | null;
|
|
27
|
+
readonly contexts: unknown[];
|
|
28
|
+
/** Every rule, in every context: any change then reaches this site. */
|
|
29
|
+
all(): Rule[];
|
|
30
|
+
/** A rule by key, its default value or a context's. */
|
|
31
|
+
rule(key: string, context?: string | null): Rule | undefined;
|
|
32
|
+
value<T = unknown>(key: string, fallback?: T, context?: string | null): T;
|
|
33
|
+
/** A text rule's words, or the fallback. */
|
|
34
|
+
text(key: string, fallback?: string): string;
|
|
35
|
+
/** The rules of a group (`color.*`), a key, or a type (`{ type: "font" }`). */
|
|
36
|
+
rules(which?: string | { type?: string }): Rule[];
|
|
37
|
+
/** The rule of a type whose key says one of the words, else (`any`) the first of that type. */
|
|
38
|
+
pick(type: string, words: string[], any?: boolean): Rule | undefined;
|
|
39
|
+
/** A rule's file as the site serves it (assets/...), or null. */
|
|
40
|
+
file(asset: Rule["assets"][number] | undefined): string | null;
|
|
41
|
+
/** A file the export holds, by its path: noted, or null. */
|
|
42
|
+
fileAt(path: string): string | null;
|
|
43
|
+
/** An asset's live address on the server, following its current version (/c/). */
|
|
44
|
+
liveUrl(id: string, transform?: string): string;
|
|
45
|
+
/** tokens.css: every rule as a custom property, and the files it names. */
|
|
46
|
+
tokensCss(): Promise<string>;
|
|
47
|
+
/** The files used so far, copied into `out`; their paths. */
|
|
48
|
+
copyFiles(out: string): Promise<string[]>;
|
|
49
|
+
manifest(): Manifest;
|
|
50
|
+
/** artbucket.site.json in `out`. */
|
|
51
|
+
writeManifest(out: string): Promise<Manifest>;
|
|
52
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @artbucket/brand: one release of a brand, read at build time from what `artbucket brand export` wrote
|
|
2
|
+
// (brand.json, tokens.css and the files they point at), and the manifest of what the site used.
|
|
3
|
+
// MIT License, see LICENSE.
|
|
4
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The release exported into `dir` (.brand by default). Every rule, file and
|
|
9
|
+
* address the build asks for is noted: `writeManifest` turns them into
|
|
10
|
+
* artbucket.site.json, which gives the site its lineage and its check in
|
|
11
|
+
* Artbucket, and lets a release that changes none of them skip its rebuild.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadBrand(dir = ".brand") {
|
|
14
|
+
const text = await readFile(join(dir, "brand.json"), "utf8").catch(() => {
|
|
15
|
+
throw new Error(`No ${dir}/brand.json: export the brand first (npx artbucket brand export ${dir} --brand <slug>)`);
|
|
16
|
+
});
|
|
17
|
+
return new Brand(JSON.parse(text), dir);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Rules read, as the manifest names them: a key, `color.*` for a group, `*` for every one. */
|
|
21
|
+
const groupOf = (key) => (key.includes(".") ? `${key.slice(0, key.indexOf("."))}.*` : key);
|
|
22
|
+
|
|
23
|
+
export class Brand {
|
|
24
|
+
#data;
|
|
25
|
+
#dir;
|
|
26
|
+
#rules = new Set();
|
|
27
|
+
#assets = new Set();
|
|
28
|
+
#files = new Set();
|
|
29
|
+
|
|
30
|
+
constructor(data, dir = ".brand") {
|
|
31
|
+
this.#data = data;
|
|
32
|
+
this.#dir = dir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get slug() {
|
|
36
|
+
return this.#data.slug;
|
|
37
|
+
}
|
|
38
|
+
get name() {
|
|
39
|
+
return this.#data.name ?? this.#data.slug;
|
|
40
|
+
}
|
|
41
|
+
/** The release this is, or null when the brand had none yet (as it stood). */
|
|
42
|
+
get release() {
|
|
43
|
+
return this.#data.release ?? null;
|
|
44
|
+
}
|
|
45
|
+
/** The Artbucket it came from, for live addresses. */
|
|
46
|
+
get server() {
|
|
47
|
+
return this.#data.server ?? null;
|
|
48
|
+
}
|
|
49
|
+
get contexts() {
|
|
50
|
+
return this.#data.contexts ?? [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Every rule, in every context: reading them all means any change reaches this site. */
|
|
54
|
+
all() {
|
|
55
|
+
this.#rules.add("*");
|
|
56
|
+
return this.#data.rules;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A rule by key, its default value or a context's. */
|
|
60
|
+
rule(key, context = null) {
|
|
61
|
+
this.#rules.add(key);
|
|
62
|
+
return this.#data.rules.find((r) => r.key === key && (r.context ?? null) === context);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
value(key, fallback, context = null) {
|
|
66
|
+
return this.rule(key, context)?.value ?? fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A text rule's words, or the fallback. */
|
|
70
|
+
text(key, fallback = "") {
|
|
71
|
+
const r = this.rule(key);
|
|
72
|
+
return r && typeof r.value === "string" ? r.value : fallback;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The rules of a group (`color.*`) or of a type (`{ type: "font" }`), default values. */
|
|
76
|
+
rules(which) {
|
|
77
|
+
if (typeof which === "string") {
|
|
78
|
+
this.#rules.add(which);
|
|
79
|
+
const prefix = which.endsWith(".*") ? which.slice(0, -1) : null;
|
|
80
|
+
return this.#data.rules.filter((r) => (r.context ?? null) === null && (prefix ? r.key.startsWith(prefix) : r.key === which));
|
|
81
|
+
}
|
|
82
|
+
const found = this.#data.rules.filter((r) => (r.context ?? null) === null && (!which?.type || r.type === which.type));
|
|
83
|
+
// A type is read through its groups: a rule added to one of them reaches this site.
|
|
84
|
+
for (const r of found) this.#rules.add(which?.type ? groupOf(r.key) : "*");
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The rule of a type whose key says one of the words (primary, accent), else (`any`) the first of that type. */
|
|
89
|
+
pick(type, words, any = true) {
|
|
90
|
+
const all = this.rules({ type });
|
|
91
|
+
return words.map((w) => all.find((r) => r.key.toLowerCase().includes(w))).find(Boolean) ?? (any ? all[0] : undefined);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A rule's file as the site serves it: its path in the export (assets/...), copied by `copyFiles`. */
|
|
95
|
+
file(asset) {
|
|
96
|
+
if (!asset?.file) return null;
|
|
97
|
+
this.#assets.add(asset.id);
|
|
98
|
+
this.#files.add(asset.file);
|
|
99
|
+
return asset.file;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A file the export holds, by its path (a text rule's link to assets/...): its asset, noted, or null. */
|
|
103
|
+
fileAt(path) {
|
|
104
|
+
const id = this.#data.files?.[path];
|
|
105
|
+
if (!id) return null;
|
|
106
|
+
this.#assets.add(id);
|
|
107
|
+
this.#files.add(path);
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* An asset's live address on the server, following its current version
|
|
113
|
+
* (/c/), so a new logo shows without a rebuild. Public assets only: a
|
|
114
|
+
* private one's address asks a visitor to sign in.
|
|
115
|
+
*/
|
|
116
|
+
liveUrl(id, transform) {
|
|
117
|
+
if (!this.server) throw new Error("This export names no server: export it again with a newer CLI");
|
|
118
|
+
this.#assets.add(id);
|
|
119
|
+
return `${this.server}/c/${id}${transform ? `/${transform}` : ""}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** tokens.css: every rule as a custom property, its fonts' @font-face. Every rule is read, and the files it names. */
|
|
123
|
+
async tokensCss() {
|
|
124
|
+
this.#rules.add("*");
|
|
125
|
+
const css = await readFile(join(this.#dir, "tokens.css"), "utf8");
|
|
126
|
+
for (const m of css.matchAll(/url\("?(assets\/[^")]+)"?\)/g)) this.fileAt(m[1]);
|
|
127
|
+
return css;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The files used so far, copied from the export into `out` at the same paths. */
|
|
131
|
+
async copyFiles(out) {
|
|
132
|
+
for (const path of this.#files) {
|
|
133
|
+
await mkdir(dirname(join(out, path)), { recursive: true });
|
|
134
|
+
await copyFile(join(this.#dir, path), join(out, path));
|
|
135
|
+
}
|
|
136
|
+
return [...this.#files];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** What the site used: the brand, the release, the rules read and the assets. */
|
|
140
|
+
manifest() {
|
|
141
|
+
const rules = this.#rules.has("*") ? ["*"] : [...this.#rules].sort();
|
|
142
|
+
return { brand: this.slug, release: this.release?.number ?? null, rules, assets: [...this.#assets] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** artbucket.site.json in `out`, which Artbucket reads as the site deploys. */
|
|
146
|
+
async writeManifest(out) {
|
|
147
|
+
const m = this.manifest();
|
|
148
|
+
await mkdir(out, { recursive: true });
|
|
149
|
+
await writeFile(join(out, "artbucket.site.json"), JSON.stringify(m, null, 2) + "\n");
|
|
150
|
+
return m;
|
|
151
|
+
}
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artbucket/brand",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "One release of a brand from Artbucket, read at build time, and the manifest of what the site used.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/pwnera/artbucket.git",
|
|
9
|
+
"directory": "kit/brand"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./index.d.ts",
|
|
15
|
+
"default": "./index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"index.d.ts",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
}
|
|
27
|
+
}
|