@baokaibo/nanofront 1.0.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 ADDED
@@ -0,0 +1,64 @@
1
+ # NanoFront
2
+
3
+ 极简类 Vue 前端框架,零依赖,体积 < 10KB
4
+
5
+ ## 特性
6
+
7
+ - 🌀 **响应式数据** - 基于 Proxy 的响应式系统
8
+ - 📝 **模板引擎** - 支持 Mustache 语法、`n-if`、`n-for`、`n-show` 指令
9
+ - 🔗 **事件绑定** - 简洁的 `@click` 等事件绑定
10
+ - 🧩 **组件系统** - 支持组件定义和复用
11
+ - 🛤️ **Hash 路由** - 轻量级 Hash 路由
12
+ - 📦 **全局状态** - 内置响应式状态管理
13
+ - 🌐 **HTTP 模块** - 简洁的请求封装
14
+ - ⚡ **CLI 工具** - `nanofront` 命令快速创建项目
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ npm install nanofront
20
+ ```
21
+
22
+ ## 快速开始
23
+
24
+ ```javascript
25
+ import Nano from 'nanofront';
26
+
27
+ // 定义组件
28
+ const Home = {
29
+ template: `
30
+ <div>
31
+ <h1>首页</h1>
32
+ <p>欢迎使用 NanoFront!</p>
33
+ </div>
34
+ `
35
+ };
36
+
37
+ // 注册路由
38
+ Nano.router([
39
+ { path: '/', component: Home },
40
+ { path: '*', component: Home }
41
+ ]);
42
+
43
+ // 启动应用
44
+ Nano.start('#app');
45
+ ```
46
+
47
+ ## CLI 使用
48
+
49
+ 安装后,可以直接运行 `nanofront` 命令启动项目创建器:
50
+
51
+ ```bash
52
+ npx nanofront
53
+ # 或全局安装后
54
+ npm install -g nanofront
55
+ nanofront
56
+ ```
57
+
58
+ ## 文档
59
+
60
+ 更多用法请查看 [demo/index.html](./demo/index.html)
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'child_process';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, resolve } from 'path';
6
+ import fs from 'fs';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const packageDir = resolve(__dirname, '..');
10
+
11
+ async function startServer() {
12
+ const port = 3000;
13
+ const url = `http://localhost:${port}`;
14
+
15
+ console.log('🚀 启动 NanoFront 项目创建器...\n');
16
+ console.log(`📍 访问 ${url} 创建您的项目\n`);
17
+
18
+ // 检查本地 node_modules 是否存在
19
+ const nodeModules = resolve(packageDir, 'node_modules');
20
+ const needsInstall = !fs.existsSync(nodeModules);
21
+
22
+ if (needsInstall) {
23
+ console.log('📦 正在安装依赖...\n');
24
+ await new Promise((resolve, reject) => {
25
+ const install = spawn('npm', ['install'], {
26
+ cwd: packageDir,
27
+ stdio: 'inherit',
28
+ shell: true
29
+ });
30
+ install.on('close', (code) => {
31
+ if (code === 0) resolve();
32
+ else reject(new Error('安装依赖失败'));
33
+ });
34
+ });
35
+ }
36
+
37
+ // 使用 Vite 启动开发服务器
38
+ const vite = spawn('npx', ['vite', '--port', String(port)], {
39
+ cwd: packageDir,
40
+ stdio: 'inherit',
41
+ shell: true,
42
+ env: {
43
+ ...process.env,
44
+ FORCE_COLOR: '1'
45
+ }
46
+ });
47
+
48
+ // 自动打开浏览器
49
+ setTimeout(() => {
50
+ const isWindows = process.platform === 'win32';
51
+ const command = isWindows ? 'start' : 'open';
52
+ const args = isWindows ? [url] : [url];
53
+
54
+ spawn(command, args, {
55
+ detached: true,
56
+ stdio: 'ignore',
57
+ shell: true
58
+ }).unref();
59
+ }, 1500);
60
+
61
+ vite.on('close', (code) => {
62
+ process.exit(code);
63
+ });
64
+ }
65
+
66
+ startServer().catch(err => {
67
+ console.error(err.message);
68
+ process.exit(1);
69
+ });
@@ -0,0 +1,66 @@
1
+ // Component system
2
+ import { reactive, effect } from '../reactive/index.js';
3
+ import { render } from '../render/index.js';
4
+
5
+ const components = new Map();
6
+
7
+ function component(name, options) {
8
+ components.set(name, options);
9
+ }
10
+
11
+ function mount(selector, options) {
12
+ const container = typeof selector === 'string'
13
+ ? document.querySelector(selector)
14
+ : selector;
15
+
16
+ if (!container) throw new Error(`Cannot find element: ${selector}`);
17
+
18
+ const data = reactive(
19
+ typeof options.data === 'function' ? options.data() : (options.data || {})
20
+ );
21
+
22
+ const methods = options.methods || {};
23
+
24
+ // Bind methods to data context
25
+ Object.keys(methods).forEach(key => {
26
+ methods[key] = methods[key].bind(data);
27
+ });
28
+
29
+ effect(() => {
30
+ render(container, options.template, data, methods);
31
+ // Mount child components after render
32
+ mountChildComponents(container, data);
33
+ // Call mounted hook if exists
34
+ if (options.mounted && typeof options.mounted === 'function') {
35
+ options.mounted.call(data);
36
+ }
37
+ });
38
+
39
+ return data;
40
+ }
41
+
42
+ function mountChildComponents(container, parentData) {
43
+ if (!container) return;
44
+ components.forEach((options, name) => {
45
+ try {
46
+ const elements = container.querySelectorAll(name);
47
+ elements.forEach(el => {
48
+ if (!el) return;
49
+ const data = reactive(
50
+ typeof options.data === 'function' ? options.data() : (options.data || {})
51
+ );
52
+ const methods = options.methods || {};
53
+ Object.keys(methods).forEach(key => {
54
+ methods[key] = methods[key].bind(data);
55
+ });
56
+ effect(() => {
57
+ render(el, options.template, data, methods);
58
+ });
59
+ });
60
+ } catch (e) {
61
+ // Ignore querySelectorAll errors
62
+ }
63
+ });
64
+ }
65
+
66
+ export { component, mount, components };
@@ -0,0 +1,34 @@
1
+ // HTTP module wrapping fetch
2
+ const http = {
3
+ async get(url, options = {}) {
4
+ const res = await fetch(url, { method: 'GET', ...options });
5
+ return res.json();
6
+ },
7
+
8
+ async post(url, body, options = {}) {
9
+ const res = await fetch(url, {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json', ...options.headers },
12
+ body: JSON.stringify(body),
13
+ ...options
14
+ });
15
+ return res.json();
16
+ },
17
+
18
+ async put(url, body, options = {}) {
19
+ const res = await fetch(url, {
20
+ method: 'PUT',
21
+ headers: { 'Content-Type': 'application/json', ...options.headers },
22
+ body: JSON.stringify(body),
23
+ ...options
24
+ });
25
+ return res.json();
26
+ },
27
+
28
+ async delete(url, options = {}) {
29
+ const res = await fetch(url, { method: 'DELETE', ...options });
30
+ return res.json();
31
+ }
32
+ };
33
+
34
+ export { http };
@@ -0,0 +1,47 @@
1
+ export interface ComponentOptions {
2
+ template: string;
3
+ data?: () => Record<string, unknown>;
4
+ methods?: Record<string, (...args: unknown[]) => unknown>;
5
+ }
6
+
7
+ export interface RouteConfig {
8
+ path: string;
9
+ component: ComponentOptions;
10
+ }
11
+
12
+ export interface StoreState extends Record<string, unknown> {}
13
+
14
+ export interface Http {
15
+ get<T = unknown>(url: string, options?: RequestInit): Promise<T>;
16
+ post<T = unknown>(url: string, body: unknown, options?: RequestInit): Promise<T>;
17
+ put<T = unknown>(url: string, body: unknown, options?: RequestInit): Promise<T>;
18
+ delete<T = unknown>(url: string, options?: RequestInit): Promise<T>;
19
+ }
20
+
21
+ export declare function reactive<T extends object>(data: T): T;
22
+ export declare function effect(fn: () => void): void;
23
+ export declare function component(name: string, options: ComponentOptions): void;
24
+ export declare function mount(selector: string | Element, options: ComponentOptions): Record<string, unknown>;
25
+ export declare function router(routes: RouteConfig[]): void;
26
+ export declare function start(containerSelector?: string): void;
27
+ export declare function navigate(path: string): void;
28
+ export declare function store<T extends StoreState>(initialState: T): T;
29
+ export declare function getStore<T extends StoreState>(): T | null;
30
+ export declare function subscribe(fn: (state: StoreState) => void): () => void;
31
+ export declare const http: Http;
32
+
33
+ declare const Nano: {
34
+ reactive: typeof reactive;
35
+ effect: typeof effect;
36
+ component: typeof component;
37
+ mount: typeof mount;
38
+ router: typeof router;
39
+ start: typeof start;
40
+ navigate: typeof navigate;
41
+ store: typeof store;
42
+ getStore: typeof getStore;
43
+ subscribe: typeof subscribe;
44
+ http: Http;
45
+ };
46
+
47
+ export default Nano;
package/nano/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // NanoFront - main entry point
2
+ import { reactive, effect } from './reactive/index.js';
3
+ import { component, mount } from './core/index.js';
4
+ import { router, start, navigate } from './router/index.js';
5
+ import { store, getStore, subscribe } from './store/index.js';
6
+ import { http } from './http/index.js';
7
+
8
+ const Nano = {
9
+ reactive,
10
+ effect,
11
+ component,
12
+ mount,
13
+ router,
14
+ start,
15
+ navigate,
16
+ store,
17
+ getStore,
18
+ subscribe,
19
+ http
20
+ };
21
+
22
+ export default Nano;
23
+ export { reactive, effect, component, mount, router, start, navigate, store, getStore, subscribe, http };
@@ -0,0 +1,202 @@
1
+ /*!
2
+ * NanoFront v1.0.0
3
+ * A minimal Vue-like frontend framework, zero dependencies, < 10KB
4
+ */
5
+ (function (global) {
6
+ 'use strict';
7
+
8
+ // ── Reactive ────────────────────────────────────────────────────────────────
9
+ let activeEffect = null;
10
+
11
+ function effect(fn) {
12
+ activeEffect = fn;
13
+ fn();
14
+ activeEffect = null;
15
+ }
16
+
17
+ function reactive(data) {
18
+ const deps = new Map();
19
+ function getDep(key) {
20
+ if (!deps.has(key)) deps.set(key, new Set());
21
+ return deps.get(key);
22
+ }
23
+ return new Proxy(data, {
24
+ get(target, key) {
25
+ if (activeEffect) getDep(key).add(activeEffect);
26
+ return target[key];
27
+ },
28
+ set(target, key, value) {
29
+ target[key] = value;
30
+ getDep(key).forEach(fn => fn());
31
+ return true;
32
+ }
33
+ });
34
+ }
35
+
36
+ // ── Render ──────────────────────────────────────────────────────────────────
37
+ function compile(template, data) {
38
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => {
39
+ const val = data[key];
40
+ return val !== undefined ? val : '';
41
+ });
42
+ }
43
+
44
+ function bindEvents(el, methods, data) {
45
+ const events = ['click', 'input', 'change', 'submit'];
46
+ events.forEach(evt => {
47
+ el.querySelectorAll('*').forEach(node => {
48
+ const method = node.getAttribute('@' + evt);
49
+ if (!method) return;
50
+ node.removeAttribute('@' + evt);
51
+ if (methods && typeof methods[method] === 'function') {
52
+ node.addEventListener(evt, e => methods[method].call(data, e));
53
+ }
54
+ });
55
+ });
56
+ }
57
+
58
+ function render(container, template, data, methods) {
59
+ const html = compile(template, data);
60
+ container.innerHTML = html;
61
+ bindEvents(container, methods, data);
62
+ }
63
+
64
+ // ── Component ───────────────────────────────────────────────────────────────
65
+ const components = new Map();
66
+
67
+ function component(name, options) {
68
+ components.set(name, options);
69
+ }
70
+
71
+ function mount(selector, options) {
72
+ const container = typeof selector === 'string'
73
+ ? document.querySelector(selector)
74
+ : selector;
75
+ if (!container) throw new Error('Cannot find element: ' + selector);
76
+
77
+ const data = reactive(
78
+ typeof options.data === 'function' ? options.data() : (options.data || {})
79
+ );
80
+ const methods = options.methods || {};
81
+ Object.keys(methods).forEach(key => { methods[key] = methods[key].bind(data); });
82
+
83
+ effect(() => {
84
+ render(container, options.template, data, methods);
85
+ mountChildComponents(container);
86
+ });
87
+
88
+ return data;
89
+ }
90
+
91
+ function mountChildComponents(container) {
92
+ components.forEach((options, name) => {
93
+ container.querySelectorAll(name).forEach(el => {
94
+ const data = reactive(
95
+ typeof options.data === 'function' ? options.data() : (options.data || {})
96
+ );
97
+ const methods = options.methods || {};
98
+ Object.keys(methods).forEach(key => { methods[key] = methods[key].bind(data); });
99
+ effect(() => { render(el, options.template, data, methods); });
100
+ });
101
+ });
102
+ }
103
+
104
+ // ── Router ──────────────────────────────────────────────────────────────────
105
+ const routes = new Map();
106
+ let currentView = null;
107
+ let outlet = null;
108
+
109
+ function router(config) {
110
+ config.forEach(({ path, component: comp }) => routes.set(path, comp));
111
+ }
112
+
113
+ function start(containerSelector) {
114
+ outlet = typeof containerSelector === 'string'
115
+ ? document.querySelector(containerSelector || '#app')
116
+ : containerSelector;
117
+ window.addEventListener('hashchange', resolve);
118
+ resolve();
119
+ }
120
+
121
+ function resolve() {
122
+ const hash = location.hash.slice(1) || '/';
123
+ const comp = routes.get(hash) || routes.get('*');
124
+ if (!comp || !outlet) return;
125
+ if (currentView === comp) return;
126
+ currentView = comp;
127
+ mount(outlet, comp);
128
+ }
129
+
130
+ function navigate(path) {
131
+ location.hash = path;
132
+ }
133
+
134
+ // ── Store ───────────────────────────────────────────────────────────────────
135
+ let _store = null;
136
+ const subscribers = new Set();
137
+
138
+ function store(initialState) {
139
+ _store = reactive(initialState);
140
+ Object.keys(initialState).forEach(key => {
141
+ effect(() => {
142
+ void _store[key];
143
+ subscribers.forEach(fn => fn(_store));
144
+ });
145
+ });
146
+ return _store;
147
+ }
148
+
149
+ function getStore() { return _store; }
150
+
151
+ function subscribe(fn) {
152
+ subscribers.add(fn);
153
+ return () => subscribers.delete(fn);
154
+ }
155
+
156
+ // ── HTTP ────────────────────────────────────────────────────────────────────
157
+ const http = {
158
+ async get(url, options) {
159
+ const res = await fetch(url, Object.assign({ method: 'GET' }, options));
160
+ return res.json();
161
+ },
162
+ async post(url, body, options) {
163
+ options = options || {};
164
+ const res = await fetch(url, Object.assign({
165
+ method: 'POST',
166
+ headers: Object.assign({ 'Content-Type': 'application/json' }, options.headers),
167
+ body: JSON.stringify(body)
168
+ }, options));
169
+ return res.json();
170
+ },
171
+ async put(url, body, options) {
172
+ options = options || {};
173
+ const res = await fetch(url, Object.assign({
174
+ method: 'PUT',
175
+ headers: Object.assign({ 'Content-Type': 'application/json' }, options.headers),
176
+ body: JSON.stringify(body)
177
+ }, options));
178
+ return res.json();
179
+ },
180
+ async delete(url, options) {
181
+ const res = await fetch(url, Object.assign({ method: 'DELETE' }, options));
182
+ return res.json();
183
+ }
184
+ };
185
+
186
+ // ── Public API ──────────────────────────────────────────────────────────────
187
+ const Nano = {
188
+ reactive, effect,
189
+ component, mount,
190
+ router, start, navigate,
191
+ store, getStore, subscribe,
192
+ http
193
+ };
194
+
195
+ // UMD export
196
+ if (typeof module !== 'undefined' && module.exports) {
197
+ module.exports = Nano;
198
+ } else {
199
+ global.Nano = Nano;
200
+ }
201
+
202
+ }(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this));
@@ -0,0 +1,31 @@
1
+ // Reactive system using Proxy
2
+ let activeEffect = null;
3
+
4
+ function effect(fn) {
5
+ activeEffect = fn;
6
+ fn();
7
+ activeEffect = null;
8
+ }
9
+
10
+ function reactive(data) {
11
+ const deps = new Map();
12
+
13
+ function getDep(key) {
14
+ if (!deps.has(key)) deps.set(key, new Set());
15
+ return deps.get(key);
16
+ }
17
+
18
+ return new Proxy(data, {
19
+ get(target, key) {
20
+ if (activeEffect) getDep(key).add(activeEffect);
21
+ return target[key];
22
+ },
23
+ set(target, key, value) {
24
+ target[key] = value;
25
+ getDep(key).forEach(fn => fn());
26
+ return true;
27
+ }
28
+ });
29
+ }
30
+
31
+ export { reactive, effect };
@@ -0,0 +1,272 @@
1
+ // Template engine: Mustache interpolation + n-if + n-for + n-show + event binding
2
+
3
+ // Helper: evaluate expression with data context
4
+ function evalExpr(expr, data) {
5
+ try {
6
+ const keys = Object.keys(data);
7
+ const values = Object.values(data);
8
+ const fn = new Function(...keys, `return (${expr})`);
9
+ return fn(...values);
10
+ } catch (e) {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ // Process n-for: <tag n-for="item in array">...</tag>
16
+ function processForLoop(template, data) {
17
+ // Match elements with n-for attribute
18
+ const forRegex = /<([a-z]+)\s+n-for="(\w+)\s+in\s+(\w+)"[^>]*>/gi;
19
+
20
+ let result = template;
21
+ let match;
22
+ const matches = [];
23
+
24
+ // Find all n-for elements
25
+ while ((match = forRegex.exec(template)) !== null) {
26
+ matches.push({
27
+ fullMatch: match[0],
28
+ tagName: match[1],
29
+ itemName: match[2],
30
+ arrayName: match[3],
31
+ startIndex: match.index
32
+ });
33
+ }
34
+
35
+ // Process from end to start to preserve indices
36
+ for (let i = matches.length - 1; i >= 0; i--) {
37
+ const m = matches[i];
38
+ const array = data[m.arrayName];
39
+
40
+ // If no array or not array, remove element
41
+ if (!array || !Array.isArray(array) || array.length === 0) {
42
+ const closeTag = `</${m.tagName}>`;
43
+ const closeIndex = template.indexOf(closeTag, m.startIndex);
44
+ if (closeIndex !== -1) {
45
+ const fullElement = template.substring(m.startIndex, closeIndex + closeTag.length);
46
+ result = result.replace(fullElement, '');
47
+ }
48
+ continue;
49
+ }
50
+
51
+ // Find the element's content in original template
52
+ const startTagEnd = m.startIndex + m.fullMatch.length;
53
+ const closeTag = `</${m.tagName}>`;
54
+ const closeIndex = template.indexOf(closeTag, startTagEnd);
55
+
56
+ if (closeIndex === -1) continue;
57
+
58
+ const innerTemplate = template.substring(startTagEnd, closeIndex);
59
+
60
+ // Generate HTML for each item
61
+ let itemsHtml = '';
62
+ array.forEach((item, idx) => {
63
+ let processed = innerTemplate;
64
+
65
+ // Process nested n-for first (recursively)
66
+ processed = processForLoop(processed, { ...data, [m.itemName]: item, index: idx });
67
+
68
+ // Process n-if inside the loop
69
+ processed = processIfInLoop(processed, item, idx, m.itemName);
70
+
71
+ // Replace {{item.name}} and {{item.age}} patterns
72
+ processed = processed.replace(/\{\{\s*(\w+)\.(\w+)\s*\}\}/g, (fullMatch, obj, prop) => {
73
+ if (obj === m.itemName && item && typeof item === 'object') {
74
+ return item[prop] !== undefined ? String(item[prop]) : '';
75
+ }
76
+ return fullMatch;
77
+ });
78
+
79
+ // Replace {{item}} pattern
80
+ processed = processed.replace(/\{\{\s*(\w+)\s*\}\}/g, (fullMatch, key) => {
81
+ if (key === m.itemName) {
82
+ return typeof item === 'object' ? '' : String(item);
83
+ }
84
+ // Check data context
85
+ if (data[key] !== undefined) {
86
+ return String(data[key]);
87
+ }
88
+ return fullMatch;
89
+ });
90
+
91
+ itemsHtml += processed;
92
+ });
93
+
94
+ // Replace the entire n-for element with generated items
95
+ const fullElement = template.substring(m.startIndex, closeIndex + closeTag.length);
96
+ result = result.replace(fullElement, itemsHtml);
97
+ }
98
+
99
+ return result;
100
+ }
101
+
102
+ // Process n-if inside n-for loop
103
+ function processIfInLoop(template, item, idx, itemName) {
104
+ let result = template;
105
+
106
+ const ifRegex = /<([a-z]+)\s+n-if="([^"]+)"[^>]*>/gi;
107
+ let match;
108
+ const matches = [];
109
+
110
+ while ((match = ifRegex.exec(template)) !== null) {
111
+ matches.push({
112
+ fullMatch: match[0],
113
+ tagName: match[1],
114
+ condition: match[2],
115
+ startIndex: match.index
116
+ });
117
+ }
118
+
119
+ for (let i = matches.length - 1; i >= 0; i--) {
120
+ const m = matches[i];
121
+
122
+ // Evaluate condition
123
+ let truthy = false;
124
+ try {
125
+ if (m.condition.includes(itemName)) {
126
+ // Handle item.property access
127
+ if (m.condition.includes('.')) {
128
+ const parts = m.condition.split('.');
129
+ const prop = parts[parts.length - 1];
130
+ truthy = !!(item && item[prop]);
131
+ } else {
132
+ truthy = !!item;
133
+ }
134
+ } else {
135
+ truthy = !!idx;
136
+ }
137
+ } catch (e) {
138
+ truthy = false;
139
+ }
140
+
141
+ const startTagEnd = m.startIndex + m.fullMatch.length;
142
+ const closeTag = `</${m.tagName}>`;
143
+ const closeIndex = result.indexOf(closeTag, startTagEnd);
144
+
145
+ if (closeIndex === -1) continue;
146
+
147
+ const innerContent = result.substring(startTagEnd, closeIndex);
148
+ const fullElement = result.substring(m.startIndex, closeIndex + closeTag.length);
149
+
150
+ result = result.replace(fullElement, truthy ? innerContent : '');
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ // Process n-if at root level
157
+ function processIfDirective(template, data) {
158
+ let result = template;
159
+
160
+ const ifRegex = /<([a-z]+)\s+n-if="([^"]+)"[^>]*>/gi;
161
+ let match;
162
+ const matches = [];
163
+
164
+ while ((match = ifRegex.exec(template)) !== null) {
165
+ matches.push({
166
+ fullMatch: match[0],
167
+ tagName: match[1],
168
+ condition: match[2],
169
+ startIndex: match.index
170
+ });
171
+ }
172
+
173
+ for (let i = matches.length - 1; i >= 0; i--) {
174
+ const m = matches[i];
175
+ const truthy = evalExpr(m.condition, data);
176
+
177
+ const startTagEnd = m.startIndex + m.fullMatch.length;
178
+ const closeTag = `</${m.tagName}>`;
179
+ const closeIndex = result.indexOf(closeTag, startTagEnd);
180
+
181
+ if (closeIndex === -1) continue;
182
+
183
+ const innerContent = result.substring(startTagEnd, closeIndex);
184
+ const fullElement = result.substring(m.startIndex, closeIndex + closeTag.length);
185
+
186
+ result = result.replace(fullElement, truthy ? innerContent : '');
187
+ }
188
+
189
+ return result;
190
+ }
191
+
192
+ // Process n-show at root level
193
+ function processShowDirective(template, data) {
194
+ let result = template;
195
+
196
+ const showRegex = /<([a-z]+)\s+n-show="([^"]+)"([^>]*)>/gi;
197
+ let match;
198
+ const matches = [];
199
+
200
+ while ((match = showRegex.exec(template)) !== null) {
201
+ matches.push({
202
+ fullMatch: match[0],
203
+ tagName: match[1],
204
+ condition: match[2],
205
+ otherAttrs: match[3],
206
+ startIndex: match.index
207
+ });
208
+ }
209
+
210
+ for (let i = matches.length - 1; i >= 0; i--) {
211
+ const m = matches[i];
212
+ const truthy = evalExpr(m.condition, data);
213
+
214
+ if (!truthy) {
215
+ const replacement = `<${m.tagName}${m.otherAttrs} style="display:none">`;
216
+ result = result.replace(m.fullMatch, replacement);
217
+ }
218
+ }
219
+
220
+ return result;
221
+ }
222
+
223
+ function compile(template, data) {
224
+ let result = template;
225
+
226
+ // 1. Process n-for (includes nested n-if)
227
+ result = processForLoop(result, data);
228
+
229
+ // 2. Process n-show at root level
230
+ result = processShowDirective(result, data);
231
+
232
+ // 3. Process n-if at root level
233
+ result = processIfDirective(result, data);
234
+
235
+ // 4. Mustache interpolation
236
+ result = result.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => {
237
+ const val = data[key];
238
+ return val !== undefined ? val : '';
239
+ });
240
+
241
+ return result;
242
+ }
243
+
244
+ function bindEvents(el, methods, data) {
245
+ if (!el) return;
246
+
247
+ const events = ['click', 'input', 'change', 'submit'];
248
+ events.forEach(evt => {
249
+ try {
250
+ const nodes = el.querySelectorAll('*');
251
+ nodes.forEach(node => {
252
+ if (!node) return;
253
+ const method = node.getAttribute(`@${evt}`);
254
+ if (!method) return;
255
+ node.removeAttribute(`@${evt}`);
256
+ if (methods && typeof methods[method] === 'function') {
257
+ node.addEventListener(evt, e => methods[method].call(data, e));
258
+ }
259
+ });
260
+ } catch (e) {
261
+ // Ignore querySelectorAll errors
262
+ }
263
+ });
264
+ }
265
+
266
+ function render(container, template, data, methods) {
267
+ const html = compile(template, data);
268
+ container.innerHTML = html;
269
+ bindEvents(container, methods, data);
270
+ }
271
+
272
+ export { compile, render, bindEvents };
@@ -0,0 +1,42 @@
1
+ // Hash-based router
2
+ import { mount } from '../core/index.js';
3
+
4
+ const routes = new Map();
5
+ let currentView = null;
6
+ let outlet = null;
7
+
8
+ function router(config) {
9
+ config.forEach(({ path, component }) => {
10
+ routes.set(path, component);
11
+ });
12
+ }
13
+
14
+ function start(containerSelector = '#app') {
15
+ outlet = typeof containerSelector === 'string'
16
+ ? document.querySelector(containerSelector)
17
+ : containerSelector;
18
+
19
+ window.addEventListener('hashchange', resolve);
20
+ resolve();
21
+ }
22
+
23
+ function resolve() {
24
+ const hash = location.hash.slice(1) || '/';
25
+ const component = routes.get(hash) || routes.get('*');
26
+ if (!component || !outlet) return;
27
+
28
+ if (currentView === component) return;
29
+ currentView = component;
30
+
31
+ try {
32
+ mount(outlet, component);
33
+ } catch (e) {
34
+ console.error('Mount error:', e);
35
+ }
36
+ }
37
+
38
+ function navigate(path) {
39
+ location.hash = path;
40
+ }
41
+
42
+ export { router, start, navigate };
@@ -0,0 +1,31 @@
1
+ // Global state store
2
+ import { reactive, effect } from '../reactive/index.js';
3
+
4
+ let _store = null;
5
+ const subscribers = new Set();
6
+
7
+ function store(initialState) {
8
+ _store = reactive(initialState);
9
+
10
+ // Notify subscribers on any change
11
+ const keys = Object.keys(initialState);
12
+ keys.forEach(key => {
13
+ effect(() => {
14
+ void _store[key]; // track
15
+ subscribers.forEach(fn => fn(_store));
16
+ });
17
+ });
18
+
19
+ return _store;
20
+ }
21
+
22
+ function getStore() {
23
+ return _store;
24
+ }
25
+
26
+ function subscribe(fn) {
27
+ subscribers.add(fn);
28
+ return () => subscribers.delete(fn);
29
+ }
30
+
31
+ export { store, getStore, subscribe };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@baokaibo/nanofront",
3
+ "version": "1.0.0",
4
+ "description": "极简类 Vue 前端框架,零依赖,< 10KB",
5
+ "type": "module",
6
+ "main": "./nano/index.js",
7
+ "module": "./nano/index.js",
8
+ "types": "./nano/index.d.ts",
9
+ "bin": {
10
+ "nanofront": "./bin/nanofront.js"
11
+ },
12
+ "preferGlobal": true,
13
+ "exports": {
14
+ ".": {
15
+ "import": "./nano/index.js",
16
+ "types": "./nano/index.d.ts"
17
+ }
18
+ },
19
+ "files": [
20
+ "nano",
21
+ "bin",
22
+ "package.json"
23
+ ],
24
+ "scripts": {
25
+ "dev": "vite",
26
+ "build": "vite build",
27
+ "preview": "vite preview"
28
+ },
29
+ "devDependencies": {
30
+ "vite": "^5.0.0"
31
+ },
32
+ "keywords": [
33
+ "framework",
34
+ "frontend",
35
+ "reactive",
36
+ "vue-like",
37
+ "minimal",
38
+ "cli"
39
+ ],
40
+ "license": "MIT"
41
+ }