@junnyontop-pixel/neo-app 1.2.1 → 2.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/README.md +10 -4
- package/compiler/NeoParser.js +48 -93
- package/compiler/main.js +37 -0
- package/core/NeoCore.js +25 -60
- package/index.html +15 -13
- package/package.json +1 -3
- package/src/App.neo +3 -9
- package/src/actions.js +3 -0
- package/src/state.js +9 -0
- package/compiler/index.js +0 -81
- package/scripts/init.js +0 -54
- package/src/App.js +0 -14
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
## Neo v2
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Neo v2 is a runtime-first DSL framework.
|
|
4
|
+
|
|
5
|
+
- No build step
|
|
6
|
+
- No compiler
|
|
7
|
+
- Runtime parsing
|
|
8
|
+
- Script + UI separation
|
|
9
|
+
- State-driven rendering (full rerender)
|
|
10
|
+
|
|
11
|
+
Nested components and partial reactivity are planned for v2.x+.
|
package/compiler/NeoParser.js
CHANGED
|
@@ -1,103 +1,58 @@
|
|
|
1
1
|
export class NeoParser {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
static parse(rawCode) {
|
|
3
|
+
const result = {
|
|
4
|
+
id: "",
|
|
5
|
+
tag: "",
|
|
6
|
+
styles: [],
|
|
7
|
+
innerHTML: "",
|
|
8
|
+
events: []
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// @ID:Tag
|
|
12
|
+
const tagMatch = rawCode.match(/@([\w-]+):([\w-]+)/);
|
|
13
|
+
if (tagMatch) {
|
|
14
|
+
result.id = tagMatch[1];
|
|
15
|
+
result.tag = tagMatch[2];
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
styles: {},
|
|
21
|
-
events: {},
|
|
22
|
-
props: {},
|
|
23
|
-
innerHtml: "",
|
|
24
|
-
children: []
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const startIndex = match.index + match[0].length;
|
|
28
|
-
let braceCount = 1;
|
|
29
|
-
let endIndex = startIndex;
|
|
18
|
+
// [styles]
|
|
19
|
+
const styleMatch = rawCode.match(/\[(.*?)\]/);
|
|
20
|
+
if (styleMatch) {
|
|
21
|
+
result.styles = styleMatch[1]
|
|
22
|
+
.split(',')
|
|
23
|
+
.map(s => s.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
}
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
// { content }
|
|
28
|
+
const contentMatch = rawCode.match(/\{([\s\S]*?)\}$/);
|
|
29
|
+
if (contentMatch) {
|
|
30
|
+
const lines = contentMatch[1].trim().split('\n');
|
|
31
|
+
|
|
32
|
+
lines.forEach(line => {
|
|
33
|
+
const text = line.trim();
|
|
34
|
+
|
|
35
|
+
if (text.startsWith('innerHTML:')) {
|
|
36
|
+
let value = text.slice('innerHTML:'.length).trim();
|
|
37
|
+
if (
|
|
38
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
39
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
40
|
+
) {
|
|
41
|
+
value = value.slice(1, -1);
|
|
42
|
+
}
|
|
43
|
+
result.innerHTML = value;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// 2. 자식이 나오기 전까지만 "내 설정 구역"으로 정의 (자식 데이터 보호)
|
|
45
|
-
const settingsZone = firstChildIndex === -1
|
|
46
|
-
? blockContent
|
|
47
|
-
: blockContent.substring(0, firstChildIndex);
|
|
48
|
-
|
|
49
|
-
// 3. 속성 추출은 오직 settingsZone에서만! (s 플래그 추가해서 줄바꿈 허용)
|
|
50
|
-
const htmlMatch = settingsZone.match(/innerHTML:\s*"([\s\S]*?)"/i);
|
|
51
|
-
if (htmlMatch) node.innerHtml = htmlMatch[1].trim();
|
|
52
|
-
|
|
53
|
-
const styleMatch = settingsZone.match(/Style\(([\s\S]*?)\)/si);
|
|
54
|
-
if (styleMatch) node.styles = this.parseKV(styleMatch[1]);
|
|
55
|
-
|
|
56
|
-
const eventMatch = settingsZone.match(/Event\(([\s\S]*?)\)/si);
|
|
57
|
-
if (eventMatch) node.events = this.parseKV(eventMatch[1]);
|
|
58
|
-
|
|
59
|
-
const propsMatch = settingsZone.match(/Props\(([\s\S]*?)\)/si);
|
|
60
|
-
if (propsMatch) node.props = this.parseKV(propsMatch[1]);
|
|
61
|
-
|
|
62
|
-
// 4. 자식 노드 탐색 (원본 blockContent에서 자식 구역만 탐색)
|
|
63
|
-
if (firstChildIndex !== -1) {
|
|
64
|
-
const childrenZone = blockContent.substring(firstChildIndex);
|
|
65
|
-
const childRegex = /@(\w+):(\w+)\s*\{/g;
|
|
66
|
-
let childMatch;
|
|
67
|
-
|
|
68
|
-
while ((childMatch = childRegex.exec(childrenZone)) !== null) {
|
|
69
|
-
// 자식의 전체 텍스트를 재귀적으로 파싱
|
|
70
|
-
const childNode = this.parseRecursive(childrenZone.substring(childMatch.index));
|
|
71
|
-
if (childNode) {
|
|
72
|
-
node.children.push(childNode);
|
|
73
|
-
|
|
74
|
-
// 자식의 중괄호가 끝나는 지점을 찾아서 다음 검색 위치 조정
|
|
75
|
-
let bc = 1;
|
|
76
|
-
let i = childrenZone.indexOf('{', childMatch.index) + 1;
|
|
77
|
-
while (bc > 0 && i < childrenZone.length) {
|
|
78
|
-
if (childrenZone[i] === '{') bc++;
|
|
79
|
-
else if (childrenZone[i] === '}') bc--;
|
|
80
|
-
i++;
|
|
81
|
-
}
|
|
82
|
-
childRegex.lastIndex = i; // 이 자식 이후부터 다음 자식 찾기
|
|
83
|
-
}
|
|
84
|
-
}
|
|
46
|
+
if (text.startsWith('on:')) {
|
|
47
|
+
const [, type, ...rest] = text.split(':');
|
|
48
|
+
result.events.push({
|
|
49
|
+
type: type.trim(),
|
|
50
|
+
action: rest.join(':').trim()
|
|
51
|
+
});
|
|
85
52
|
}
|
|
86
|
-
|
|
87
|
-
return node;
|
|
53
|
+
});
|
|
88
54
|
}
|
|
89
55
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (!kvString) return {};
|
|
93
|
-
const obj = {};
|
|
94
|
-
kvString.split(';').forEach(pair => {
|
|
95
|
-
const colonIndex = pair.indexOf(':');
|
|
96
|
-
if (colonIndex === -1) return;
|
|
97
|
-
const key = pair.substring(0, colonIndex).trim();
|
|
98
|
-
const value = pair.substring(colonIndex + 1).trim();
|
|
99
|
-
if (key && value) obj[key] = value;
|
|
100
|
-
});
|
|
101
|
-
return obj;
|
|
102
|
-
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
103
58
|
}
|
package/compiler/main.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NeoParser } from './NeoParser.js';
|
|
2
|
+
import { NeoCore } from '../core/NeoCore.js';
|
|
3
|
+
|
|
4
|
+
let currentPath = 'App.neo';
|
|
5
|
+
|
|
6
|
+
async function render() {
|
|
7
|
+
const app = document.getElementById('app');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(`../src/${currentPath}`);
|
|
11
|
+
const rawCode = await response.text();
|
|
12
|
+
|
|
13
|
+
const parsed = NeoParser.parse(rawCode);
|
|
14
|
+
const ui = NeoCore.create(parsed);
|
|
15
|
+
|
|
16
|
+
app.replaceChildren(ui);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
app.innerHTML = `<pre style="color:red">${e.message}</pre>`;
|
|
19
|
+
console.error(e);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// async function loadUserCode(path) {
|
|
24
|
+
// const res = await fetch(path);
|
|
25
|
+
// const code = await res.text();
|
|
26
|
+
|
|
27
|
+
// // ⭐ window 스코프에서 실행
|
|
28
|
+
// new Function(`
|
|
29
|
+
// with (window) {
|
|
30
|
+
// ${code}
|
|
31
|
+
// }
|
|
32
|
+
// `)();
|
|
33
|
+
// }
|
|
34
|
+
|
|
35
|
+
window.__neoRender = render;
|
|
36
|
+
// await loadUserCode('../src/actions.js');
|
|
37
|
+
render();
|
package/core/NeoCore.js
CHANGED
|
@@ -1,71 +1,36 @@
|
|
|
1
1
|
export class NeoCore {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
this.rootRenderFn = rootRenderFn;
|
|
5
|
-
|
|
6
|
-
this.state = new Proxy(state, {
|
|
7
|
-
set: (target, key, value) => {
|
|
8
|
-
target[key] = value;
|
|
9
|
-
this.mount();
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
mount() {
|
|
16
|
-
if (!this.container) return;
|
|
17
|
-
this.container.innerHTML = '';
|
|
18
|
-
const domTree = this.rootRenderFn(this.state);
|
|
19
|
-
if (domTree) this.container.appendChild(domTree); // domTree가 있을 때만 추가
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function h(tag, props, children = []) {
|
|
24
|
-
const el = document.createElement(tag);
|
|
2
|
+
static create(data) {
|
|
3
|
+
const el = document.createElement(data.tag || 'div');
|
|
25
4
|
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
// 2. [Neo 철학] innerHTML을 '먼저' 설정
|
|
30
|
-
// 이렇게 해야 나중에 appendChild로 들어오는 자식들이 이 텍스트 '뒤에' 붙을 수 있어
|
|
31
|
-
const content = props.innerHTML || props.innerHtml;
|
|
32
|
-
if (content !== undefined) {
|
|
33
|
-
el.innerHTML = content;
|
|
5
|
+
if (data.id) el.id = data.id;
|
|
6
|
+
if (Array.isArray(data.styles)) {
|
|
7
|
+
el.className = data.styles.join(' ');
|
|
34
8
|
}
|
|
35
9
|
|
|
36
|
-
|
|
37
|
-
Object.keys(props).forEach(key => {
|
|
38
|
-
const reserved = ['id', 'style', 'innerHTML', 'innerHtml'];
|
|
39
|
-
|
|
40
|
-
// 이미 처리한 예약어(id, style, html)는 건너뛰기
|
|
41
|
-
if (reserved.includes(key)) return;
|
|
10
|
+
el.innerHTML = NeoCore.renderTemplate(data.innerHTML);
|
|
42
11
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
// 일반 속성(type, placeholder, class 등) 처리
|
|
49
|
-
else {
|
|
50
|
-
el.setAttribute(key, props[key]);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
12
|
+
(data.events || []).forEach(evt => {
|
|
13
|
+
el.addEventListener(evt.type, () => {
|
|
14
|
+
try {
|
|
15
|
+
new Function(evt.action)();
|
|
53
16
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// 자식이 실제 DOM 노드(태그)인 경우에만 추가
|
|
61
|
-
if (child instanceof HTMLElement || child instanceof Node) {
|
|
62
|
-
el.appendChild(child);
|
|
63
|
-
}
|
|
64
|
-
// 혹시나 문자열이 자식으로 들어왔을 때를 대비한 안전장치 (필요시)
|
|
65
|
-
else if (typeof child === 'string' || typeof child === 'number') {
|
|
66
|
-
el.appendChild(document.createTextNode(String(child)));
|
|
17
|
+
if (window.__neoRender) {
|
|
18
|
+
window.__neoRender();
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error("Neo Event Error:", e);
|
|
67
22
|
}
|
|
23
|
+
});
|
|
68
24
|
});
|
|
69
25
|
|
|
70
26
|
return el;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static renderTemplate(template = "") {
|
|
30
|
+
return template.replace(/\$([\w.]+)/g, (_, path) => {
|
|
31
|
+
return path
|
|
32
|
+
.split('.')
|
|
33
|
+
.reduce((obj, key) => obj?.[key], window) ?? '';
|
|
34
|
+
});
|
|
35
|
+
}
|
|
71
36
|
}
|
package/index.html
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Neo App</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
</head>
|
|
3
9
|
<body>
|
|
4
|
-
|
|
5
|
-
<script type="module">
|
|
6
|
-
import { NeoCore } from './core/NeoCore.js';
|
|
7
|
-
import render from './src/App.js'; // 컴파일러가 만든 결과물!
|
|
10
|
+
<div id="app"></div>
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
<!-- 유저 로직 (ESM) -->
|
|
13
|
+
<script type="module">
|
|
14
|
+
import './src/state.js';
|
|
15
|
+
import './src/actions.js';
|
|
16
|
+
</script>
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if(e.target.id === 'Btn') neo.state.count++;
|
|
16
|
-
});
|
|
17
|
-
</script>
|
|
18
|
+
<!-- Neo 런타임 -->
|
|
19
|
+
<script type="module" src="./compiler/main.js"></script>
|
|
18
20
|
</body>
|
|
19
21
|
</html>
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junnyontop-pixel/neo-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "나만의 커스텀 프레임워크 Neo",
|
|
5
5
|
"main": "core/NeoCore.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"neoc": "compiler/index.js",
|
|
9
8
|
"neoc-init": "scripts/init.js"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
@@ -17,7 +16,6 @@
|
|
|
17
16
|
"README.md"
|
|
18
17
|
],
|
|
19
18
|
"scripts": {
|
|
20
|
-
"prepare": "node compiler/index.js src/App.neo",
|
|
21
19
|
"build": "node compiler/index.js src/App.neo",
|
|
22
20
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
23
21
|
},
|
package/src/App.neo
CHANGED
package/src/actions.js
ADDED
package/src/state.js
ADDED
package/compiler/index.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { NeoParser } from './NeoParser.js';
|
|
5
|
-
|
|
6
|
-
// 1. 입력 파일 경로 확인
|
|
7
|
-
const inputFile = process.argv[2];
|
|
8
|
-
|
|
9
|
-
if (!inputFile) {
|
|
10
|
-
console.error("❌ 컴파일할 .neo 파일을 입력해주세요. (예: npx neoc src/App.neo)");
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// 2. 소스 코드 읽기 및 파싱
|
|
15
|
-
try {
|
|
16
|
-
const source = fs.readFileSync(inputFile, 'utf8');
|
|
17
|
-
const { root, scriptContent } = NeoParser.parse(source);
|
|
18
|
-
|
|
19
|
-
if (!root) {
|
|
20
|
-
throw new Error("파싱 결과가 비어있습니다. .neo 파일의 형식을 확인해주세요.");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 3. 코드 생성 함수 (재귀 구조)
|
|
24
|
-
function generateCode(node, indent = " ") {
|
|
25
|
-
// 1. 자식들은 태그들만 (글자는 여기서 완전히 제외!)
|
|
26
|
-
const childrenCode = node.children
|
|
27
|
-
.map(child => generateCode(child, indent + " "))
|
|
28
|
-
.join(',\n');
|
|
29
|
-
|
|
30
|
-
// 2. 속성 객체 만들기 (스타일도 여기에 포함!)
|
|
31
|
-
const mergedProps = {
|
|
32
|
-
id: node.id,
|
|
33
|
-
style: node.styles, // 스타일을 객체 안으로 넣음
|
|
34
|
-
...node.props
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// 3. innerHTML이 있으면 객체에 추가
|
|
38
|
-
if (node.innerHtml) {
|
|
39
|
-
const processedText = node.innerHtml.replace(/\$(\w+)/g, '${state.$1}');
|
|
40
|
-
mergedProps.innerHTML = `__BT__${processedText}__BT__`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 4. 이벤트 처리
|
|
44
|
-
const eventProps = Object.entries(node.events).map(([evt, action]) => {
|
|
45
|
-
const propName = `on${evt.charAt(0).toUpperCase() + evt.slice(1)}`;
|
|
46
|
-
let processedAction = action.replace(/\$(\w+)/g, 'state.$1');
|
|
47
|
-
return `"${propName}": () => { ${processedAction} }`;
|
|
48
|
-
}).join(`,\n${indent} `);
|
|
49
|
-
|
|
50
|
-
// 5. Props 객체를 문자열로 바꾸고 백틱 마법 적용
|
|
51
|
-
let propsContent = JSON.stringify(mergedProps, null, 4)
|
|
52
|
-
.replace(/"__BT__(.*?)__BT__"/g, '`$1`');
|
|
53
|
-
|
|
54
|
-
// 6. 최종 h 함수 호출 (글자 노드 없이 childrenCode만 넣음)
|
|
55
|
-
return `${indent}h('${node.tag}', ${propsContent.replace(/\n/g, '\n' + indent)}${eventProps ? ',\n' + indent + ' ' + eventProps : ''}, [${childrenCode ? '\n' + childrenCode + '\n' + indent : ''}])`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// render 함수 생성 부분
|
|
59
|
-
const finalJS = `
|
|
60
|
-
import { h } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
61
|
-
|
|
62
|
-
export default function render(state) {
|
|
63
|
-
// 값이 없을 때만 초기화 (중요: 매번 0으로 덮어쓰지 않음)
|
|
64
|
-
if (state.count === undefined) state.count = 0;
|
|
65
|
-
if (state.title === undefined) state.title = "hello, neo";
|
|
66
|
-
|
|
67
|
-
return ${generateCode(root).trimStart()};
|
|
68
|
-
}
|
|
69
|
-
`.trim();
|
|
70
|
-
|
|
71
|
-
// 5. .js 파일로 저장
|
|
72
|
-
const outputPath = inputFile.replace('.neo', '.js');
|
|
73
|
-
fs.writeFileSync(outputPath, finalJS);
|
|
74
|
-
|
|
75
|
-
console.log(`✅ [컴파일 성공] -> ${outputPath}`);
|
|
76
|
-
|
|
77
|
-
} catch (err) {
|
|
78
|
-
console.error("❌ 컴파일 중 에러 발생:");
|
|
79
|
-
console.error(err.message);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
package/scripts/init.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
// 1. 템플릿 정의
|
|
6
|
-
const htmlContent = `<!DOCTYPE html>
|
|
7
|
-
<html>
|
|
8
|
-
<head>
|
|
9
|
-
<meta charset="UTF-8">
|
|
10
|
-
<title>My Neo App</title>
|
|
11
|
-
</head>
|
|
12
|
-
<body>
|
|
13
|
-
<div id="app"></div>
|
|
14
|
-
<script type="module">
|
|
15
|
-
import { NeoCore } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
16
|
-
import render from './src/App.js';
|
|
17
|
-
|
|
18
|
-
const state = { title: "Hello Neo!", count: 0 };
|
|
19
|
-
new NeoCore(state, render, 'app').mount();
|
|
20
|
-
</script>
|
|
21
|
-
</body>
|
|
22
|
-
</html>`;
|
|
23
|
-
|
|
24
|
-
const neoContent = `@Script {
|
|
25
|
-
// Logic here
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@Main:div {
|
|
29
|
-
Innerhtml: "🚀 $title"
|
|
30
|
-
Style(padding: 20px; text-align: center; font-family: sans-serif)
|
|
31
|
-
|
|
32
|
-
@Counter:button {
|
|
33
|
-
Innerhtml: "클릭 수: $count"
|
|
34
|
-
Style(background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer)
|
|
35
|
-
Event(click: $count++)
|
|
36
|
-
}
|
|
37
|
-
}`;
|
|
38
|
-
|
|
39
|
-
// 2. 파일 생성 로직
|
|
40
|
-
const targetDir = process.cwd(); // 명령어를 실행한 현재 폴더
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
if (!fs.existsSync(path.join(targetDir, 'src'))) {
|
|
44
|
-
fs.mkdirSync(path.join(targetDir, 'src'));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
fs.writeFileSync(path.join(targetDir, 'index.html'), htmlContent);
|
|
48
|
-
fs.writeFileSync(path.join(targetDir, 'src/App.neo'), neoContent);
|
|
49
|
-
|
|
50
|
-
console.log("✅ [Neo] index.html 및 src/App.neo 생성이 완료되었습니다!");
|
|
51
|
-
console.log("👉 이제 'npx neoc src/App.neo'를 실행하여 첫 컴파일을 완료하세요.");
|
|
52
|
-
} catch (err) {
|
|
53
|
-
console.error("❌ 초기화 중 에러 발생:", err.message);
|
|
54
|
-
}
|
package/src/App.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { h } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
2
|
-
|
|
3
|
-
export default function render(state) {
|
|
4
|
-
// 값이 없을 때만 초기화 (중요: 매번 0으로 덮어쓰지 않음)
|
|
5
|
-
if (state.count === undefined) state.count = 0;
|
|
6
|
-
if (state.title === undefined) state.title = "hello, neo";
|
|
7
|
-
|
|
8
|
-
return h('button', {
|
|
9
|
-
"id": "Btn",
|
|
10
|
-
"style": {},
|
|
11
|
-
"innerHTML": `인사하기`
|
|
12
|
-
},
|
|
13
|
-
"onClick": () => { sayHello( }, []);
|
|
14
|
-
}
|