@pantoken/icon-font 0.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/README.md +64 -0
- package/dist/index.d.mts +79 -0
- package/dist/index.mjs +229 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @pantoken/icon-font
|
|
2
|
+
|
|
3
|
+
Build an installable Instructure icon font from the Canvas by Instructure icon set. You get a WOFF2 file, a TTF file, a stylesheet, and a codepoint map. Install the font on a device and use the glyphs in design apps, documents, and offline contexts where the web tooling isn't reachable.
|
|
4
|
+
|
|
5
|
+
## Why a font
|
|
6
|
+
|
|
7
|
+
The other pantoken icon formats emit inline SVG or per-framework components. Those need a build step and a browser. A font doesn't. Once it's installed, every app that picks a font can use the icons: Figma, Sketch, Google Slides, Keynote, Word, and any native text field.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Instructure ships two kinds of glyphs:
|
|
12
|
+
|
|
13
|
+
- Fill glyphs (the custom Instructure set) already describe filled regions, so they pass straight through.
|
|
14
|
+
- Stroke glyphs (the Lucide set) describe a centerline with a stroke width. A font glyph has no stroke, so this package outlines each stroke into a filled shape. It flattens every subpath to points, then offsets that polyline by half the stroke width with round joins and caps.
|
|
15
|
+
|
|
16
|
+
The result is one filled path per icon, streamed through `svgicons2svgfont`, `svg2ttf`, and `ttf2woff2`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm i -D @pantoken/icon-font
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This package isn't part of the unscoped `pantoken` meta package. The font toolchain is heavy, so `npm i pantoken` stays lean. Install this one directly when you need a font.
|
|
25
|
+
|
|
26
|
+
## CLI
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pantoken generate icon-font --out ./fonts
|
|
30
|
+
pantoken generate icon-font --out ./fonts --class Instructure --icons arrow-left,check-mark
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Flags:
|
|
34
|
+
|
|
35
|
+
- `--out` is the output directory (default `./pantoken-out`).
|
|
36
|
+
- `--class` is the font family name (default `PanTokens`).
|
|
37
|
+
- `--icons` is a comma-separated subset (default: every icon).
|
|
38
|
+
- `--theme` is the source theme (default `rebrand`).
|
|
39
|
+
|
|
40
|
+
The command writes four files: `<class>.woff2`, `<class>.ttf`, `icons.css`, and `codepoints.json`.
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { buildIconFont } from "@pantoken/icon-font";
|
|
46
|
+
|
|
47
|
+
const font = await buildIconFont({ fontName: "PanTokens", icons: ["arrow-left"] });
|
|
48
|
+
// font.woff2 → Uint8Array
|
|
49
|
+
// font.ttf → Uint8Array
|
|
50
|
+
// font.css → string (@font-face + .instui-icon-<name> rules)
|
|
51
|
+
// font.codepoints → { "arrow-left": "e000", ... }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Use the font on the web
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<link rel="stylesheet" href="./icons.css" /> <i class="instui-icon instui-icon-arrow-left"></i>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Each glyph lives in the Unicode Private Use Area, starting at U+E000. The `codepoints.json` map records the assignment for every icon, so you can also type a glyph directly once the font is installed.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Theme } from "@pantoken/model";
|
|
2
|
+
|
|
3
|
+
//#region src/build.d.ts
|
|
4
|
+
/** The generated font artifacts. */
|
|
5
|
+
interface IconFontResult {
|
|
6
|
+
/** The TrueType font bytes. */
|
|
7
|
+
ttf: Uint8Array;
|
|
8
|
+
/** The WOFF2 font bytes. */
|
|
9
|
+
woff2: Uint8Array;
|
|
10
|
+
/** The `@font-face` + `.instui-icon-<name>` stylesheet. */
|
|
11
|
+
css: string;
|
|
12
|
+
/** Icon name → hex codepoint (e.g. `"e001"`). */
|
|
13
|
+
codepoints: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
/** Options for {@link buildIconFont}. */
|
|
16
|
+
interface BuildIconFontOptions {
|
|
17
|
+
/** Icon names to include (default: the whole set). */
|
|
18
|
+
icons?: string[];
|
|
19
|
+
/** The font family name (default `"PanTokens"`). */
|
|
20
|
+
fontName?: string;
|
|
21
|
+
/** The theme to source glyphs from (default `"rebrand"`). */
|
|
22
|
+
theme?: Theme;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the icon font.
|
|
26
|
+
*
|
|
27
|
+
* @param options - {@link BuildIconFontOptions}.
|
|
28
|
+
* @returns The {@link IconFontResult}.
|
|
29
|
+
*
|
|
30
|
+
* @example Build the full font and write the artifacts
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { buildIconFont } from "@pantoken/icon-font";
|
|
33
|
+
* import { writeFileSync } from "node:fs";
|
|
34
|
+
*
|
|
35
|
+
* const font = await buildIconFont();
|
|
36
|
+
* writeFileSync("PanTokens.woff2", font.woff2);
|
|
37
|
+
* writeFileSync("PanTokens.ttf", font.ttf);
|
|
38
|
+
* writeFileSync("icons.css", font.css);
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example A custom font name and a subset of icons from another theme
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { buildIconFont } from "@pantoken/icon-font";
|
|
44
|
+
*
|
|
45
|
+
* const font = await buildIconFont({
|
|
46
|
+
* fontName: "Instructure",
|
|
47
|
+
* icons: ["arrow-left", "check-mark"],
|
|
48
|
+
* theme: "canvas",
|
|
49
|
+
* });
|
|
50
|
+
* font.codepoints; // { "arrow-left": "e000", "check-mark": "e001" }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function buildIconFont(options?: BuildIconFontOptions): Promise<IconFontResult>;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/outline.d.ts
|
|
56
|
+
/** A filled glyph path plus the SVG viewBox size to wrap it in. */
|
|
57
|
+
interface GlyphPath {
|
|
58
|
+
d: string;
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Produce a filled glyph path for an icon SVG.
|
|
64
|
+
*
|
|
65
|
+
* @param svg - Inline SVG markup.
|
|
66
|
+
* @returns The filled path `d` (in viewBox coordinates) and the viewBox size.
|
|
67
|
+
*
|
|
68
|
+
* @example Outline a stroke-based (Lucide) icon and a fill-based icon
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { svgToGlyphPath } from "@pantoken/icon-font";
|
|
71
|
+
* import { getIcon } from "@pantoken/icons";
|
|
72
|
+
*
|
|
73
|
+
* const { d, width, height } = svgToGlyphPath(getIcon("arrow-left")!.svg);
|
|
74
|
+
* // d: a single filled path; width/height: the viewBox size (e.g. 24, 24)
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
declare function svgToGlyphPath(svg: string): GlyphPath;
|
|
78
|
+
//#endregion
|
|
79
|
+
export { type BuildIconFontOptions, type GlyphPath, type IconFontResult, buildIconFont, svgToGlyphPath };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { getIconSvgs } from "@pantoken/core";
|
|
3
|
+
import { byTheme } from "@pantoken/tokens";
|
|
4
|
+
import { SVGIcons2SVGFontStream } from "svgicons2svgfont";
|
|
5
|
+
import svg2ttf from "svg2ttf";
|
|
6
|
+
import ttf2woff2 from "ttf2woff2";
|
|
7
|
+
import ClipperLib from "clipper-lib";
|
|
8
|
+
import { pointsOnPath } from "points-on-path";
|
|
9
|
+
//#region src/outline.ts
|
|
10
|
+
/**
|
|
11
|
+
* Turn an icon SVG into a single **filled** glyph path (what a font glyph needs). Fill-based icons
|
|
12
|
+
* (Instructure custom glyphs) pass through; stroke-based icons (Lucide) are converted from a
|
|
13
|
+
* centerline stroke to a filled outline by flattening each subpath to points and offsetting it by
|
|
14
|
+
* half the stroke width with round joins/caps (`points-on-path` + `clipper-lib`, pure JS).
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
const SCALE = 100;
|
|
19
|
+
function attr(tag, name) {
|
|
20
|
+
return new RegExp(`\\b${name}=["']([^"']*)["']`).exec(tag)?.[1];
|
|
21
|
+
}
|
|
22
|
+
const STROKED_RE = /\bstroke=["'](?!none)[^"']+["']/;
|
|
23
|
+
function nums(value) {
|
|
24
|
+
return (value ?? "").split(/[\s,]+/).filter(Boolean).map(Number);
|
|
25
|
+
}
|
|
26
|
+
/** Convert an SVG primitive element to a subpath `d` + closure. */
|
|
27
|
+
function primitiveToSubpath(tag, el) {
|
|
28
|
+
switch (tag) {
|
|
29
|
+
case "line": {
|
|
30
|
+
const [x1, y1, x2, y2] = [
|
|
31
|
+
attr(el, "x1"),
|
|
32
|
+
attr(el, "y1"),
|
|
33
|
+
attr(el, "x2"),
|
|
34
|
+
attr(el, "y2")
|
|
35
|
+
].map(Number);
|
|
36
|
+
return {
|
|
37
|
+
d: `M${x1},${y1} L${x2},${y2}`,
|
|
38
|
+
closed: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
case "circle": {
|
|
42
|
+
const cx = Number(attr(el, "cx"));
|
|
43
|
+
const cy = Number(attr(el, "cy"));
|
|
44
|
+
const r = Number(attr(el, "r"));
|
|
45
|
+
return {
|
|
46
|
+
d: `M${cx - r},${cy} a${r},${r} 0 1,0 ${r * 2},0 a${r},${r} 0 1,0 ${-r * 2},0`,
|
|
47
|
+
closed: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
case "rect": {
|
|
51
|
+
const x = Number(attr(el, "x") ?? "0");
|
|
52
|
+
const y = Number(attr(el, "y") ?? "0");
|
|
53
|
+
const w = Number(attr(el, "width"));
|
|
54
|
+
return {
|
|
55
|
+
d: `M${x},${y} h${w} v${Number(attr(el, "height"))} h${-w} z`,
|
|
56
|
+
closed: true
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
case "polyline":
|
|
60
|
+
case "polygon": {
|
|
61
|
+
const pts = nums(attr(el, "points"));
|
|
62
|
+
if (pts.length < 4) return void 0;
|
|
63
|
+
const parts = [`M${pts[0]},${pts[1]}`];
|
|
64
|
+
for (let i = 2; i < pts.length; i += 2) parts.push(`L${pts[i]},${pts[i + 1]}`);
|
|
65
|
+
if (tag === "polygon") parts.push("z");
|
|
66
|
+
return {
|
|
67
|
+
d: parts.join(" "),
|
|
68
|
+
closed: tag === "polygon"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
default: return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Split a path `d` into its subpaths (at each M/m), marking each closed if it contains z/Z. */
|
|
75
|
+
function splitSubpaths(d) {
|
|
76
|
+
return (d.match(/[Mm][^Mm]*/g) ?? []).map((chunk) => ({
|
|
77
|
+
d: chunk.trim(),
|
|
78
|
+
closed: /[Zz]/.test(chunk)
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Produce a filled glyph path for an icon SVG.
|
|
83
|
+
*
|
|
84
|
+
* @param svg - Inline SVG markup.
|
|
85
|
+
* @returns The filled path `d` (in viewBox coordinates) and the viewBox size.
|
|
86
|
+
*
|
|
87
|
+
* @example Outline a stroke-based (Lucide) icon and a fill-based icon
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { svgToGlyphPath } from "@pantoken/icon-font";
|
|
90
|
+
* import { getIcon } from "@pantoken/icons";
|
|
91
|
+
*
|
|
92
|
+
* const { d, width, height } = svgToGlyphPath(getIcon("arrow-left")!.svg);
|
|
93
|
+
* // d: a single filled path; width/height: the viewBox size (e.g. 24, 24)
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
function svgToGlyphPath(svg) {
|
|
97
|
+
const vb = nums(/viewBox=["']([^"']+)["']/.exec(svg)?.[1]);
|
|
98
|
+
const [width, height] = vb.length === 4 ? [vb[2], vb[3]] : [24, 24];
|
|
99
|
+
const rootTag = /<svg\b[^>]*>/.exec(svg)?.[0] ?? "";
|
|
100
|
+
const strokeWidth = Number(attr(rootTag, "stroke-width") ?? "2");
|
|
101
|
+
const stroked = STROKED_RE.test(rootTag);
|
|
102
|
+
const subpaths = [];
|
|
103
|
+
for (const [, tag, el] of svg.matchAll(/<(path|line|circle|rect|polyline|polygon)\b([^>]*)>/g)) if (tag === "path") {
|
|
104
|
+
const d = attr(el, "d");
|
|
105
|
+
if (d) subpaths.push(...splitSubpaths(d));
|
|
106
|
+
} else {
|
|
107
|
+
const sub = primitiveToSubpath(tag, el);
|
|
108
|
+
if (sub) subpaths.push(sub);
|
|
109
|
+
}
|
|
110
|
+
if (!stroked) return {
|
|
111
|
+
d: subpaths.map((s) => s.d).join(" "),
|
|
112
|
+
width,
|
|
113
|
+
height
|
|
114
|
+
};
|
|
115
|
+
const offset = new ClipperLib.ClipperOffset(2, .25);
|
|
116
|
+
for (const sub of subpaths) for (const poly of pointsOnPath(sub.d, .2, .2)) {
|
|
117
|
+
if (poly.length < 2) continue;
|
|
118
|
+
const path = poly.map(([x, y]) => ({
|
|
119
|
+
X: Math.round(x * SCALE),
|
|
120
|
+
Y: Math.round(y * SCALE)
|
|
121
|
+
}));
|
|
122
|
+
offset.AddPath(path, ClipperLib.JoinType.jtRound, sub.closed ? ClipperLib.EndType.etClosedLine : ClipperLib.EndType.etOpenRound);
|
|
123
|
+
}
|
|
124
|
+
const solution = new ClipperLib.Paths();
|
|
125
|
+
offset.Execute(solution, strokeWidth / 2 * SCALE);
|
|
126
|
+
return {
|
|
127
|
+
d: solution.map((poly) => `M${poly.map((pt) => `${pt.X / SCALE},${pt.Y / SCALE}`).join(" L")} Z`).join(" "),
|
|
128
|
+
width,
|
|
129
|
+
height
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/build.ts
|
|
134
|
+
/**
|
|
135
|
+
* Build a full Instructure icon font. Stroke glyphs are outlined (see {@link svgToGlyphPath}), then
|
|
136
|
+
* the glyph set is streamed through `svgicons2svgfont` → `svg2ttf` → `ttf2woff2`, with a matching
|
|
137
|
+
* `icons.css` and `codepoints.json`. Codepoints start at the Unicode Private Use Area (U+E000).
|
|
138
|
+
*
|
|
139
|
+
* @module
|
|
140
|
+
*/
|
|
141
|
+
function css(fontName, codepoints) {
|
|
142
|
+
return `${[
|
|
143
|
+
[
|
|
144
|
+
"@font-face {",
|
|
145
|
+
` font-family: "${fontName}";`,
|
|
146
|
+
` src: url("./${fontName}.woff2") format("woff2"), url("./${fontName}.ttf") format("truetype");`,
|
|
147
|
+
" font-weight: normal;",
|
|
148
|
+
" font-style: normal;",
|
|
149
|
+
" font-display: block;",
|
|
150
|
+
"}"
|
|
151
|
+
].join("\n"),
|
|
152
|
+
`.instui-icon { font-family: "${fontName}"; font-style: normal; font-weight: normal; line-height: 1; -webkit-font-smoothing: antialiased; }`,
|
|
153
|
+
Object.entries(codepoints).map(([name, hex]) => `.instui-icon-${name}::before { content: "\\${hex}"; }`).join("\n")
|
|
154
|
+
].join("\n\n")}\n`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build the icon font.
|
|
158
|
+
*
|
|
159
|
+
* @param options - {@link BuildIconFontOptions}.
|
|
160
|
+
* @returns The {@link IconFontResult}.
|
|
161
|
+
*
|
|
162
|
+
* @example Build the full font and write the artifacts
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { buildIconFont } from "@pantoken/icon-font";
|
|
165
|
+
* import { writeFileSync } from "node:fs";
|
|
166
|
+
*
|
|
167
|
+
* const font = await buildIconFont();
|
|
168
|
+
* writeFileSync("PanTokens.woff2", font.woff2);
|
|
169
|
+
* writeFileSync("PanTokens.ttf", font.ttf);
|
|
170
|
+
* writeFileSync("icons.css", font.css);
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* @example A custom font name and a subset of icons from another theme
|
|
174
|
+
* ```ts
|
|
175
|
+
* import { buildIconFont } from "@pantoken/icon-font";
|
|
176
|
+
*
|
|
177
|
+
* const font = await buildIconFont({
|
|
178
|
+
* fontName: "Instructure",
|
|
179
|
+
* icons: ["arrow-left", "check-mark"],
|
|
180
|
+
* theme: "canvas",
|
|
181
|
+
* });
|
|
182
|
+
* font.codepoints; // { "arrow-left": "e000", "check-mark": "e001" }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
async function buildIconFont(options = {}) {
|
|
186
|
+
const fontName = options.fontName ?? "PanTokens";
|
|
187
|
+
const svgs = getIconSvgs(byTheme(options.theme ?? "rebrand"));
|
|
188
|
+
const names = (options.icons?.length ? options.icons : [...svgs.keys()]).filter((n) => svgs.has(n)).sort();
|
|
189
|
+
const stream = new SVGIcons2SVGFontStream({
|
|
190
|
+
fontName,
|
|
191
|
+
fontHeight: 1e3,
|
|
192
|
+
normalize: true
|
|
193
|
+
});
|
|
194
|
+
let svgFont = "";
|
|
195
|
+
stream.on("data", (chunk) => {
|
|
196
|
+
svgFont += chunk.toString();
|
|
197
|
+
});
|
|
198
|
+
const done = new Promise((resolve, reject) => {
|
|
199
|
+
stream.on("end", resolve);
|
|
200
|
+
stream.on("error", reject);
|
|
201
|
+
});
|
|
202
|
+
const codepoints = {};
|
|
203
|
+
let codepoint = 57344;
|
|
204
|
+
for (const name of names) {
|
|
205
|
+
const svg = svgs.get(name);
|
|
206
|
+
if (!svg) continue;
|
|
207
|
+
const { d, width, height } = svgToGlyphPath(svg);
|
|
208
|
+
if (!d) continue;
|
|
209
|
+
const code = codepoint++;
|
|
210
|
+
codepoints[name] = code.toString(16);
|
|
211
|
+
const glyph = Readable.from([`<svg viewBox="0 0 ${width} ${height}"><path d="${d}"/></svg>`]);
|
|
212
|
+
glyph.metadata = {
|
|
213
|
+
unicode: [String.fromCodePoint(code)],
|
|
214
|
+
name
|
|
215
|
+
};
|
|
216
|
+
stream.write(glyph);
|
|
217
|
+
}
|
|
218
|
+
stream.end();
|
|
219
|
+
await done;
|
|
220
|
+
const ttf = new Uint8Array(svg2ttf(svgFont, { description: "Instructure icons (pantoken)" }).buffer);
|
|
221
|
+
return {
|
|
222
|
+
ttf,
|
|
223
|
+
woff2: new Uint8Array(ttf2woff2(ttf)),
|
|
224
|
+
css: css(fontName, codepoints),
|
|
225
|
+
codepoints
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
export { buildIconFont, svgToGlyphPath };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pantoken/icon-font",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build an installable Instructure icon font (WOFF2 + TTF) with CSS and codepoints — stroke glyphs outlined to fills.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"clipper-lib": "^6.4.2",
|
|
19
|
+
"points-on-path": "^0.2.1",
|
|
20
|
+
"svg2ttf": "^6.1.0",
|
|
21
|
+
"svgicons2svgfont": "^16.0.0",
|
|
22
|
+
"ttf2woff2": "^8.0.1",
|
|
23
|
+
"@pantoken/core": "0.1.0",
|
|
24
|
+
"@pantoken/model": "0.1.0",
|
|
25
|
+
"@pantoken/tokens": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^24.13.3",
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
|
|
31
|
+
"vite-plus": "0.2.4"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "vp pack",
|
|
35
|
+
"dev": "vp pack --watch",
|
|
36
|
+
"test": "vp test",
|
|
37
|
+
"check": "vp check"
|
|
38
|
+
}
|
|
39
|
+
}
|