@ynor/ynor 1.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.
@@ -0,0 +1,472 @@
1
+ // src/lib/core/compiler.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { parse as acornParse } from 'acorn';
5
+
6
+ // ============================================
7
+ // VERSION
8
+ // ============================================
9
+ export const VERSION = '1.0.0';
10
+
11
+ // ============================================
12
+ // BASE NODE CLASS
13
+ // ============================================
14
+ class BaseNode {
15
+ constructor(type, start, end) {
16
+ this.type = type;
17
+ this.start = start;
18
+ this.end = end;
19
+ }
20
+ }
21
+
22
+ // ============================================
23
+ // AST NAMESPACE
24
+ // ============================================
25
+ export const AST = {
26
+ BaseNode,
27
+ Root: class extends BaseNode {
28
+ constructor(start, end) {
29
+ super('Root', start, end);
30
+ this.options = null;
31
+ this.fragment = null;
32
+ this.css = null;
33
+ this.instance = null;
34
+ this.module = null;
35
+ this.comments = [];
36
+ }
37
+ },
38
+ Fragment: class extends BaseNode {
39
+ constructor(start, end) {
40
+ super('Fragment', start, end);
41
+ this.nodes = [];
42
+ }
43
+ },
44
+ Text: class extends BaseNode {
45
+ constructor(start, end, data, raw) {
46
+ super('Text', start, end);
47
+ this.data = data;
48
+ this.raw = raw || data;
49
+ }
50
+ },
51
+ ExpressionTag: class extends BaseNode {
52
+ constructor(start, end, expression) {
53
+ super('ExpressionTag', start, end);
54
+ this.expression = expression;
55
+ }
56
+ },
57
+ };
58
+
59
+ // ============================================
60
+ // YNOR COMPILER CLASS
61
+ // ============================================
62
+ export class YnorCompiler {
63
+ constructor(options = {}) {
64
+ this.options = {
65
+ generate: 'dom',
66
+ dev: true,
67
+ css: 'injected',
68
+ ...options
69
+ };
70
+ }
71
+
72
+ compile(content, filename) {
73
+ let script = '';
74
+ let template = '';
75
+ let styles = '';
76
+
77
+ console.log('📄 === COMPILING ===', filename);
78
+ console.log('📄 Content length:', content.length);
79
+
80
+ // Check if content is empty
81
+ if (!content || content.trim() === '') {
82
+ console.error('❌ Content is empty!');
83
+ template = `<div style="padding:40px;text-align:center;font-family:system-ui;color:red;">
84
+ <h1>Error: Empty file</h1>
85
+ <p>File: ${path.basename(filename)}</p>
86
+ </div>`;
87
+ return this.generateComponent(script, template, styles, filename);
88
+ }
89
+
90
+ // Extract template using multiple methods
91
+ let templateStart = content.indexOf('<template>');
92
+ let templateEnd = content.indexOf('</template>');
93
+
94
+ if (templateStart === -1) {
95
+ templateStart = content.indexOf('<template ');
96
+ }
97
+
98
+ if (templateStart !== -1 && templateEnd !== -1) {
99
+ const startTagEnd = content.indexOf('>', templateStart) + 1;
100
+ if (startTagEnd > 0 && startTagEnd < templateEnd) {
101
+ template = content.substring(startTagEnd, templateEnd).trim();
102
+ console.log('✅ Template extracted, length:', template.length);
103
+ } else {
104
+ template = content.substring(templateStart + 10, templateEnd).trim();
105
+ console.log('✅ Template extracted (fallback), length:', template.length);
106
+ }
107
+ }
108
+
109
+ // Extract script
110
+ const scriptRegex = /<script\s*>([\s\S]*?)<\/script>/;
111
+ const scriptMatch = content.match(scriptRegex);
112
+ if (scriptMatch) {
113
+ script = scriptMatch[1].trim();
114
+ console.log('✅ Script extracted, length:', script.length);
115
+ }
116
+
117
+ // Extract styles
118
+ const styleRegex = /<style\s*>([\s\S]*?)<\/style>/;
119
+ const styleMatch = content.match(styleRegex);
120
+ if (styleMatch) {
121
+ styles = styleMatch[1].trim();
122
+ console.log('✅ Styles extracted, length:', styles.length);
123
+ }
124
+
125
+ // If no template, create fallback without HTML comments
126
+ if (!template) {
127
+ console.warn('⚠️ No template found, using fallback');
128
+ template = `<div style="padding:40px;text-align:center;font-family:system-ui;max-width:600px;margin:20px auto;background:white;border-radius:20px;box-shadow:0 8px 30px rgba(0,0,0,0.08);">
129
+ <h1 style="color:#4f46e5;">⚡ .ynor</h1>
130
+ <p style="color:#1e293b;font-size:1.2rem;">Component: ${path.basename(filename)}</p>
131
+ <div style="background:#fef2f2;padding:20px;border-radius:12px;margin:20px;border:2px solid #fecaca;">
132
+ <p style="color:#dc2626;font-weight:600;">No template found</p>
133
+ <p style="color:#64748b;font-size:0.9rem;">Expected to find template section</p>
134
+ <p style="color:#64748b;font-size:0.8rem;">File: ${path.basename(filename)}</p>
135
+ <p style="color:#64748b;font-size:0.8rem;">Content length: ${content.length} chars</p>
136
+ </div>
137
+ </div>`;
138
+ }
139
+
140
+ return this.generateComponent(script, template, styles, filename);
141
+ }
142
+
143
+ // src/lib/core/compiler.js - Update the generateComponent method
144
+
145
+ generateComponent(script, template, styles, filename) {
146
+ const componentName = path.basename(filename, '.ynor');
147
+ const className = `Ynor${componentName}`;
148
+ const processedTemplate = this.processTemplate(template);
149
+ const runtimePath = this.getRuntimePath(filename);
150
+
151
+ console.log(`🔧 Generating component: ${componentName}`);
152
+
153
+ return `
154
+ // Generated from ${filename}
155
+ import { YnorComponent, runtime } from '${runtimePath}';
156
+
157
+ export default class ${className} extends YnorComponent {
158
+ constructor(options) {
159
+ super(options);
160
+ this.name = '${componentName}';
161
+ this._setup();
162
+ }
163
+
164
+ _setup() {
165
+ // Execute script in the context of 'this'
166
+ // This ensures all variables and functions are attached to the component instance
167
+ (function() {
168
+ // Define variables on 'this'
169
+ ${script}
170
+ }).call(this);
171
+
172
+ // Log for debugging
173
+ console.log('🔧 Component setup complete:', this);
174
+ }
175
+
176
+ render() {
177
+ const template = \`${processedTemplate}\`;
178
+ const container = document.createElement('div');
179
+ container.className = 'ynor-component ynor-${componentName}';
180
+ container.innerHTML = template;
181
+
182
+ this._bindEvents(container);
183
+ this._bindInputs(container);
184
+
185
+ this._element = container;
186
+ return this._element;
187
+ }
188
+
189
+ _renderComponent(componentName) {
190
+ const Component = runtime.components.get(componentName);
191
+ if (!Component) {
192
+ console.warn(\`Component "\${componentName}" not found\`);
193
+ return '';
194
+ }
195
+ const tempDiv = document.createElement('div');
196
+ const instance = new Component({ target: tempDiv });
197
+ instance.$mount();
198
+ return tempDiv.innerHTML;
199
+ }
200
+
201
+ _bindEvents(container) {
202
+ const clickElements = container.querySelectorAll('[data-on-click]');
203
+ clickElements.forEach(el => {
204
+ const handlerName = el.getAttribute('data-on-click');
205
+ if (this[handlerName]) {
206
+ el.addEventListener('click', this[handlerName].bind(this));
207
+ }
208
+ });
209
+
210
+ const changeElements = container.querySelectorAll('[data-on-change]');
211
+ changeElements.forEach(el => {
212
+ const handlerName = el.getAttribute('data-on-change');
213
+ if (this[handlerName]) {
214
+ el.addEventListener('change', this[handlerName].bind(this));
215
+ }
216
+ });
217
+
218
+ const keydownElements = container.querySelectorAll('[data-on-keydown]');
219
+ keydownElements.forEach(el => {
220
+ const handlerName = el.getAttribute('data-on-keydown');
221
+ if (this[handlerName]) {
222
+ el.addEventListener('keydown', this[handlerName].bind(this));
223
+ }
224
+ });
225
+ }
226
+
227
+ _bindInputs(container) {
228
+ const inputs = container.querySelectorAll('[data-bind]');
229
+ inputs.forEach(el => {
230
+ const variable = el.getAttribute('data-bind');
231
+ if (this[variable] !== undefined) {
232
+ el.value = this[variable];
233
+ el.addEventListener('input', (e) => {
234
+ this[variable] = e.target.value;
235
+ this._update();
236
+ });
237
+ }
238
+ });
239
+ }
240
+
241
+ _update() {
242
+ if (this._destroyed) return;
243
+ this.render();
244
+ if (this._element && this.target) {
245
+ this.target.innerHTML = '';
246
+ this.target.appendChild(this._element);
247
+ }
248
+ }
249
+
250
+ get styles() {
251
+ return \`${styles}\`;
252
+ }
253
+
254
+ $mount(target) {
255
+ super.$mount(target);
256
+ this._injectStyles();
257
+ return this;
258
+ }
259
+
260
+ _injectStyles() {
261
+ if (this.styles && !document.getElementById('ynor-${componentName}-styles')) {
262
+ const styleEl = document.createElement('style');
263
+ styleEl.id = 'ynor-${componentName}-styles';
264
+ styleEl.textContent = this.styles;
265
+ document.head.appendChild(styleEl);
266
+ }
267
+ }
268
+ }
269
+
270
+ runtime.registerComponent('${componentName}', ${className});
271
+ `;
272
+ }
273
+
274
+
275
+ transformScript(script) {
276
+ if (!script) return '';
277
+
278
+ // Simple approach: wrap the script in a function that binds to this
279
+ // This is safer than trying to parse and transform each line
280
+ return `
281
+ // Auto-bind variables and functions to this
282
+ const self = this;
283
+
284
+ // Simple variables that will be attached to this
285
+ ${script}
286
+
287
+ // After script execution, attach all local variables to this
288
+ // This captures any let/const/var declarations
289
+ const localVars = Object.keys(this).filter(key => key !== 'self');
290
+ for (const key of localVars) {
291
+ if (this[key] !== undefined && typeof this[key] !== 'function') {
292
+ // Already attached
293
+ }
294
+ }
295
+ `;
296
+ }
297
+
298
+ getRuntimePath(filename) {
299
+ const normalizedPath = filename.replace(/\\/g, '/');
300
+ const fileDir = path.dirname(normalizedPath);
301
+ const coreDir = path.join(process.cwd(), 'src', 'lib', 'core');
302
+
303
+ let relativePath = path.relative(fileDir, coreDir);
304
+ relativePath = relativePath.replace(/\\/g, '/');
305
+
306
+ if (!relativePath.startsWith('.')) {
307
+ relativePath = './' + relativePath;
308
+ }
309
+
310
+ if (relativePath === './') {
311
+ relativePath = '.';
312
+ }
313
+
314
+ return relativePath + '/runtime.js';
315
+ }
316
+
317
+ // src/lib/core/compiler.js - Fix processTemplate
318
+
319
+ processTemplate(template) {
320
+ if (!template) {
321
+ return '';
322
+ }
323
+
324
+ let processed = template;
325
+
326
+ // Handle component tags
327
+ processed = processed.replace(/<([A-Z][a-zA-Z0-9]*)\s*\/>/g, (match, componentName) => {
328
+ return '${this._renderComponent("' + componentName + '")}';
329
+ });
330
+
331
+ // Handle on:click={handler} - FIX THIS
332
+ processed = processed.replace(/on:([a-zA-Z]+)=\{([^}]+)\}/g, (match, event, handler) => {
333
+ // Store the handler name as a data attribute
334
+ return `data-on-${event}="${handler.trim()}"`;
335
+ });
336
+
337
+ // Handle expressions {variable}
338
+ processed = processed.replace(/\{([^}]+)\}/g, (match, expr) => {
339
+ const trimmed = expr.trim();
340
+ // For simple variables, access from this
341
+ return '${this.' + trimmed + ' !== undefined ? this.' + trimmed + ' : 0}';
342
+ });
343
+
344
+ // Handle class:active={condition}
345
+ processed = processed.replace(/class:([a-zA-Z-]+)=\{([^}]+)\}/g, (match, className, condition) => {
346
+ return '${' + condition.trim() + ' ? "' + className + '" : ""}';
347
+ });
348
+
349
+ // Handle bind:value={variable}
350
+ processed = processed.replace(/bind:value=\{([^}]+)\}/g, (match, variable) => {
351
+ return `data-bind="${variable.trim()}" value="\${this.${variable.trim()}}"`;
352
+ });
353
+
354
+ // Handle #each
355
+ processed = processed.replace(/{#each\s+([^}]+)\s+as\s+([^}]+)}/g, (match, iterable, item) => {
356
+ return '${' + iterable.trim() + '.map(' + item.trim() + ' => `';
357
+ });
358
+ processed = processed.replace(/{\/each}/g, '`).join("")}');
359
+
360
+ // Handle #if with :else if and :else
361
+ processed = processed.replace(/{:else if\s+([^}]+)}/g, (match, condition) => {
362
+ return '` : (' + condition.trim() + ' ? `';
363
+ });
364
+ processed = processed.replace(/{:else}/g, '` : `');
365
+ processed = processed.replace(/{#if\s+([^}]+)}/g, (match, condition) => {
366
+ return '${' + condition.trim() + ' ? `';
367
+ });
368
+ processed = processed.replace(/{\/if}/g, '` : ""}');
369
+
370
+ console.log('📝 Processed template:', processed);
371
+
372
+ return processed;
373
+ }}
374
+
375
+ // ============================================
376
+ // PARSER
377
+ // ============================================
378
+ export function parse(source, options = {}) {
379
+ const filename = options.filename || 'unknown.ynor';
380
+ const root = new AST.Root(0, source.length);
381
+
382
+ const scriptMatch = source.match(/<script\s*>([\s\S]*?)<\/script>/);
383
+ if (scriptMatch) {
384
+ try {
385
+ const program = acornParse(scriptMatch[1], {
386
+ ecmaVersion: 2022,
387
+ sourceType: 'module',
388
+ locations: true,
389
+ sourceFilename: filename
390
+ });
391
+ root.instance = { content: program, context: 'default', attributes: [] };
392
+ } catch (e) {
393
+ console.warn('Failed to parse script:', e.message);
394
+ }
395
+ }
396
+
397
+ return options.modern ? root : root;
398
+ }
399
+
400
+ // ============================================
401
+ // COMPILE
402
+ // ============================================
403
+ export function compile(source, options = {}) {
404
+ const filename = options.filename || 'component.ynor';
405
+ const compiler = new YnorCompiler(options);
406
+ const compiled = compiler.compile(source, filename);
407
+
408
+ return {
409
+ js: { code: compiled, map: null },
410
+ css: { code: '', map: null },
411
+ ast: parse(source, { ...options, modern: true }),
412
+ warnings: [],
413
+ vars: []
414
+ };
415
+ }
416
+
417
+ // ============================================
418
+ // OTHER EXPORTS
419
+ // ============================================
420
+ export function compileModule(source, options = {}) {
421
+ try {
422
+ const ast = acornParse(source, {
423
+ ecmaVersion: 2022,
424
+ sourceType: 'module',
425
+ locations: true
426
+ });
427
+ return { js: { code: source, map: null }, ast, warnings: [], vars: [] };
428
+ } catch (error) {
429
+ throw new Error(`Failed to compile module: ${error.message}`);
430
+ }
431
+ }
432
+
433
+ export function migrate(source) {
434
+ let code = source;
435
+ code = code.replace(/{#each\s+([^}]+)\s+as\s+([^}]+)}/g, (match, iterable, item) => {
436
+ return `{#each ${iterable} as ${item}}`;
437
+ });
438
+ code = code.replace(/{#if\s+([^}]+)}/g, (match, condition) => {
439
+ return `{#if ${condition}}`;
440
+ });
441
+ return { code };
442
+ }
443
+
444
+ export function preprocess(source, preprocessor) {
445
+ return Promise.resolve({ code: source, toString: () => source });
446
+ }
447
+
448
+ export function print(ast) {
449
+ return { code: '', map: null };
450
+ }
451
+
452
+ export function parseCss(source) {
453
+ return { type: 'StyleSheet', nodes: [] };
454
+ }
455
+
456
+ export function walk(ast, options) {
457
+ throw new Error('Please import { walk } from "estree-walker" instead');
458
+ }
459
+
460
+ export default {
461
+ VERSION,
462
+ compile,
463
+ compileModule,
464
+ migrate,
465
+ parse,
466
+ parseCss,
467
+ preprocess,
468
+ print,
469
+ walk,
470
+ AST,
471
+ YnorCompiler
472
+ };
@@ -0,0 +1,43 @@
1
+ // src/lib/core/index.js
2
+ export {
3
+ VERSION,
4
+ compile,
5
+ compileModule,
6
+ migrate,
7
+ parse,
8
+ parseCss,
9
+ preprocess,
10
+ print,
11
+ walk,
12
+ AST,
13
+ YnorCompiler
14
+ } from './compiler.js';
15
+
16
+ export { ynorPlugin } from './plugin.js';
17
+
18
+ export {
19
+ YnorRuntime,
20
+ YnorComponent,
21
+ runtime,
22
+ store,
23
+ mount
24
+ } from './runtime.js';
25
+
26
+ // Default export
27
+ export default {
28
+ VERSION,
29
+ compile,
30
+ compileModule,
31
+ migrate,
32
+ parse,
33
+ parseCss,
34
+ preprocess,
35
+ print,
36
+ walk,
37
+ AST,
38
+ YnorCompiler,
39
+ ynorPlugin,
40
+ runtime,
41
+ store,
42
+ mount
43
+ };
@@ -0,0 +1,114 @@
1
+ // src/lib/core/plugin.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { YnorCompiler } from './compiler.js';
5
+
6
+ export function ynorPlugin(options = {}) {
7
+ const compiler = new YnorCompiler(options);
8
+
9
+ return {
10
+ name: 'vite-plugin-ynor',
11
+ enforce: 'pre',
12
+
13
+ resolveId(id) {
14
+ if (id.endsWith('.ynor')) {
15
+ const root = process.cwd();
16
+ const possiblePaths = [
17
+ path.resolve(root, 'src', id),
18
+ path.resolve(root, 'src', 'lib', 'components', id),
19
+ path.resolve(root, id),
20
+ ];
21
+
22
+ if (id.includes('/') || id.includes('\\')) {
23
+ possiblePaths.unshift(path.resolve(root, id));
24
+ }
25
+
26
+ for (const p of possiblePaths) {
27
+ try {
28
+ if (fs.existsSync(p)) {
29
+ return p;
30
+ }
31
+ } catch (e) {}
32
+ }
33
+
34
+ console.warn(`⚠️ .ynor file not found: ${id}`);
35
+ return id;
36
+ }
37
+ return null;
38
+ },
39
+
40
+ load(id) {
41
+ if (id.endsWith('.ynor')) {
42
+ try {
43
+ let filePath = id;
44
+ if (id.startsWith('\0')) {
45
+ filePath = id.slice(1);
46
+ }
47
+
48
+ if (filePath.startsWith('virtual:')) {
49
+ return null;
50
+ }
51
+
52
+ if (!fs.existsSync(filePath)) {
53
+ console.warn(`⚠️ .ynor file not found: ${filePath}`);
54
+ return null;
55
+ }
56
+
57
+ const content = fs.readFileSync(filePath, 'utf-8');
58
+ const compiled = compiler.compile(content, filePath);
59
+
60
+ return {
61
+ code: compiled,
62
+ map: null
63
+ };
64
+ } catch (error) {
65
+ console.error(`❌ Error compiling .ynor file: ${id}`, error.message);
66
+ throw error;
67
+ }
68
+ }
69
+ return null;
70
+ },
71
+
72
+ transform(code, id) {
73
+ if (id.endsWith('.ynor')) {
74
+ try {
75
+ let filePath = id;
76
+ if (id.startsWith('\0')) {
77
+ filePath = id.slice(1);
78
+ }
79
+
80
+ if (filePath.startsWith('virtual:')) {
81
+ return null;
82
+ }
83
+
84
+ // Always read fresh from disk
85
+ if (fs.existsSync(filePath)) {
86
+ const content = fs.readFileSync(filePath, 'utf-8');
87
+ const compiled = compiler.compile(content, filePath);
88
+ return {
89
+ code: compiled,
90
+ map: null
91
+ };
92
+ } else {
93
+ console.warn(`⚠️ .ynor file not found for transform: ${filePath}`);
94
+ return null;
95
+ }
96
+ } catch (error) {
97
+ console.error(`❌ Error transforming .ynor file: ${id}`, error.message);
98
+ throw error;
99
+ }
100
+ }
101
+ return null;
102
+ },
103
+
104
+ handleHotUpdate({ file, server }) {
105
+ if (file.endsWith('.ynor')) {
106
+ console.log(`🔄 Hot reload .ynor file: ${file}`);
107
+ server.ws.send({
108
+ type: 'full-reload',
109
+ path: file
110
+ });
111
+ }
112
+ }
113
+ };
114
+ }