@ts-svg/core 0.0.1-beta.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/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ts-svg/core",
3
+ "version": "0.0.1-beta.0",
4
+ "description": "Vite Plugin for loading all svg files inside a folder with types.",
5
+ "keywords": [
6
+ "vite-plugin",
7
+ "vite",
8
+ "svg",
9
+ "typescript"
10
+ ],
11
+ "author": {
12
+ "name": "Chun Nam Wong",
13
+ "url": "https://chunnamwong.com"
14
+ },
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/chunnamwong/ts-svg.git",
19
+ "directory": "packages/core"
20
+ },
21
+ "type": "module",
22
+ "main": "src/index.js",
23
+ "types": [
24
+ "src/index.d.ts",
25
+ "src/cli.d.ts"
26
+ ],
27
+ "exports": {
28
+ ".": "./src/index.js",
29
+ "./cli": "./src/cli.js"
30
+ },
31
+ "bin": {
32
+ "ts-svg": "src/cli.js"
33
+ },
34
+ "files": [
35
+ "src"
36
+ ],
37
+ "peerDependencies": {
38
+ "vite": ">=3.2.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.6.0"
42
+ },
43
+ "scripts": {
44
+ "format": "prettier . --write"
45
+ }
46
+ }
package/src/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare module '@ts-svg/core/cli' {
2
+ export const main: () => Promise<void>;
3
+ }
package/src/cli.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolveConfig } from 'vite';
4
+ import path from 'node:path';
5
+ import { syncTypes } from './index.js';
6
+
7
+ export async function main() {
8
+ const config = await resolveConfig({}, 'build');
9
+
10
+ const tsSvgPluginConfig = config.plugins.find(({ name }) => name === 'ts-svg');
11
+
12
+ if (!tsSvgPluginConfig) {
13
+ throw new Error('cannot find ts-svg plugin from vite config');
14
+ }
15
+
16
+ const { path: svgFolderPath, query, svgModuleDeclaration } =
17
+ /**
18
+ * @type {import('vite').Plugin & {tsSvgOptions: import('./index.js').Options}}
19
+ */
20
+ (tsSvgPluginConfig).tsSvgOptions;
21
+
22
+ if (!path) {
23
+ throw new Error('cannot find path option');
24
+ }
25
+
26
+ const svgFolderFullPath = path.join(process.cwd(), svgFolderPath);
27
+
28
+ syncTypes(svgFolderFullPath, query, svgModuleDeclaration);
29
+ }
30
+
31
+ await main();
package/src/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Plugin, PluginOption } from 'vite';
2
+
3
+ type Options = {
4
+ path: string;
5
+ query?: '?raw' | '?react' | '?svelte' | (string & Record<never, never>);
6
+ transform?: Plugin['transform'];
7
+ svgModuleDeclaration?: string;
8
+ };
9
+
10
+ export function tsSvg(opts: Options): PluginOption & { tsSvgOptions: Options };
11
+ export function syncTypes(
12
+ svgFolderFullPath: string,
13
+ query?: string,
14
+ svgModuleDeclaration?: string,
15
+ ): void;
package/src/index.js ADDED
@@ -0,0 +1,192 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ /**
5
+ * @typedef Options
6
+ * @type {object}
7
+ * @property {string} path The svg folder path
8
+ * @property {'?raw' | '?react' | '?svelte' | (string & Record<never, never>)} query The import query
9
+ * @property {string} svgModuleDeclaration
10
+ * @property {import('vite').Plugin['transform']} transform
11
+ */
12
+
13
+ /**
14
+ * Returns the Vite plugin.
15
+ * @param {Options} options
16
+ * @returns {import('vite').PluginOption & { tsSvgOptions: Options}}
17
+ */
18
+ export function tsSvg(options) {
19
+ const pluginName = 'ts-svg';
20
+ const virtualModuleId = `virtual:${pluginName}`;
21
+ const resolvedVirtualModuleId = resolveVirtualModuleId(virtualModuleId);
22
+
23
+ if (!path) {
24
+ throw new Error('path is not specified.');
25
+ }
26
+
27
+ const handledQuery = options.query ?? '?raw';
28
+ const cwd = process.cwd();
29
+ const svgFolderFullPath = path.join(cwd, options.path);
30
+
31
+ return {
32
+ name: pluginName,
33
+ /**
34
+ * Include the options being passed to here so that the cli can get the svg folder path.
35
+ */
36
+ tsSvgOptions: options,
37
+ /**
38
+ * Configure the dev server to listen to svg file changes and sync the types.
39
+ */
40
+ configureServer({ watcher, environments }) {
41
+ /**
42
+ * Sync the types once when starting the dev server.
43
+ */
44
+ syncTypes(svgFolderFullPath, handledQuery, options.svgModuleDeclaration);
45
+
46
+ /**
47
+ * Sync the types and virtual module whenever svg file changes are detected.
48
+ */
49
+ watcher.on('all', (_, file) => {
50
+ if (file.startsWith(svgFolderFullPath) && /\.svg$/.test(file)) {
51
+ syncTypes(svgFolderFullPath, handledQuery, options.svgModuleDeclaration);
52
+
53
+ /**
54
+ * Reload the virtual module to include new changes
55
+ */
56
+ for (const environment of Object.values(environments)) {
57
+ const moduleId = path.dirname(
58
+ path.join(resolvedVirtualModuleId, path.relative(svgFolderFullPath, file)),
59
+ );
60
+ const module = environment.moduleGraph.getModuleById(moduleId);
61
+ if (module) {
62
+ environment.reloadModule(module);
63
+ }
64
+ }
65
+ }
66
+ });
67
+ },
68
+ resolveId(id) {
69
+ if (id.startsWith(virtualModuleId)) {
70
+ return resolveVirtualModuleId(id);
71
+ }
72
+ },
73
+ load(id) {
74
+ if (id.startsWith(resolvedVirtualModuleId)) {
75
+ const subPath = id.slice(resolvedVirtualModuleId.length);
76
+ const actualPath = path.join(svgFolderFullPath, subPath);
77
+ const svgFiles = fs
78
+ .readdirSync(actualPath, {
79
+ withFileTypes: true,
80
+ })
81
+ .filter((dirent) => dirent.isFile())
82
+ .map((dirent) => dirent.name)
83
+ .filter(isSvgFilePath);
84
+
85
+ const exports = svgFiles.map((file) => {
86
+ return `export { default as ${convertSvgName(file)} } from '${actualPath}/${file}${handledQuery}';`;
87
+ });
88
+
89
+ const code = exports.join('\n');
90
+
91
+ return {
92
+ code,
93
+ map: { mappings: '' },
94
+ };
95
+ }
96
+ },
97
+ transform: options.transform,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Resolve the virtual module id.
103
+ * @param {string} virtualModuleId
104
+ */
105
+ function resolveVirtualModuleId(virtualModuleId) {
106
+ return '\0' + virtualModuleId;
107
+ }
108
+
109
+ /**
110
+ * Convert the svg file name to a component name.
111
+ * @param {string} file
112
+ */
113
+ function convertSvgName(file) {
114
+ const fileName = file.replace(/\.svg$/, '');
115
+ return ` ${fileName}`.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase());
116
+ }
117
+
118
+ /**
119
+ * Sync the types of the virtual module.
120
+ * @param {string} svgFolderFullPath
121
+ * @param {string | undefined} query
122
+ * @param {string | undefined} svgModuleDeclaration
123
+ */
124
+ export function syncTypes(svgFolderFullPath, query = '', svgModuleDeclaration = '') {
125
+ const svgFolders = fs
126
+ .readdirSync(svgFolderFullPath, {
127
+ recursive: true,
128
+ withFileTypes: true,
129
+ })
130
+ .filter((dirent) => dirent.isDirectory())
131
+ .map((dirent) => path.relative(svgFolderFullPath, path.join(dirent.parentPath, dirent.name)))
132
+ .concat('');
133
+
134
+ let content = `
135
+ // Auto generated by ts-svg
136
+ // Do not edit it manually
137
+
138
+ ${svgModuleDeclaration}
139
+ `;
140
+
141
+ for (const svgFolder of svgFolders) {
142
+ const files = fs
143
+ .readdirSync(path.join(svgFolderFullPath, svgFolder), {
144
+ withFileTypes: true,
145
+ })
146
+ .filter((dirent) => dirent.isFile())
147
+ .map((dirent) => dirent.name)
148
+ .filter(isSvgFilePath);
149
+
150
+ content += `
151
+ declare module "${path.join('virtual:ts-svg', svgFolder)}" {
152
+ ${files.map((file) => `export const ${convertSvgName(file)}: typeof import('*.svg${query}').default;`).join('\n\t')}
153
+ }
154
+ `;
155
+ }
156
+
157
+ writeIfChanged(path.join(process.cwd(), '.ts-svg/ambient.d.ts'), content);
158
+ }
159
+
160
+ /**
161
+ * Check if the entry returned by readdirSync is a svg file path
162
+ * @param {string | Buffer<ArrayBufferLike>} file
163
+ * @returns {file is string}
164
+ */
165
+ const isSvgFilePath = (file) => typeof file === 'string' && file.endsWith('.svg');
166
+
167
+ /**
168
+ * Taken from https://github.com/sveltejs/kit/blob/%40sveltejs/package%402.5.0/packages/kit/src/core/sync/utils.js
169
+ */
170
+
171
+ /** @type {Map<string, string>} */
172
+ const previousContents = new Map();
173
+
174
+ /**
175
+ * @param {string} file
176
+ * @param {string} code
177
+ */
178
+ function writeIfChanged(file, code) {
179
+ if (code !== previousContents.get(file)) {
180
+ write(file, code);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * @param {string} file
186
+ * @param {string} code
187
+ */
188
+ function write(file, code) {
189
+ previousContents.set(file, code);
190
+ fs.mkdirSync(path.dirname(file), { recursive: true });
191
+ fs.writeFileSync(file, code);
192
+ }