@rettangoli/fe 0.0.3 → 0.0.4
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 -3
- package/src/cli/blank/blank.handlers.js +8 -0
- package/src/cli/blank/blank.store.js +18 -0
- package/src/cli/blank/blank.view.yaml +16 -0
- package/src/cli/build.js +133 -0
- package/src/cli/examples.js +158 -0
- package/src/cli/index.js +11 -0
- package/src/cli/scaffold.js +43 -0
- package/src/cli/watch.js +66 -0
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/fe",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Frontend framework for building reactive web components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"keywords": ["frontend", "reactive", "components", "web", "framework"],
|
|
8
8
|
"files": [
|
|
9
|
-
"src
|
|
10
|
-
"src/!(cli)",
|
|
9
|
+
"src",
|
|
11
10
|
"README.md",
|
|
12
11
|
"LICENSE"
|
|
13
12
|
],
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const INITIAL_STATE = Object.freeze({
|
|
2
|
+
//
|
|
3
|
+
});
|
|
4
|
+
|
|
5
|
+
export const toViewData = ({ state, props }) => {
|
|
6
|
+
return state;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const selectState = ({ state }) => {
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const setState = (state) => {
|
|
14
|
+
// do doSomething
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
package/src/cli/build.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
|
|
8
|
+
import esbuild from "esbuild";
|
|
9
|
+
import { load as loadYaml } from "js-yaml";
|
|
10
|
+
import { parse } from 'jempl';
|
|
11
|
+
import { extractCategoryAndComponent } from '../common.js';
|
|
12
|
+
import { getAllFiles } from '../commonBuild.js';
|
|
13
|
+
|
|
14
|
+
function capitalize(word) {
|
|
15
|
+
return word ? word[0].toUpperCase() + word.slice(1) : word;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Function to process view files - loads YAML and creates temporary JS file
|
|
19
|
+
export const writeViewFile = (view, category, component) => {
|
|
20
|
+
// const { category, component } = extractCategoryAndComponent(filePath);
|
|
21
|
+
|
|
22
|
+
const dir = `./.temp/${category}`;
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
writeFileSync(
|
|
27
|
+
`${dir}/${component}.view.js`,
|
|
28
|
+
`export default ${JSON.stringify(view)};`,
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const bundleFile = async (options) => {
|
|
33
|
+
const { outfile = "./viz/static/main.js" } = options;
|
|
34
|
+
await esbuild.build({
|
|
35
|
+
entryPoints: ["./.temp/dynamicImport.js"],
|
|
36
|
+
bundle: true,
|
|
37
|
+
minify: false,
|
|
38
|
+
sourcemap: true,
|
|
39
|
+
outfile: outfile,
|
|
40
|
+
format: "esm",
|
|
41
|
+
loader: {
|
|
42
|
+
".wasm": "binary",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const buildRettangoliFrontend = async (options) => {
|
|
48
|
+
console.log("running build with options", options);
|
|
49
|
+
|
|
50
|
+
const { dirs = ["./example"], outfile = "./viz/static/main.js", setup = "setup.js" } = options;
|
|
51
|
+
|
|
52
|
+
const allFiles = getAllFiles(dirs).filter((filePath) => {
|
|
53
|
+
return (
|
|
54
|
+
filePath.endsWith(".store.js") ||
|
|
55
|
+
filePath.endsWith(".handlers.js") ||
|
|
56
|
+
filePath.endsWith(".view.yaml")
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let output = "";
|
|
61
|
+
|
|
62
|
+
const categories = [];
|
|
63
|
+
const imports = {};
|
|
64
|
+
|
|
65
|
+
// unique identifier needed for replacing
|
|
66
|
+
let count = 10000000000;
|
|
67
|
+
|
|
68
|
+
const replaceMap = {};
|
|
69
|
+
|
|
70
|
+
for (const filePath of allFiles) {
|
|
71
|
+
const { category, component, fileType } =
|
|
72
|
+
extractCategoryAndComponent(filePath);
|
|
73
|
+
|
|
74
|
+
if (!imports[category]) {
|
|
75
|
+
imports[category] = {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!imports[category][component]) {
|
|
79
|
+
imports[category][component] = {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!categories.includes(category)) {
|
|
83
|
+
categories.push(category);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (["handlers", "store"].includes(fileType)) {
|
|
87
|
+
output += `import * as ${component}${capitalize(
|
|
88
|
+
fileType,
|
|
89
|
+
)} from '../${filePath}';\n`;
|
|
90
|
+
|
|
91
|
+
replaceMap[count] = `${component}${capitalize(fileType)}`;
|
|
92
|
+
imports[category][component][fileType] = count;
|
|
93
|
+
count++;
|
|
94
|
+
} else if (["view"].includes(fileType)) {
|
|
95
|
+
const view = loadYaml(readFileSync(filePath, "utf8"));
|
|
96
|
+
view.template = parse(view.template);
|
|
97
|
+
writeViewFile(view, category, component);
|
|
98
|
+
output += `import ${component}${capitalize(
|
|
99
|
+
fileType,
|
|
100
|
+
)} from './${category}/${component}.view.js';\n`;
|
|
101
|
+
replaceMap[count] = `${component}${capitalize(fileType)}`;
|
|
102
|
+
|
|
103
|
+
imports[category][component][fileType] = count;
|
|
104
|
+
count++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
output += `
|
|
109
|
+
import { createComponent } from 'rettangoli-fe';
|
|
110
|
+
import { deps, patch, h } from '../${setup}';
|
|
111
|
+
const imports = ${JSON.stringify(imports, null, 2)};
|
|
112
|
+
|
|
113
|
+
Object.keys(imports).forEach(category => {
|
|
114
|
+
Object.keys(imports[category]).forEach(component => {
|
|
115
|
+
const webComponent = createComponent({ ...imports[category][component], patch, h }, deps[category])
|
|
116
|
+
customElements.define(imports[category][component].view.elementName, webComponent);
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
Object.keys(replaceMap).forEach((key) => {
|
|
123
|
+
output = output.replace(key, replaceMap[key]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
writeFileSync("./.temp/dynamicImport.js", output);
|
|
127
|
+
|
|
128
|
+
await bundleFile({ outfile });
|
|
129
|
+
|
|
130
|
+
console.log(`Build complete. Output file: ${outfile}`);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default buildRettangoliFrontend;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { load as loadYaml, loadAll } from "js-yaml";
|
|
4
|
+
import { render, parse } from "jempl";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
extractCategoryAndComponent,
|
|
8
|
+
flattenArrays,
|
|
9
|
+
} from "../common.js";
|
|
10
|
+
import { getAllFiles } from "../commonBuild.js";
|
|
11
|
+
import path, { dirname } from "node:path";
|
|
12
|
+
|
|
13
|
+
const yamlToHtml = (renderedView) => {
|
|
14
|
+
const processNode = (node) => {
|
|
15
|
+
if (typeof node === "string") {
|
|
16
|
+
return node;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
return node.map(processNode).join("");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof node === "object" && node !== null) {
|
|
24
|
+
return Object.entries(node)
|
|
25
|
+
.map(([key, value]) => {
|
|
26
|
+
// Parse the key to extract element info
|
|
27
|
+
const [elementPart, ...attributeParts] = key.split(" ");
|
|
28
|
+
const [tagName, idPart] = elementPart.split("#");
|
|
29
|
+
|
|
30
|
+
// Use tag name as-is
|
|
31
|
+
const actualTagName = tagName;
|
|
32
|
+
|
|
33
|
+
// Build attributes
|
|
34
|
+
const attributes = [];
|
|
35
|
+
|
|
36
|
+
// Add id if present
|
|
37
|
+
if (idPart) {
|
|
38
|
+
attributes.push(`id="${idPart}"`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Parse other attributes from the key
|
|
42
|
+
const attrString = attributeParts.join(" ");
|
|
43
|
+
if (attrString) {
|
|
44
|
+
// Handle quoted attributes with regex
|
|
45
|
+
const quotedMatches = attrString.match(/(\w+)="([^"]+)"/g);
|
|
46
|
+
if (quotedMatches) {
|
|
47
|
+
quotedMatches.forEach(match => {
|
|
48
|
+
attributes.push(match);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle shorthand attributes (key=value format without quotes)
|
|
53
|
+
const shorthandRegex = /(\w+)=([^\s"]+)/g;
|
|
54
|
+
let shorthandMatch;
|
|
55
|
+
const quotedAttributeNames = new Set();
|
|
56
|
+
|
|
57
|
+
// First, collect all quoted attribute names to avoid duplicates
|
|
58
|
+
if (quotedMatches) {
|
|
59
|
+
quotedMatches.forEach(match => {
|
|
60
|
+
const matchResult = match.match(/(\w+)="[^"]+"/);
|
|
61
|
+
if (matchResult) {
|
|
62
|
+
const [, attrName] = matchResult;
|
|
63
|
+
quotedAttributeNames.add(attrName);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Then add shorthand attributes that aren't already quoted
|
|
69
|
+
while ((shorthandMatch = shorthandRegex.exec(attrString)) !== null) {
|
|
70
|
+
const [, attrName, attrValue] = shorthandMatch;
|
|
71
|
+
if (!quotedAttributeNames.has(attrName)) {
|
|
72
|
+
attributes.push(`${attrName}="${attrValue}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle standalone attributes (attributes without values)
|
|
77
|
+
const remainingAttrs = attrString
|
|
78
|
+
.replace(/(\w+)="[^"]+"/g, '') // Remove quoted attributes
|
|
79
|
+
.replace(/(\w+)=[^\s"]+/g, '') // Remove shorthand attributes
|
|
80
|
+
.trim()
|
|
81
|
+
.split(/\s+/)
|
|
82
|
+
.filter(attr => attr.length > 0);
|
|
83
|
+
|
|
84
|
+
remainingAttrs.forEach(attr => {
|
|
85
|
+
if (attr && !attr.includes('=')) {
|
|
86
|
+
attributes.push(attr);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const attrStr =
|
|
92
|
+
attributes.length > 0 ? " " + attributes.join(" ") : "";
|
|
93
|
+
|
|
94
|
+
// Handle self-closing elements
|
|
95
|
+
if (value === null) {
|
|
96
|
+
if (actualTagName === "input") {
|
|
97
|
+
return `<${actualTagName}${attrStr} />`;
|
|
98
|
+
}
|
|
99
|
+
return `<${actualTagName}${attrStr}></${actualTagName}>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle elements with content
|
|
103
|
+
const content = processNode(value);
|
|
104
|
+
return `<${actualTagName}${attrStr}>${content}</${actualTagName}>`;
|
|
105
|
+
})
|
|
106
|
+
.join("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return "";
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return processNode(renderedView);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const examples = (options = {}) => {
|
|
116
|
+
const { dirs, outputDir } = options;
|
|
117
|
+
|
|
118
|
+
const allFiles = getAllFiles(dirs);
|
|
119
|
+
|
|
120
|
+
const output = [];
|
|
121
|
+
|
|
122
|
+
const examplesFiles = allFiles
|
|
123
|
+
.filter((filePath) => {
|
|
124
|
+
return filePath.endsWith(".examples.yaml");
|
|
125
|
+
})
|
|
126
|
+
.map((filePath) => {
|
|
127
|
+
const viewFilePath = filePath.replace(".examples.yaml", ".view.yaml");
|
|
128
|
+
const { category, component, fileType } =
|
|
129
|
+
extractCategoryAndComponent(filePath);
|
|
130
|
+
const [config, ...examples] = loadAll(readFileSync(filePath, "utf8"));
|
|
131
|
+
const { template } = loadYaml(readFileSync(viewFilePath, "utf8"));
|
|
132
|
+
|
|
133
|
+
for (const [index, example] of examples.entries()) {
|
|
134
|
+
const { name, viewData } = example;
|
|
135
|
+
const ast = parse(template);
|
|
136
|
+
const renderedView = flattenArrays(render({ ast, data: viewData }));
|
|
137
|
+
const html = yamlToHtml(renderedView);
|
|
138
|
+
output.push({
|
|
139
|
+
category,
|
|
140
|
+
component,
|
|
141
|
+
index,
|
|
142
|
+
html,
|
|
143
|
+
name,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
for (const { category, component, index, html, name } of output) {
|
|
149
|
+
const fileName = path.join(outputDir, category, component, `${name}.html`);
|
|
150
|
+
mkdirSync(dirname(fileName), { recursive: true });
|
|
151
|
+
const addfrontMatter = (content) => {
|
|
152
|
+
return `---\ntitle: ${component}-${index}\n---\n\n${content}`;
|
|
153
|
+
};
|
|
154
|
+
writeFileSync(fileName, addfrontMatter(html));
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default examples;
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
5
|
+
|
|
6
|
+
// only have blank template now. may add more later
|
|
7
|
+
const templateDir = path.join(__dirname, 'blank');
|
|
8
|
+
|
|
9
|
+
const scaffoldPage = (options) => {
|
|
10
|
+
const { dir, category, componentName } = options;
|
|
11
|
+
const targetDir = path.join(dir, category, componentName);
|
|
12
|
+
|
|
13
|
+
if (fs.existsSync(targetDir)) {
|
|
14
|
+
console.log(`Stopping because ${targetDir} already exists`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
const files = fs.readdirSync(templateDir);
|
|
21
|
+
files.forEach(file => {
|
|
22
|
+
const sourcePath = path.join(templateDir, file);
|
|
23
|
+
const targetPath = path.join(targetDir, file.replace('blank', componentName));
|
|
24
|
+
|
|
25
|
+
// If it's a directory, copy recursively
|
|
26
|
+
if (fs.statSync(sourcePath).isDirectory()) {
|
|
27
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
28
|
+
} else {
|
|
29
|
+
// Read file content
|
|
30
|
+
let content = fs.readFileSync(sourcePath, 'utf8');
|
|
31
|
+
|
|
32
|
+
// Replace all occurrences of 'blank' with componentName in the content
|
|
33
|
+
content = content.replace(/blank/g, componentName);
|
|
34
|
+
|
|
35
|
+
// Write to new file
|
|
36
|
+
fs.writeFileSync(targetPath, content);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(`Successfully scaffolded ${targetDir} from template`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default scaffoldPage;
|
package/src/cli/watch.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, watch } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { load as loadYaml } from "js-yaml";
|
|
5
|
+
import { createServer } from 'vite'
|
|
6
|
+
import { writeViewFile } from './build.js';
|
|
7
|
+
import buildRettangoliFrontend from './build.js';
|
|
8
|
+
import { extractCategoryAndComponent } from '../common.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const setupWatcher = (directory) => {
|
|
12
|
+
watch(
|
|
13
|
+
directory,
|
|
14
|
+
{ recursive: true },
|
|
15
|
+
async (event, filename) => {
|
|
16
|
+
console.log(`Detected ${event} in ${directory}/${filename}`);
|
|
17
|
+
if (filename) {
|
|
18
|
+
try {
|
|
19
|
+
if (filename.endsWith('.view.yaml')) {
|
|
20
|
+
const view = loadYaml(readFileSync(path.join(directory, filename), "utf8"));
|
|
21
|
+
const { category, component } = extractCategoryAndComponent(filename);
|
|
22
|
+
await writeViewFile(view, category, component);
|
|
23
|
+
}
|
|
24
|
+
await buildRettangoliFrontend({ dirs: [directory] });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`Error processing ${filename}:`, error);
|
|
27
|
+
// Keep the watcher running
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function startViteServer(options) {
|
|
35
|
+
const { port = 3001, root = './viz/static' } = options;
|
|
36
|
+
try {
|
|
37
|
+
const server = await createServer({
|
|
38
|
+
// any valid user config options, plus `mode` and `configFile`
|
|
39
|
+
// configFile: false,
|
|
40
|
+
root,
|
|
41
|
+
server: {
|
|
42
|
+
port,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
await server.listen();
|
|
46
|
+
|
|
47
|
+
server.printUrls();
|
|
48
|
+
server.bindCLIShortcuts({ print: true });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("Error during Vite server startup:", error);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
const startWatching = (options) => {
|
|
57
|
+
const { dirs = ['src'], port = 3001 } = options;
|
|
58
|
+
|
|
59
|
+
dirs.forEach(dir => {
|
|
60
|
+
setupWatcher(dir);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
startViteServer({ port });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default startWatching;
|