pulse-js-framework 1.4.10 → 1.5.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.
- package/cli/analyze.js +8 -7
- package/cli/build.js +14 -13
- package/cli/dev.js +28 -7
- package/cli/format.js +13 -12
- package/cli/index.js +20 -1
- package/cli/lint.js +8 -7
- package/cli/release.js +493 -0
- package/compiler/parser.js +41 -20
- package/compiler/transformer/constants.js +54 -0
- package/compiler/transformer/export.js +33 -0
- package/compiler/transformer/expressions.js +273 -0
- package/compiler/transformer/imports.js +101 -0
- package/compiler/transformer/index.js +319 -0
- package/compiler/transformer/router.js +95 -0
- package/compiler/transformer/state.js +118 -0
- package/compiler/transformer/store.js +97 -0
- package/compiler/transformer/style.js +130 -0
- package/compiler/transformer/view.js +428 -0
- package/compiler/transformer.js +17 -1310
- package/core/errors.js +300 -0
- package/package.json +11 -3
- package/runtime/dom.js +61 -10
- package/runtime/lru-cache.js +141 -0
- package/runtime/native.js +6 -1
- package/runtime/pulse.js +46 -2
- package/runtime/router.js +4 -1
- package/runtime/store.js +35 -1
- package/runtime/utils.js +348 -0
- package/types/index.d.ts +19 -0
- package/types/lru-cache.d.ts +118 -0
- package/types/utils.d.ts +255 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transformer Export Module
|
|
3
|
+
* Handles component export generation
|
|
4
|
+
* @module pulse-js-framework/compiler/transformer/export
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate component export
|
|
9
|
+
* @param {Object} transformer - Transformer instance
|
|
10
|
+
* @returns {string} JavaScript code
|
|
11
|
+
*/
|
|
12
|
+
export function generateExport(transformer) {
|
|
13
|
+
const pageName = transformer.ast.page?.name || 'Component';
|
|
14
|
+
const routePath = transformer.ast.route?.path || null;
|
|
15
|
+
|
|
16
|
+
const lines = ['// Export'];
|
|
17
|
+
lines.push(`export const ${pageName} = {`);
|
|
18
|
+
lines.push(' render,');
|
|
19
|
+
|
|
20
|
+
if (routePath) {
|
|
21
|
+
lines.push(` route: ${JSON.stringify(routePath)},`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
lines.push(' mount: (target) => {');
|
|
25
|
+
lines.push(' const el = render();');
|
|
26
|
+
lines.push(' return mount(target, el);');
|
|
27
|
+
lines.push(' }');
|
|
28
|
+
lines.push('};');
|
|
29
|
+
lines.push('');
|
|
30
|
+
lines.push(`export default ${pageName};`);
|
|
31
|
+
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transformer Expressions Module
|
|
3
|
+
* Handles AST expression transformation to JavaScript code
|
|
4
|
+
* @module pulse-js-framework/compiler/transformer/expressions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NodeType } from '../parser.js';
|
|
8
|
+
import {
|
|
9
|
+
NO_SPACE_AFTER,
|
|
10
|
+
NO_SPACE_BEFORE,
|
|
11
|
+
STATEMENT_KEYWORDS,
|
|
12
|
+
BUILTIN_FUNCTIONS,
|
|
13
|
+
STATEMENT_TOKEN_TYPES,
|
|
14
|
+
STATEMENT_END_TYPES
|
|
15
|
+
} from './constants.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Transform AST expression to JS code
|
|
19
|
+
* @param {Object} transformer - Transformer instance
|
|
20
|
+
* @param {Object} node - AST node to transform
|
|
21
|
+
* @returns {string} JavaScript code
|
|
22
|
+
*/
|
|
23
|
+
export function transformExpression(transformer, node) {
|
|
24
|
+
if (!node) return '';
|
|
25
|
+
|
|
26
|
+
switch (node.type) {
|
|
27
|
+
case NodeType.Identifier:
|
|
28
|
+
if (transformer.stateVars.has(node.name)) {
|
|
29
|
+
return `${node.name}.get()`;
|
|
30
|
+
}
|
|
31
|
+
// Props are accessed directly (already destructured)
|
|
32
|
+
return node.name;
|
|
33
|
+
|
|
34
|
+
case NodeType.Literal:
|
|
35
|
+
if (typeof node.value === 'string') {
|
|
36
|
+
return JSON.stringify(node.value);
|
|
37
|
+
}
|
|
38
|
+
return String(node.value);
|
|
39
|
+
|
|
40
|
+
case NodeType.TemplateLiteral:
|
|
41
|
+
// Transform state vars in template literal
|
|
42
|
+
return '`' + transformExpressionString(transformer, node.value) + '`';
|
|
43
|
+
|
|
44
|
+
case NodeType.MemberExpression: {
|
|
45
|
+
const obj = transformExpression(transformer, node.object);
|
|
46
|
+
// Use optional chaining when accessing properties on function call results
|
|
47
|
+
const isCallResult = node.object.type === NodeType.CallExpression;
|
|
48
|
+
const accessor = isCallResult ? '?.' : '.';
|
|
49
|
+
if (node.computed) {
|
|
50
|
+
const prop = transformExpression(transformer, node.property);
|
|
51
|
+
return isCallResult ? `${obj}?.[${prop}]` : `${obj}[${prop}]`;
|
|
52
|
+
}
|
|
53
|
+
return `${obj}${accessor}${node.property}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case NodeType.CallExpression: {
|
|
57
|
+
const callee = transformExpression(transformer, node.callee);
|
|
58
|
+
const args = node.arguments.map(a => transformExpression(transformer, a)).join(', ');
|
|
59
|
+
return `${callee}(${args})`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case NodeType.BinaryExpression: {
|
|
63
|
+
const left = transformExpression(transformer, node.left);
|
|
64
|
+
const right = transformExpression(transformer, node.right);
|
|
65
|
+
return `(${left} ${node.operator} ${right})`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case NodeType.UnaryExpression: {
|
|
69
|
+
const argument = transformExpression(transformer, node.argument);
|
|
70
|
+
return `${node.operator}${argument}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case NodeType.UpdateExpression: {
|
|
74
|
+
const argument = transformExpression(transformer, node.argument);
|
|
75
|
+
// For state variables, convert x++ to x.set(x.get() + 1)
|
|
76
|
+
if (node.argument.type === NodeType.Identifier &&
|
|
77
|
+
transformer.stateVars.has(node.argument.name)) {
|
|
78
|
+
const name = node.argument.name;
|
|
79
|
+
const delta = node.operator === '++' ? 1 : -1;
|
|
80
|
+
return `${name}.set(${name}.get() + ${delta})`;
|
|
81
|
+
}
|
|
82
|
+
return node.prefix
|
|
83
|
+
? `${node.operator}${argument}`
|
|
84
|
+
: `${argument}${node.operator}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case NodeType.ConditionalExpression: {
|
|
88
|
+
const test = transformExpression(transformer, node.test);
|
|
89
|
+
const consequent = transformExpression(transformer, node.consequent);
|
|
90
|
+
const alternate = transformExpression(transformer, node.alternate);
|
|
91
|
+
return `(${test} ? ${consequent} : ${alternate})`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case NodeType.ArrowFunction: {
|
|
95
|
+
const params = node.params.join(', ');
|
|
96
|
+
if (node.block) {
|
|
97
|
+
// Block body - transform tokens
|
|
98
|
+
const body = transformFunctionBody(transformer, node.body);
|
|
99
|
+
return `(${params}) => { ${body} }`;
|
|
100
|
+
} else {
|
|
101
|
+
// Expression body
|
|
102
|
+
const body = transformExpression(transformer, node.body);
|
|
103
|
+
return `(${params}) => ${body}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case NodeType.AssignmentExpression: {
|
|
108
|
+
const left = transformExpression(transformer, node.left);
|
|
109
|
+
const right = transformExpression(transformer, node.right);
|
|
110
|
+
// For state variables, convert to .set()
|
|
111
|
+
if (node.left.type === NodeType.Identifier &&
|
|
112
|
+
transformer.stateVars.has(node.left.name)) {
|
|
113
|
+
return `${node.left.name}.set(${right})`;
|
|
114
|
+
}
|
|
115
|
+
return `(${left} = ${right})`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case NodeType.ArrayLiteral: {
|
|
119
|
+
const elements = node.elements.map(e => transformExpression(transformer, e)).join(', ');
|
|
120
|
+
return `[${elements}]`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case NodeType.ObjectLiteral: {
|
|
124
|
+
const props = node.properties.map(p => {
|
|
125
|
+
if (p.type === NodeType.SpreadElement) {
|
|
126
|
+
return `...${transformExpression(transformer, p.argument)}`;
|
|
127
|
+
}
|
|
128
|
+
if (p.shorthand) {
|
|
129
|
+
// Check if it's a state var
|
|
130
|
+
if (transformer.stateVars.has(p.name)) {
|
|
131
|
+
return `${p.name}: ${p.name}.get()`;
|
|
132
|
+
}
|
|
133
|
+
return p.name;
|
|
134
|
+
}
|
|
135
|
+
return `${p.name}: ${transformExpression(transformer, p.value)}`;
|
|
136
|
+
}).join(', ');
|
|
137
|
+
return `{ ${props} }`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case NodeType.SpreadElement:
|
|
141
|
+
return `...${transformExpression(transformer, node.argument)}`;
|
|
142
|
+
|
|
143
|
+
default:
|
|
144
|
+
return '/* unknown expression */';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Transform expression string (from interpolation)
|
|
150
|
+
* @param {Object} transformer - Transformer instance
|
|
151
|
+
* @param {string} exprStr - Expression string
|
|
152
|
+
* @returns {string} Transformed expression string
|
|
153
|
+
*/
|
|
154
|
+
export function transformExpressionString(transformer, exprStr) {
|
|
155
|
+
// Simple transformation: wrap state vars with .get()
|
|
156
|
+
let result = exprStr;
|
|
157
|
+
for (const stateVar of transformer.stateVars) {
|
|
158
|
+
result = result.replace(
|
|
159
|
+
new RegExp(`\\b${stateVar}\\b`, 'g'),
|
|
160
|
+
`${stateVar}.get()`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
// Add optional chaining after function calls followed by property access
|
|
164
|
+
result = result.replace(/(\w+\([^)]*\))\.(\w)/g, '$1?.$2');
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Transform function body tokens back to code
|
|
170
|
+
* @param {Object} transformer - Transformer instance
|
|
171
|
+
* @param {Array} tokens - Body tokens
|
|
172
|
+
* @returns {string} JavaScript code
|
|
173
|
+
*/
|
|
174
|
+
export function transformFunctionBody(transformer, tokens) {
|
|
175
|
+
const { stateVars, actionNames } = transformer;
|
|
176
|
+
let code = '';
|
|
177
|
+
let lastToken = null;
|
|
178
|
+
let lastNonSpaceToken = null;
|
|
179
|
+
|
|
180
|
+
const needsManualSemicolon = (token, nextToken, lastNonSpace) => {
|
|
181
|
+
if (!token || lastNonSpace?.value === 'new') return false;
|
|
182
|
+
if (STATEMENT_TOKEN_TYPES.has(token.type)) return true;
|
|
183
|
+
if (token.type !== 'IDENT') return false;
|
|
184
|
+
if (STATEMENT_KEYWORDS.has(token.value)) return true;
|
|
185
|
+
if (stateVars.has(token.value) && nextToken?.type === 'EQ') return true;
|
|
186
|
+
if (nextToken?.type === 'LPAREN' &&
|
|
187
|
+
(BUILTIN_FUNCTIONS.has(token.value) || actionNames.has(token.value))) return true;
|
|
188
|
+
if (nextToken?.type === 'DOT' && BUILTIN_FUNCTIONS.has(token.value)) return true;
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const afterStatementEnd = (t) => t && STATEMENT_END_TYPES.has(t.type);
|
|
193
|
+
|
|
194
|
+
let afterIfCondition = false;
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
197
|
+
const token = tokens[i];
|
|
198
|
+
const nextToken = tokens[i + 1];
|
|
199
|
+
|
|
200
|
+
// Detect if we just closed an if/for/while condition
|
|
201
|
+
if (token.type === 'RPAREN') {
|
|
202
|
+
let parenDepth = 1;
|
|
203
|
+
for (let j = i - 1; j >= 0 && parenDepth > 0; j--) {
|
|
204
|
+
if (tokens[j].type === 'RPAREN') parenDepth++;
|
|
205
|
+
else if (tokens[j].type === 'LPAREN') parenDepth--;
|
|
206
|
+
if (parenDepth === 0) {
|
|
207
|
+
if (j > 0 && (tokens[j - 1].type === 'IF' || tokens[j - 1].type === 'FOR' ||
|
|
208
|
+
tokens[j - 1].type === 'EACH' || tokens[j - 1].value === 'while')) {
|
|
209
|
+
afterIfCondition = true;
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Add semicolon before statement starters
|
|
217
|
+
if (needsManualSemicolon(token, nextToken, lastNonSpaceToken) &&
|
|
218
|
+
afterStatementEnd(lastNonSpaceToken)) {
|
|
219
|
+
if (!afterIfCondition && lastToken && lastToken.value !== ';' && lastToken.value !== '{') {
|
|
220
|
+
code += '; ';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Reset afterIfCondition after processing the token following the condition
|
|
225
|
+
if (afterIfCondition && token.type !== 'RPAREN') {
|
|
226
|
+
afterIfCondition = false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Emit the token value
|
|
230
|
+
if (token.type === 'STRING') {
|
|
231
|
+
code += token.raw || JSON.stringify(token.value);
|
|
232
|
+
} else if (token.type === 'TEMPLATE') {
|
|
233
|
+
code += token.raw || ('`' + token.value + '`');
|
|
234
|
+
} else {
|
|
235
|
+
code += token.value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Decide whether to add space after this token
|
|
239
|
+
const noSpaceAfter = NO_SPACE_AFTER.has(token.type) || NO_SPACE_AFTER.has(token.value);
|
|
240
|
+
const noSpaceBefore = nextToken &&
|
|
241
|
+
(NO_SPACE_BEFORE.has(nextToken.type) || NO_SPACE_BEFORE.has(nextToken.value));
|
|
242
|
+
if (!noSpaceAfter && !noSpaceBefore && nextToken) code += ' ';
|
|
243
|
+
|
|
244
|
+
lastToken = token;
|
|
245
|
+
lastNonSpaceToken = token;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Build patterns for state variable transformation
|
|
249
|
+
const stateVarPattern = [...stateVars].join('|');
|
|
250
|
+
const funcPattern = [...actionNames, ...BUILTIN_FUNCTIONS].join('|');
|
|
251
|
+
const keywordsPattern = [...STATEMENT_KEYWORDS].join('|');
|
|
252
|
+
|
|
253
|
+
// Transform state var assignments: stateVar = value -> stateVar.set(value)
|
|
254
|
+
for (const stateVar of stateVars) {
|
|
255
|
+
const boundaryPattern = `\\s+(?:${stateVarPattern})(?:\\s*=(?!=)|\\s*\\.set\\()|\\s+(?:${funcPattern})\\s*\\(|\\s+(?:${keywordsPattern})\\b|;|$`;
|
|
256
|
+
const assignPattern = new RegExp(`\\b${stateVar}\\s*=(?!=)\\s*(.+?)(?=${boundaryPattern})`, 'g');
|
|
257
|
+
code = code.replace(assignPattern, (_, value) => `${stateVar}.set(${value.trim()});`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Clean up any double semicolons
|
|
261
|
+
code = code.replace(/;+/g, ';');
|
|
262
|
+
code = code.replace(/; ;/g, ';');
|
|
263
|
+
|
|
264
|
+
// Replace state var reads
|
|
265
|
+
for (const stateVar of stateVars) {
|
|
266
|
+
code = code.replace(
|
|
267
|
+
new RegExp(`(?<!\\.\\s*)\\b${stateVar}\\b(?!\\s*=(?!=)|\\s*\\(|\\s*\\.(?:get|set))`, 'g'),
|
|
268
|
+
`${stateVar}.get()`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return code.trim();
|
|
273
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transformer Imports Module
|
|
3
|
+
* Handles import generation for compiled Pulse components
|
|
4
|
+
* @module pulse-js-framework/compiler/transformer/imports
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract imported component names from AST imports
|
|
9
|
+
* @param {Object} transformer - Transformer instance
|
|
10
|
+
* @param {Array} imports - Array of import statements from AST
|
|
11
|
+
*/
|
|
12
|
+
export function extractImportedComponents(transformer, imports) {
|
|
13
|
+
for (const imp of imports) {
|
|
14
|
+
for (const spec of imp.specifiers) {
|
|
15
|
+
transformer.importedComponents.set(spec.local, {
|
|
16
|
+
source: imp.source,
|
|
17
|
+
type: spec.type,
|
|
18
|
+
imported: spec.imported
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate import statements (runtime + user imports)
|
|
26
|
+
* @param {Object} transformer - Transformer instance
|
|
27
|
+
* @returns {string} Generated import statements
|
|
28
|
+
*/
|
|
29
|
+
export function generateImports(transformer) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
const { ast, options } = transformer;
|
|
32
|
+
|
|
33
|
+
// Runtime imports
|
|
34
|
+
const runtimeImports = [
|
|
35
|
+
'pulse',
|
|
36
|
+
'computed',
|
|
37
|
+
'effect',
|
|
38
|
+
'batch',
|
|
39
|
+
'el',
|
|
40
|
+
'text',
|
|
41
|
+
'on',
|
|
42
|
+
'list',
|
|
43
|
+
'when',
|
|
44
|
+
'mount',
|
|
45
|
+
'model'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
lines.push(`import { ${runtimeImports.join(', ')} } from '${options.runtime}';`);
|
|
49
|
+
|
|
50
|
+
// Router imports (if router block exists)
|
|
51
|
+
if (ast.router) {
|
|
52
|
+
lines.push(`import { createRouter } from '${options.runtime}/router';`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Store imports (if store block exists)
|
|
56
|
+
if (ast.store) {
|
|
57
|
+
const storeImports = ['createStore', 'createActions', 'createGetters'];
|
|
58
|
+
lines.push(`import { ${storeImports.join(', ')} } from '${options.runtime}/store';`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// User imports from .pulse files
|
|
62
|
+
if (ast.imports && ast.imports.length > 0) {
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push('// Component imports');
|
|
65
|
+
|
|
66
|
+
for (const imp of ast.imports) {
|
|
67
|
+
// Handle default + named imports
|
|
68
|
+
const defaultSpec = imp.specifiers.find(s => s.type === 'default');
|
|
69
|
+
const namedSpecs = imp.specifiers.filter(s => s.type === 'named');
|
|
70
|
+
const namespaceSpec = imp.specifiers.find(s => s.type === 'namespace');
|
|
71
|
+
|
|
72
|
+
let importStr = 'import ';
|
|
73
|
+
if (defaultSpec) {
|
|
74
|
+
importStr += defaultSpec.local;
|
|
75
|
+
if (namedSpecs.length > 0) {
|
|
76
|
+
importStr += ', ';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (namespaceSpec) {
|
|
80
|
+
importStr += `* as ${namespaceSpec.local}`;
|
|
81
|
+
}
|
|
82
|
+
if (namedSpecs.length > 0) {
|
|
83
|
+
const named = namedSpecs.map(s =>
|
|
84
|
+
s.local !== s.imported ? `${s.imported} as ${s.local}` : s.local
|
|
85
|
+
);
|
|
86
|
+
importStr += `{ ${named.join(', ')} }`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Convert .pulse extension to .js
|
|
90
|
+
let source = imp.source;
|
|
91
|
+
if (source.endsWith('.pulse')) {
|
|
92
|
+
source = source.replace('.pulse', '.js');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
importStr += ` from '${source}';`;
|
|
96
|
+
lines.push(importStr);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|