@sylphx/silk-vite-plugin 1.1.1 → 2.0.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/dist/index.d.ts CHANGED
@@ -1,24 +1,22 @@
1
1
  /**
2
2
  * @sylphx/silk-vite-plugin
3
- * Build-time CSS extraction with pre-compression (15-25% smaller)
3
+ * Universal plugin for zero-runtime Silk CSS-in-TypeScript
4
+ *
5
+ * Uses unplugin for cross-bundler compatibility
4
6
  */
5
- import type { Plugin } from 'vite';
6
7
  export interface CompressionOptions {
7
8
  /**
8
9
  * Enable Brotli compression (.css.br)
9
- * 15-25% smaller than gzip, 70% compression for CSS
10
10
  * @default true
11
11
  */
12
12
  brotli?: boolean;
13
13
  /**
14
- * Brotli quality (0-11, higher = better compression but slower)
15
- * Use 11 for production (static compression, no runtime cost)
14
+ * Brotli quality (0-11)
16
15
  * @default 11
17
16
  */
18
17
  brotliQuality?: number;
19
18
  /**
20
19
  * Enable gzip compression (.css.gz)
21
- * Fallback for older servers
22
20
  * @default true
23
21
  */
24
22
  gzip?: boolean;
@@ -30,37 +28,37 @@ export interface CompressionOptions {
30
28
  }
31
29
  export interface SilkPluginOptions {
32
30
  /**
33
- * Output CSS file path (relative to outDir)
31
+ * Output CSS file path
34
32
  * @default 'silk.css'
35
33
  */
36
34
  outputFile?: string;
37
- /**
38
- * Include CSS in HTML automatically
39
- * @default true
40
- */
41
- inject?: boolean;
42
35
  /**
43
36
  * Minify CSS output
44
37
  * @default true in production
45
38
  */
46
39
  minify?: boolean;
47
40
  /**
48
- * Watch mode for development
49
- * @default true
41
+ * Pre-compression options
50
42
  */
51
- watch?: boolean;
43
+ compression?: CompressionOptions;
52
44
  /**
53
- * Pre-compression options (Brotli + gzip)
54
- * Generates .br and .gz files for web servers
55
- * @default { brotli: true, gzip: true }
45
+ * Babel plugin options
56
46
  */
57
- compression?: CompressionOptions;
47
+ babelOptions?: {
48
+ production?: boolean;
49
+ classPrefix?: string;
50
+ importSources?: string[];
51
+ tokens?: Record<string, any>;
52
+ breakpoints?: Record<string, string>;
53
+ };
58
54
  }
59
- export declare function silk(options?: SilkPluginOptions): Plugin;
60
- export default silk;
61
55
  /**
62
- * Client-side script for hot CSS updates
63
- * This should be imported in the app entry point
56
+ * Silk unplugin instance
64
57
  */
65
- export declare const silkClient = "\nif (import.meta.hot) {\n import.meta.hot.on('silk:update', ({ css }) => {\n let style = document.querySelector('style[data-silk]')\n if (!style) {\n style = document.createElement('style')\n style.setAttribute('data-silk', '')\n document.head.appendChild(style)\n }\n style.textContent = css\n })\n}\n";
58
+ export declare const unpluginSilk: import("unplugin").UnpluginInstance<SilkPluginOptions, boolean>;
59
+ export declare const vite: (options: SilkPluginOptions) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
60
+ export declare const webpack: (options: SilkPluginOptions) => WebpackPluginInstance;
61
+ export declare const rollup: (options: SilkPluginOptions) => import("rollup").Plugin<any> | import("rollup").Plugin<any>[];
62
+ export declare const esbuild: (options: SilkPluginOptions) => import("unplugin").EsbuildPlugin;
63
+ export default vite;
66
64
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAOjD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAA;CACjC;AAED,wBAAgB,IAAI,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAoN5D;AAED,eAAe,IAAI,CAAA;AAEnB;;;GAGG;AACH,eAAO,MAAM,UAAU,iVAYtB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;OAEG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAA;IAEhC;;OAEG;IACH,YAAY,CAAC,EAAE;QACb,UAAU,CAAC,EAAE,OAAO,CAAA;QACpB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACrC,CAAA;CACF;AAyBD;;GAEG;AACH,eAAO,MAAM,YAAY,iEA0LvB,CAAA;AAGF,eAAO,MAAM,IAAI,2FAAoB,CAAA;AACrC,eAAO,MAAM,OAAO,uDAAuB,CAAA;AAC3C,eAAO,MAAM,MAAM,+FAAsB,CAAA;AACzC,eAAO,MAAM,OAAO,kEAAuB,CAAA;AAG3C,eAAe,IAAI,CAAA"}
package/dist/index.js CHANGED
@@ -1,216 +1,202 @@
1
1
  /**
2
2
  * @sylphx/silk-vite-plugin
3
- * Build-time CSS extraction with pre-compression (15-25% smaller)
3
+ * Universal plugin for zero-runtime Silk CSS-in-TypeScript
4
+ *
5
+ * Uses unplugin for cross-bundler compatibility
4
6
  */
5
- import { cssRules } from '@sylphx/silk';
6
- import { compress } from 'brotli-wasm';
7
- import { gzipSync } from 'node:zlib';
8
- export function silk(options = {}) {
9
- const { outputFile = 'silk.css', inject = true, minify, watch = true, compression = {} } = options;
7
+ import { createUnplugin } from 'unplugin';
8
+ import * as path from 'node:path';
9
+ import { gzipSync, brotliCompressSync, constants } from 'node:zlib';
10
+ // @ts-ignore - Babel preset types not needed
11
+ import presetReact from '@babel/preset-react';
12
+ // @ts-ignore - Babel preset types not needed
13
+ import presetTypeScript from '@babel/preset-typescript';
14
+ // Global CSS registry
15
+ const cssRules = new Map();
16
+ /**
17
+ * Minify CSS
18
+ */
19
+ function minifyCSS(css) {
20
+ return css
21
+ .replace(/\s+/g, ' ')
22
+ .replace(/\s*([{}:;,])\s*/g, '$1')
23
+ .replace(/;}/g, '}')
24
+ .trim();
25
+ }
26
+ /**
27
+ * Format bytes
28
+ */
29
+ function formatBytes(bytes) {
30
+ if (bytes < 1024)
31
+ return `${bytes}B`;
32
+ if (bytes < 1024 * 1024)
33
+ return `${(bytes / 1024).toFixed(1)}KB`;
34
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
35
+ }
36
+ /**
37
+ * Silk unplugin instance
38
+ */
39
+ export const unpluginSilk = createUnplugin((options = {}) => {
40
+ const { outputFile = 'silk.css', minify: shouldMinify, compression = {}, babelOptions = {}, } = options;
10
41
  const compressionConfig = {
11
42
  brotli: compression.brotli ?? true,
12
43
  brotliQuality: compression.brotliQuality ?? 11,
13
44
  gzip: compression.gzip ?? true,
14
45
  gzipLevel: compression.gzipLevel ?? 9,
15
46
  };
16
- let server;
17
- let isBuild = false;
18
- const cssCache = new Set();
19
- /**
20
- * Generate compressed versions of CSS
21
- */
22
- async function generateCompressedAssets(css, fileName) {
23
- const outputs = [];
24
- // Generate Brotli (.br)
25
- if (compressionConfig.brotli) {
26
- try {
27
- const compressed = await compress(Buffer.from(css, 'utf-8'), {
28
- quality: compressionConfig.brotliQuality,
29
- });
30
- outputs.push({
31
- fileName: `${fileName}.br`,
32
- source: Buffer.from(compressed),
33
- });
47
+ let isProduction = false;
48
+ return {
49
+ name: 'unplugin-silk',
50
+ enforce: 'pre', // Run before other plugins
51
+ // Only transform TypeScript/JSX files
52
+ transformInclude(id) {
53
+ // Skip node_modules and virtual modules
54
+ if (id.includes('node_modules') || id.includes('\0')) {
55
+ return false;
34
56
  }
35
- catch (error) {
36
- console.warn('Brotli compression failed:', error);
57
+ return /\.[jt]sx?$/.test(id);
58
+ },
59
+ // Transform code with Babel
60
+ async transform(code, id) {
61
+ // Skip if no silk imports
62
+ if (!code.includes('@sylphx/silk')) {
63
+ return null;
37
64
  }
38
- }
39
- // Generate gzip (.gz)
40
- if (compressionConfig.gzip) {
41
65
  try {
42
- const compressed = gzipSync(css, { level: compressionConfig.gzipLevel });
43
- outputs.push({
44
- fileName: `${fileName}.gz`,
45
- source: compressed,
66
+ // Dynamic import to avoid bundling
67
+ const { transformSync } = await import('@babel/core');
68
+ const babelPluginSilk = (await import('@sylphx/babel-plugin-silk')).default;
69
+ // Set production mode if not explicitly set
70
+ const babelOpts = {
71
+ ...babelOptions,
72
+ production: babelOptions.production ?? isProduction,
73
+ };
74
+ // Transform with Babel plugin
75
+ const result = transformSync(code, {
76
+ filename: id,
77
+ presets: [
78
+ [presetReact, { runtime: 'automatic' }],
79
+ [presetTypeScript, { isTSX: true, allExtensions: true }],
80
+ ],
81
+ plugins: [[babelPluginSilk, babelOpts]],
82
+ sourceMaps: true,
83
+ configFile: false,
84
+ babelrc: false,
46
85
  });
86
+ if (!result || !result.code) {
87
+ return null;
88
+ }
89
+ // Extract CSS from metadata
90
+ const metadata = result.metadata;
91
+ if (metadata?.silk?.cssRules) {
92
+ for (const [className, rule] of metadata.silk.cssRules) {
93
+ cssRules.set(className, rule);
94
+ }
95
+ // Log in dev mode
96
+ if (!isProduction && metadata.silk.cssRules.length > 0) {
97
+ console.log(`[Silk] Compiled ${metadata.silk.cssRules.length} CSS rules from ${path.basename(id)}`);
98
+ }
99
+ }
100
+ return {
101
+ code: result.code,
102
+ map: result.map || undefined,
103
+ };
47
104
  }
48
105
  catch (error) {
49
- console.warn('Gzip compression failed:', error);
106
+ console.error(`[Silk] Transform error in ${id}:`, error);
107
+ return null;
50
108
  }
51
- }
52
- return outputs;
53
- }
54
- /**
55
- * Collect CSS rules from runtime
56
- */
57
- function collectCSS() {
58
- const rules = [];
59
- for (const [className, rule] of cssRules) {
60
- if (!cssCache.has(className)) {
61
- rules.push(rule);
62
- cssCache.add(className);
63
- }
64
- }
65
- return rules.join('\n');
66
- }
67
- /**
68
- * Minify CSS (basic implementation)
69
- */
70
- function minifyCSS(css) {
71
- return css
72
- .replace(/\s+/g, ' ')
73
- .replace(/\s*([{}:;,])\s*/g, '$1')
74
- .replace(/;}/g, '}')
75
- .trim();
76
- }
77
- /**
78
- * Generate CSS output
79
- */
80
- function generateCSS() {
81
- let css = collectCSS();
82
- if (minify ?? isBuild) {
83
- css = minifyCSS(css);
84
- }
85
- return css;
86
- }
87
- return {
88
- name: 'silk',
89
- configResolved(config) {
90
- isBuild = config.command === 'build';
91
- },
92
- configureServer(_server) {
93
- server = _server;
94
- // Hot reload CSS in dev mode
95
- if (watch) {
96
- const watcher = setInterval(() => {
97
- const newCSS = collectCSS();
98
- if (newCSS) {
99
- server?.ws.send({
100
- type: 'custom',
101
- event: 'silk:update',
102
- data: { css: newCSS },
103
- });
104
- }
105
- }, 100);
106
- server.httpServer?.on('close', () => {
107
- clearInterval(watcher);
108
- });
109
- }
110
- },
111
- transformIndexHtml: {
112
- order: 'post',
113
- handler(html) {
114
- if (!inject)
115
- return html;
116
- const css = generateCSS();
117
- if (!css)
118
- return html;
119
- // Inject CSS into head
120
- const styleTag = `<style data-silk>${css}</style>`;
121
- if (html.includes('</head>')) {
122
- return html.replace('</head>', `${styleTag}\n</head>`);
123
- }
124
- return `${styleTag}\n${html}`;
125
- },
126
109
  },
127
- async generateBundle(_, bundle) {
128
- if (!isBuild)
129
- return;
130
- const css = generateCSS();
131
- if (!css)
110
+ // Universal hook - works for all bundlers
111
+ async generateBundle() {
112
+ if (cssRules.size === 0)
132
113
  return;
114
+ // Generate CSS
115
+ let css = Array.from(cssRules.values()).join('\n');
116
+ if (shouldMinify ?? isProduction) {
117
+ css = minifyCSS(css);
118
+ }
133
119
  // Emit main CSS file
134
120
  this.emitFile({
135
121
  type: 'asset',
136
122
  fileName: outputFile,
137
123
  source: css,
138
124
  });
139
- // Generate and emit compressed versions
140
- const compressedAssets = await generateCompressedAssets(css, outputFile);
141
- for (const asset of compressedAssets) {
142
- this.emitFile({
143
- type: 'asset',
144
- fileName: asset.fileName,
145
- source: asset.source,
146
- });
147
- }
148
- // Log compression results
149
- if (compressedAssets.length > 0) {
150
- const originalSize = Buffer.byteLength(css, 'utf-8');
151
- console.log('\nšŸ“¦ Silk CSS Bundle:');
152
- console.log(` Original: ${formatBytes(originalSize)} (${outputFile})`);
153
- for (const asset of compressedAssets) {
154
- const compressedSize = asset.source.length;
155
- const savings = ((1 - compressedSize / originalSize) * 100).toFixed(1);
156
- const ext = asset.fileName.split('.').pop();
157
- console.log(` ${ext?.toUpperCase()}: ${formatBytes(compressedSize)} (-${savings}%)`);
125
+ // Generate compressed versions
126
+ if (isProduction) {
127
+ const cssBuffer = Buffer.from(css, 'utf-8');
128
+ const originalSize = cssBuffer.length;
129
+ // Brotli compression
130
+ if (compressionConfig.brotli) {
131
+ try {
132
+ const compressed = brotliCompressSync(cssBuffer, {
133
+ params: {
134
+ [constants.BROTLI_PARAM_QUALITY]: compressionConfig.brotliQuality,
135
+ },
136
+ });
137
+ this.emitFile({
138
+ type: 'asset',
139
+ fileName: `${outputFile}.br`,
140
+ source: compressed,
141
+ });
142
+ const savings = ((1 - compressed.length / originalSize) * 100).toFixed(1);
143
+ console.log(`[Silk] Brotli: ${formatBytes(compressed.length)} (-${savings}%)`);
144
+ }
145
+ catch (error) {
146
+ console.warn('[Silk] Brotli compression failed:', error);
147
+ }
158
148
  }
159
- console.log('');
160
- }
161
- // Update HTML to reference external CSS file
162
- for (const fileName in bundle) {
163
- const chunk = bundle[fileName];
164
- if (chunk && chunk.type === 'asset' && fileName.endsWith('.html')) {
165
- const asset = chunk; // OutputAsset type
166
- const html = asset.source;
167
- const linkTag = `<link rel="stylesheet" href="/${outputFile}">`;
168
- // Replace inline style with link tag
169
- asset.source = html
170
- .replace(/<style data-silk>[\s\S]*?<\/style>/, linkTag)
171
- .replace('</head>', `${linkTag}\n</head>`);
149
+ // Gzip compression
150
+ if (compressionConfig.gzip) {
151
+ try {
152
+ const compressed = gzipSync(cssBuffer, {
153
+ level: compressionConfig.gzipLevel,
154
+ });
155
+ this.emitFile({
156
+ type: 'asset',
157
+ fileName: `${outputFile}.gz`,
158
+ source: compressed,
159
+ });
160
+ const savings = ((1 - compressed.length / originalSize) * 100).toFixed(1);
161
+ console.log(`[Silk] Gzip: ${formatBytes(compressed.length)} (-${savings}%)`);
162
+ }
163
+ catch (error) {
164
+ console.warn('[Silk] Gzip compression failed:', error);
165
+ }
172
166
  }
167
+ // Summary
168
+ console.log(`\nšŸ“¦ Silk CSS Bundle:`);
169
+ console.log(` Original: ${formatBytes(originalSize)} (${outputFile})`);
170
+ console.log(` Rules: ${cssRules.size} atomic classes\n`);
173
171
  }
174
172
  },
175
- // Handle hot updates in dev mode
176
- handleHotUpdate({ file, server }) {
177
- if (file.endsWith('.ts') || file.endsWith('.tsx')) {
178
- // Trigger CSS update
179
- const css = generateCSS();
180
- server.ws.send({
181
- type: 'custom',
182
- event: 'silk:update',
183
- data: { css },
184
- });
185
- }
173
+ // Vite-specific hooks
174
+ vite: {
175
+ configResolved(config) {
176
+ isProduction = config.command === 'build' && config.mode === 'production';
177
+ },
178
+ },
179
+ // Webpack-specific hooks
180
+ webpack(compiler) {
181
+ compiler.hooks.emit.tapPromise('SilkPlugin', async (compilation) => {
182
+ if (cssRules.size === 0)
183
+ return;
184
+ let css = Array.from(cssRules.values()).join('\n');
185
+ if (shouldMinify ?? true) {
186
+ css = minifyCSS(css);
187
+ }
188
+ compilation.assets[outputFile] = {
189
+ source: () => css,
190
+ size: () => css.length,
191
+ };
192
+ });
186
193
  },
187
194
  };
188
- }
189
- export default silk;
190
- /**
191
- * Client-side script for hot CSS updates
192
- * This should be imported in the app entry point
193
- */
194
- export const silkClient = `
195
- if (import.meta.hot) {
196
- import.meta.hot.on('silk:update', ({ css }) => {
197
- let style = document.querySelector('style[data-silk]')
198
- if (!style) {
199
- style = document.createElement('style')
200
- style.setAttribute('data-silk', '')
201
- document.head.appendChild(style)
202
- }
203
- style.textContent = css
204
- })
205
- }
206
- `;
207
- /**
208
- * Format bytes to human-readable format
209
- */
210
- function formatBytes(bytes) {
211
- if (bytes < 1024)
212
- return `${bytes}B`;
213
- if (bytes < 1024 * 1024)
214
- return `${(bytes / 1024).toFixed(1)}KB`;
215
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
216
- }
195
+ });
196
+ // Export for different bundlers
197
+ export const vite = unpluginSilk.vite;
198
+ export const webpack = unpluginSilk.webpack;
199
+ export const rollup = unpluginSilk.rollup;
200
+ export const esbuild = unpluginSilk.esbuild;
201
+ // Default export for Vite
202
+ export default vite;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/silk-vite-plugin",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Vite plugin for Silk - Build-time CSS extraction with production optimizations",
5
5
  "keywords": [
6
6
  "silk",
@@ -40,10 +40,15 @@
40
40
  "prepublishOnly": "bun run build"
41
41
  },
42
42
  "dependencies": {
43
- "@sylphx/silk": "workspace:*"
43
+ "@babel/core": "^7.23.0",
44
+ "@babel/preset-react": "^7.23.0",
45
+ "@babel/preset-typescript": "^7.23.0",
46
+ "@sylphx/babel-plugin-silk": "workspace:*",
47
+ "@sylphx/silk": "workspace:*",
48
+ "unplugin": "^2.3.10"
44
49
  },
45
50
  "devDependencies": {
46
- "brotli-wasm": "^3.0.1",
51
+ "@types/babel__core": "^7.20.5",
47
52
  "typescript": "^5.3.0",
48
53
  "vite": "^5.0.0"
49
54
  },