@ispriter/core 2.0.0 → 2.0.2
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 +26 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +82 -28
- package/package.json +14 -11
- package/LICENSE +0 -21
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @ispriter/core
|
|
2
|
+
|
|
3
|
+
Core sprite generation engine for [iSpriter](https://github.com/iazrael/ispriter).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ispriter/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Spriter } from '@ispriter/core';
|
|
15
|
+
|
|
16
|
+
const spriter = new Spriter({
|
|
17
|
+
input: { cssSource: './src/css/' },
|
|
18
|
+
output: { cssDist: './dist/css/' },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const result = await spriter.run({ css, images, cssBaseDir: './src/css/' });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -175,6 +175,7 @@ interface SpriterRunInput {
|
|
|
175
175
|
css: Map<string, string>;
|
|
176
176
|
images: Map<string, Buffer>;
|
|
177
177
|
cssBaseDir: string;
|
|
178
|
+
dryRun?: boolean;
|
|
178
179
|
}
|
|
179
180
|
/** 打包后的精灵图条目 */
|
|
180
181
|
interface PackedSprite {
|
|
@@ -201,6 +202,8 @@ interface SpriterResult {
|
|
|
201
202
|
height: number;
|
|
202
203
|
}>;
|
|
203
204
|
skippedImages: string[];
|
|
205
|
+
/** Only present when dryRun=true */
|
|
206
|
+
dryRunReport?: string;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
interface PackOutput<T> {
|
|
@@ -270,6 +273,7 @@ declare class Spriter {
|
|
|
270
273
|
/** Fix #13: 按 maxSingleSize 拆分精灵图 */
|
|
271
274
|
private splitByMaxSize;
|
|
272
275
|
private buildManifest;
|
|
276
|
+
private buildDryRunReport;
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
interface PluginRunOptions {
|
package/dist/index.js
CHANGED
|
@@ -37,10 +37,12 @@ var SpriterConfigSchema = z.object({
|
|
|
37
37
|
unit: z.enum(["px", "rem"]).default("px"),
|
|
38
38
|
remBase: z.number().positive().default(16)
|
|
39
39
|
}),
|
|
40
|
-
groups: z.array(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
groups: z.array(
|
|
41
|
+
z.object({
|
|
42
|
+
name: z.string(),
|
|
43
|
+
images: z.union([z.string(), z.array(z.string())])
|
|
44
|
+
})
|
|
45
|
+
).optional()
|
|
44
46
|
});
|
|
45
47
|
function normalizeConfig(input) {
|
|
46
48
|
if (typeof input === "string") {
|
|
@@ -66,13 +68,11 @@ function parseConfig(raw) {
|
|
|
66
68
|
validateNoTraversal(result.data.output.imageDist, "output.imageDist");
|
|
67
69
|
return result.data;
|
|
68
70
|
}
|
|
69
|
-
function validateNoTraversal(pathStr,
|
|
70
|
-
if (pathStr.includes("
|
|
71
|
-
throw new IspriterError(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{ field: fieldName }
|
|
75
|
-
);
|
|
71
|
+
function validateNoTraversal(pathStr, _fieldName) {
|
|
72
|
+
if (pathStr.includes("\0")) {
|
|
73
|
+
throw new IspriterError(`Invalid path in ${_fieldName}: null byte detected`, "CONFIG_INVALID", {
|
|
74
|
+
field: _fieldName
|
|
75
|
+
});
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -103,7 +103,9 @@ function analyseBackground(value) {
|
|
|
103
103
|
}
|
|
104
104
|
if (result.imageUrl) {
|
|
105
105
|
const afterUrl = value.replace(/url\([^)]*\)/, "").trim();
|
|
106
|
-
const posTokens = afterUrl.split(/\s+/).filter(
|
|
106
|
+
const posTokens = afterUrl.split(/\s+/).filter(
|
|
107
|
+
(t) => t && !["no-repeat", "repeat", "repeat-x", "repeat-y", "repeat-space"].includes(t)
|
|
108
|
+
).filter((t) => !t.startsWith("/") && !t.startsWith("#"));
|
|
107
109
|
if (posTokens.length >= 1) {
|
|
108
110
|
result.positionX = parsePositionValue(posTokens[0]);
|
|
109
111
|
}
|
|
@@ -128,8 +130,10 @@ function shouldSkip(parsed) {
|
|
|
128
130
|
}
|
|
129
131
|
if (parsed.positionX === "100%" || parsed.positionY === "100%") return true;
|
|
130
132
|
if (parsed.positionX === "center" || parsed.positionY === "center") return true;
|
|
131
|
-
if (typeof parsed.positionX === "string" && parsed.positionX.endsWith("%") && parsed.positionX !== "100%")
|
|
132
|
-
|
|
133
|
+
if (typeof parsed.positionX === "string" && parsed.positionX.endsWith("%") && parsed.positionX !== "100%")
|
|
134
|
+
return true;
|
|
135
|
+
if (typeof parsed.positionY === "string" && parsed.positionY.endsWith("%") && parsed.positionY !== "100%")
|
|
136
|
+
return true;
|
|
133
137
|
return false;
|
|
134
138
|
}
|
|
135
139
|
function cleanImageUrl(url) {
|
|
@@ -173,7 +177,9 @@ async function extractBackgrounds(cssContents) {
|
|
|
173
177
|
processRules(root, filename, false, rules);
|
|
174
178
|
root.walkAtRules(/keyframes/i, (atRule) => {
|
|
175
179
|
if (atRule.nodes) {
|
|
176
|
-
const kfRoot = postcss2.parse(
|
|
180
|
+
const kfRoot = postcss2.parse(
|
|
181
|
+
atRule.toString().replace(/@[^{]+\{/, "").replace(/\}$/, "")
|
|
182
|
+
);
|
|
177
183
|
processRules(kfRoot, filename, true, rules);
|
|
178
184
|
}
|
|
179
185
|
});
|
|
@@ -328,14 +334,19 @@ async function emitCSS(cssContents, packedSprites, config, retinaInfo) {
|
|
|
328
334
|
);
|
|
329
335
|
const minified = cleaner.minify(output);
|
|
330
336
|
if (minified.errors.length > 0) {
|
|
331
|
-
throw new IspriterError(
|
|
337
|
+
throw new IspriterError(
|
|
338
|
+
`CSS compression failed: ${minified.errors.join(", ")}`,
|
|
339
|
+
"OUTPUT_ERROR"
|
|
340
|
+
);
|
|
332
341
|
}
|
|
333
342
|
output = minified.styles;
|
|
334
343
|
}
|
|
335
344
|
results.set(filename, output);
|
|
336
345
|
} catch (e) {
|
|
337
346
|
if (e instanceof IspriterError) throw e;
|
|
338
|
-
throw new IspriterError(`Failed to emit CSS for ${filename}`, "OUTPUT_ERROR", {
|
|
347
|
+
throw new IspriterError(`Failed to emit CSS for ${filename}`, "OUTPUT_ERROR", {
|
|
348
|
+
file: filename
|
|
349
|
+
});
|
|
339
350
|
}
|
|
340
351
|
}
|
|
341
352
|
if (config.output.combine && results.size > 1) {
|
|
@@ -368,13 +379,15 @@ async function generateSprites(packedSprites, config) {
|
|
|
368
379
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
369
380
|
}
|
|
370
381
|
});
|
|
371
|
-
const composites = await Promise.all(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
382
|
+
const composites = await Promise.all(
|
|
383
|
+
items.map(async (item) => {
|
|
384
|
+
let inputBuf = item.asset.buffer;
|
|
385
|
+
if (item.width !== item.asset.naturalWidth || item.height !== item.asset.naturalHeight) {
|
|
386
|
+
inputBuf = await sharp(item.asset.buffer).resize(item.width, item.height, { fit: "fill" }).toBuffer();
|
|
387
|
+
}
|
|
388
|
+
return { input: inputBuf, left: item.x, top: item.y };
|
|
389
|
+
})
|
|
390
|
+
);
|
|
378
391
|
let output = canvas.composite(composites);
|
|
379
392
|
if (config.output.format === "webp") {
|
|
380
393
|
output = output.webp({ quality: config.output.quality });
|
|
@@ -385,10 +398,14 @@ async function generateSprites(packedSprites, config) {
|
|
|
385
398
|
results.set(sprite.spriteFile, buffer);
|
|
386
399
|
} catch (e) {
|
|
387
400
|
if (e instanceof IspriterError) throw e;
|
|
388
|
-
throw new IspriterError(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
401
|
+
throw new IspriterError(
|
|
402
|
+
`Failed to generate sprite: ${sprite.spriteFile}: ${e.message}`,
|
|
403
|
+
"OUTPUT_ERROR",
|
|
404
|
+
{
|
|
405
|
+
spriteFile: sprite.spriteFile,
|
|
406
|
+
cause: e
|
|
407
|
+
}
|
|
408
|
+
);
|
|
392
409
|
}
|
|
393
410
|
}
|
|
394
411
|
return results;
|
|
@@ -415,6 +432,16 @@ var Spriter = class {
|
|
|
415
432
|
allPackedSprites.push(...packed);
|
|
416
433
|
}
|
|
417
434
|
const finalSprites = this.splitByMaxSize(allPackedSprites);
|
|
435
|
+
if (input.dryRun) {
|
|
436
|
+
const manifest2 = this.buildManifest(finalSprites);
|
|
437
|
+
return {
|
|
438
|
+
cssFiles: /* @__PURE__ */ new Map(),
|
|
439
|
+
spriteImages: /* @__PURE__ */ new Map(),
|
|
440
|
+
manifest: manifest2,
|
|
441
|
+
skippedImages: skipped,
|
|
442
|
+
dryRunReport: this.buildDryRunReport(finalSprites, skipped, assets.length)
|
|
443
|
+
};
|
|
444
|
+
}
|
|
418
445
|
const spriteImages = await generateSprites(finalSprites, this.config);
|
|
419
446
|
let retinaInfo;
|
|
420
447
|
if (this.config.output.retina) {
|
|
@@ -583,6 +610,33 @@ var Spriter = class {
|
|
|
583
610
|
}
|
|
584
611
|
return manifest;
|
|
585
612
|
}
|
|
613
|
+
buildDryRunReport(packedSprites, skipped, totalAssets) {
|
|
614
|
+
const lines = [];
|
|
615
|
+
lines.push("");
|
|
616
|
+
lines.push("\u{1F4CB} Dry-run report");
|
|
617
|
+
lines.push("\u2500".repeat(50));
|
|
618
|
+
lines.push(`Images found: ${totalAssets}`);
|
|
619
|
+
lines.push(`Images sprited: ${totalAssets - skipped.length}`);
|
|
620
|
+
lines.push(`Images skipped: ${skipped.length}`);
|
|
621
|
+
lines.push(`Sprite sheets: ${packedSprites.length}`);
|
|
622
|
+
lines.push("");
|
|
623
|
+
for (const sprite of packedSprites) {
|
|
624
|
+
lines.push(`\u{1F4C4} ${sprite.spriteFile} (${sprite.canvasWidth}\xD7${sprite.canvasHeight}px)`);
|
|
625
|
+
lines.push(` Contains ${sprite.items.length} image(s):`);
|
|
626
|
+
for (const item of sprite.items) {
|
|
627
|
+
lines.push(` \u2022 ${item.asset.url} \u2192 (${item.x}, ${item.y}) ${item.width}\xD7${item.height}`);
|
|
628
|
+
}
|
|
629
|
+
lines.push("");
|
|
630
|
+
}
|
|
631
|
+
if (skipped.length > 0) {
|
|
632
|
+
lines.push(`\u26A0\uFE0F Skipped (${skipped.length}):`);
|
|
633
|
+
for (const url of skipped) {
|
|
634
|
+
lines.push(` \u2022 ${url}`);
|
|
635
|
+
}
|
|
636
|
+
lines.push("");
|
|
637
|
+
}
|
|
638
|
+
return lines.join("\n");
|
|
639
|
+
}
|
|
586
640
|
};
|
|
587
641
|
|
|
588
642
|
// src/plugin-helper.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ispriter/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -10,31 +10,34 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"test": "vitest run"
|
|
17
|
+
},
|
|
13
18
|
"dependencies": {
|
|
19
|
+
"@ispriter/shared": "workspace:*",
|
|
14
20
|
"clean-css": "^5.3.0",
|
|
15
21
|
"postcss": "^8.5.0",
|
|
16
22
|
"postcss-import": "^16.1.1",
|
|
17
23
|
"sharp": "^0.33.0",
|
|
18
|
-
"zod": "^3.24.0"
|
|
19
|
-
"@ispriter/shared": "2.0.0"
|
|
24
|
+
"zod": "^3.24.0"
|
|
20
25
|
},
|
|
21
26
|
"devDependencies": {
|
|
22
27
|
"@types/clean-css": "^4.2.0",
|
|
23
28
|
"tsup": "^8.4.0",
|
|
24
|
-
"vitest": "^3.1.0"
|
|
29
|
+
"vitest": "^3.1.0",
|
|
30
|
+
"zod-to-json-schema": "^3.25.2"
|
|
25
31
|
},
|
|
26
32
|
"files": [
|
|
27
|
-
"dist"
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
28
36
|
],
|
|
29
37
|
"engines": {
|
|
30
38
|
"node": ">=20.0.0"
|
|
31
39
|
},
|
|
32
40
|
"publishConfig": {
|
|
33
41
|
"access": "public"
|
|
34
|
-
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "tsup",
|
|
37
|
-
"dev": "tsup --watch",
|
|
38
|
-
"test": "vitest run"
|
|
39
42
|
}
|
|
40
|
-
}
|
|
43
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2014 iazrael.
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a
|
|
6
|
-
copy of this software and associated documentation files (the "Software"),
|
|
7
|
-
to deal in the Software without restriction, including without limitation
|
|
8
|
-
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
9
|
-
and/or sell copies of the Software, and to permit persons to whom the
|
|
10
|
-
Software is furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
all 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
|
|
20
|
-
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
21
|
-
DEALINGS IN THE SOFTWARE.
|