@rettangoli/fe 0.0.7-rc9 → 0.0.9
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/package.json +2 -2
- package/src/cli/blank/blank.handlers.js +2 -2
- package/src/cli/blank/blank.store.js +3 -3
- package/src/cli/build.js +12 -7
- package/src/cli/watch.js +23 -3
- package/src/createComponent.js +32 -11
- package/src/parser.js +24 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/fe",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Frontend framework for building reactive web components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"esbuild": "^0.25.5",
|
|
31
31
|
"immer": "^10.1.1",
|
|
32
|
-
"jempl": "0.
|
|
32
|
+
"jempl": "0.3.2-rc2",
|
|
33
33
|
"js-yaml": "^4.1.0",
|
|
34
34
|
"rxjs": "^7.8.2",
|
|
35
35
|
"snabbdom": "^3.6.2",
|
package/src/cli/build.js
CHANGED
|
@@ -18,7 +18,7 @@ function capitalize(word) {
|
|
|
18
18
|
// Function to process view files - loads YAML and creates temporary JS file
|
|
19
19
|
export const writeViewFile = (view, category, component) => {
|
|
20
20
|
// const { category, component } = extractCategoryAndComponent(filePath);
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
const dir = `./.temp/${category}`;
|
|
23
23
|
if (!existsSync(dir)) {
|
|
24
24
|
mkdirSync(dir, { recursive: true });
|
|
@@ -30,12 +30,12 @@ export const writeViewFile = (view, category, component) => {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
export const bundleFile = async (options) => {
|
|
33
|
-
const { outfile = "./vt/static/main.js" } = options;
|
|
33
|
+
const { outfile = "./vt/static/main.js", development = false } = options;
|
|
34
34
|
await esbuild.build({
|
|
35
35
|
entryPoints: ["./.temp/dynamicImport.js"],
|
|
36
36
|
bundle: true,
|
|
37
|
-
minify:
|
|
38
|
-
sourcemap:
|
|
37
|
+
minify: !development,
|
|
38
|
+
sourcemap: !!development,
|
|
39
39
|
outfile: outfile,
|
|
40
40
|
format: "esm",
|
|
41
41
|
loader: {
|
|
@@ -47,7 +47,7 @@ export const bundleFile = async (options) => {
|
|
|
47
47
|
const buildRettangoliFrontend = async (options) => {
|
|
48
48
|
console.log("running build with options", options);
|
|
49
49
|
|
|
50
|
-
const { dirs = ["./example"], outfile = "./vt/static/main.js", setup = "setup.js" } = options;
|
|
50
|
+
const { dirs = ["./example"], outfile = "./vt/static/main.js", setup = "setup.js", development = false } = options;
|
|
51
51
|
|
|
52
52
|
const allFiles = getAllFiles(dirs).filter((filePath) => {
|
|
53
53
|
return (
|
|
@@ -93,7 +93,12 @@ const buildRettangoliFrontend = async (options) => {
|
|
|
93
93
|
count++;
|
|
94
94
|
} else if (["view"].includes(fileType)) {
|
|
95
95
|
const view = loadYaml(readFileSync(filePath, "utf8"));
|
|
96
|
-
|
|
96
|
+
try {
|
|
97
|
+
view.template = parse(view.template);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`Error parsing template in file: ${filePath}`);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
97
102
|
writeViewFile(view, category, component);
|
|
98
103
|
output += `import ${component}${capitalize(
|
|
99
104
|
fileType,
|
|
@@ -125,7 +130,7 @@ Object.keys(imports).forEach(category => {
|
|
|
125
130
|
|
|
126
131
|
writeFileSync("./.temp/dynamicImport.js", output);
|
|
127
132
|
|
|
128
|
-
await bundleFile({ outfile });
|
|
133
|
+
await bundleFile({ outfile, development });
|
|
129
134
|
|
|
130
135
|
console.log(`Build complete. Output file: ${outfile}`);
|
|
131
136
|
};
|
package/src/cli/watch.js
CHANGED
|
@@ -7,6 +7,10 @@ import { writeViewFile } from './build.js';
|
|
|
7
7
|
import buildRettangoliFrontend from './build.js';
|
|
8
8
|
import { extractCategoryAndComponent } from '../common.js';
|
|
9
9
|
|
|
10
|
+
// Debounce mechanism to prevent excessive rebuilds
|
|
11
|
+
let rebuildTimeout = null;
|
|
12
|
+
const DEBOUNCE_DELAY = 200; // 200ms delay
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
const setupWatcher = (directory, options) => {
|
|
12
16
|
watch(
|
|
@@ -21,7 +25,17 @@ const setupWatcher = (directory, options) => {
|
|
|
21
25
|
const { category, component } = extractCategoryAndComponent(filename);
|
|
22
26
|
await writeViewFile(view, category, component);
|
|
23
27
|
}
|
|
24
|
-
|
|
28
|
+
|
|
29
|
+
// Debounce the rebuild
|
|
30
|
+
if (rebuildTimeout) {
|
|
31
|
+
clearTimeout(rebuildTimeout);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
rebuildTimeout = setTimeout(async () => {
|
|
35
|
+
console.log('Triggering rebuild...');
|
|
36
|
+
await buildRettangoliFrontend(options);
|
|
37
|
+
}, DEBOUNCE_DELAY);
|
|
38
|
+
|
|
25
39
|
} catch (error) {
|
|
26
40
|
console.error(`Error processing ${filename}:`, error);
|
|
27
41
|
// Keep the watcher running
|
|
@@ -64,13 +78,19 @@ async function startViteServer(options) {
|
|
|
64
78
|
const startWatching = async (options) => {
|
|
65
79
|
const { dirs = ['src'], port = 3001 } = options;
|
|
66
80
|
|
|
81
|
+
// Set development mode for all builds in watch mode
|
|
82
|
+
const watchOptions = {
|
|
83
|
+
development: true,
|
|
84
|
+
...options
|
|
85
|
+
};
|
|
86
|
+
|
|
67
87
|
// Do initial build with all directories
|
|
68
88
|
console.log('Starting initial build...');
|
|
69
|
-
await buildRettangoliFrontend(
|
|
89
|
+
await buildRettangoliFrontend(watchOptions);
|
|
70
90
|
console.log('Initial build complete');
|
|
71
91
|
|
|
72
92
|
dirs.forEach(dir => {
|
|
73
|
-
setupWatcher(dir,
|
|
93
|
+
setupWatcher(dir, watchOptions);
|
|
74
94
|
});
|
|
75
95
|
|
|
76
96
|
startViteServer({ port, outfile: options.outfile });
|
package/src/createComponent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { produce } from "immer";
|
|
2
2
|
import { parseView } from "./parser.js";
|
|
3
|
+
import { parseAndRender } from "jempl";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* covert this format of json into raw css strings
|
|
@@ -261,8 +262,10 @@ class BaseComponent extends HTMLElement {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
get viewData() {
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
let data = {};
|
|
266
|
+
if (this.store.selectViewData) {
|
|
267
|
+
data = this.store.selectViewData();
|
|
268
|
+
}
|
|
266
269
|
return data;
|
|
267
270
|
}
|
|
268
271
|
|
|
@@ -295,7 +298,6 @@ class BaseComponent extends HTMLElement {
|
|
|
295
298
|
this.renderTarget = document.createElement("div");
|
|
296
299
|
this.renderTarget.style.cssText = "display: contents;";
|
|
297
300
|
this.shadow.appendChild(this.renderTarget);
|
|
298
|
-
this.transformedHandlers = {};
|
|
299
301
|
if (!this.renderTarget.parentNode) {
|
|
300
302
|
this.appendChild(this.renderTarget);
|
|
301
303
|
}
|
|
@@ -307,10 +309,25 @@ class BaseComponent extends HTMLElement {
|
|
|
307
309
|
dispatchEvent: this.dispatchEvent.bind(this),
|
|
308
310
|
};
|
|
309
311
|
|
|
312
|
+
this.transformedHandlers = {
|
|
313
|
+
handleCallStoreAction: (payload) => {
|
|
314
|
+
const { render, store } = deps;
|
|
315
|
+
const { _event, _action } = payload;
|
|
316
|
+
const context = parseAndRender(payload, {
|
|
317
|
+
event: _event
|
|
318
|
+
})
|
|
319
|
+
console.log('context', context)
|
|
320
|
+
if (!store[_action]) {
|
|
321
|
+
throw new Error(`store action store.${store._action} is not defined`)
|
|
322
|
+
}
|
|
323
|
+
store[_action](context);
|
|
324
|
+
render();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
310
327
|
// TODO don't include onmount, subscriptions, etc in transformedHandlers
|
|
311
328
|
Object.keys(this.handlers || {}).forEach((key) => {
|
|
312
|
-
this.transformedHandlers[key] = (payload) => {
|
|
313
|
-
const result = this.handlers[key](
|
|
329
|
+
this.transformedHandlers[key] = (event, payload) => {
|
|
330
|
+
const result = this.handlers[key](deps, event, payload);
|
|
314
331
|
return result;
|
|
315
332
|
};
|
|
316
333
|
});
|
|
@@ -348,7 +365,10 @@ class BaseComponent extends HTMLElement {
|
|
|
348
365
|
if (oldValue !== newValue && this.render) {
|
|
349
366
|
// Call handleOnUpdate if it exists
|
|
350
367
|
if (this.handlers?.handleOnUpdate) {
|
|
351
|
-
const changes = {
|
|
368
|
+
const changes = {
|
|
369
|
+
oldAttrs: { [name]: oldValue },
|
|
370
|
+
newAttrs: { [name]: newValue }
|
|
371
|
+
};
|
|
352
372
|
const deps = {
|
|
353
373
|
...this.deps,
|
|
354
374
|
refIds: this.refIds,
|
|
@@ -421,17 +441,19 @@ class BaseComponent extends HTMLElement {
|
|
|
421
441
|
/**
|
|
422
442
|
* Binds store functions with actual framework data flow
|
|
423
443
|
* Makes state changes immutable with immer
|
|
424
|
-
* Passes props to selectors
|
|
444
|
+
* Passes props to selectors
|
|
425
445
|
* @param {*} store
|
|
426
446
|
* @param {*} props
|
|
427
447
|
* @returns
|
|
428
448
|
*/
|
|
429
449
|
const bindStore = (store, props, attrs) => {
|
|
430
|
-
const {
|
|
450
|
+
const { createInitialState, ...selectorsAndActions } = store;
|
|
431
451
|
const selectors = {};
|
|
432
452
|
const actions = {};
|
|
433
|
-
let currentState =
|
|
434
|
-
|
|
453
|
+
let currentState = {};
|
|
454
|
+
if (createInitialState) {
|
|
455
|
+
currentState = createInitialState();
|
|
456
|
+
}
|
|
435
457
|
Object.entries(selectorsAndActions).forEach(([key, fn]) => {
|
|
436
458
|
if (key.startsWith("select")) {
|
|
437
459
|
selectors[key] = (...args) => {
|
|
@@ -448,7 +470,6 @@ const bindStore = (store, props, attrs) => {
|
|
|
448
470
|
});
|
|
449
471
|
|
|
450
472
|
return {
|
|
451
|
-
toViewData: () => toViewData({ state: currentState, props, attrs }),
|
|
452
473
|
getState: () => currentState,
|
|
453
474
|
...actions,
|
|
454
475
|
...selectors,
|
package/src/parser.js
CHANGED
|
@@ -164,7 +164,7 @@ export const createVirtualDom = ({
|
|
|
164
164
|
const attrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]+))/g;
|
|
165
165
|
let match;
|
|
166
166
|
const processedAttrs = new Set();
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
while ((match = attrRegex.exec(attrsString)) !== null) {
|
|
169
169
|
processedAttrs.add(match[1]);
|
|
170
170
|
if (match[1].startsWith(".")) {
|
|
@@ -175,7 +175,7 @@ export const createVirtualDom = ({
|
|
|
175
175
|
// Handle conditional boolean attributes
|
|
176
176
|
const attrName = match[1].substring(1);
|
|
177
177
|
const attrValue = match[2] || match[3] || match[4];
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
// Convert string values to boolean
|
|
180
180
|
let evalValue;
|
|
181
181
|
if (attrValue === "true") {
|
|
@@ -186,7 +186,7 @@ export const createVirtualDom = ({
|
|
|
186
186
|
// Try to get from viewData if it's not a literal boolean
|
|
187
187
|
evalValue = lodashGet(viewData, attrValue);
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
// Only add attribute if value is truthy
|
|
191
191
|
if (evalValue) {
|
|
192
192
|
attrs[attrName] = "";
|
|
@@ -195,7 +195,7 @@ export const createVirtualDom = ({
|
|
|
195
195
|
attrs[match[1]] = match[2] || match[3] || match[4];
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
// Then, handle boolean attributes without values
|
|
200
200
|
// Remove all processed attribute-value pairs from the string first
|
|
201
201
|
let remainingAttrsString = attrsString;
|
|
@@ -209,7 +209,7 @@ export const createVirtualDom = ({
|
|
|
209
209
|
processedMatches.forEach(match => {
|
|
210
210
|
remainingAttrsString = remainingAttrsString.replace(match, ' ');
|
|
211
211
|
});
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
|
|
214
214
|
let boolMatch;
|
|
215
215
|
while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
|
|
@@ -327,9 +327,27 @@ export const createVirtualDom = ({
|
|
|
327
327
|
const eventListeners = refs[bestMatchRefKey].eventListeners;
|
|
328
328
|
Object.entries(eventListeners).forEach(
|
|
329
329
|
([eventType, eventConfig]) => {
|
|
330
|
+
if (eventConfig.handler && eventConfig.action) {
|
|
331
|
+
throw new Error('Each listener can have hanlder or action but not both')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (eventConfig.action) {
|
|
335
|
+
eventHandlers[eventType] = (event) => {
|
|
336
|
+
handlers.handleCallStoreAction({
|
|
337
|
+
...eventConfig.payload,
|
|
338
|
+
_event: event,
|
|
339
|
+
_action: eventConfig.action,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
330
345
|
if (eventConfig.handler && handlers[eventConfig.handler]) {
|
|
331
346
|
eventHandlers[eventType] = (event) => {
|
|
332
|
-
handlers[eventConfig.handler](
|
|
347
|
+
handlers[eventConfig.handler]({
|
|
348
|
+
...eventConfig.payload,
|
|
349
|
+
_event: event,
|
|
350
|
+
});
|
|
333
351
|
};
|
|
334
352
|
} else if (eventConfig.handler) {
|
|
335
353
|
// Keep this warning for missing handlers
|