@rettangoli/fe 0.0.3 → 0.0.5

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 CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@rettangoli/fe",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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/*.js",
10
- "src/!(cli)",
9
+ "src",
11
10
  "README.md",
12
11
  "LICENSE"
13
12
  ],
@@ -20,16 +19,15 @@
20
19
  ".": "./src/index.js",
21
20
  "./cli": "./src/cli/index.js"
22
21
  },
23
- "devDependencies": {
24
- "js-yaml": "^4.1.0",
25
- "esbuild": "^0.25.4",
26
- "vite": "^6.3.5"
27
- },
22
+ "devDependencies": {},
28
23
  "dependencies": {
24
+ "esbuild": "^0.25.5",
29
25
  "immer": "^10.1.1",
30
26
  "jempl": "^0.0.6",
27
+ "js-yaml": "^4.1.0",
31
28
  "rxjs": "^7.8.2",
32
- "snabbdom": "^3.6.2"
29
+ "snabbdom": "^3.6.2",
30
+ "vite": "^6.3.5"
33
31
  },
34
32
  "scripts": {
35
33
  "dev": "node watch.js --watch"
@@ -0,0 +1,8 @@
1
+
2
+ export const handleOnMount = (deps) => {
3
+ //
4
+ }
5
+
6
+ export const handlerSomeEvent = (e, deps) => {
7
+ //
8
+ }
@@ -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
+
@@ -0,0 +1,16 @@
1
+ elementName: custom-blank
2
+
3
+ viewDataSchema:
4
+ type: object
5
+
6
+ propsSchema:
7
+ type: object
8
+ properties: {}
9
+
10
+ refs: {}
11
+
12
+ events: {}
13
+
14
+ template:
15
+ - rtgl-view w=f h=f p=md:
16
+ - rtgl-text s=h2: blank
@@ -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;
@@ -0,0 +1,11 @@
1
+ import build from './build.js';
2
+ import scaffold from './scaffold.js';
3
+ import watch from './watch.js';
4
+ import examples from './examples.js';
5
+
6
+ export {
7
+ build,
8
+ scaffold,
9
+ watch,
10
+ examples
11
+ }
@@ -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;
@@ -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;