dolphincss 1.3.1 → 1.3.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/vite-plugin.js CHANGED
@@ -1,76 +1,385 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import components from './scripts/components.js';
3
+ import { fileURLToPath } from 'url';
4
4
 
5
- export default function dolphincssPlugin() {
6
- return {
7
- name: 'vite-plugin-dolphincss',
8
- enforce: 'pre',
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ function indentHtmlOrJsx(htmlStr, initialIndent = 8) {
9
+ const lines = htmlStr.split('\n');
10
+ let currentIndent = initialIndent;
11
+ const indentStep = 2; // 2 spaces
12
+
13
+ const formattedLines = lines.map(line => {
14
+ const trimmed = line.trim();
15
+ if (!trimmed) return '';
9
16
 
10
- // transform hook runs for every file processed by Vite
11
- transform(code, id) {
12
- // Only process our target files
13
- const ext = path.extname(id);
14
- if (!['.jsx', '.tsx', '.js', '.html'].includes(ext) || id.includes('node_modules')) {
15
- return null;
16
- }
17
+ // Count tags
18
+ const openCount = (trimmed.match(/<[a-zA-Z0-9-]+(?:\s|>)/gi) || []).length;
19
+ const closeCount = (trimmed.match(/<\/[a-zA-Z0-9-]+>/gi) || []).length;
20
+ const selfCloseCount = (trimmed.match(/\/>/g) || []).length;
21
+
22
+ const netChange = (openCount - selfCloseCount) - closeCount;
23
+
24
+ // If the line starts with a closing tag, adjust its indent before printing
25
+ let lineIndent = currentIndent;
26
+ if (trimmed.startsWith('</') || trimmed.startsWith('}')) {
27
+ lineIndent = Math.max(initialIndent, currentIndent - indentStep);
28
+ }
29
+
30
+ const spaces = ' '.repeat(lineIndent);
31
+ const result = spaces + trimmed;
32
+
33
+ // Apply net depth change for the next lines
34
+ currentIndent = Math.max(initialIndent, currentIndent + netChange * indentStep);
35
+
36
+ return result;
37
+ });
38
+
39
+ return formattedLines.filter(Boolean).join('\n');
40
+ }
41
+
42
+ export default function dolphincssPlugin() {
43
+ const components = {};
44
+ let remoteMarkerMap = null;
45
+ let remoteBaseUrl = '';
46
+ let initPromise = null;
47
+ const activeFetches = new Map();
48
+ let isDev = false;
17
49
 
18
- // We read the original file directly to avoid writing Vite's injected boilerplate back to disk
19
- let originalCode;
50
+ async function ensureInitialized(projectRoot) {
51
+ if (initPromise) return initPromise;
52
+
53
+ initPromise = (async () => {
54
+ // 1. Try to load local markers and templates
20
55
  try {
21
- originalCode = fs.readFileSync(id, 'utf8');
56
+ let configPath = path.join(__dirname, 'config', 'markers.json');
57
+ if (!fs.existsSync(configPath)) {
58
+ configPath = path.join(__dirname, 'marker.json');
59
+ }
60
+ if (fs.existsSync(configPath)) {
61
+ const localMarkers = JSON.parse(fs.readFileSync(configPath, 'utf8'));
62
+ for (const [marker, data] of Object.entries(localMarkers)) {
63
+ const templateFile = typeof data === 'string' ? data : data.templateFile;
64
+ let templatePath = path.join(__dirname, 'templates', templateFile);
65
+ if (!fs.existsSync(templatePath)) {
66
+ templatePath = path.join(__dirname, 'core-templates', templateFile);
67
+ }
68
+ if (fs.existsSync(templatePath)) {
69
+ components[marker] = {
70
+ content: fs.readFileSync(templatePath, 'utf8'),
71
+ addClasses: data.addClasses || '',
72
+ isJsxTemplate: templateFile.endsWith('.jsx') || templateFile.endsWith('.tsx')
73
+ };
74
+ }
75
+ }
76
+ }
22
77
  } catch (err) {
23
- return null;
78
+ console.error('⚠️ DolphinCSS: Error loading local markers', err);
24
79
  }
25
80
 
26
- let modified = false;
27
- let newOriginalCode = originalCode;
81
+ // 2. Load remote configuration
82
+ let remoteUrl = '';
83
+ const dolphinConfigPath = path.join(projectRoot, 'dolphin.config.json');
84
+ if (fs.existsSync(dolphinConfigPath)) {
85
+ try {
86
+ const userConfig = JSON.parse(fs.readFileSync(dolphinConfigPath, 'utf8'));
87
+ let rawUrl = userConfig.remoteUrl;
88
+ if (rawUrl && rawUrl.includes('github.com') && rawUrl.includes('/blob/')) {
89
+ rawUrl = rawUrl.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/');
90
+ }
91
+ remoteUrl = rawUrl;
92
+ } catch (e) {}
93
+ }
28
94
 
29
- for (const [marker, replacement] of Object.entries(components)) {
30
- // Regex to match <div className="marker"></div> with any spacing/newlines inside
31
- // Using [\"'] instead of capture groups to simplify and avoid backreference issues
32
- const regex = new RegExp(
33
- `<div\\s+class(?:Name)?=[\"']${marker}[\"']\\s*>\\s*</div>|<div\\s+class(?:Name)?=[\"']${marker}[\"']\\s*/>`,
34
- 'g'
35
- );
95
+ if (!remoteUrl) {
96
+ remoteUrl = 'https://raw.githubusercontent.com/Phuyalshankar/dolphincss-template/main/config/markers.json';
97
+ }
36
98
 
37
- if (regex.test(newOriginalCode)) {
38
- // IMPORTANT: Use () => replacement to avoid $1, $2 substitution from things like $120.50
39
- newOriginalCode = newOriginalCode.replace(regex, () => replacement);
40
- modified = true;
41
- console.log(`\n✨ DolphinCSS: Injected '${marker}' into ${path.basename(id)}`);
99
+ try {
100
+ const cacheBustUrl = `${remoteUrl}?t=${Date.now()}`;
101
+ console.log(`🌐 DolphinCSS: Syncing markers from: ${cacheBustUrl}`);
102
+ const response = await fetch(cacheBustUrl);
103
+ if (response.ok) {
104
+ const text = await response.text();
105
+ remoteMarkerMap = JSON.parse(text);
106
+
107
+ const configDirUrl = remoteUrl.substring(0, remoteUrl.lastIndexOf('/'));
108
+ remoteBaseUrl = configDirUrl.substring(0, configDirUrl.lastIndexOf('/'));
109
+ console.log(`✅ DolphinCSS: Remote markers loaded (${Object.keys(remoteMarkerMap).length} items).`);
42
110
  }
111
+ } catch (err) {
112
+ console.error(`⚠️ DolphinCSS: Failed to load remote markers: ${err.message}`);
43
113
  }
114
+ })();
115
+
116
+ return initPromise;
117
+ }
118
+
119
+ async function fetchRemote(url) {
120
+ const response = await fetch(url);
121
+ if (!response.ok) {
122
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
123
+ }
124
+ return await response.text();
125
+ }
126
+
127
+ async function handleTransform(code, id) {
128
+ const ext = path.extname(id).toLowerCase();
129
+ if (!['.jsx', '.tsx', '.js', '.ts', '.html'].includes(ext) || id.includes('node_modules')) {
130
+ return null;
131
+ }
132
+
133
+ const projectRoot = process.cwd();
134
+ await ensureInitialized(projectRoot);
135
+
136
+ let content = code;
137
+ let modified = false;
138
+ const isReact = ['.jsx', '.tsx', '.js', '.ts'].includes(ext);
139
+ const classAttrName = isReact ? 'className' : 'class';
140
+
141
+ // 1. Fetch missing markers on-demand (with race-condition protection)
142
+ const possibleMarkers = content.match(/dolphin-[a-zA-Z0-9-]+/g);
143
+ if (possibleMarkers && remoteMarkerMap) {
144
+ for (const markerClass of possibleMarkers) {
145
+ if (!components[markerClass]) {
146
+ if (activeFetches.has(markerClass)) {
147
+ // If already fetching, wait for the existing fetch to complete
148
+ await activeFetches.get(markerClass);
149
+ } else if (remoteMarkerMap[markerClass]) {
150
+ const fetchPromise = (async () => {
151
+ try {
152
+ const data = remoteMarkerMap[markerClass];
153
+ const templateFile = typeof data === 'string' ? data : data.templateFile;
154
+ let localTemplatePath = path.join(__dirname, 'core-templates', templateFile);
155
+ if (!fs.existsSync(localTemplatePath)) {
156
+ localTemplatePath = path.join(__dirname, 'templates', templateFile);
157
+ }
158
+
159
+ let templateContent = '';
160
+ if (fs.existsSync(localTemplatePath)) {
161
+ templateContent = fs.readFileSync(localTemplatePath, 'utf8');
162
+ } else if (remoteBaseUrl) {
163
+ const templateUrl = `${remoteBaseUrl}/templates/${templateFile}?t=${Date.now()}`;
164
+ console.log(`🌐 DolphinCSS: Fetching remote template for ${markerClass}...`);
165
+ templateContent = await fetchRemote(templateUrl);
166
+ }
44
167
 
45
- if (modified) {
46
- // Write the clean replaced code back to the file system so the user sees it
47
- setTimeout(() => {
48
- try {
49
- fs.writeFileSync(id, newOriginalCode, 'utf8');
50
- } catch (err) {
51
- console.error(`❌ DolphinCSS: Error writing to ${id}`, err);
168
+ if (templateContent) {
169
+ templateContent = templateContent.replace(/>\s*</g, '>\n<');
170
+ templateContent = indentHtmlOrJsx(templateContent, 8);
171
+ components[markerClass] = {
172
+ content: templateContent,
173
+ addClasses: data.addClasses || '',
174
+ isJsxTemplate: templateFile.endsWith('.jsx') || templateFile.endsWith('.tsx')
175
+ };
176
+ console.log(`✅ DolphinCSS: Registered on-demand marker: ${markerClass}`);
177
+ }
178
+ } catch (err) {
179
+ console.error(`❌ DolphinCSS: Failed to fetch template for ${markerClass}:`, err.message);
180
+ }
181
+ })();
182
+
183
+ activeFetches.set(markerClass, fetchPromise);
184
+ await fetchPromise;
185
+ activeFetches.delete(markerClass);
52
186
  }
53
- }, 0);
187
+ }
188
+ }
189
+ }
190
+
191
+ const markerKeys = Object.keys(components);
192
+ if (markerKeys.length === 0) return null;
193
+
194
+ // 2. Expand markers in code
195
+ const regex = new RegExp(
196
+ `(<([a-z0-9-]+)\\s+[^>]*${classAttrName}=\\s*["']([^"']*?\\s)?(dolphin-[a-zA-Z0-9-]+)(\\s[^"']*?)?["'][^>]*>)`,
197
+ 'gi'
198
+ );
199
+
200
+ let found = true;
201
+ while (found) {
202
+ found = false;
203
+ regex.lastIndex = 0;
204
+ let match;
205
+
206
+ while ((match = regex.exec(content)) !== null) {
207
+ const fullOpeningTag = match[1];
208
+ const tagName = match[2];
209
+ const beforeClasses = match[3];
210
+ const markerClass = match[4];
211
+ const afterClasses = match[5];
212
+
213
+ if (!components[markerClass]) {
214
+ continue;
215
+ }
216
+
217
+ // Find matching closing tag
218
+ let depth = 1;
219
+ const startIndex = match.index + fullOpeningTag.length;
220
+ const tagPattern = new RegExp(`</?${tagName}(?:\\s|>|/)`, 'gi');
221
+ tagPattern.lastIndex = startIndex;
54
222
 
55
- // Also apply the replacement to the current transform stream
56
- let newCode = code;
57
- for (const [marker, replacement] of Object.entries(components)) {
58
- const regex = new RegExp(
59
- `<div\\s+class(?:Name)?=[\"']${marker}[\"']\\s*>\\s*</div>|<div\\s+class(?:Name)?=[\"']${marker}[\"']\\s*/>`,
60
- 'g'
61
- );
62
- if (regex.test(newCode)) {
63
- newCode = newCode.replace(regex, () => replacement);
223
+ let closingTagIndex = -1;
224
+ let tagMatch;
225
+ while ((tagMatch = tagPattern.exec(content)) !== null) {
226
+ const isClosing = tagMatch[0].startsWith('</');
227
+ if (!isClosing) {
228
+ const tagEnd = content.indexOf('>', tagMatch.index);
229
+ if (tagEnd !== -1 && content[tagEnd - 1] === '/') {
230
+ continue;
231
+ }
232
+ depth++;
233
+ } else {
234
+ depth--;
235
+ }
236
+ if (depth === 0) {
237
+ closingTagIndex = tagMatch.index;
238
+ break;
64
239
  }
65
240
  }
66
-
67
- return {
68
- code: newCode,
69
- map: null
70
- };
241
+
242
+ if (closingTagIndex !== -1) {
243
+ const innerContent = content.substring(startIndex, closingTagIndex);
244
+ const closingTagMatch = content.substring(closingTagIndex).match(new RegExp(`^</${tagName}\\s*>`, 'i'));
245
+ const closingTag = closingTagMatch ? closingTagMatch[0] : `</${tagName}>`;
246
+
247
+ modified = true;
248
+ const templateData = components[markerClass];
249
+
250
+ const classParts = (beforeClasses || '') + (afterClasses || '');
251
+ let allClasses = classParts.trim().split(/\s+/).filter(Boolean);
252
+ allClasses = allClasses.filter(c => c !== markerClass && c !== '');
253
+
254
+ if (templateData.addClasses) {
255
+ const addCls = templateData.addClasses.trim().split(/\s+/).filter(Boolean);
256
+ allClasses = [...new Set([...allClasses, ...addCls])];
257
+ }
258
+
259
+ const newClassAttr = allClasses.length > 0 ? ` ${classAttrName}="${allClasses.join(' ')}"` : '';
260
+
261
+ let newOpeningTag = fullOpeningTag.replace(new RegExp(`\\s*${classAttrName}=["'][^"']*["']`, 'i'), newClassAttr ? `${newClassAttr}` : '');
262
+ newOpeningTag = newOpeningTag.replace(/\s+(?:class|className)=["']\s*["']/i, '');
263
+
264
+ let finalTemplate = templateData.content;
265
+
266
+ if (isReact) {
267
+ if (!templateData.isJsxTemplate) {
268
+ finalTemplate = finalTemplate
269
+ .replace(/class=/g, 'className=')
270
+ .replace(/for=/g, 'htmlFor=')
271
+ .replace(/tabindex=/g, 'tabIndex=')
272
+ .replace(/onclick=/g, 'onClick=')
273
+ .replace(/stroke-linecap=/g, 'strokeLinecap=')
274
+ .replace(/stroke-linejoin=/g, 'strokeLinejoin=')
275
+ .replace(/stroke-width=/g, 'strokeWidth=')
276
+ .replace(/fill-rule=/g, 'fillRule=')
277
+ .replace(/clip-rule=/g, 'clipRule=')
278
+ .replace(/<!--([\s\S]*?)-->/g, '{/*$1*/}')
279
+ .replace(/stop-color=/g, 'stopColor=')
280
+ .replace(/style="([^"]*)"/g, (_, s) => {
281
+ const obj = s.split(';').filter(Boolean).map(p => {
282
+ const [k, v] = p.split(':').map(x => x.trim());
283
+ if (!k) return '';
284
+ const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
285
+ return `${camel}: '${v}'`;
286
+ }).filter(Boolean).join(', ');
287
+ return `style={{${obj}}}`;
288
+ })
289
+ .replace(/<(input|img|br|hr|meta|link)\b([^>]*?)>/gi, (m, t, a) => {
290
+ if (a.trim().endsWith('/')) return m;
291
+ return `<${t}${a} />`;
292
+ });
293
+ }
294
+ } else {
295
+ finalTemplate = finalTemplate.replace(/className=/g, 'class=');
296
+ }
297
+
298
+ if (finalTemplate.includes('{/* INNER */}')) {
299
+ finalTemplate = finalTemplate.replace('{/* INNER */}', innerContent.trim());
300
+ }
301
+
302
+ const fullMatchString = content.substring(match.index, closingTagIndex + closingTag.length);
303
+ if (templateData.isJsxTemplate && finalTemplate.includes('export default') && content.trim() === fullMatchString.trim()) {
304
+ content = finalTemplate;
305
+ found = false;
306
+ break;
307
+ }
308
+
309
+ const expanded = `${newOpeningTag}\n${finalTemplate}\n${closingTag}`;
310
+ content = content.substring(0, match.index) + expanded + content.substring(closingTagIndex + closingTag.length);
311
+ found = true;
312
+ break;
313
+ }
71
314
  }
315
+ }
72
316
 
73
- return null;
317
+ if (modified) {
318
+ return {
319
+ code: content,
320
+ map: null
321
+ };
322
+ }
323
+
324
+ return null;
325
+ }
326
+
327
+ return {
328
+ name: 'vite-plugin-dolphincss',
329
+ enforce: 'pre',
330
+
331
+ configResolved(config) {
332
+ isDev = config.command === 'serve';
333
+ },
334
+
335
+ async buildStart() {
336
+ if (!isDev) return;
337
+ const projectRoot = process.cwd();
338
+ await ensureInitialized(projectRoot);
339
+
340
+ const srcDir = path.join(projectRoot, 'src');
341
+ if (!fs.existsSync(srcDir)) return;
342
+
343
+ const scanDirectory = async (dir) => {
344
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
345
+ for (const entry of entries) {
346
+ const fullPath = path.join(dir, entry.name);
347
+ if (entry.isDirectory()) {
348
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
349
+ await scanDirectory(fullPath);
350
+ }
351
+ } else if (entry.isFile()) {
352
+ const ext = path.extname(entry.name).toLowerCase();
353
+ if (['.jsx', '.tsx', '.js', '.ts', '.html'].includes(ext)) {
354
+ try {
355
+ const code = fs.readFileSync(fullPath, 'utf8');
356
+ const result = await handleTransform(code, fullPath);
357
+ if (result && result.code) {
358
+ fs.writeFileSync(fullPath, result.code, 'utf8');
359
+ console.log(`\n✨ DolphinCSS: Auto-injected template on startup in: ${entry.name}`);
360
+ }
361
+ } catch (e) {
362
+ // Ignore individual file errors
363
+ }
364
+ }
365
+ }
366
+ }
367
+ };
368
+
369
+ await scanDirectory(srcDir);
370
+ },
371
+
372
+ async transform(code, id) {
373
+ const result = await handleTransform(code, id);
374
+ if (result && result.code && isDev) {
375
+ try {
376
+ fs.writeFileSync(id, result.code, 'utf8');
377
+ console.log(`\n✨ DolphinCSS: Injected and updated source file: ${path.basename(id)}`);
378
+ } catch (err) {
379
+ console.error(`⚠️ DolphinCSS: Failed to write back to ${id}:`, err.message);
380
+ }
381
+ }
382
+ return result;
74
383
  }
75
384
  };
76
385
  }