@vamidicreations/arc-ui 0.1.0
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/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/compiler/template-component.compiler.js +292 -0
- package/dist/component-plugin.js +45 -0
- package/dist/include-effect-plugin.js +48 -0
- package/dist/index.js +3 -0
- package/dist/main.mjs +48 -0
- package/dist/onejs/copy-assets.js +79 -0
- package/dist/onejs/decorator-fix.js +14 -0
- package/dist/onejs/import-transform.js +86 -0
- package/dist/onejs/index.js +3 -0
- package/dist/src/compiler/template-component.compiler.js +292 -0
- package/dist/src/component-plugin.js +45 -0
- package/dist/src/include-effect-plugin.js +48 -0
- package/dist/src/index.js +3 -0
- package/dist/src/onejs/copy-assets.js +79 -0
- package/dist/src/onejs/decorator-fix.js +14 -0
- package/dist/src/onejs/import-transform.js +86 -0
- package/dist/src/onejs/index.js +3 -0
- package/jest.config.js +13 -0
- package/main.mjs +48 -0
- package/main.mts +58 -0
- package/package.json +26 -0
- package/tsconfig.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Valencio Hoffman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny template compiler (rewritten for View.buildNodeTree)
|
|
3
|
+
*
|
|
4
|
+
* Instead of generating a standalone render function,
|
|
5
|
+
* we now generate code that overrides `Component.prototype.view.buildNodeTree`.
|
|
6
|
+
*/
|
|
7
|
+
import * as parse5 from "parse5";
|
|
8
|
+
// small HTML tag whitelist (add more as needed)
|
|
9
|
+
const HTML_TAGS = new Set([
|
|
10
|
+
"a", "abbr", "address", "area", "article", "aside", "audio",
|
|
11
|
+
"b", "base", "bdi", "bdo", "blockquote", "body", "br", "button",
|
|
12
|
+
"canvas", "caption", "cite", "code", "col", "colgroup",
|
|
13
|
+
"data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt",
|
|
14
|
+
"em", "embed",
|
|
15
|
+
"fieldset", "figcaption", "figure", "footer", "form",
|
|
16
|
+
"h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html",
|
|
17
|
+
"i", "iframe", "img", "input", "ins",
|
|
18
|
+
"kbd", "label", "legend", "li", "link",
|
|
19
|
+
"main", "map", "mark", "meta", "meter",
|
|
20
|
+
"nav", "noscript",
|
|
21
|
+
"object", "ol", "optgroup", "option", "output",
|
|
22
|
+
"p", "picture", "pre", "progress",
|
|
23
|
+
"q",
|
|
24
|
+
"rp", "rt", "ruby",
|
|
25
|
+
"s", "samp", "script", "section", "select", "small", "source", "span", "strong", "style", "sub", "summary", "sup",
|
|
26
|
+
"table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track",
|
|
27
|
+
"u", "ul",
|
|
28
|
+
"var", "video", "wbr"
|
|
29
|
+
]);
|
|
30
|
+
class TemplateComponentCompiler {
|
|
31
|
+
idCounter = 0;
|
|
32
|
+
generated = [];
|
|
33
|
+
rootVar = "root";
|
|
34
|
+
ctxVar = "ctx";
|
|
35
|
+
compileHtmlToRender(html, /* compName: string, */ selector) {
|
|
36
|
+
this.resetCounter();
|
|
37
|
+
this.resetGeneratedHtml();
|
|
38
|
+
const fragment = this.parseHtml(html);
|
|
39
|
+
// Build the node tree from the app to all the elements and components
|
|
40
|
+
this.generateNodeTree(fragment, selector);
|
|
41
|
+
return this.generated.join("\n");
|
|
42
|
+
}
|
|
43
|
+
id(name) {
|
|
44
|
+
return `${name}_${++this.idCounter}`;
|
|
45
|
+
}
|
|
46
|
+
isNativeHtmlTag(tagName) {
|
|
47
|
+
// custom elements per spec must contain a hyphen => treat as custom
|
|
48
|
+
const tag = String(tagName).toLowerCase();
|
|
49
|
+
if (tag.includes('-')) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// otherwise, if it's a known HTML tag, it's native
|
|
53
|
+
return HTML_TAGS.has(tag);
|
|
54
|
+
}
|
|
55
|
+
resetCounter() {
|
|
56
|
+
this.idCounter = 0;
|
|
57
|
+
}
|
|
58
|
+
resetGeneratedHtml() {
|
|
59
|
+
this.generated = [];
|
|
60
|
+
}
|
|
61
|
+
parseHtml(html) {
|
|
62
|
+
return parse5.parseFragment(html);
|
|
63
|
+
}
|
|
64
|
+
generateNodeTree(fragment, selector) {
|
|
65
|
+
// buildNodeTree override
|
|
66
|
+
// Instead of attaching to prototype, register in central registry
|
|
67
|
+
this.generated.push(`registry.registerBuildTree("${selector}", function() {`);
|
|
68
|
+
this.generated.push(` const ${this.rootVar} = [];`);
|
|
69
|
+
this.generated.push(` const ${this.ctxVar} = this;`);
|
|
70
|
+
this.generateChildNodes(fragment);
|
|
71
|
+
this.generated.push(` return ${this.rootVar};`);
|
|
72
|
+
this.generated.push(`});`);
|
|
73
|
+
}
|
|
74
|
+
generateChildNodes(fragment) {
|
|
75
|
+
for (const node of fragment.childNodes) {
|
|
76
|
+
this.walkNodes([node], null, " ");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walkNodes(nodes, parentVar, indent = " ") {
|
|
80
|
+
for (const node of nodes) {
|
|
81
|
+
if (node.nodeName === "#text") {
|
|
82
|
+
const raw = node.value;
|
|
83
|
+
if (!raw || !raw.trim())
|
|
84
|
+
continue;
|
|
85
|
+
this.processTextNode(raw, parentVar, indent);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
this.walkElement(node, parentVar, indent);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
walkElement(node, parentVar, indent = " ") {
|
|
92
|
+
const tagName = node.tagName;
|
|
93
|
+
const customComponent = this.detectCustomComponentAndGenerate(tagName, parentVar, indent);
|
|
94
|
+
if (customComponent) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const routerComponent = this.detectRouterOutletComponent(tagName, parentVar, indent);
|
|
98
|
+
if (routerComponent) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const element = this.createElement(node, indent);
|
|
102
|
+
this.createStylesForElement(node, element, indent);
|
|
103
|
+
this.generateFunctions(node, element, indent);
|
|
104
|
+
this.addToParentOrRoot(element, parentVar, indent);
|
|
105
|
+
if (node.childNodes && node.childNodes.length) {
|
|
106
|
+
this.walkNodes(node.childNodes, element, indent + " ");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
createElement(node, indent) {
|
|
110
|
+
const element = this.id("el");
|
|
111
|
+
this.generated.push(`${indent}const ${element} = onejsDocument.createElement(${JSON.stringify(node.tagName)});`);
|
|
112
|
+
(node.attrs || []).forEach((attr) => {
|
|
113
|
+
if (attr.name.startsWith("("))
|
|
114
|
+
return;
|
|
115
|
+
if (attr.name === "style")
|
|
116
|
+
return;
|
|
117
|
+
// support element refs
|
|
118
|
+
if (attr.name.startsWith('#')) {
|
|
119
|
+
const refName = attr.name.slice(1);
|
|
120
|
+
this.generated.push(`viewRegistry.add('${refName}', ${element});`);
|
|
121
|
+
}
|
|
122
|
+
this.generated.push(`${indent}${element}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(attr.value)});`);
|
|
123
|
+
});
|
|
124
|
+
return element;
|
|
125
|
+
}
|
|
126
|
+
createStylesForElement(node, element, indent) {
|
|
127
|
+
const styleAttr = this.getAttr(node, "style");
|
|
128
|
+
if (styleAttr) {
|
|
129
|
+
const stylePairs = this.parseStyleString(styleAttr);
|
|
130
|
+
stylePairs.forEach((pair) => {
|
|
131
|
+
this.generated.push(`${indent}${element}.style.setProperty(${JSON.stringify(pair.prop)}, ${JSON.stringify(pair.val)});`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
createTextNode(id, text, indent) {
|
|
136
|
+
const textVar = this.id(id);
|
|
137
|
+
this.generated.push(`${indent}const ${textVar} = onejsDocument.createTextNode(${text});`);
|
|
138
|
+
return textVar;
|
|
139
|
+
}
|
|
140
|
+
generateFunctions(node, element, indent) {
|
|
141
|
+
(node.attrs || []).forEach((attr) => {
|
|
142
|
+
if (!attr.name.startsWith("("))
|
|
143
|
+
return;
|
|
144
|
+
const ev = attr.name.slice(1, -1);
|
|
145
|
+
const handlerCode = attr.value.trim();
|
|
146
|
+
const generateButtonEvt = `
|
|
147
|
+
${indent}${element}.addEventListener(${JSON.stringify(ev)}, function(ev){ ${this.ctxVar}.${handlerCode} });
|
|
148
|
+
`;
|
|
149
|
+
this.generated.push(generateButtonEvt);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
addToParentOrRoot(element, parentVar, indent) {
|
|
153
|
+
if (parentVar) {
|
|
154
|
+
this.generated.push(`${indent}${parentVar}.appendChild(${element});`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.generated.push(` ${this.rootVar}.push(${element});`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 🔹 Detect custom component tags (PascalCase convention, or however you decide)
|
|
162
|
+
* @param tagName
|
|
163
|
+
* @param parentVar
|
|
164
|
+
* @param indent
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
detectCustomComponentAndGenerate(tagName, parentVar, indent) {
|
|
168
|
+
if (this.isNativeHtmlTag(tagName)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const compTypeVar = this.id("compType");
|
|
172
|
+
const compVar = this.id("comp");
|
|
173
|
+
const treeVar = this.id("tree");
|
|
174
|
+
const componentGeneration = `
|
|
175
|
+
${indent}// component <${tagName}>
|
|
176
|
+
${indent}const ${compTypeVar} = registry.getComponentFromRegistry("${tagName}");
|
|
177
|
+
|
|
178
|
+
if(!${compTypeVar}) {
|
|
179
|
+
throw new Error(\`${tagName} can't be found!\`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
${indent}const ${compVar} = new ${compTypeVar}();
|
|
183
|
+
${indent}const ${treeVar} = ${compVar}.buildNodeTree();
|
|
184
|
+
`;
|
|
185
|
+
this.generated.push(componentGeneration);
|
|
186
|
+
// Add the children to the component
|
|
187
|
+
this.generated.push(`${indent}${treeVar}.forEach(n => {`);
|
|
188
|
+
this.addToParentOrRoot('n', parentVar, indent);
|
|
189
|
+
this.generated.push(`${indent}});`);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
detectRouterOutletComponent(tagName, parentVar, indent) {
|
|
193
|
+
if (tagName !== "router-outlet") {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const outletVar = this.id("outlet");
|
|
197
|
+
this.generated.push(`
|
|
198
|
+
const ${outletVar} = onejsDocument.createElement("div");
|
|
199
|
+
root.push(${outletVar});
|
|
200
|
+
|
|
201
|
+
privateEffect(async () => {
|
|
202
|
+
const compType = await router.getComponent();
|
|
203
|
+
if (compType) {
|
|
204
|
+
const comp = new compType();
|
|
205
|
+
const tree = comp.buildNodeTree();
|
|
206
|
+
${outletVar}.innerHTML = ""; // clear old view
|
|
207
|
+
tree.forEach(n => ${outletVar}.appendChild(n));
|
|
208
|
+
}
|
|
209
|
+
}, [router.currentPath]);
|
|
210
|
+
`);
|
|
211
|
+
this.addToParentOrRoot(outletVar, parentVar, indent);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
processTextNode(rawValue, parentVar, indent) {
|
|
215
|
+
// Detect {{ expr }}
|
|
216
|
+
const regex = /\{\{([^}]+)}}/g;
|
|
217
|
+
let match;
|
|
218
|
+
let lastIndex = 0;
|
|
219
|
+
let hasInterpolation = false;
|
|
220
|
+
while ((match = regex.exec(rawValue))) {
|
|
221
|
+
hasInterpolation = true;
|
|
222
|
+
const staticText = rawValue.slice(lastIndex, match.index);
|
|
223
|
+
// we first have to check if the value is a signal, because
|
|
224
|
+
// then we need to attach them together immediately instead of making to text nodes.
|
|
225
|
+
if (match[1]) {
|
|
226
|
+
const expr = match[1].trim();
|
|
227
|
+
const dynTextVar = this.createTextNode("dynText", "\"\"", indent);
|
|
228
|
+
this.addToParentOrRoot(dynTextVar, parentVar, indent);
|
|
229
|
+
const isFunctionExpr = /\(\s*\)$/.test(expr); // true if expr ends with ()
|
|
230
|
+
const cleanExpr = expr.replace(/\(\s*\)$/, ""); // remove () for deps if present
|
|
231
|
+
if (isFunctionExpr) {
|
|
232
|
+
this.generated.push(`
|
|
233
|
+
${indent}// listen to signal change
|
|
234
|
+
${indent}privateEffect(() => {
|
|
235
|
+
${indent}try {
|
|
236
|
+
${indent}${dynTextVar}.nodeValue = String(${JSON.stringify(staticText)} + ${this.ctxVar}.${expr});
|
|
237
|
+
${indent}} catch (e) {
|
|
238
|
+
${indent}console.error(e);
|
|
239
|
+
${indent}}
|
|
240
|
+
${indent}}, [${this.ctxVar}.${cleanExpr}]);
|
|
241
|
+
`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this.generated.push(`
|
|
245
|
+
${indent}try {
|
|
246
|
+
${indent}${dynTextVar}.nodeValue = String(${JSON.stringify(staticText)} + ${this.ctxVar}.${expr});
|
|
247
|
+
${indent}} catch (e) {
|
|
248
|
+
${indent}console.error(e);
|
|
249
|
+
${indent}}
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (staticText.trim()) {
|
|
255
|
+
console.log('parentVar', parentVar);
|
|
256
|
+
console.log('staticText', staticText);
|
|
257
|
+
const textVar = this.createTextNode("text", JSON.stringify(staticText), indent);
|
|
258
|
+
this.addToParentOrRoot(textVar, parentVar, indent);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
lastIndex = regex.lastIndex;
|
|
262
|
+
}
|
|
263
|
+
if (!hasInterpolation) {
|
|
264
|
+
// plain text
|
|
265
|
+
const textVar = this.createTextNode("text", JSON.stringify(rawValue), indent);
|
|
266
|
+
this.addToParentOrRoot(textVar, parentVar, indent);
|
|
267
|
+
}
|
|
268
|
+
else if (lastIndex < rawValue.length) {
|
|
269
|
+
// leftover static text
|
|
270
|
+
const leftover = rawValue.slice(lastIndex);
|
|
271
|
+
const textVar = this.createTextNode("text", JSON.stringify(leftover), indent);
|
|
272
|
+
this.addToParentOrRoot(textVar, parentVar, indent);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
parseStyleString(style) {
|
|
276
|
+
const out = [];
|
|
277
|
+
style.split(";").forEach((pair) => {
|
|
278
|
+
const [k, v] = pair.split(":");
|
|
279
|
+
if (!k || !v)
|
|
280
|
+
return;
|
|
281
|
+
out.push({ prop: k.trim(), val: v.trim() });
|
|
282
|
+
});
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
getAttr(node, name) {
|
|
286
|
+
if (!node.attrs)
|
|
287
|
+
return null;
|
|
288
|
+
const a = node.attrs.find((x) => x.name === name);
|
|
289
|
+
return a ? a.value : null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export const templateComponentCompiler = new TemplateComponentCompiler();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* component-plugin.ts
|
|
2
|
+
* tiny-compiler.js should export a function that takes (html, compName) and returns JS code
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { templateComponentCompiler } from "./compiler/template-component.compiler";
|
|
7
|
+
const TS_EXT_PATTERN = /\.(ts|mts|cts|tsx)/;
|
|
8
|
+
export function componentPlugin() {
|
|
9
|
+
return {
|
|
10
|
+
name: "component-plugin",
|
|
11
|
+
setup(build) {
|
|
12
|
+
build.onLoad({ filter: TS_EXT_PATTERN }, async (args) => {
|
|
13
|
+
console.log("PLUGIN onLoad:", args.path);
|
|
14
|
+
let source = await fs.readFile(args.path, { encoding: "utf8" });
|
|
15
|
+
// Look for @Component with templateUrl
|
|
16
|
+
const match = source.match(/@Component\s*\(\s*{[^}]*templateUrl\s*:\s*['"](.+?)['"]/);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return { contents: source, loader: "ts" };
|
|
19
|
+
}
|
|
20
|
+
if (!match[1]) {
|
|
21
|
+
return { contents: source, loader: "ts" };
|
|
22
|
+
}
|
|
23
|
+
const htmlPath = resolve(dirname(args.path), match[1]);
|
|
24
|
+
const html = await fs.readFile(htmlPath, "utf8");
|
|
25
|
+
const compNameMatch = source.match(/export\s+class\s+(\w+)/);
|
|
26
|
+
const compName = compNameMatch ? compNameMatch[1] : null; // "AnonymousComponent";
|
|
27
|
+
const selectorMatch = source.match(/selector\s*:\s*["'`](.*?)["'`]/);
|
|
28
|
+
const selector = selectorMatch ? selectorMatch[1] : null;
|
|
29
|
+
if (!compName || !selector) {
|
|
30
|
+
return { contents: source, loader: "ts" };
|
|
31
|
+
}
|
|
32
|
+
// Compile HTML → render fn
|
|
33
|
+
const renderFnCode = templateComponentCompiler.compileHtmlToRender(html, selector);
|
|
34
|
+
// Inject
|
|
35
|
+
const newSource = source +
|
|
36
|
+
"\n\n// --- generated render fn ---\n" +
|
|
37
|
+
renderFnCode;
|
|
38
|
+
return {
|
|
39
|
+
contents: newSource,
|
|
40
|
+
loader: "ts", // 👈 important: hand it back as TypeScript
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
const privateEffectText = `
|
|
3
|
+
"use strict";
|
|
4
|
+
function privateEffect(effectFn, deps) {
|
|
5
|
+
let node = createViewEffect(effectFn, deps);
|
|
6
|
+
const effectRef = new EffectRefImpl(node);
|
|
7
|
+
effectFn();
|
|
8
|
+
return effectRef;
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
const effectText = `
|
|
12
|
+
"use strict";
|
|
13
|
+
${privateEffectText}
|
|
14
|
+
function createViewEffect(effectFn, deps) {
|
|
15
|
+
const node = alienEffect(effectFn);
|
|
16
|
+
|
|
17
|
+
const effectNode = node as unknown as EffectNode;
|
|
18
|
+
effectNode.fn = effectFn;
|
|
19
|
+
if (/* ngDevMode */ true) {
|
|
20
|
+
effectNode.toString = () => \`[Effect: router}]\`;
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
effectNode.debugName = options?.debugName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return effectNode;
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
export function injectEffectPlugin() {
|
|
29
|
+
return {
|
|
30
|
+
name: "inject-effect",
|
|
31
|
+
setup(build) {
|
|
32
|
+
build.onEnd(async (result) => {
|
|
33
|
+
for (const out of result.outputFiles || []) {
|
|
34
|
+
let text = out.text;
|
|
35
|
+
if (!text.includes("function effect(")) {
|
|
36
|
+
text = effectText + '\n' + text;
|
|
37
|
+
console.log(`[effectPlugin] Injected effect() into ${out.path}`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
text = privateEffectText + '\n' + text;
|
|
41
|
+
}
|
|
42
|
+
// ✅ Always write, even if unmodified (prevents missing file issue)
|
|
43
|
+
await fs.writeFile(out.path, text);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/index.js
ADDED
package/dist/main.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default OneJS ESbuild Config
|
|
3
|
+
*/
|
|
4
|
+
import { context } from "esbuild";
|
|
5
|
+
import { componentPlugin, injectEffectPlugin, importTransformationPlugin, copyAssetsPlugin, decoratorFixPlugin } from "./src";
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
const once = process.argv.includes("--once");
|
|
8
|
+
// !once && outputWatcherPlugin(),
|
|
9
|
+
const ctx = await context({
|
|
10
|
+
entryPoints: ["./src/main.ts"],
|
|
11
|
+
bundle: true,
|
|
12
|
+
plugins: [
|
|
13
|
+
componentPlugin(),
|
|
14
|
+
importTransformationPlugin(),
|
|
15
|
+
copyAssetsPlugin(),
|
|
16
|
+
decoratorFixPlugin(),
|
|
17
|
+
{
|
|
18
|
+
name: 'add-mjs',
|
|
19
|
+
setup(build) {
|
|
20
|
+
build.onResolve({ filter: /.*/ }, args => {
|
|
21
|
+
if (args.importer)
|
|
22
|
+
return { path: args.path + '.mjs', external: true };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
injectEffectPlugin(),
|
|
27
|
+
],
|
|
28
|
+
inject: ["onejs-core/dist/index.js"],
|
|
29
|
+
platform: "node",
|
|
30
|
+
sourcemap: true,
|
|
31
|
+
sourceRoot: process.cwd() + "/index",
|
|
32
|
+
alias: {
|
|
33
|
+
"onejs": "onejs-core",
|
|
34
|
+
"esbuild": "esbuild",
|
|
35
|
+
},
|
|
36
|
+
outfile: "@outputs/esbuild/app.js",
|
|
37
|
+
write: false, // override
|
|
38
|
+
});
|
|
39
|
+
if (once) {
|
|
40
|
+
await ctx.rebuild();
|
|
41
|
+
await ctx.dispose();
|
|
42
|
+
console.log("Build finished.");
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
await ctx.watch();
|
|
47
|
+
console.log("Watching for changes…");
|
|
48
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { cpSync, existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
function getPackagesWithAssets() {
|
|
5
|
+
let packagesWithAssets = [];
|
|
6
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules');
|
|
7
|
+
const packages = readdirSync(nodeModulesPath);
|
|
8
|
+
// console.log("There are " + packages.length + " packages in node_modules");
|
|
9
|
+
for (const packageName of packages) {
|
|
10
|
+
const packagePath = join(nodeModulesPath, packageName);
|
|
11
|
+
if (!statSync(packagePath).isDirectory())
|
|
12
|
+
continue;
|
|
13
|
+
const pkgJsonPath = join(packagePath, 'package.json');
|
|
14
|
+
try {
|
|
15
|
+
const pkgJsonContent = readFileSync(pkgJsonPath, 'utf8');
|
|
16
|
+
const pkgJson = JSON.parse(pkgJsonContent);
|
|
17
|
+
if (pkgJson.onejs && pkgJson.onejs['assets-path']) {
|
|
18
|
+
packagesWithAssets.push({
|
|
19
|
+
name: packageName,
|
|
20
|
+
src: join("node_modules", packageName, pkgJson.onejs['assets-path'])
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== 'ENOENT') {
|
|
26
|
+
console.error(`Error processing ${packageName}:`, error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// console.log("Found " + packagesWithAssets.length + " packages with assets");
|
|
31
|
+
return packagesWithAssets;
|
|
32
|
+
}
|
|
33
|
+
const getDigest = (string) => {
|
|
34
|
+
const hash = createHash('md5');
|
|
35
|
+
const data = hash.update(string, 'utf-8');
|
|
36
|
+
return data.digest('hex');
|
|
37
|
+
};
|
|
38
|
+
const getFileDigest = (path) => {
|
|
39
|
+
if (!existsSync(path)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (statSync(path).isDirectory()) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return getDigest(readFileSync(path));
|
|
46
|
+
};
|
|
47
|
+
function filter(src, dest) {
|
|
48
|
+
if (!existsSync(dest)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (statSync(dest).isDirectory()) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return getFileDigest(src) !== getFileDigest(dest);
|
|
55
|
+
}
|
|
56
|
+
export function copyAssetsPlugin(options = {}) {
|
|
57
|
+
let dest = options.dest || 'assets';
|
|
58
|
+
return {
|
|
59
|
+
name: "onejs-copy-assets",
|
|
60
|
+
setup(build) {
|
|
61
|
+
build.onEnd(async () => {
|
|
62
|
+
const packagesWithAssets = getPackagesWithAssets();
|
|
63
|
+
console.log(`[esbuild] syncing assets from:`);
|
|
64
|
+
for (const pkg of packagesWithAssets) {
|
|
65
|
+
const destPath = join(dest, "@" + pkg.name);
|
|
66
|
+
cpSync(pkg.src, destPath, {
|
|
67
|
+
dereference: options.dereference || true,
|
|
68
|
+
errorOnExist: options.errorOnExist || false,
|
|
69
|
+
filter: options.filter || filter,
|
|
70
|
+
force: options.force || true,
|
|
71
|
+
preserveTimestamps: options.preserveTimestamps || true,
|
|
72
|
+
recursive: options.recursive || true,
|
|
73
|
+
});
|
|
74
|
+
console.log(`[esbuild] > ${pkg.name}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
export function decoratorFixPlugin(options = {}) {
|
|
3
|
+
const outfile = options.outfile || "@outputs/esbuild/app.js";
|
|
4
|
+
return {
|
|
5
|
+
name: "decorator-fix-plugin",
|
|
6
|
+
setup(build) {
|
|
7
|
+
build.onEnd(() => {
|
|
8
|
+
let content = readFileSync(outfile, "utf8");
|
|
9
|
+
content = content.replace(/exports\s*&&\s*exports\.__decorate/g, 'typeof exports !== "undefined" && exports.__decorate');
|
|
10
|
+
writeFileSync(outfile, content, "utf8");
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { extname } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Copied from OneJs
|
|
5
|
+
* An esbuild plugin that transforms imports from modules starting with a capital letter.
|
|
6
|
+
*
|
|
7
|
+
* ## Functionality:
|
|
8
|
+
* 1. **Externalizing Capitalized Module Imports**:
|
|
9
|
+
* - Marks imports from modules with names starting with a capital letter as external
|
|
10
|
+
* during the resolution phase.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Transforming Import Statements**:
|
|
13
|
+
* - Rewrites ES module import statements to reference a global `CS` object.
|
|
14
|
+
* - Converts:
|
|
15
|
+
* ```js
|
|
16
|
+
* import { Foo, Bar } from "MyModule";
|
|
17
|
+
* ```
|
|
18
|
+
* Into:
|
|
19
|
+
* ```js
|
|
20
|
+
* const { Foo, Bar } = CS.MyModule;
|
|
21
|
+
* ```
|
|
22
|
+
* - If the import is a default import or namespace import:
|
|
23
|
+
* ```js
|
|
24
|
+
* import MyModule from "MyModule";
|
|
25
|
+
* ```
|
|
26
|
+
* Becomes:
|
|
27
|
+
* ```js
|
|
28
|
+
* const MyModule = CS.MyModule;
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* 3. **Handling `require` Statements**:
|
|
32
|
+
* - Transforms `__require("MyModule")` into `CS.MyModule`.
|
|
33
|
+
*
|
|
34
|
+
* ## Parameters:
|
|
35
|
+
* @param {Object} options - Configuration options.
|
|
36
|
+
* @param {Function} [options.moduleFilter] - A filter function that determines if a module should be transformed.
|
|
37
|
+
*
|
|
38
|
+
* ## Returns:
|
|
39
|
+
* @returns {Object} An esbuild plugin object.
|
|
40
|
+
*
|
|
41
|
+
* ## Example Usage:
|
|
42
|
+
*
|
|
43
|
+
* ```js
|
|
44
|
+
* importTransformation({
|
|
45
|
+
* moduleFilter: (moduleName) => moduleName.startsWith("MyLib")
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
*/
|
|
50
|
+
export function importTransformationPlugin(options = {}) {
|
|
51
|
+
const moduleFilter = options.moduleFilter || (() => true);
|
|
52
|
+
return {
|
|
53
|
+
name: "onejs-import-transform",
|
|
54
|
+
setup(build) {
|
|
55
|
+
// First pass: Mark imports from modules starting with a capital letter as external
|
|
56
|
+
build.onResolve({ filter: /^[A-Z]/ }, (args) => {
|
|
57
|
+
return { path: args.path, external: true };
|
|
58
|
+
});
|
|
59
|
+
// Second pass: Transform all JS and TSX files
|
|
60
|
+
build.onLoad({ filter: /\.(js|jsx|ts|tsx)$/ }, async (args) => {
|
|
61
|
+
let contents = await fs.readFile(args.path, "utf8");
|
|
62
|
+
// Transform imports from modules starting with a capital letter
|
|
63
|
+
contents = contents.replace(/import\s+(?:{([^}]+)})?\s*from\s*["']([A-Z][^"']*)["'];?/g, (match, imports, moduleName) => {
|
|
64
|
+
if (!moduleFilter(moduleName))
|
|
65
|
+
return match;
|
|
66
|
+
moduleName = moduleName.replace(/\//g, ".");
|
|
67
|
+
if (imports) {
|
|
68
|
+
const importItems = imports.split(",").map((item) => item.trim());
|
|
69
|
+
return `const { ${importItems.join(", ")} } = CS.${moduleName};`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const namespaceName = moduleName.split(".").pop();
|
|
73
|
+
return `const ${namespaceName} = CS.${moduleName};`;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Transform any remaining require statements for such modules
|
|
77
|
+
contents = contents.replace(/__require\(["']([A-Z][^"']*)["']\)/g, (match, moduleName) => {
|
|
78
|
+
if (!moduleFilter(moduleName))
|
|
79
|
+
return match;
|
|
80
|
+
return `CS.${moduleName.replace(/\//g, ".")}`;
|
|
81
|
+
});
|
|
82
|
+
return { contents, loader: extname(args.path).slice(1) };
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|