@tinloof/typed-svg-sprite 0.0.1
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/CHANGELOG.md +13 -0
- package/README.md +190 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +118 -0
- package/dist/components.d.ts +24 -0
- package/dist/components.js +102 -0
- package/dist/core.d.ts +65 -0
- package/dist/core.js +245 -0
- package/dist/next.d.ts +111 -0
- package/dist/next.js +202 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +115 -0
- package/package.json +64 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @tinloof/typed-svg-sprite
|
|
2
|
+
|
|
3
|
+
## 0.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Initial release of @tinloof/typed-svg-sprite
|
|
8
|
+
- Generate optimized SVG sprites with full TypeScript support
|
|
9
|
+
- CLI tool for generating sprites from SVG files
|
|
10
|
+
- Next.js integration with `withSpriteLoader`
|
|
11
|
+
- Auto-generated TypeScript definitions
|
|
12
|
+
- Auto-generated React Icon component
|
|
13
|
+
- Watch mode for development
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @tinloof/typed-svg-sprite
|
|
2
|
+
|
|
3
|
+
> Generate optimized SVG sprites with full TypeScript support
|
|
4
|
+
|
|
5
|
+
Automatically generates SVG sprite files with type-safe TypeScript definitions and a ready-to-use React component.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @tinloof/typed-svg-sprite
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### CLI
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Generate sprite
|
|
19
|
+
typed-svg-sprite --input public/icons --output public/sprite.svg
|
|
20
|
+
|
|
21
|
+
# Watch mode
|
|
22
|
+
typed-svg-sprite -i public/icons -o public/sprite.svg --watch
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Next.js
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// next.config.ts
|
|
29
|
+
import { withSpriteLoader } from "@tinloof/typed-svg-sprite/next";
|
|
30
|
+
|
|
31
|
+
export default withSpriteLoader({});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Place SVGs in `public/icons/` and use:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { HOME, SETTINGS } from "@/generated/icons";
|
|
38
|
+
import { Icon } from "@/generated/Icon";
|
|
39
|
+
|
|
40
|
+
function MyComponent() {
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<Icon href={HOME} />
|
|
44
|
+
<Icon href={SETTINGS} size={32} />
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### CLI Options
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
typed-svg-sprite --input <dir> --output <file> [options]
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
-i, --input <dir> Directory containing SVG files
|
|
59
|
+
-o, --output <file> Output sprite file path
|
|
60
|
+
-w, --watch Watch for changes and regenerate
|
|
61
|
+
-h, --help Show help message
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Next.js Configuration
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
export default withSpriteLoader(
|
|
68
|
+
{
|
|
69
|
+
// your existing Next.js config
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
inputDir?: string; // default: "public/icons"
|
|
73
|
+
outputFile?: string; // default: "public/sprite.svg"
|
|
74
|
+
url?: string; // default: "/"
|
|
75
|
+
filename?: string; // default: "sprite.svg"
|
|
76
|
+
typesOutputFile?: string; // default: "generated/icons.ts"
|
|
77
|
+
generateIconComponent?: boolean; // default: true
|
|
78
|
+
iconComponentOutputFile?: string; // default: "generated/Icon.tsx"
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Generated Files
|
|
84
|
+
|
|
85
|
+
### 1. Sprite (`public/sprite.svg`)
|
|
86
|
+
|
|
87
|
+
```xml
|
|
88
|
+
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
|
89
|
+
<symbol id="a" viewBox="0 0 24 24"><!-- icon content --></symbol>
|
|
90
|
+
<symbol id="b" viewBox="0 0 24 24"><!-- icon content --></symbol>
|
|
91
|
+
</svg>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2. TypeScript Types (`generated/icons.ts`)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
export enum IconId {
|
|
98
|
+
HOME = "a",
|
|
99
|
+
SETTINGS = "b",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type IconHref = `/sprite.svg#${IconId}`;
|
|
103
|
+
|
|
104
|
+
export const HOME: IconHref = "/sprite.svg#a";
|
|
105
|
+
export const SETTINGS: IconHref = "/sprite.svg#b";
|
|
106
|
+
|
|
107
|
+
export function getIconHref(iconId: IconId): IconHref;
|
|
108
|
+
export const allIcons: IconId[];
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3. React Component (`generated/Icon.tsx`)
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { IconHref } from "./icons";
|
|
115
|
+
|
|
116
|
+
export interface IconProps extends React.SVGProps<SVGSVGElement> {
|
|
117
|
+
href: IconHref;
|
|
118
|
+
size?: number | string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function Icon({ href, size = 24, ...props }: IconProps) {
|
|
122
|
+
// ...
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Examples
|
|
127
|
+
|
|
128
|
+
### Basic Usage
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { HOME, SEARCH, SETTINGS } from "@/generated/icons";
|
|
132
|
+
import { Icon } from "@/generated/Icon";
|
|
133
|
+
|
|
134
|
+
<Icon href={HOME} />
|
|
135
|
+
<Icon href={SEARCH} size={20} />
|
|
136
|
+
<Icon href={SETTINGS} className="text-blue-500" />
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Dynamic Icons
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { IconId, getIconHref } from "@/generated/icons";
|
|
143
|
+
|
|
144
|
+
function DynamicIcon({ iconId }: { iconId: IconId }) {
|
|
145
|
+
return <Icon href={getIconHref(iconId)} />;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Build Script Integration
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"scripts": {
|
|
154
|
+
"generate:icons": "typed-svg-sprite --input public/icons --output public/sprite.svg",
|
|
155
|
+
"build": "npm run generate:icons && next build"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Without React
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Generate sprite and types
|
|
164
|
+
typed-svg-sprite --input src/icons --output public/sprite.svg
|
|
165
|
+
|
|
166
|
+
# Use generated types
|
|
167
|
+
import { HOME, SETTINGS } from "./generated/icons";
|
|
168
|
+
|
|
169
|
+
// In your HTML/JS
|
|
170
|
+
<svg><use href={HOME} /></svg>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## How It Works
|
|
174
|
+
|
|
175
|
+
1. Scans directory for `.svg` files
|
|
176
|
+
2. Extracts and optimizes SVG content
|
|
177
|
+
3. Generates compact base-52 IDs (`a`, `b`, `aa`, etc.)
|
|
178
|
+
4. Combines into single sprite file
|
|
179
|
+
5. Generates TypeScript types with file-based names
|
|
180
|
+
6. Generates React component (optional)
|
|
181
|
+
|
|
182
|
+
**Symbol IDs**: Short IDs (`a`, `b`) in sprite, original names (`HOME`, `SETTINGS`) in TypeScript exports.
|
|
183
|
+
|
|
184
|
+
## Roadmap
|
|
185
|
+
|
|
186
|
+
- [ ] Integrate SVGO for advanced SVG optimization
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const core_js_1 = require("./core.js");
|
|
40
|
+
function parseArgs() {
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const options = {};
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
switch (args[i]) {
|
|
45
|
+
case "--input":
|
|
46
|
+
case "-i":
|
|
47
|
+
options.input = args[++i];
|
|
48
|
+
break;
|
|
49
|
+
case "--output":
|
|
50
|
+
case "-o":
|
|
51
|
+
options.output = args[++i];
|
|
52
|
+
break;
|
|
53
|
+
case "--watch":
|
|
54
|
+
case "-w":
|
|
55
|
+
options.watch = true;
|
|
56
|
+
break;
|
|
57
|
+
case "--help":
|
|
58
|
+
case "-h":
|
|
59
|
+
printHelp();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!options.input || !options.output) {
|
|
64
|
+
console.error("Error: --input and --output are required\n");
|
|
65
|
+
printHelp();
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
return options;
|
|
69
|
+
}
|
|
70
|
+
function printHelp() {
|
|
71
|
+
console.log(`
|
|
72
|
+
SVG Sprite Generator
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
typed-svg-sprite --input <dir> --output <file> [options]
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
-i, --input <dir> Directory containing SVG files
|
|
79
|
+
-o, --output <file> Output sprite file path
|
|
80
|
+
-w, --watch Watch for changes and regenerate
|
|
81
|
+
-h, --help Show this help message
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
typed-svg-sprite --input public/icons --output public/sprite.svg
|
|
85
|
+
typed-svg-sprite -i ./icons -o ./dist/sprite.svg --watch
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
function runSpriteGeneration(inputDir, outputFile) {
|
|
89
|
+
try {
|
|
90
|
+
(0, core_js_1.generateSprite)({
|
|
91
|
+
inputDir,
|
|
92
|
+
outputFile,
|
|
93
|
+
verbose: true,
|
|
94
|
+
minify: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error("Error generating sprite:", error);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function main() {
|
|
103
|
+
const options = parseArgs();
|
|
104
|
+
// Initial generation
|
|
105
|
+
runSpriteGeneration(options.input, options.output);
|
|
106
|
+
// Watch mode
|
|
107
|
+
if (options.watch) {
|
|
108
|
+
const absoluteInputDir = path.resolve(process.cwd(), options.input);
|
|
109
|
+
console.log(`\n👀 Watching ${options.input} for changes...`);
|
|
110
|
+
fs.watch(absoluteInputDir, { recursive: true }, (_eventType, filename) => {
|
|
111
|
+
if (filename?.endsWith(".svg")) {
|
|
112
|
+
console.log(`\n📝 Change detected: ${filename}`);
|
|
113
|
+
runSpriteGeneration(options.input, options.output);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
main();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for Icon component generation
|
|
3
|
+
*/
|
|
4
|
+
export interface IconComponentGenerationOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Output path for the Icon component file
|
|
7
|
+
* Can be relative or absolute path
|
|
8
|
+
*/
|
|
9
|
+
outputFile: string;
|
|
10
|
+
/**
|
|
11
|
+
* Path to the generated types file (for import statement)
|
|
12
|
+
* Relative to the component output file
|
|
13
|
+
*/
|
|
14
|
+
typesFileRelativePath: string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to log output messages
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate React Icon component file
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateIconComponent(options: IconComponentGenerationOptions): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateIconComponent = generateIconComponent;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Generate React Icon component file
|
|
41
|
+
*/
|
|
42
|
+
function generateIconComponent(options) {
|
|
43
|
+
const { outputFile, typesFileRelativePath, verbose = true } = options;
|
|
44
|
+
try {
|
|
45
|
+
const projectRoot = process.cwd();
|
|
46
|
+
const absoluteOutputFile = path.resolve(projectRoot, outputFile);
|
|
47
|
+
// Normalize the import path (ensure it uses / separator and doesn't start with ./ if in same dir)
|
|
48
|
+
const normalizedImportPath = typesFileRelativePath
|
|
49
|
+
.replace(/\\/g, "/")
|
|
50
|
+
.replace(/\.tsx?$/, "");
|
|
51
|
+
const content = `// Auto-generated by typed-svg-sprite
|
|
52
|
+
// Do not edit this file manually
|
|
53
|
+
|
|
54
|
+
import { IconHref } from "${normalizedImportPath}";
|
|
55
|
+
|
|
56
|
+
export interface IconProps extends React.SVGProps<SVGSVGElement> {
|
|
57
|
+
/** The icon href (import from generated/icons.ts) */
|
|
58
|
+
href: IconHref;
|
|
59
|
+
/** Size shorthand for width and height */
|
|
60
|
+
size?: number | string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function Icon({
|
|
64
|
+
href,
|
|
65
|
+
size = 24,
|
|
66
|
+
width,
|
|
67
|
+
height,
|
|
68
|
+
className = "",
|
|
69
|
+
viewBox = "0 0 24 24",
|
|
70
|
+
fill = "currentColor",
|
|
71
|
+
...props
|
|
72
|
+
}: IconProps) {
|
|
73
|
+
return (
|
|
74
|
+
<svg
|
|
75
|
+
className={className}
|
|
76
|
+
width={width || size}
|
|
77
|
+
height={height || size}
|
|
78
|
+
viewBox={viewBox}
|
|
79
|
+
fill={fill}
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
<use href={href} />
|
|
84
|
+
</svg>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
// Write component file
|
|
89
|
+
const outputDir = path.dirname(absoluteOutputFile);
|
|
90
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
91
|
+
fs.writeFileSync(absoluteOutputFile, content, "utf8");
|
|
92
|
+
if (verbose) {
|
|
93
|
+
console.log(`[svg-sprite] ✅ Generated Icon component → ${outputFile}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (verbose) {
|
|
98
|
+
console.error("[svg-sprite] Error generating Icon component:", error);
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an SVG symbol with its metadata
|
|
3
|
+
*/
|
|
4
|
+
export interface SvgSymbol {
|
|
5
|
+
id: string;
|
|
6
|
+
originalId: string;
|
|
7
|
+
viewBox: string;
|
|
8
|
+
content: string;
|
|
9
|
+
attributes: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Options for sprite generation
|
|
13
|
+
*/
|
|
14
|
+
export interface SpriteGenerationOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Directory containing SVG files to include in the sprite.
|
|
17
|
+
* Can be relative or absolute path.
|
|
18
|
+
*/
|
|
19
|
+
inputDir: string;
|
|
20
|
+
/**
|
|
21
|
+
* Output path for the sprite file.
|
|
22
|
+
* Can be relative or absolute path.
|
|
23
|
+
*/
|
|
24
|
+
outputFile: string;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to log output messages
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
verbose?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether to minify the output
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
minify?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to optimize/compress the SVG content
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
optimize?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generate a symbol ID from a file path relative to a base directory.
|
|
43
|
+
* Converts the path to kebab-case and sanitizes it.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* generateSymbolIdFromPath("/path/to/icons/social/github.svg", "/path/to/icons")
|
|
47
|
+
* // Returns: "social-github"
|
|
48
|
+
*/
|
|
49
|
+
export declare function generateSymbolIdFromPath(filePath: string, baseDir: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Parse an SVG file and extract its symbol data
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseSvgFile(svgContent: string, originalId: string, shortId: string, shouldOptimize?: boolean): SvgSymbol;
|
|
54
|
+
/**
|
|
55
|
+
* Generate the SVG sprite content from symbols
|
|
56
|
+
*/
|
|
57
|
+
export declare function generateSpriteSvg(symbols: SvgSymbol[], minify?: boolean): string;
|
|
58
|
+
/**
|
|
59
|
+
* Find and parse all SVG files in a directory
|
|
60
|
+
*/
|
|
61
|
+
export declare function findAndParseSvgFiles(inputDir: string, verbose?: boolean, optimize?: boolean): SvgSymbol[];
|
|
62
|
+
/**
|
|
63
|
+
* Generate an SVG sprite file from a directory of SVG files
|
|
64
|
+
*/
|
|
65
|
+
export declare function generateSprite(options: SpriteGenerationOptions): SvgSymbol[];
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateSymbolIdFromPath = generateSymbolIdFromPath;
|
|
37
|
+
exports.parseSvgFile = parseSvgFile;
|
|
38
|
+
exports.generateSpriteSvg = generateSpriteSvg;
|
|
39
|
+
exports.findAndParseSvgFiles = findAndParseSvgFiles;
|
|
40
|
+
exports.generateSprite = generateSprite;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const cheerio = __importStar(require("cheerio"));
|
|
44
|
+
const glob_1 = require("glob");
|
|
45
|
+
/**
|
|
46
|
+
* Generate a symbol ID from a file path relative to a base directory.
|
|
47
|
+
* Converts the path to kebab-case and sanitizes it.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* generateSymbolIdFromPath("/path/to/icons/social/github.svg", "/path/to/icons")
|
|
51
|
+
* // Returns: "social-github"
|
|
52
|
+
*/
|
|
53
|
+
function generateSymbolIdFromPath(filePath, baseDir) {
|
|
54
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
55
|
+
const pathWithoutExt = relativePath.replace(/\.svg$/, "");
|
|
56
|
+
const symbolId = pathWithoutExt
|
|
57
|
+
.replace(/[\/\\]/g, "-")
|
|
58
|
+
.replace(/[^a-zA-Z0-9-_]/g, "-")
|
|
59
|
+
.replace(/--+/g, "-")
|
|
60
|
+
.replace(/^-+|-+$/g, "");
|
|
61
|
+
return symbolId;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate a short, unique ID for a symbol
|
|
65
|
+
* Uses base-52 encoding (a-z, A-Z) for compact IDs
|
|
66
|
+
*/
|
|
67
|
+
function generateShortId(index) {
|
|
68
|
+
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
69
|
+
const maxIndex = chars.length;
|
|
70
|
+
let id = "";
|
|
71
|
+
let num = index;
|
|
72
|
+
do {
|
|
73
|
+
id = chars[num % maxIndex] + id;
|
|
74
|
+
num = Math.floor(num / maxIndex);
|
|
75
|
+
} while (num > 0);
|
|
76
|
+
return id;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Optimize SVG content by removing unnecessary whitespace and simplifying values
|
|
80
|
+
*/
|
|
81
|
+
function optimizeSvgContent(content) {
|
|
82
|
+
return (content
|
|
83
|
+
// Remove comments
|
|
84
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
85
|
+
// Remove unnecessary whitespace between tags
|
|
86
|
+
.replace(/>\s+</g, "><")
|
|
87
|
+
// Simplify numbers: remove trailing zeros after decimal
|
|
88
|
+
.replace(/(\d+\.\d*?)0+(?=\D)/g, "$1")
|
|
89
|
+
// Remove unnecessary decimal points (1.0 -> 1)
|
|
90
|
+
.replace(/(\d+)\.0+(?=\D)/g, "$1")
|
|
91
|
+
// Compress multiple spaces to single space
|
|
92
|
+
.replace(/\s+/g, " ")
|
|
93
|
+
// Remove spaces around equals signs in attributes
|
|
94
|
+
.replace(/\s*=\s*/g, "=")
|
|
95
|
+
// Remove leading/trailing whitespace
|
|
96
|
+
.trim());
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse an SVG file and extract its symbol data
|
|
100
|
+
*/
|
|
101
|
+
function parseSvgFile(svgContent, originalId, shortId, shouldOptimize = true) {
|
|
102
|
+
// Optimize content if requested
|
|
103
|
+
const processedContent = shouldOptimize
|
|
104
|
+
? optimizeSvgContent(svgContent)
|
|
105
|
+
: svgContent;
|
|
106
|
+
const $ = cheerio.load(processedContent, { xmlMode: true });
|
|
107
|
+
const $svg = $("svg").first();
|
|
108
|
+
if ($svg.length === 0) {
|
|
109
|
+
throw new Error("Invalid SVG content");
|
|
110
|
+
}
|
|
111
|
+
let viewBox = $svg.attr("viewBox");
|
|
112
|
+
if (!viewBox) {
|
|
113
|
+
const width = $svg.attr("width") || "24";
|
|
114
|
+
const height = $svg.attr("height") || "24";
|
|
115
|
+
viewBox = `0 0 ${width} ${height}`;
|
|
116
|
+
}
|
|
117
|
+
const attributes = {};
|
|
118
|
+
const svgAttribs = $svg.get(0)?.attribs || {};
|
|
119
|
+
const excludeAttribs = new Set([
|
|
120
|
+
"viewBox",
|
|
121
|
+
"width",
|
|
122
|
+
"height",
|
|
123
|
+
"xmlns",
|
|
124
|
+
"xmlns:xlink",
|
|
125
|
+
]);
|
|
126
|
+
for (const [key, value] of Object.entries(svgAttribs)) {
|
|
127
|
+
if (!excludeAttribs.has(key) && value) {
|
|
128
|
+
attributes[key] = value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
id: shortId,
|
|
133
|
+
originalId: originalId,
|
|
134
|
+
viewBox: viewBox,
|
|
135
|
+
content: $svg.html() || "",
|
|
136
|
+
attributes: attributes,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Generate the SVG sprite content from symbols
|
|
141
|
+
*/
|
|
142
|
+
function generateSpriteSvg(symbols, minify = true) {
|
|
143
|
+
if (symbols.length === 0) {
|
|
144
|
+
return minify
|
|
145
|
+
? '<svg xmlns="http://www.w3.org/2000/svg" style="display:none"></svg>'
|
|
146
|
+
: `<svg xmlns="http://www.w3.org/2000/svg" class="svg-sprite" style="display: none;">
|
|
147
|
+
</svg>`;
|
|
148
|
+
}
|
|
149
|
+
const symbolElements = symbols
|
|
150
|
+
.map((symbol) => {
|
|
151
|
+
let attributesStr = "";
|
|
152
|
+
for (const [key, value] of Object.entries(symbol.attributes)) {
|
|
153
|
+
attributesStr += ` ${key}="${value}"`;
|
|
154
|
+
}
|
|
155
|
+
return `<symbol id="${symbol.id}" viewBox="${symbol.viewBox}"${attributesStr}>${symbol.content}</symbol>`;
|
|
156
|
+
})
|
|
157
|
+
.join(minify ? "" : "\n ");
|
|
158
|
+
if (minify) {
|
|
159
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">${symbolElements}</svg>`;
|
|
160
|
+
}
|
|
161
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" class="svg-sprite" style="display: none;">
|
|
162
|
+
${symbolElements}
|
|
163
|
+
</svg>`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Find and parse all SVG files in a directory
|
|
167
|
+
*/
|
|
168
|
+
function findAndParseSvgFiles(inputDir, verbose = true, optimize = true) {
|
|
169
|
+
const absoluteInputDir = path.resolve(process.cwd(), inputDir);
|
|
170
|
+
// Check if input directory exists
|
|
171
|
+
if (!fs.existsSync(absoluteInputDir)) {
|
|
172
|
+
if (verbose) {
|
|
173
|
+
console.warn(`Input directory not found: ${inputDir}`);
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
// Find all SVG files
|
|
178
|
+
const svgFiles = (0, glob_1.globSync)("**/*.svg", {
|
|
179
|
+
cwd: absoluteInputDir,
|
|
180
|
+
absolute: true,
|
|
181
|
+
});
|
|
182
|
+
if (svgFiles.length === 0) {
|
|
183
|
+
if (verbose) {
|
|
184
|
+
console.warn(`No SVG files found in ${inputDir}`);
|
|
185
|
+
}
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
if (verbose) {
|
|
189
|
+
console.log(`Found ${svgFiles.length} SVG files in ${inputDir}`);
|
|
190
|
+
}
|
|
191
|
+
// Parse all SVG files
|
|
192
|
+
const symbols = [];
|
|
193
|
+
for (let i = 0; i < svgFiles.length; i++) {
|
|
194
|
+
const svgFile = svgFiles[i];
|
|
195
|
+
try {
|
|
196
|
+
const content = fs.readFileSync(svgFile, "utf8");
|
|
197
|
+
const originalId = generateSymbolIdFromPath(svgFile, absoluteInputDir);
|
|
198
|
+
const shortId = generateShortId(i);
|
|
199
|
+
const symbol = parseSvgFile(content, originalId, shortId, optimize);
|
|
200
|
+
symbols.push(symbol);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (verbose) {
|
|
204
|
+
console.error(`Error parsing ${svgFile}:`, error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return symbols;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Generate an SVG sprite file from a directory of SVG files
|
|
212
|
+
*/
|
|
213
|
+
function generateSprite(options) {
|
|
214
|
+
const { inputDir, outputFile, verbose = true, minify = true, optimize = true, } = options;
|
|
215
|
+
try {
|
|
216
|
+
const projectRoot = process.cwd();
|
|
217
|
+
const absoluteOutputFile = path.resolve(projectRoot, outputFile);
|
|
218
|
+
// Find and parse SVG files
|
|
219
|
+
const symbols = findAndParseSvgFiles(inputDir, verbose, optimize);
|
|
220
|
+
if (symbols.length === 0) {
|
|
221
|
+
return symbols;
|
|
222
|
+
}
|
|
223
|
+
// Generate sprite
|
|
224
|
+
let spriteContent = generateSpriteSvg(symbols, minify);
|
|
225
|
+
// Additional optimization on final sprite
|
|
226
|
+
if (optimize) {
|
|
227
|
+
spriteContent = optimizeSvgContent(spriteContent);
|
|
228
|
+
}
|
|
229
|
+
// Write sprite file
|
|
230
|
+
const outputDir = path.dirname(absoluteOutputFile);
|
|
231
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
232
|
+
fs.writeFileSync(absoluteOutputFile, spriteContent, "utf8");
|
|
233
|
+
if (verbose) {
|
|
234
|
+
const sizeKB = (Buffer.byteLength(spriteContent, "utf8") / 1024).toFixed(2);
|
|
235
|
+
console.log(`✅ Generated sprite with ${symbols.length} symbols (${sizeKB} KB) → ${outputFile}`);
|
|
236
|
+
}
|
|
237
|
+
return symbols;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (verbose) {
|
|
241
|
+
console.error("Error generating sprite:", error);
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
interface NextConfig {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
}
|
|
4
|
+
interface SpriteLoaderOptions {
|
|
5
|
+
/**
|
|
6
|
+
* URL path where the sprite file will be served from.
|
|
7
|
+
* @default "/"
|
|
8
|
+
*/
|
|
9
|
+
url?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Filename for the sprite file.
|
|
12
|
+
* @default "sprite.svg"
|
|
13
|
+
*/
|
|
14
|
+
filename?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Directory containing SVG files to include in the sprite.
|
|
17
|
+
* Relative to project root.
|
|
18
|
+
* @default "public/icons"
|
|
19
|
+
*/
|
|
20
|
+
inputDir?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Output path for the sprite file.
|
|
23
|
+
* Relative to project root.
|
|
24
|
+
* @default "public/sprite.svg"
|
|
25
|
+
*/
|
|
26
|
+
outputFile?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Output path for the TypeScript types file.
|
|
29
|
+
* Relative to project root.
|
|
30
|
+
* @default "generated/icons.ts"
|
|
31
|
+
*/
|
|
32
|
+
typesOutputFile?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to generate the Icon React component.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
generateIconComponent?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Output path for the Icon component file.
|
|
40
|
+
* Relative to project root.
|
|
41
|
+
* @default "generated/Icon.tsx"
|
|
42
|
+
*/
|
|
43
|
+
iconComponentOutputFile?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generates SVG sprite and TypeScript types for Next.js projects
|
|
47
|
+
*
|
|
48
|
+
* This function generates:
|
|
49
|
+
* 1. An SVG sprite file containing all icons
|
|
50
|
+
* 2. A TypeScript file with typed icon IDs and helper functions
|
|
51
|
+
*
|
|
52
|
+
* No loader is needed - just import the typed icon references directly.
|
|
53
|
+
*
|
|
54
|
+
* @param nextConfig - The existing Next.js configuration object
|
|
55
|
+
* @param options - Optional sprite generation configuration
|
|
56
|
+
* @returns Next.js configuration (unchanged, sprite generation happens during config)
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // next.config.ts
|
|
61
|
+
* import { withSpriteLoader } from '@tinloof/typed-svg-sprite/next';
|
|
62
|
+
*
|
|
63
|
+
* export default withSpriteLoader({
|
|
64
|
+
* // your existing Next.js config
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* // Then in your components:
|
|
68
|
+
* import { HOME, SETTINGS } from '@/generated/icons';
|
|
69
|
+
* import { Icon } from '@/generated/Icon'; // Icon component is auto-generated
|
|
70
|
+
*
|
|
71
|
+
* function MyComponent() {
|
|
72
|
+
* return (
|
|
73
|
+
* <>
|
|
74
|
+
* <Icon href={HOME} />
|
|
75
|
+
* <Icon href={SETTINGS} />
|
|
76
|
+
* </>
|
|
77
|
+
* );
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // With custom options
|
|
84
|
+
* export default withSpriteLoader(
|
|
85
|
+
* {
|
|
86
|
+
* // your existing Next.js config
|
|
87
|
+
* },
|
|
88
|
+
* {
|
|
89
|
+
* inputDir: "assets/icons",
|
|
90
|
+
* outputFile: "public/icons-sprite.svg",
|
|
91
|
+
* typesOutputFile: "lib/icons.ts",
|
|
92
|
+
* iconComponentOutputFile: "components/Icon.tsx",
|
|
93
|
+
* url: "/",
|
|
94
|
+
* filename: "icons-sprite.svg"
|
|
95
|
+
* }
|
|
96
|
+
* );
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* // Disable Icon component generation
|
|
102
|
+
* export default withSpriteLoader(
|
|
103
|
+
* { /* your config *\/ },
|
|
104
|
+
* {
|
|
105
|
+
* generateIconComponent: false
|
|
106
|
+
* }
|
|
107
|
+
* );
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function withSpriteLoader(nextConfig: any, options?: SpriteLoaderOptions): NextConfig;
|
|
111
|
+
export {};
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.withSpriteLoader = withSpriteLoader;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const core_js_1 = require("./core.js");
|
|
40
|
+
const types_js_1 = require("./types.js");
|
|
41
|
+
const components_js_1 = require("./components.js");
|
|
42
|
+
// Track if we've already started watching
|
|
43
|
+
let watcherInitialized = false;
|
|
44
|
+
function generateSpriteAndTypes(inputDir, outputFile, spriteUrl, spriteFilename, typesOutputFile, generateIcon, iconComponentOutputFile) {
|
|
45
|
+
// Generate sprite and get symbols
|
|
46
|
+
const symbols = (0, core_js_1.generateSprite)({
|
|
47
|
+
inputDir,
|
|
48
|
+
outputFile,
|
|
49
|
+
verbose: true,
|
|
50
|
+
minify: true,
|
|
51
|
+
optimize: true,
|
|
52
|
+
});
|
|
53
|
+
// Generate TypeScript types file if specified
|
|
54
|
+
if (typesOutputFile && symbols.length > 0) {
|
|
55
|
+
(0, types_js_1.generateTypesFile)({
|
|
56
|
+
symbols,
|
|
57
|
+
spriteUrl,
|
|
58
|
+
spriteFilename,
|
|
59
|
+
outputFile: typesOutputFile,
|
|
60
|
+
verbose: false,
|
|
61
|
+
});
|
|
62
|
+
// Generate Icon component if enabled
|
|
63
|
+
if (generateIcon && iconComponentOutputFile) {
|
|
64
|
+
// Calculate relative path from component to types file
|
|
65
|
+
const componentDir = path.dirname(path.resolve(process.cwd(), iconComponentOutputFile));
|
|
66
|
+
const typesFile = path.resolve(process.cwd(), typesOutputFile);
|
|
67
|
+
const typesDir = path.dirname(typesFile);
|
|
68
|
+
const typesFilename = path.basename(typesFile, path.extname(typesFile));
|
|
69
|
+
// Get relative path from component dir to types dir
|
|
70
|
+
const relativePath = path
|
|
71
|
+
.relative(componentDir, typesDir)
|
|
72
|
+
.replace(/\\/g, "/");
|
|
73
|
+
// Construct the import path
|
|
74
|
+
// If same directory, use ./filename
|
|
75
|
+
// If parent/child relationship, ensure proper path format
|
|
76
|
+
let importPath;
|
|
77
|
+
if (relativePath === "") {
|
|
78
|
+
importPath = `./${typesFilename}`;
|
|
79
|
+
}
|
|
80
|
+
else if (relativePath.startsWith("..")) {
|
|
81
|
+
// Parent directory - ensure proper path
|
|
82
|
+
importPath = `${relativePath}/${typesFilename}`.replace(/\/+/g, "/");
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Child directory - ensure it starts with ./
|
|
86
|
+
const normalizedPath = relativePath.startsWith("./")
|
|
87
|
+
? relativePath
|
|
88
|
+
: `./${relativePath}`;
|
|
89
|
+
importPath = `${normalizedPath}/${typesFilename}`.replace(/\/+/g, "/");
|
|
90
|
+
}
|
|
91
|
+
(0, components_js_1.generateIconComponent)({
|
|
92
|
+
outputFile: iconComponentOutputFile,
|
|
93
|
+
typesFileRelativePath: importPath,
|
|
94
|
+
verbose: false,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function startWatcher(inputDir, outputFile, spriteUrl, spriteFilename, typesOutputFile, generateIcon, iconComponentOutputFile) {
|
|
100
|
+
if (watcherInitialized) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const projectRoot = process.cwd();
|
|
104
|
+
const absoluteInputDir = path.resolve(projectRoot, inputDir);
|
|
105
|
+
if (!fs.existsSync(absoluteInputDir)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
watcherInitialized = true;
|
|
109
|
+
console.log(`[svg-sprite] 👀 Watching ${inputDir} for changes...`);
|
|
110
|
+
fs.watch(absoluteInputDir, { recursive: true }, (eventType, filename) => {
|
|
111
|
+
if (filename?.endsWith(".svg")) {
|
|
112
|
+
console.log(`[svg-sprite] Change detected: ${filename}`);
|
|
113
|
+
generateSpriteAndTypes(inputDir, outputFile, spriteUrl, spriteFilename, typesOutputFile, generateIcon, iconComponentOutputFile);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generates SVG sprite and TypeScript types for Next.js projects
|
|
119
|
+
*
|
|
120
|
+
* This function generates:
|
|
121
|
+
* 1. An SVG sprite file containing all icons
|
|
122
|
+
* 2. A TypeScript file with typed icon IDs and helper functions
|
|
123
|
+
*
|
|
124
|
+
* No loader is needed - just import the typed icon references directly.
|
|
125
|
+
*
|
|
126
|
+
* @param nextConfig - The existing Next.js configuration object
|
|
127
|
+
* @param options - Optional sprite generation configuration
|
|
128
|
+
* @returns Next.js configuration (unchanged, sprite generation happens during config)
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* // next.config.ts
|
|
133
|
+
* import { withSpriteLoader } from '@tinloof/typed-svg-sprite/next';
|
|
134
|
+
*
|
|
135
|
+
* export default withSpriteLoader({
|
|
136
|
+
* // your existing Next.js config
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* // Then in your components:
|
|
140
|
+
* import { HOME, SETTINGS } from '@/generated/icons';
|
|
141
|
+
* import { Icon } from '@/generated/Icon'; // Icon component is auto-generated
|
|
142
|
+
*
|
|
143
|
+
* function MyComponent() {
|
|
144
|
+
* return (
|
|
145
|
+
* <>
|
|
146
|
+
* <Icon href={HOME} />
|
|
147
|
+
* <Icon href={SETTINGS} />
|
|
148
|
+
* </>
|
|
149
|
+
* );
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // With custom options
|
|
156
|
+
* export default withSpriteLoader(
|
|
157
|
+
* {
|
|
158
|
+
* // your existing Next.js config
|
|
159
|
+
* },
|
|
160
|
+
* {
|
|
161
|
+
* inputDir: "assets/icons",
|
|
162
|
+
* outputFile: "public/icons-sprite.svg",
|
|
163
|
+
* typesOutputFile: "lib/icons.ts",
|
|
164
|
+
* iconComponentOutputFile: "components/Icon.tsx",
|
|
165
|
+
* url: "/",
|
|
166
|
+
* filename: "icons-sprite.svg"
|
|
167
|
+
* }
|
|
168
|
+
* );
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* // Disable Icon component generation
|
|
174
|
+
* export default withSpriteLoader(
|
|
175
|
+
* { /* your config *\/ },
|
|
176
|
+
* {
|
|
177
|
+
* generateIconComponent: false
|
|
178
|
+
* }
|
|
179
|
+
* );
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
function withSpriteLoader(nextConfig, options) {
|
|
183
|
+
const inputDir = options?.inputDir ?? "public/icons";
|
|
184
|
+
const outputFile = options?.outputFile ?? "public/sprite.svg";
|
|
185
|
+
const spriteUrl = options?.url ?? "/";
|
|
186
|
+
const spriteFilename = options?.filename ?? "sprite.svg";
|
|
187
|
+
const typesOutputFile = options?.typesOutputFile ?? "generated/icons.ts";
|
|
188
|
+
const generateIcon = options?.generateIconComponent !== false
|
|
189
|
+
? options?.generateIconComponent ?? true
|
|
190
|
+
: false;
|
|
191
|
+
const iconComponentOutputFile = options?.iconComponentOutputFile ?? "generated/Icon.tsx";
|
|
192
|
+
// Generate sprite and types on startup
|
|
193
|
+
generateSpriteAndTypes(inputDir, outputFile, spriteUrl, spriteFilename, typesOutputFile, generateIcon, generateIcon ? iconComponentOutputFile : undefined);
|
|
194
|
+
// Start watcher in dev mode
|
|
195
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
196
|
+
if (isDev) {
|
|
197
|
+
startWatcher(inputDir, outputFile, spriteUrl, spriteFilename, typesOutputFile, generateIcon, generateIcon ? iconComponentOutputFile : undefined);
|
|
198
|
+
}
|
|
199
|
+
// Return config without loader modifications
|
|
200
|
+
// Users should import from the generated types file instead
|
|
201
|
+
return nextConfig;
|
|
202
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SvgSymbol } from "./core.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options for TypeScript types generation
|
|
4
|
+
*/
|
|
5
|
+
export interface TypesGenerationOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Symbols to generate types for
|
|
8
|
+
*/
|
|
9
|
+
symbols: SvgSymbol[];
|
|
10
|
+
/**
|
|
11
|
+
* URL path where the sprite file will be served from
|
|
12
|
+
* @example "/"
|
|
13
|
+
*/
|
|
14
|
+
spriteUrl: string;
|
|
15
|
+
/**
|
|
16
|
+
* Filename of the sprite file
|
|
17
|
+
* @example "sprite.svg"
|
|
18
|
+
*/
|
|
19
|
+
spriteFilename: string;
|
|
20
|
+
/**
|
|
21
|
+
* Output path for the TypeScript types file
|
|
22
|
+
* Can be relative or absolute path
|
|
23
|
+
*/
|
|
24
|
+
outputFile: string;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to log output messages
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
verbose?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate TypeScript types file from SVG symbols
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateTypesFile(options: TypesGenerationOptions): void;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateTypesFile = generateTypesFile;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Generate TypeScript types file from SVG symbols
|
|
41
|
+
*/
|
|
42
|
+
function generateTypesFile(options) {
|
|
43
|
+
const { symbols, spriteUrl, spriteFilename, outputFile, verbose = true, } = options;
|
|
44
|
+
try {
|
|
45
|
+
const projectRoot = process.cwd();
|
|
46
|
+
const absoluteOutputFile = path.resolve(projectRoot, outputFile);
|
|
47
|
+
const spriteUrlWithFilename = spriteUrl.endsWith("/")
|
|
48
|
+
? `${spriteUrl}${spriteFilename}`
|
|
49
|
+
: `${spriteUrl}/${spriteFilename}`;
|
|
50
|
+
// Generate TypeScript content
|
|
51
|
+
const enumEntries = symbols
|
|
52
|
+
.map((s) => {
|
|
53
|
+
// Convert kebab-case to UPPER_SNAKE_CASE for enum key (use originalId)
|
|
54
|
+
const enumKey = s.originalId.replace(/-/g, "_").toUpperCase();
|
|
55
|
+
// But reference the short ID in the value
|
|
56
|
+
return ` ${enumKey} = "${s.id}",`;
|
|
57
|
+
})
|
|
58
|
+
.join("\n");
|
|
59
|
+
const iconExports = symbols
|
|
60
|
+
.map((s) => {
|
|
61
|
+
const constantName = s.originalId.replace(/-/g, "_").toUpperCase();
|
|
62
|
+
return `export const ${constantName}: IconHref = "${spriteUrlWithFilename}#${s.id}";`;
|
|
63
|
+
})
|
|
64
|
+
.join("\n");
|
|
65
|
+
const content = `// Auto-generated by typed-svg-sprite
|
|
66
|
+
// Do not edit this file manually
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Available icon IDs in the sprite
|
|
70
|
+
*/
|
|
71
|
+
export enum IconId {
|
|
72
|
+
${enumEntries}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Type-safe icon href (sprite URL + fragment identifier)
|
|
77
|
+
*/
|
|
78
|
+
export type IconHref = \`${spriteUrlWithFilename}#\${IconId}\`;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the full sprite URL for an icon ID
|
|
82
|
+
*/
|
|
83
|
+
export function getIconHref(iconId: IconId): IconHref {
|
|
84
|
+
return \`${spriteUrlWithFilename}#\${iconId}\` as IconHref;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pre-built icon references
|
|
89
|
+
*/
|
|
90
|
+
${iconExports}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* All available icons as an array
|
|
94
|
+
*/
|
|
95
|
+
export const allIcons: IconId[] = [
|
|
96
|
+
${symbols
|
|
97
|
+
.map((s) => ` IconId.${s.originalId.replace(/-/g, "_").toUpperCase()},`)
|
|
98
|
+
.join("\n")}
|
|
99
|
+
];
|
|
100
|
+
`;
|
|
101
|
+
// Write types file
|
|
102
|
+
const outputDir = path.dirname(absoluteOutputFile);
|
|
103
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(absoluteOutputFile, content, "utf8");
|
|
105
|
+
if (verbose) {
|
|
106
|
+
console.log(`[svg-sprite] ✅ Generated types file → ${outputFile}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (verbose) {
|
|
111
|
+
console.error("[svg-sprite] Error generating types file:", error);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tinloof/typed-svg-sprite",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Generate optimized SVG sprites with full TypeScript support",
|
|
5
|
+
"bin": {
|
|
6
|
+
"typed-svg-sprite": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./next": {
|
|
10
|
+
"types": "./dist/next.d.ts",
|
|
11
|
+
"default": "./dist/next.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepare": "npm run build",
|
|
17
|
+
"clean": "rimraf dist",
|
|
18
|
+
"changeset": "changeset",
|
|
19
|
+
"version": "changeset version",
|
|
20
|
+
"release": "npm run build && changeset publish"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"svg",
|
|
29
|
+
"sprite",
|
|
30
|
+
"icon",
|
|
31
|
+
"typescript",
|
|
32
|
+
"nextjs",
|
|
33
|
+
"cli"
|
|
34
|
+
],
|
|
35
|
+
"author": "LHDi <elhadi98@gmail.com>",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/tinloof/typed-svg-sprite.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/tinloof/typed-svg-sprite/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/tinloof/typed-svg-sprite#readme",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=14.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"cheerio": "^1.1.2",
|
|
50
|
+
"glob": "^10.3.10"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"next": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@changesets/cli": "^2.29.7",
|
|
59
|
+
"@types/cheerio": "^0.22.35",
|
|
60
|
+
"@types/node": "^20.0.0",
|
|
61
|
+
"rimraf": "^5.0.5",
|
|
62
|
+
"typescript": "^5.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|