@love-sqjm/magic 2026.4.15

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.
Files changed (56) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +93 -0
  3. package/app.js +51 -0
  4. package/doc/api/examples.md +563 -0
  5. package/doc/api/index.md +563 -0
  6. package/doc/architecture.md +322 -0
  7. package/doc/config-reference.md +646 -0
  8. package/doc/data-model.md +622 -0
  9. package/doc/development-guide.md +582 -0
  10. package/doc/magic-file/index.md +610 -0
  11. package/doc/user-guide/index.md +485 -0
  12. package/doc/workflow.md +548 -0
  13. package/index.js +157 -0
  14. package/magic.bat +2 -0
  15. package/magic.ps1 +5 -0
  16. package/package.json +44 -0
  17. package/script/build-project.js +16 -0
  18. package/script/config.js +23 -0
  19. package/script/create-project.js +73 -0
  20. package/script/global/printf.js +13 -0
  21. package/script/global/project-build-config.js +161 -0
  22. package/script/global/support-platform.js +5 -0
  23. package/script/module/compiler/global.js +43 -0
  24. package/script/module/compiler/id-generate.js +18 -0
  25. package/script/module/compiler/index-dom.js +78 -0
  26. package/script/module/compiler/macro-replace.js +22 -0
  27. package/script/module/compiler/macro.js +6 -0
  28. package/script/module/compiler/start.js +10 -0
  29. package/script/module/compiler/step/1.js +253 -0
  30. package/script/module/compiler/step/2.js +79 -0
  31. package/script/module/compiler/step/3.js +37 -0
  32. package/script/module/compiler/step/4.js +20 -0
  33. package/script/module/compiler/step/5.js +634 -0
  34. package/script/module/compiler/step/6.js +304 -0
  35. package/script/module/compiler/step/end.js +124 -0
  36. package/script/run-project.js +249 -0
  37. package/script/util/bun-fs.js +40 -0
  38. package/script/util/copy-dir.js +21 -0
  39. package/script/util/create-simple-dom-element.js +23 -0
  40. package/script/util/file-util.js +95 -0
  41. package/script/util/filtration-file.js +20 -0
  42. package/script/util/get-dir-all-file.js +28 -0
  43. package/script/util/get-first-object-key.js +9 -0
  44. package/script/util/is-empty-object.js +8 -0
  45. package/script/util/is-string-over-size.js +4 -0
  46. package/script/util/is.js +18 -0
  47. package/script/util/logging.js +142 -0
  48. package/script/util/task.js +16 -0
  49. package/script/util/traversal.js +28 -0
  50. package/template/platform-config/node-webkit +23 -0
  51. package/template/platform-config/web +1 -0
  52. package/template/project-base/app.xml +5 -0
  53. package/template/project-base/build.module.toml +37 -0
  54. package/template/project-base/build.toml +43 -0
  55. package/template/runtime/runtime.css +3 -0
  56. package/template/runtime/runtime.js +895 -0
@@ -0,0 +1,304 @@
1
+ import path from "node:path";
2
+ import { Config } from "../../../config.js";
3
+ import { copyDir } from "../../../util/copy-dir.js";
4
+ import { fileUtil } from "../../../util/file-util.js";
5
+ import { isStringOverSize } from "../../../util/is-string-over-size.js";
6
+ import { traversal } from "../../../util/traversal.js";
7
+ import { project } from "../global.js";
8
+ import { _end } from "./end.js";
9
+ import { bunFS } from "../../../util/bun-fs.js";
10
+
11
+ function asyncWriteFile( filePath, content ) {
12
+ return new Promise( ( resolve, reject ) => {
13
+ bunFS.writeFileSync( filePath, content );
14
+ resolve();
15
+ } );
16
+ }
17
+
18
+ function createMJs( md ) {
19
+ const data = md.data;
20
+
21
+ let element_var = "", ct = "", ct_number = 0, init_kw_id = "", init_kw_name = "", init_att = "", init_eve = "", sh = "", iei = "";
22
+
23
+ try {
24
+ traversal.object( data.template.var, ( v, i, k ) => {
25
+ element_var += `${ k },`;
26
+ ct_number++;
27
+ if ( v.type === "element" ) {
28
+ ct += `${ k } = e(\`${ v.tagName }\`)`;
29
+ } else if ( v.type === "import" ) {
30
+ let listen = false;
31
+ if ( v.keyword.listen ) {
32
+ listen = ",{ ";
33
+ traversal.object( v.keyword.listen, ( _v, i, _k ) => {
34
+ if ( _v === "" ) {
35
+ throw `你对一个元素使用了监听功能但是没有绑定事件`;
36
+ }
37
+ listen += `"${ _k }": __magic_listen_${ _v },`;
38
+ } );
39
+ listen += " }";
40
+ }
41
+ ct += `${ k } = i(\`${ v.import }\`,${ JSON.stringify( v.args ) }${ listen ? listen : "" })`;
42
+ } else if ( v.type === "text" ) {
43
+ ct += `${ k } = t(\`${ v.content.replaceAll( '\\', '\\\\' ).replace( /\$\{([^}]+)\}/g, '\\${$1}' ) }\`)`;
44
+ }
45
+
46
+ if ( v.type !== "text" ) {
47
+ if ( !isEmptyObject( v.attribs ) ) {
48
+ init_att += `att(${ k },${ JSON.stringify( v.attribs ) });\n`;
49
+ }
50
+
51
+ traversal.object( v.event, ( v, i, en ) => {
52
+ const evn = v[ 0 ];
53
+ if ( !data.event.list.includes( evn ) ) {
54
+ if ( evn === "" ) {
55
+ throw `你对一个元素绑定了空事件`;
56
+ } else {
57
+ throw `你对一个元素绑定了 ${ evn } 事件,但是没有实现 ${ evn } 事件`;
58
+ }
59
+ }
60
+ init_eve += `eve(${ k },"${ en }",this,"${ evn }",${ v[ 1 ] });\n`;
61
+ } );
62
+
63
+ if ( v.keyword.hasOwnProperty( "id" ) ) {
64
+ let r = "";
65
+ if ( !isEmptyObject( data.cssScope ) )
66
+ traversal.object( data.cssScope, ( _v, i, k ) => {
67
+ if ( k === v.keyword.id ) {
68
+ r = `,"${ _v }"`;
69
+ }
70
+ } );
71
+ init_kw_id += `this._$id.s("${ v.keyword.id }",${ k }${ r });\n`
72
+ }
73
+ if ( v.keyword.hasOwnProperty( "name" ) ) {
74
+ init_kw_name += `${ k }._$name = "${ v.keyword.name }";\n`
75
+ }
76
+ }
77
+ ct += ";\n"
78
+ } );
79
+
80
+ if ( element_var !== "" ) {
81
+ element_var = element_var.slice( 0, -1 );
82
+ element_var.split( ',' ).filter( ev => ev.at( 0 ) === "i" ).reverse().forEach( ev => {
83
+ iei += `${ ev },`;
84
+ } );
85
+ if ( iei !== "" ) {
86
+ iei = `magic.importElementInit(${ iei.slice( 0, -1 ) });`;
87
+ }
88
+ element_var = "let " + element_var;
89
+ }
90
+
91
+ data.template.sh.forEach( t => {
92
+ sh += `${ t }\n`;
93
+ } );
94
+ } catch ( e ) {
95
+ throw `${ e }\n[path:${ data.originalFile }]`
96
+ }
97
+ return `window[ "__MAGIC__" ][ "M" ][ "${ data.name }" ] = function ( __args__ , __listen__) {
98
+ const call = magic.call(this);
99
+ this._$id = magic.mapIdElement();
100
+ const $id = magic.$id(this._$id);
101
+ const _args = {_file:"${ data.originalFile }",_id: magic.idGenerate(),...magic.parserArgs(__args__)};
102
+ const _listen = magic.parserListen(__listen__);
103
+ const emit_event = magic.emit.event(_listen);
104
+
105
+ ${ data[ "use-element-id-list" ].length > 0 ? `var ${ data[ "use-element-id-list" ].join( "," ) };` : "" }
106
+
107
+ this.__magic_element_root = document.createDocumentFragment();
108
+
109
+ ${ data.before.code }
110
+
111
+ this.__magic_template = ( () => {
112
+ const { element: e, text: t, attribute: att,event: eve , append } = magic.dom, i = magic.importM;
113
+ ${ element_var };
114
+
115
+ return {
116
+ render : () => {
117
+ ${ ct }${ init_kw_name }\n${ init_att }\n${ init_kw_id }\n({${ data[ "use-element-id-list" ].join( ',' ) } }= $id());\n${ sh }\n${ iei }
118
+ },
119
+ bind_event : ()=>{ ${ init_eve } },
120
+ export_element : () => {
121
+ return magic.BindScope(${ element_var.length > 1 ? `[ ${ element_var.substring( 4 ) } ]` : "[]" },this,_args._id);
122
+ }
123
+ };
124
+ } )();
125
+
126
+ this.__magic_template.render();
127
+
128
+ ${ data.global.code }
129
+
130
+ ${ data.listen.code }
131
+
132
+ ${ data.event.code }
133
+
134
+ this.__magic_template.bind_event();
135
+
136
+ ${ data.interface.code }
137
+
138
+ ${ data.script.code }
139
+ ${ ( () => {
140
+ let r = "";
141
+ data.once_interface.forEach( t => {
142
+ r += `call.interface.${ t }();\n`;
143
+ } );
144
+ return r;
145
+ } )() }
146
+ ${
147
+ ( () => {
148
+ if ( !data.template.fragment ) {
149
+ return `const __root__element__ = this.__magic_template.export_element().at(0);
150
+ __root__element__.__file = "${ data.originalFile }",
151
+ __root__element__.mid = _args._id,
152
+ __root__element__.fragment = this.__magic_element_root,
153
+ __root__element__.interface = this.__magic_interface,
154
+ __root__element__.templateArgs = ${ JSON.stringify( data.templateArgs ) },
155
+ __root__element__.exposeEvent = ${ data[ "expose-event" ] };
156
+ return __root__element__;`;
157
+ } else {
158
+ return `return {
159
+ __file : "${ data.originalFile }",
160
+ __fragment : true,
161
+ mid : _args._id,
162
+ fragment : this.__magic_element_root,
163
+ interface : this.__magic_interface,
164
+ templateArgs : ${ JSON.stringify( data.templateArgs ) },
165
+ exposeEvent : ${ data[ "expose-event" ] },
166
+ childNodes : [...this.__magic_template.export_element()]
167
+ };`;
168
+ }
169
+ } )()
170
+ }
171
+ }`;
172
+ }
173
+
174
+ function isEmptyObject( obj ) {
175
+ return Object.keys( obj ).length === 0;
176
+ }
177
+
178
+ export function _6( mDatas, CSS_VAR ) {
179
+ if ( project.build_config.build.platform.target !== "module" && project.build_config.build.import.module )
180
+ project.build_config.build.import.module.forEach( p => {
181
+ const tp = path.normalize( p );
182
+ if ( !bunFS.existsSync( tp ) ) {
183
+ throw `目标导入模块不存在 ${ tp }`
184
+ }
185
+ copyDir( tp, project.outDir + path.basename( p ) );
186
+
187
+ const md = JSON.parse( bunFS.readFileSync( tp + "/module.info.json" ) );
188
+ md.files.forEach( file => {
189
+ const p = md.dir + "/" + file;
190
+ const ext = fileUtil.getExtensionName( p );
191
+ if ( ext === "js" ) {
192
+ project.index_dom.add( "begin", {
193
+ tag : "script",
194
+ attrs : [
195
+ { src : `./${ p }` }
196
+ ]
197
+ } );
198
+ } else if ( ext === "css" ) {
199
+ project.index_dom.add( "begin", {
200
+ tag : "link",
201
+ one : true,
202
+ attrs : [
203
+ { href : `./${ p }` },
204
+ { rel : `stylesheet` }
205
+ ]
206
+ } );
207
+ }
208
+ } );
209
+ } );
210
+
211
+ const paths = [];
212
+
213
+ bunFS.writeFileSync( `${ project.outDirMagic }/default-theme-var.css`, CSS_VAR );
214
+
215
+ if ( project.build_config.build.model === "debug" ) {
216
+ const ps = [];
217
+ mDatas.forEach( md => {
218
+ const jsPath = project.outDirMagic + `${ md.data.name }.js`;
219
+ const cssPath = project.outDirMagic + `${ md.data.name }.css`;
220
+ const jsContent = createMJs( md );
221
+
222
+ ps.push( asyncWriteFile( jsPath, jsContent ) );
223
+ ps.push( asyncWriteFile( cssPath, md.data.css ) );
224
+
225
+ project.index_dom.add( "begin", {
226
+ tag : "script",
227
+ attrs : [
228
+ { src : `./magic/${ md.data.name }.js` }
229
+ ]
230
+ } );
231
+ project.index_dom.add( "begin", {
232
+ tag : "link",
233
+ one : true,
234
+ attrs : [
235
+ { href : `./magic/${ md.data.name }.css` },
236
+ { rel : `stylesheet` }
237
+ ]
238
+ } );
239
+ paths.push( `${ md.data.name }.js` );
240
+ paths.push( `${ md.data.name }.css` );
241
+ } );
242
+ Promise.all( ps ).then( () => {
243
+ _end( paths );
244
+ } );
245
+ } else if ( project.build_config.build.model === "release" ) {
246
+ const AllDta = {
247
+ mScript : [],
248
+ css : []
249
+ };
250
+ let currentMScriptBlock = "", currentCSSBlock = "";
251
+ mDatas.forEach( md => {
252
+ currentMScriptBlock += createMJs( md ) + "\n";
253
+ if ( isStringOverSize( currentMScriptBlock, Config.build.MScriptBlockSize ) ) {
254
+ AllDta.mScript.push( currentMScriptBlock );
255
+ currentMScriptBlock = "";
256
+ }
257
+ currentCSSBlock += md.data.css + "\n";
258
+ if ( isStringOverSize( currentCSSBlock, Config.build.CSSBlockSize ) ) {
259
+ AllDta.css.push( currentCSSBlock );
260
+ currentCSSBlock = "";
261
+ }
262
+ } );
263
+
264
+ {
265
+ AllDta.css.push( currentCSSBlock );
266
+ let i = 0, c = "";
267
+ AllDta.css.forEach( block => {
268
+ project.index_dom.add( "begin", {
269
+ tag : "link",
270
+ one : true,
271
+ attrs : [
272
+ { href : `./magic/m${ c }.css` },
273
+ { rel : `stylesheet` }
274
+ ]
275
+ } );
276
+ paths.push( `m${ c }.css` );
277
+ bunFS.writeFileSync( project.outDirMagic + `m${ c }.css`, block );
278
+
279
+ i += 1;
280
+ c = "-" + i;
281
+ } );
282
+ }
283
+
284
+ {
285
+ AllDta.mScript.push( currentMScriptBlock );
286
+ let i = 0, c = "";
287
+ AllDta.mScript.forEach( block => {
288
+ project.index_dom.add( "begin", {
289
+ tag : "script",
290
+ attrs : [
291
+ { type : `text/javascript` },
292
+ { src : `./magic/m${ c }.js` }
293
+ ]
294
+ } );
295
+ paths.push( `m${ c }.js` );
296
+ bunFS.writeFileSync( project.outDirMagic + `m${ c }.js`, block );
297
+
298
+ i += 1;
299
+ c = "-" + i;
300
+ } );
301
+ }
302
+ _end( paths );
303
+ }
304
+ }
@@ -0,0 +1,124 @@
1
+ import htmlMinifier from "html-minifier-terser";
2
+ import postcss from 'postcss';
3
+ import autoprefixer from 'autoprefixer';
4
+ import { minify_sync } from "terser";
5
+ import { app } from "../../../../app.js";
6
+ import { printf } from "../../../global/printf.js";
7
+ import { fileUtil } from "../../../util/file-util.js";
8
+ import { getDirAllFile } from "../../../util/get-dir-all-file.js";
9
+ import { project } from "../global.js";
10
+ import { START_TIME } from "../start.js";
11
+ import { bunFS } from "../../../util/bun-fs.js";
12
+
13
+ async function processCssString( cssString ) {
14
+ try {
15
+ const result = await postcss( [
16
+ autoprefixer( {
17
+ overrideBrowserslist : [
18
+ 'last 2 versions',
19
+ 'Firefox >= 140',
20
+ 'Chrome >= 140',
21
+ 'Safari >= 15'
22
+ ]
23
+ } )
24
+ ] ).process( cssString, {
25
+ from : undefined,
26
+ map : false
27
+ } );
28
+
29
+ return result.css;
30
+ } catch ( error ) {
31
+ console.error( 'CSS 处理失败:', error );
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ export function _end( paths ) {
37
+ const outDir = app.project.dir + project.build_config.build.out;
38
+ if ( project.build_config.build.platform.target === "module" ) {
39
+ const moduleData = {
40
+ ...project.build_config.config,
41
+ dir : project.build_config.build.out,
42
+ model : project.build_config.build.model,
43
+ files : [
44
+ "default-theme-var.css",
45
+ ...paths
46
+ ]
47
+ };
48
+ delete moduleData.src;
49
+
50
+ bunFS.writeFileSync( project.outDir + "module.info.json", JSON.stringify( moduleData, null, 2 ) );
51
+ } else {
52
+ bunFS.writeFileSync( `${ outDir }/index.html`, project.index_dom.generate() );
53
+ }
54
+
55
+ const ps = [];
56
+
57
+ getDirAllFile( project.outDir ).forEach( ( file ) => {
58
+ ps.push( new Promise( async ( resolve, reject ) => {
59
+ const ext = fileUtil.getExtensionName( file );
60
+ let data = bunFS.readFileSync( file );
61
+ if ( ext === "html" ) {
62
+ if ( project.build_config.build.optimize[ "min-code" ].html )
63
+ data = await htmlMinifier.minify( data, {
64
+ collapseWhitespace : true,
65
+ removeEmptyAttributes : false,
66
+ collapseBooleanAttributes : false,
67
+ removeAttributeQuotes : true,
68
+ minifyCSS : false,
69
+ minifyJS : false,
70
+ removeStyleLinkTypeAttributes : false,
71
+ removeScriptTypeAttributes : false
72
+ } );
73
+ } else if ( ext === "js" ) {
74
+ try {
75
+ data = await minify_sync( data, {
76
+ compress : project.build_config.build.optimize[ "min-code" ].js,
77
+ mangle : project.build_config.build.optimize[ "min-code" ].js,
78
+ format : {
79
+ beautify : !project.build_config.build.optimize[ "min-code" ].js,
80
+ indent_level : 2,
81
+ indent_start : 0,
82
+ quote_style : 1,
83
+ wrap_iife : false,
84
+ preserve_annotations : !project.build_config.build.optimize[ "min-code" ].js
85
+ }
86
+ } ).code;
87
+ } catch ( e ) {
88
+ console.error( e )
89
+ }
90
+ } else if ( ext === "css" ) {
91
+ if ( project.build_config.build.optimize[ "min-code" ].css )
92
+ data = await htmlMinifier.minify( data, {
93
+ collapseWhitespace : true,
94
+ removeEmptyAttributes : false,
95
+ collapseBooleanAttributes : false,
96
+ removeAttributeQuotes : true,
97
+ minifyCSS : true,
98
+ minifyJS : false,
99
+ removeStyleLinkTypeAttributes : false,
100
+ removeScriptTypeAttributes : false
101
+ } );
102
+ data = await processCssString( data );
103
+ } else {
104
+ resolve();
105
+ return;
106
+ }
107
+ bunFS.writeFileSync( file, data );
108
+ resolve();
109
+ } ) );
110
+ } );
111
+
112
+ Promise.all( ps ).then( () => {
113
+ console.log( `\n构建完成 ヾ(๑╹◡╹)ノ` + `\n耗时 ${ ( new Date().getTime() - START_TIME ) / 1000 }(s)` );
114
+ if ( project.build_config.build.platform.target === "web" ) {
115
+ const c = project.build_config.build.platform.config.server;
116
+ if ( c && c.port && c.host ) {
117
+ fetch( `http://${ c.host }:${ c.port }/==UPDATE==` )
118
+ .catch( e => {
119
+ printf.outFile.log( e );
120
+ } );
121
+ }
122
+ }
123
+ } );
124
+ }
@@ -0,0 +1,249 @@
1
+ import mime from "mime-types";
2
+ import { exec } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import * as http from "node:http";
5
+ import * as os from "node:os";
6
+ import path from "node:path";
7
+ import * as url from "node:url";
8
+ import { app } from "../app.js";
9
+ import { printf } from "./global/printf.js";
10
+ import { macroReplace } from "./module/compiler/macro-replace.js";
11
+ import { examine_BuildConfig } from "./module/compiler/step/1.js";
12
+ import WebSocket from "ws"
13
+
14
+ const platform = {
15
+ "node-webkit" : ( platform_config ) => {
16
+ try {
17
+ const pk = `${ app.project.dir }/package.json`;
18
+ if ( !fs.existsSync( pk ) ) {
19
+ fs.writeFileSync( pk, "{}" );
20
+ }
21
+
22
+ let pkJson = {};
23
+
24
+ const data = fs.readFileSync( pk ).toString();
25
+ if ( data ) {
26
+ pkJson = JSON.parse( data );
27
+ }
28
+
29
+ const newPk = Object.assign( pkJson, platform_config.build.platform.config );
30
+ newPk[ "main" ] = platform_config.build.out + "/" + platform_config.config.main + ".html";
31
+ delete newPk[ "app" ];
32
+ fs.writeFileSync( pk, JSON.stringify( newPk ) );
33
+ } catch ( e ) {
34
+ throw e;
35
+ }
36
+
37
+ const command = `${ platform_config.build.platform.config.app } ${ app.project.dir }`;
38
+ exec( command, ( error, stdout, stderr ) => {
39
+ if ( error ) {
40
+ throw error;
41
+ }
42
+ printf.log( stdout );
43
+ printf.error( stderr );
44
+ } );
45
+ },
46
+ "web" : ( platform_config ) => {
47
+ if ( platform_config.build.platform.config.browser ) {
48
+ const command = `start ${ platform_config.config.main }.html`;
49
+ exec( command, ( error, stdout, stderr ) => {
50
+ if ( error ) {
51
+ throw error;
52
+ }
53
+ printf.log( stdout );
54
+ printf.error( stderr );
55
+ } );
56
+ } else if ( platform_config.build.platform.config.server ) {
57
+ const hostname = platform_config.build.platform.config.server[ "host" ];
58
+ const port = platform_config.build.platform.config.server[ "port" ];
59
+
60
+ const rootDir = platform_config[ "build-dir" ];
61
+
62
+
63
+ // 0 没有操作 1 刷新
64
+ let clientData = 0;
65
+ const connectedClients = new Set();
66
+
67
+ const server = http.createServer( ( req, res ) => {
68
+ const parsedUrl = url.parse( req.url, true );
69
+ const targetPath = parsedUrl.pathname;
70
+
71
+ if ( targetPath === "/==UPDATE==" ) {
72
+ res.setHeader( 'Content-Type', "text/txt; charset=utf-8" );
73
+ res.statusCode = 200;
74
+ res.end( `OK` );
75
+ clientData = 1;
76
+ return;
77
+ }
78
+
79
+ let target = path.resolve( rootDir, `.${ targetPath }` );
80
+ if ( !target.startsWith( rootDir ) ) {
81
+ target = rootDir + "index.html";
82
+ }
83
+ if ( fs.existsSync( target ) ) {
84
+ const ext = path.extname( target ).toLowerCase();
85
+ const stat = fs.statSync( target );
86
+ if ( stat.isDirectory() ) {
87
+ target = path.join( target, 'index.html' );
88
+ if ( !fs.existsSync( target ) ) {
89
+ res.statusCode = 404;
90
+ res.setHeader( 'Content-Type', 'text/plain' );
91
+ res.end( `404 Not Found: ${ req.url }/index.html` );
92
+ return;
93
+ }
94
+ }
95
+
96
+ const contentType = mime.lookup( ext ) || 'application/octet-stream';
97
+ res.setHeader( 'Content-Type', contentType );
98
+ res.statusCode = 200;
99
+ fs.readFile( target, ( err, data ) => {
100
+ if ( err ) {
101
+ res.statusCode = 500;
102
+ res.end( `500 Server Error: ${ err.message }` );
103
+ printf.error( `文件读取失败: ${ target } - ${ err.message }` );
104
+ return;
105
+ }
106
+
107
+ if ( path.basename( target ) === "index.html" ) {
108
+ const wsScript = `
109
+ <script>
110
+ document.addEventListener('DOMContentLoaded', function () {
111
+ const reconnectInterval = 5000;
112
+ let ws = null;
113
+ let timeid = null;
114
+ let reconnectTimer = null;
115
+
116
+ function createWebSocket() {
117
+ if (ws) {
118
+ ws.close();
119
+ }
120
+
121
+ ws = new WebSocket(\`ws://\${window.location.hostname}:${ port }\`);
122
+
123
+ ws.onopen = () => {
124
+ console.log('WebSocket link');
125
+ clearTimeout(reconnectTimer);
126
+ };
127
+
128
+ ws.onerror = (err) => {
129
+ console.error('WebSocket fali:', err);
130
+ };
131
+
132
+ ws.onmessage = (event) => {
133
+ if (event.data === "1") {
134
+ clearTimeout(timeid);
135
+ location.reload();
136
+ timeid = setTimeout(() => {
137
+ if (document.getElementById("app")?.childNodes.length === 0) {
138
+ location.reload();
139
+ }
140
+ }, 500);
141
+ }
142
+ };
143
+
144
+ ws.onclose = (event) => {
145
+ console.log('WebSocket disconnect');
146
+
147
+ clearTimeout(reconnectTimer);
148
+
149
+ reconnectTimer = setTimeout(() => {
150
+ createWebSocket();
151
+ }, reconnectInterval);
152
+ };
153
+ }
154
+
155
+ createWebSocket();
156
+
157
+ window.addEventListener('beforeunload', () => {
158
+ clearTimeout(timeid);
159
+ clearTimeout(reconnectTimer);
160
+ if (ws) {
161
+ ws.close();
162
+ }
163
+ });
164
+ });
165
+ </script>`;
166
+ data = Buffer.concat( [ data, Buffer.from( wsScript ) ] );
167
+ }
168
+ res.end( data );
169
+ } );
170
+ } else {
171
+ res.statusCode = 404;
172
+ res.setHeader( 'Content-Type', 'text/plain' );
173
+ res.end( 'file not exist ' + target );
174
+ }
175
+ } );
176
+
177
+ const wss = new WebSocket.Server( { server } );
178
+
179
+ function broadcastRefresh() {
180
+ connectedClients.forEach( ( ws ) => {
181
+ if ( ws.readyState === WebSocket.OPEN ) {
182
+ try {
183
+ ws.send( clientData );
184
+ } catch ( error ) {
185
+ console.error( `send message fail: ${ error.message }` );
186
+ }
187
+ }
188
+ } );
189
+ clientData = 0;
190
+ }
191
+
192
+ setInterval( broadcastRefresh, 500 );
193
+
194
+ wss.on( 'connection', ws => {
195
+ connectedClients.add( ws );
196
+
197
+ ws.on( 'message', ( data ) => {
198
+ const message = data.toString( "utf-8" );
199
+ console.log( `client message: ${ message }` );
200
+ } );
201
+
202
+ ws.on( 'close', ( code, reason ) => {
203
+ connectedClients.delete( ws );
204
+ } );
205
+
206
+ ws.on( 'error', ( error ) => {
207
+ console.error( `WebSocket client fail: ${ error.message }` );
208
+ connectedClients.delete( ws );
209
+ } );
210
+ } );
211
+
212
+ server.listen( parseInt( port ), hostname, () => {
213
+ console.log( `HTTP 服务器已启动: http://localhost:${ port }/index.html` );
214
+ console.log( `使用 Ctrl + C 停止服务器` );
215
+
216
+ const command = `start http://${ hostname }:${ port }/index.html`;
217
+ exec( command, ( error, stdout, stderr ) => {
218
+ if ( error ) {
219
+ printf.outFile.error( error );
220
+ throw error;
221
+ }
222
+ printf.outFile.log( stdout );
223
+ } );
224
+ } );
225
+ }
226
+ }
227
+ };
228
+
229
+ /**
230
+ * @returns {Promise<void>}
231
+ */
232
+ export function RunProject() {
233
+ printf.outFile.info( `运行项目` );
234
+
235
+ const file = path.normalize( app.project.dir + "build.toml" );
236
+ import(file ).then( root => {
237
+ printf.outFile.info( `预处理 Build 配置文件 [path:${ file }]` );
238
+ const config = examine_BuildConfig( macroReplace( root ) );
239
+ if ( config ) {
240
+ if ( config.build.platform.target === "module" ) {
241
+ throw `无法运行模块`;
242
+ } else {
243
+ config[ "build-dir" ] = path.normalize( app.project.dir + config.build.out + '/' );
244
+ platform[ config.build.platform.target ]( config );
245
+ }
246
+ }
247
+ } );
248
+ }
249
+