nexa-compiler 0.7.1 → 0.7.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.
@@ -40,7 +40,13 @@ function genIf(node) {
40
40
  }
41
41
  function genFor(node) {
42
42
  const childrenCode = node.children.length === 1 ? genNode(node.children[0]) : `h("div", null, [\n ${node.children.map(genNode).join(',\n ')}\n ])`;
43
- return `${node.list}.map((${node.item}, ${node.index}) => ${childrenCode})`;
43
+ return genForIterate(node.list, node.item, node.index, childrenCode);
44
+ }
45
+ function genForIterate(list, item, index, body) {
46
+ if (/^\d+$/.test(list)) {
47
+ return `Array.from({length: ${list}}, (_, ${item}) =>\n ${body}\n )`;
48
+ }
49
+ return `(${list}).map((${item}, ${index}) =>\n ${body}\n )`;
44
50
  }
45
51
  function genElement(node) {
46
52
  const { tag, props, children } = node;
@@ -48,7 +54,7 @@ function genElement(node) {
48
54
  const elementCode = buildElementCode(tag, props, childStr, node.scopeId);
49
55
  if (node.vFor) {
50
56
  const { item, index, list } = node.vFor;
51
- return `${list}.map((${item}, ${index}) =>\n ${elementCode}\n )`;
57
+ return genForIterate(list, item, index, elementCode);
52
58
  }
53
59
  // Note: v-if is handled by transformChildren and genIf
54
60
  return elementCode;
@@ -0,0 +1,48 @@
1
+ export interface SourceLocation {
2
+ line: number;
3
+ column: number;
4
+ offset: number;
5
+ }
6
+ export interface CompilerError {
7
+ code: string;
8
+ message: string;
9
+ location: SourceLocation;
10
+ endLocation?: SourceLocation;
11
+ suggestion?: string;
12
+ helpUrl?: string;
13
+ }
14
+ declare const ERROR_CODES: {
15
+ readonly E001: "UNCLOSED_TAG";
16
+ readonly E002: "UNEXPECTED_CLOSING_TAG";
17
+ readonly E003: "INVALID_TAG_NAME";
18
+ readonly E004: "MALFORMED_ATTRIBUTE";
19
+ readonly E005: "MISSING_ATTRIBUTE_VALUE";
20
+ readonly E006: "INVALID_EXPRESSION";
21
+ readonly E007: "UNCLOSED_MUSTACHE";
22
+ readonly E008: "V_FOR_REQUIRED";
23
+ readonly E009: "V_IF_REQUIRED";
24
+ readonly E010: "SELF_CLOSING_NON_VOID";
25
+ readonly E011: "DUPLICATE_ATTRIBUTE";
26
+ readonly E012: "INVALID_DIRECTIVE";
27
+ readonly E013: "UNCLOSED_COMMENT";
28
+ readonly E014: "NESTED_SFC_TAG";
29
+ readonly E015: "MISSING_CLOSING_BRACKET";
30
+ };
31
+ type ErrorCode = keyof typeof ERROR_CODES;
32
+ export declare class NexaCompilerError extends Error {
33
+ readonly code: string;
34
+ readonly location: SourceLocation;
35
+ readonly endLocation?: SourceLocation;
36
+ readonly suggestion?: string;
37
+ readonly helpUrl?: string;
38
+ constructor(error: CompilerError);
39
+ toString(): string;
40
+ toMarkdown(): string;
41
+ }
42
+ export declare function createError(code: ErrorCode, message: string, location: SourceLocation, options?: {
43
+ endLocation?: SourceLocation;
44
+ suggestion?: string;
45
+ helpUrl?: string;
46
+ }): NexaCompilerError;
47
+ export declare function getErrorHelp(code: string): string | undefined;
48
+ export { ERROR_CODES };
@@ -0,0 +1,85 @@
1
+ const ERROR_CODES = {
2
+ E001: 'UNCLOSED_TAG',
3
+ E002: 'UNEXPECTED_CLOSING_TAG',
4
+ E003: 'INVALID_TAG_NAME',
5
+ E004: 'MALFORMED_ATTRIBUTE',
6
+ E005: 'MISSING_ATTRIBUTE_VALUE',
7
+ E006: 'INVALID_EXPRESSION',
8
+ E007: 'UNCLOSED_MUSTACHE',
9
+ E008: 'V_FOR_REQUIRED',
10
+ E009: 'V_IF_REQUIRED',
11
+ E010: 'SELF_CLOSING_NON_VOID',
12
+ E011: 'DUPLICATE_ATTRIBUTE',
13
+ E012: 'INVALID_DIRECTIVE',
14
+ E013: 'UNCLOSED_COMMENT',
15
+ E014: 'NESTED_SFC_TAG',
16
+ E015: 'MISSING_CLOSING_BRACKET',
17
+ };
18
+ export class NexaCompilerError extends Error {
19
+ code;
20
+ location;
21
+ endLocation;
22
+ suggestion;
23
+ helpUrl;
24
+ constructor(error) {
25
+ super(error.message);
26
+ this.name = 'NexaCompilerError';
27
+ this.code = error.code;
28
+ this.location = error.location;
29
+ this.endLocation = error.endLocation;
30
+ this.suggestion = error.suggestion;
31
+ this.helpUrl = error.helpUrl;
32
+ }
33
+ toString() {
34
+ const location = formatLocation(this.location);
35
+ let result = `\n${this.code}: ${this.message}\n`;
36
+ result += ` at ${location}\n`;
37
+ if (this.suggestion) {
38
+ result += `\n Suggestion: ${this.suggestion}\n`;
39
+ }
40
+ return result;
41
+ }
42
+ toMarkdown() {
43
+ const location = formatLocation(this.location);
44
+ let result = `### ${this.code}: ${this.message}\n\n`;
45
+ result += `**Location:** ${location}\n`;
46
+ if (this.suggestion) {
47
+ result += `\n**Suggestion:** ${this.suggestion}\n`;
48
+ }
49
+ if (this.helpUrl) {
50
+ result += `\n**Learn more:** ${this.helpUrl}\n`;
51
+ }
52
+ return result;
53
+ }
54
+ }
55
+ function formatLocation(loc) {
56
+ return `${loc.line}:${loc.column}`;
57
+ }
58
+ function getLineCol(text, offset) {
59
+ const lines = text.slice(0, offset).split('\n');
60
+ return {
61
+ line: lines.length,
62
+ column: (lines[lines.length - 1]?.length || 0) + 1,
63
+ offset,
64
+ };
65
+ }
66
+ export function createError(code, message, location, options) {
67
+ return new NexaCompilerError({
68
+ code: ERROR_CODES[code],
69
+ message,
70
+ location,
71
+ ...options,
72
+ });
73
+ }
74
+ export function getErrorHelp(code) {
75
+ const helpUrls = {
76
+ E001: 'https://nexajs.dev/docs/compiler/errors#e001',
77
+ E002: 'https://nexajs.dev/docs/compiler/errors#e002',
78
+ E007: 'https://nexajs.dev/docs/compiler/errors#e007',
79
+ E008: 'https://nexajs.dev/docs/compiler/errors#e008',
80
+ E009: 'https://nexajs.dev/docs/compiler/errors#e009',
81
+ E010: 'https://nexajs.dev/docs/compiler/errors#e010',
82
+ };
83
+ return helpUrls[code];
84
+ }
85
+ export { ERROR_CODES };
@@ -1,40 +1,79 @@
1
+ import { createError, getErrorHelp } from './errors.js';
1
2
  const tagRegex = /<(\/?)(\w[\w-]*)((?:\s+[^'">\s/]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s/]+))?)*)\s*(\/?)>/g;
2
- const attrRegex = /([#@:]?[\w.-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\{[^}]+\})))?/g;
3
+ const attrRegex = /([#@:]?[\w.:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\{[^}]+\})))?/g;
3
4
  const mustacheRegex = /\{\{(.*?)\}\}/g;
5
+ const voidElements = new Set([
6
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
7
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
8
+ ]);
4
9
  export function parseTemplate(template) {
5
10
  template = template.replace(/<!--[\s\S]*?-->/g, '');
6
11
  const root = { type: 'root', children: [] };
7
- const stack = [root.children];
12
+ const stack = [];
8
13
  let lastIndex = 0;
9
14
  let match;
10
15
  tagRegex.lastIndex = 0;
11
16
  while ((match = tagRegex.exec(template)) !== null) {
12
17
  const [, isClose, tag, attrsStr, selfClose] = match;
13
18
  const matchStart = match.index;
14
- // Process text between tags
15
19
  if (matchStart > lastIndex) {
16
20
  const text = template.slice(lastIndex, matchStart);
17
21
  const nodes = parseText(text);
18
22
  for (const node of nodes) {
19
- stack[stack.length - 1].push(node);
23
+ const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
24
+ if (parentStack && 'children' in parentStack.node) {
25
+ parentStack.node.children.push(node);
26
+ }
27
+ else {
28
+ root.children.push(node);
29
+ }
20
30
  }
21
31
  }
22
32
  if (isClose) {
23
- // Closing tag
24
- if (stack.length > 1) {
25
- stack.pop();
33
+ if (stack.length === 0) {
34
+ const location = getLineCol(template, matchStart);
35
+ const topTag = stack.length > 0 ? stack[stack.length - 1].tag : null;
36
+ const suggestion = topTag
37
+ ? `Did you mean to close </${topTag}> instead?`
38
+ : `This closing tag has no matching opening tag.`;
39
+ throw createError('E002', `Unexpected closing tag </${tag}>`, location, {
40
+ suggestion,
41
+ helpUrl: getErrorHelp('E002'),
42
+ });
43
+ }
44
+ const parent = stack[stack.length - 1];
45
+ if (parent.tag !== tag) {
46
+ const location = getLineCol(template, matchStart);
47
+ throw createError('E002', `Unexpected closing tag </${tag}>. Expected </${parent.tag}>`, location, {
48
+ suggestion: `Did you mean </${parent.tag}>?`,
49
+ helpUrl: getErrorHelp('E002'),
50
+ });
51
+ }
52
+ stack.pop();
53
+ lastIndex = tagRegex.lastIndex;
54
+ continue;
55
+ }
56
+ const attrs = parseAttrs(attrsStr.trim(), template, matchStart);
57
+ const isVoid = isVoidElement(tag);
58
+ const isSelfClosing = selfClose === '/' || isVoid;
59
+ if (isSelfClosing) {
60
+ const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
61
+ const node = { type: 'element', tag, attrs, children: [], isSelfClosing: true };
62
+ if (parentStack && 'children' in parentStack.node) {
63
+ parentStack.node.children.push(node);
64
+ }
65
+ else {
66
+ root.children.push(node);
26
67
  }
27
68
  lastIndex = tagRegex.lastIndex;
28
69
  continue;
29
70
  }
30
- const attrs = parseAttrs(attrsStr.trim());
31
- const isSelfClosing = selfClose === '/' || isVoidElement(tag);
32
71
  const node = {
33
72
  type: 'element',
34
73
  tag,
35
74
  attrs,
36
75
  children: [],
37
- isSelfClosing: isSelfClosing,
76
+ isSelfClosing: false,
38
77
  };
39
78
  const isSlot = tag === 'slot';
40
79
  if (isSlot) {
@@ -42,41 +81,66 @@ export function parseTemplate(template) {
42
81
  type: 'slot',
43
82
  name: attrs.find(a => a.name === 'name')?.value || 'default',
44
83
  attrs,
45
- children: []
84
+ children: [],
46
85
  };
47
- stack[stack.length - 1].push(slotNode);
48
- if (!isSelfClosing) {
49
- stack.push(slotNode.children);
86
+ const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
87
+ if (parentStack && 'children' in parentStack.node) {
88
+ parentStack.node.children.push(slotNode);
89
+ }
90
+ else {
91
+ root.children.push(slotNode);
50
92
  }
93
+ stack.push({ tag, node: slotNode, start: matchStart });
51
94
  }
52
95
  else {
53
- stack[stack.length - 1].push(node);
54
- if (!isSelfClosing) {
55
- stack.push(node.children);
96
+ const parentStack = stack.length > 0 ? stack[stack.length - 1] : null;
97
+ if (parentStack && 'children' in parentStack.node) {
98
+ parentStack.node.children.push(node);
56
99
  }
100
+ else {
101
+ root.children.push(node);
102
+ }
103
+ stack.push({ tag, node, start: matchStart });
57
104
  }
58
105
  lastIndex = tagRegex.lastIndex;
59
106
  }
60
- // Process remaining text
107
+ if (stack.length > 0) {
108
+ const unclosed = stack[stack.length - 1];
109
+ const location = getLineCol(template, unclosed.start);
110
+ throw createError('E001', `Unclosed tag <${unclosed.tag}>`, location, {
111
+ suggestion: `Add closing </${unclosed.tag}> tag after the content.`,
112
+ helpUrl: getErrorHelp('E001'),
113
+ });
114
+ }
61
115
  if (lastIndex < template.length) {
62
116
  const text = template.slice(lastIndex);
63
117
  const nodes = parseText(text);
64
118
  for (const node of nodes) {
65
- stack[stack.length - 1].push(node);
119
+ root.children.push(node);
66
120
  }
67
121
  }
68
122
  return root;
69
123
  }
70
- function parseAttrs(attrsStr) {
124
+ function parseAttrs(attrsStr, template, tagStart) {
71
125
  if (!attrsStr)
72
126
  return [];
73
127
  const attrs = [];
74
128
  attrRegex.lastIndex = 0;
129
+ const seenAttrs = new Map();
75
130
  let match;
76
131
  while ((match = attrRegex.exec(attrsStr)) !== null) {
77
132
  let rawName = match[1];
78
133
  let quotedValue = match[2] ?? match[3] ?? match[4];
79
134
  const isDynamic = rawName.startsWith(':') || rawName.startsWith('@');
135
+ if (seenAttrs.has(rawName)) {
136
+ const attrIndex = tagStart + 4 + attrsStr.indexOf(rawName);
137
+ const location = getLineCol(template, attrIndex);
138
+ throw createError('E011', `Duplicate attribute "${rawName}"`, location, {
139
+ suggestion: `Remove one of the duplicate attributes.`,
140
+ helpUrl: getErrorHelp('E011'),
141
+ });
142
+ }
143
+ seenAttrs.set(rawName, match.index);
80
144
  let name;
81
145
  if (rawName.startsWith(':')) {
82
146
  name = rawName.slice(1);
@@ -102,6 +166,17 @@ function parseAttrs(attrsStr) {
102
166
  }
103
167
  return attrs;
104
168
  }
169
+ function isVoidElement(tag) {
170
+ return voidElements.has(tag);
171
+ }
172
+ function getLineCol(text, offset) {
173
+ const lines = text.slice(0, offset).split('\n');
174
+ return {
175
+ line: lines.length,
176
+ column: (lines[lines.length - 1]?.length || 0) + 1,
177
+ offset,
178
+ };
179
+ }
105
180
  function decodeEntities(text) {
106
181
  return text.replace(/&[a-zA-Z#]+;/g, match => {
107
182
  const entities = {
@@ -120,6 +195,10 @@ function parseText(text) {
120
195
  if (match.index > lastIdx) {
121
196
  nodes.push({ type: 'text', content: decodeEntities(text.slice(lastIdx, match.index)) });
122
197
  }
198
+ if (!match[1].trim()) {
199
+ lastIdx = mustacheRegex.lastIndex;
200
+ continue;
201
+ }
123
202
  nodes.push({ type: 'expression', content: match[1].trim() });
124
203
  lastIdx = mustacheRegex.lastIndex;
125
204
  }
@@ -128,10 +207,3 @@ function parseText(text) {
128
207
  }
129
208
  return nodes;
130
209
  }
131
- const voidElements = new Set([
132
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
133
- 'link', 'meta', 'param', 'source', 'track', 'wbr',
134
- ]);
135
- function isVoidElement(tag) {
136
- return voidElements.has(tag);
137
- }
@@ -7,6 +7,13 @@ function transformNode(node, scopeId) {
7
7
  case 'comment':
8
8
  return { type: 'comment' };
9
9
  case 'element': {
10
+ if (node.tag === 'slot') {
11
+ const dynamicAttrs = node.attrs.filter(a => a.dynamic && a.name !== 'name');
12
+ const slotProps = dynamicAttrs.length > 0
13
+ ? dynamicAttrs.map(a => `${a.name}: ${a.value}`).join(', ')
14
+ : undefined;
15
+ return { type: 'slot', name: node.attrs.find(a => a.name === 'name')?.value || 'default', slotProps, children: node.children?.map(c => transformNode(c)) };
16
+ }
10
17
  const vForAttr = node.attrs.find(a => a.name === 'v-for');
11
18
  const vIfAttr = node.attrs.find(a => a.name === 'v-if');
12
19
  const filteredAttrs = node.attrs.filter(a => a.name !== 'v-if' && a.name !== 'v-for');
@@ -118,10 +125,16 @@ function genProps(attrs, tag) {
118
125
  continue;
119
126
  if (attr.name === 'class')
120
127
  continue;
128
+ if (tag === 'slot' && attr.name === 'name')
129
+ continue;
121
130
  let propName = attr.name;
122
131
  if (propName.startsWith('v-')) {
123
132
  propName = propName.split('-').map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join('');
124
133
  }
134
+ const isComponentTag = /^[A-Z]/.test(tag) || tag.includes('-');
135
+ if (isComponentTag && propName.includes('-')) {
136
+ propName = propName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
137
+ }
125
138
  if (propName === 'vShow' && typeof attr.value === 'string') {
126
139
  parts.push(`${propName}: ${attr.value}`);
127
140
  }
@@ -164,7 +177,8 @@ function genProps(attrs, tag) {
164
177
  handler = `($event) => { $event.stopPropagation(); (${handler})($event) }`;
165
178
  }
166
179
  }
167
- parts.push(`${realName}: ${handler}`);
180
+ const safeKey = /[^a-zA-Z0-9_$]/.test(realName) ? JSON.stringify(realName) : realName;
181
+ parts.push(`${safeKey}: ${handler}`);
168
182
  }
169
183
  else if (attr.dynamic) {
170
184
  const key = /[^a-zA-Z0-9_$]/.test(propName) ? JSON.stringify(propName) : propName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexa-compiler",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",