@nasti-toolchain/nasti 1.6.4 → 1.7.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 +25 -0
- package/dist/cli.cjs +84 -24
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +84 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +85 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.js +85 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { InputOptions, OutputOptions } from 'rolldown';
|
|
2
|
+
|
|
1
3
|
interface NastiConfig {
|
|
2
4
|
/** 项目根目录 */
|
|
3
5
|
root?: string;
|
|
@@ -87,8 +89,33 @@ interface BuildConfig {
|
|
|
87
89
|
minify?: boolean | 'oxc';
|
|
88
90
|
sourcemap?: boolean | 'inline' | 'hidden';
|
|
89
91
|
target?: string | string[];
|
|
90
|
-
|
|
92
|
+
/**
|
|
93
|
+
* 透传给 Rolldown 的底层选项,供生产应用手动控制代码拆分与 Tree-shaking。
|
|
94
|
+
*
|
|
95
|
+
* - input 侧(`treeshake`、`resolve`、`external`、`platform` 等)会合并进 `rolldown()`;
|
|
96
|
+
* - `output` 会合并进 `bundle.write()`,用于控制代码拆分
|
|
97
|
+
* (`output.advancedChunks` / `output.codeSplitting`)、chunk 命名等。
|
|
98
|
+
*
|
|
99
|
+
* 注:`input` 与 `plugins` 由 Nasti 管理,故不在此暴露;`output.dir` 始终由
|
|
100
|
+
* `build.outDir` 决定(HTML 改写依赖产物路径),传入会被忽略。
|
|
101
|
+
*/
|
|
102
|
+
rolldownOptions?: NastiRolldownOptions;
|
|
91
103
|
emptyOutDir?: boolean;
|
|
104
|
+
css?: CssConfig;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Nasti 暴露的 Rolldown 选项:在 Rolldown {@link InputOptions} 基础上去掉由 Nasti
|
|
108
|
+
* 接管的 `input` / `plugins`,并补充一个 `output` 出口用于 `bundle.write()`。
|
|
109
|
+
*/
|
|
110
|
+
type NastiRolldownOptions = Omit<InputOptions, 'input' | 'plugins'> & {
|
|
111
|
+
/** 传给 `bundle.write()` 的输出选项:代码拆分(`advancedChunks` / `codeSplitting`)、chunk 命名等 */
|
|
112
|
+
output?: OutputOptions;
|
|
113
|
+
};
|
|
114
|
+
interface CssConfig {
|
|
115
|
+
/** CSP nonce to add to inline <style> tags */
|
|
116
|
+
nonce?: string;
|
|
117
|
+
/** Emit CSS as separate files instead of inline injection (CSP-friendly) */
|
|
118
|
+
emitCssFile?: boolean;
|
|
92
119
|
}
|
|
93
120
|
interface NastiPlugin {
|
|
94
121
|
name: string;
|
|
@@ -143,6 +170,7 @@ type LoadResult = string | null | undefined | {
|
|
|
143
170
|
type TransformResult = string | null | undefined | {
|
|
144
171
|
code: string;
|
|
145
172
|
map?: unknown;
|
|
173
|
+
moduleType?: string;
|
|
146
174
|
};
|
|
147
175
|
interface EmittedFile {
|
|
148
176
|
type: 'asset' | 'chunk';
|
|
@@ -322,4 +350,4 @@ interface MonacoEditorPluginOptions {
|
|
|
322
350
|
}
|
|
323
351
|
declare function monacoEditorPlugin(options?: MonacoEditorPluginOptions): NastiPlugin;
|
|
324
352
|
|
|
325
|
-
export { type DevServer, type ElectronConfig, type HmrPayload, type ModuleNode, type MonacoCustomWorker, type MonacoEditorLanguageWorker, type MonacoEditorPluginOptions, type NastiConfig, type NastiPlugin, type ResolvedConfig, type TransformResult, build, buildElectron, createServer, defineConfig, electronPlugin, monacoEditorPlugin, resolveConfig, startElectronDev };
|
|
353
|
+
export { type BuildConfig, type DevServer, type ElectronConfig, type HmrPayload, type ModuleNode, type MonacoCustomWorker, type MonacoEditorLanguageWorker, type MonacoEditorPluginOptions, type NastiConfig, type NastiPlugin, type NastiRolldownOptions, type ResolvedConfig, type TransformResult, build, buildElectron, createServer, defineConfig, electronPlugin, monacoEditorPlugin, resolveConfig, startElectronDev };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { InputOptions, OutputOptions } from 'rolldown';
|
|
2
|
+
|
|
1
3
|
interface NastiConfig {
|
|
2
4
|
/** 项目根目录 */
|
|
3
5
|
root?: string;
|
|
@@ -87,8 +89,33 @@ interface BuildConfig {
|
|
|
87
89
|
minify?: boolean | 'oxc';
|
|
88
90
|
sourcemap?: boolean | 'inline' | 'hidden';
|
|
89
91
|
target?: string | string[];
|
|
90
|
-
|
|
92
|
+
/**
|
|
93
|
+
* 透传给 Rolldown 的底层选项,供生产应用手动控制代码拆分与 Tree-shaking。
|
|
94
|
+
*
|
|
95
|
+
* - input 侧(`treeshake`、`resolve`、`external`、`platform` 等)会合并进 `rolldown()`;
|
|
96
|
+
* - `output` 会合并进 `bundle.write()`,用于控制代码拆分
|
|
97
|
+
* (`output.advancedChunks` / `output.codeSplitting`)、chunk 命名等。
|
|
98
|
+
*
|
|
99
|
+
* 注:`input` 与 `plugins` 由 Nasti 管理,故不在此暴露;`output.dir` 始终由
|
|
100
|
+
* `build.outDir` 决定(HTML 改写依赖产物路径),传入会被忽略。
|
|
101
|
+
*/
|
|
102
|
+
rolldownOptions?: NastiRolldownOptions;
|
|
91
103
|
emptyOutDir?: boolean;
|
|
104
|
+
css?: CssConfig;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Nasti 暴露的 Rolldown 选项:在 Rolldown {@link InputOptions} 基础上去掉由 Nasti
|
|
108
|
+
* 接管的 `input` / `plugins`,并补充一个 `output` 出口用于 `bundle.write()`。
|
|
109
|
+
*/
|
|
110
|
+
type NastiRolldownOptions = Omit<InputOptions, 'input' | 'plugins'> & {
|
|
111
|
+
/** 传给 `bundle.write()` 的输出选项:代码拆分(`advancedChunks` / `codeSplitting`)、chunk 命名等 */
|
|
112
|
+
output?: OutputOptions;
|
|
113
|
+
};
|
|
114
|
+
interface CssConfig {
|
|
115
|
+
/** CSP nonce to add to inline <style> tags */
|
|
116
|
+
nonce?: string;
|
|
117
|
+
/** Emit CSS as separate files instead of inline injection (CSP-friendly) */
|
|
118
|
+
emitCssFile?: boolean;
|
|
92
119
|
}
|
|
93
120
|
interface NastiPlugin {
|
|
94
121
|
name: string;
|
|
@@ -143,6 +170,7 @@ type LoadResult = string | null | undefined | {
|
|
|
143
170
|
type TransformResult = string | null | undefined | {
|
|
144
171
|
code: string;
|
|
145
172
|
map?: unknown;
|
|
173
|
+
moduleType?: string;
|
|
146
174
|
};
|
|
147
175
|
interface EmittedFile {
|
|
148
176
|
type: 'asset' | 'chunk';
|
|
@@ -322,4 +350,4 @@ interface MonacoEditorPluginOptions {
|
|
|
322
350
|
}
|
|
323
351
|
declare function monacoEditorPlugin(options?: MonacoEditorPluginOptions): NastiPlugin;
|
|
324
352
|
|
|
325
|
-
export { type DevServer, type ElectronConfig, type HmrPayload, type ModuleNode, type MonacoCustomWorker, type MonacoEditorLanguageWorker, type MonacoEditorPluginOptions, type NastiConfig, type NastiPlugin, type ResolvedConfig, type TransformResult, build, buildElectron, createServer, defineConfig, electronPlugin, monacoEditorPlugin, resolveConfig, startElectronDev };
|
|
353
|
+
export { type BuildConfig, type DevServer, type ElectronConfig, type HmrPayload, type ModuleNode, type MonacoCustomWorker, type MonacoEditorLanguageWorker, type MonacoEditorPluginOptions, type NastiConfig, type NastiPlugin, type NastiRolldownOptions, type ResolvedConfig, type TransformResult, build, buildElectron, createServer, defineConfig, electronPlugin, monacoEditorPlugin, resolveConfig, startElectronDev };
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,8 @@ var init_defaults = __esm({
|
|
|
35
35
|
sourcemap: false,
|
|
36
36
|
target: "es2022",
|
|
37
37
|
rolldownOptions: {},
|
|
38
|
-
emptyOutDir: true
|
|
38
|
+
emptyOutDir: true,
|
|
39
|
+
css: {}
|
|
39
40
|
};
|
|
40
41
|
defaultElectron = {
|
|
41
42
|
main: "src/electron/main.ts",
|
|
@@ -282,20 +283,29 @@ import { createRequire } from "module";
|
|
|
282
283
|
function resolvePlugin(config) {
|
|
283
284
|
const { alias, extensions } = config.resolve;
|
|
284
285
|
const require2 = createRequire(path2.resolve(config.root, "package.json"));
|
|
286
|
+
const aliasEntries = Object.entries(alias).sort(
|
|
287
|
+
([a], [b]) => b.length - a.length
|
|
288
|
+
);
|
|
285
289
|
return {
|
|
286
290
|
name: "nasti:resolve",
|
|
287
291
|
enforce: "pre",
|
|
288
292
|
resolveId(source, importer) {
|
|
289
|
-
for (const [key, value] of
|
|
293
|
+
for (const [key, value] of aliasEntries) {
|
|
290
294
|
if (source === key || source.startsWith(key + "/")) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
+
const aliasBase = resolveAliasTarget(value, config.root);
|
|
296
|
+
const sub = source.slice(key.length).replace(/^\//, "");
|
|
297
|
+
const target = sub ? path2.join(aliasBase, sub) : aliasBase;
|
|
298
|
+
const resolved = tryResolveFile(target, extensions);
|
|
299
|
+
if (resolved) return resolved;
|
|
295
300
|
break;
|
|
296
301
|
}
|
|
297
302
|
}
|
|
298
|
-
if (
|
|
303
|
+
if (source.startsWith("/") && !source.startsWith("//")) {
|
|
304
|
+
const rootRelative = path2.join(config.root, source.slice(1));
|
|
305
|
+
const resolved = tryResolveFile(rootRelative, extensions);
|
|
306
|
+
if (resolved) return resolved;
|
|
307
|
+
}
|
|
308
|
+
if (path2.isAbsolute(source) && fs2.existsSync(source)) {
|
|
299
309
|
const resolved = tryResolveFile(source, extensions);
|
|
300
310
|
if (resolved) return resolved;
|
|
301
311
|
}
|
|
@@ -318,6 +328,7 @@ function resolvePlugin(config) {
|
|
|
318
328
|
return null;
|
|
319
329
|
},
|
|
320
330
|
load(id) {
|
|
331
|
+
if (id.startsWith("\0")) return null;
|
|
321
332
|
if (!fs2.existsSync(id)) return null;
|
|
322
333
|
if (id.endsWith(".json")) {
|
|
323
334
|
const content = fs2.readFileSync(id, "utf-8");
|
|
@@ -327,6 +338,11 @@ function resolvePlugin(config) {
|
|
|
327
338
|
}
|
|
328
339
|
};
|
|
329
340
|
}
|
|
341
|
+
function resolveAliasTarget(value, root) {
|
|
342
|
+
if (path2.isAbsolute(value) && fs2.existsSync(value)) return value;
|
|
343
|
+
if (value.startsWith("/")) return path2.join(root, value.slice(1));
|
|
344
|
+
return path2.resolve(root, value);
|
|
345
|
+
}
|
|
330
346
|
function tryResolveFile(file, extensions) {
|
|
331
347
|
if (fs2.existsSync(file) && fs2.statSync(file).isFile()) {
|
|
332
348
|
return file;
|
|
@@ -423,8 +439,8 @@ function cssPlugin(config) {
|
|
|
423
439
|
cssSource = compiled.css;
|
|
424
440
|
}
|
|
425
441
|
const rewritten = rewriteCssUrls(cssSource, id, config.root);
|
|
442
|
+
const escaped = JSON.stringify(rewritten);
|
|
426
443
|
if (config.command === "serve") {
|
|
427
|
-
const escaped = JSON.stringify(rewritten);
|
|
428
444
|
return {
|
|
429
445
|
code: `
|
|
430
446
|
const css = ${escaped};
|
|
@@ -448,7 +464,42 @@ export default css;
|
|
|
448
464
|
`
|
|
449
465
|
};
|
|
450
466
|
}
|
|
451
|
-
|
|
467
|
+
const cssConfig = config.build.css || {};
|
|
468
|
+
const nonce = cssConfig.nonce;
|
|
469
|
+
const emitCssFile = cssConfig.emitCssFile;
|
|
470
|
+
if (emitCssFile) {
|
|
471
|
+
const fileName = `assets/${path4.basename(id, ".css")}.css`;
|
|
472
|
+
this.emitFile({
|
|
473
|
+
type: "asset",
|
|
474
|
+
fileName,
|
|
475
|
+
source: rewritten
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
code: `
|
|
479
|
+
const link = document.createElement('link');
|
|
480
|
+
link.rel = 'stylesheet';
|
|
481
|
+
link.href = ${JSON.stringify("/" + fileName)};
|
|
482
|
+
document.head.appendChild(link);
|
|
483
|
+
|
|
484
|
+
export default ${escaped};
|
|
485
|
+
`,
|
|
486
|
+
moduleType: "js"
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const nonceAttr = nonce ? `style.setAttribute('nonce', ${JSON.stringify(nonce)});` : "";
|
|
490
|
+
return {
|
|
491
|
+
code: `
|
|
492
|
+
const css = ${escaped};
|
|
493
|
+
const style = document.createElement('style');
|
|
494
|
+
style.setAttribute('data-nasti-css', ${JSON.stringify(id)});
|
|
495
|
+
${nonceAttr}
|
|
496
|
+
style.textContent = css;
|
|
497
|
+
document.head.appendChild(style);
|
|
498
|
+
|
|
499
|
+
export default css;
|
|
500
|
+
`,
|
|
501
|
+
moduleType: "js"
|
|
502
|
+
};
|
|
452
503
|
}
|
|
453
504
|
};
|
|
454
505
|
}
|
|
@@ -852,7 +903,7 @@ import pc from "picocolors";
|
|
|
852
903
|
async function build(inlineConfig = {}) {
|
|
853
904
|
const config = await resolveConfig(inlineConfig, "build");
|
|
854
905
|
const startTime = performance.now();
|
|
855
|
-
console.log(pc.cyan("\n\u{1F528} nasti build") + pc.dim(` v${"1.
|
|
906
|
+
console.log(pc.cyan("\n\u{1F528} nasti build") + pc.dim(` v${"1.7.0"}`));
|
|
856
907
|
console.log(pc.dim(` root: ${config.root}`));
|
|
857
908
|
console.log(pc.dim(` mode: ${config.mode}`));
|
|
858
909
|
const outDir = path8.resolve(config.root, config.build.outDir);
|
|
@@ -906,9 +957,12 @@ async function build(inlineConfig = {}) {
|
|
|
906
957
|
};
|
|
907
958
|
const env = loadEnv(config.mode, config.root, config.envPrefix);
|
|
908
959
|
const envDefine = buildEnvDefine(env, config.mode);
|
|
960
|
+
const { output: userOutput, transform: userTransform, ...restInputOptions } = config.build.rolldownOptions;
|
|
961
|
+
const mergedDefine = { ...userTransform?.define ?? {}, ...envDefine };
|
|
909
962
|
const bundle = await rolldown({
|
|
963
|
+
...restInputOptions,
|
|
910
964
|
input: entryPoints,
|
|
911
|
-
define:
|
|
965
|
+
transform: { ...userTransform, define: mergedDefine },
|
|
912
966
|
plugins: [
|
|
913
967
|
oxcTransformPlugin,
|
|
914
968
|
// 转换 Nasti 插件为 Rolldown 插件格式
|
|
@@ -924,17 +978,19 @@ async function build(inlineConfig = {}) {
|
|
|
924
978
|
// manifest/SW writers) rely on for final-stage artifact emission.
|
|
925
979
|
closeBundle: p.closeBundle
|
|
926
980
|
}))
|
|
927
|
-
]
|
|
928
|
-
...config.build.rolldownOptions
|
|
981
|
+
]
|
|
929
982
|
});
|
|
930
983
|
const { output } = await bundle.write({
|
|
931
|
-
dir: outDir,
|
|
932
984
|
format: "esm",
|
|
933
985
|
sourcemap: !!config.build.sourcemap,
|
|
934
986
|
minify: !!config.build.minify,
|
|
935
987
|
entryFileNames: "assets/[name].[hash].js",
|
|
936
988
|
chunkFileNames: "assets/[name].[hash].js",
|
|
937
|
-
assetFileNames: "assets/[name].[hash][extname]"
|
|
989
|
+
assetFileNames: "assets/[name].[hash][extname]",
|
|
990
|
+
// 用户可覆盖默认输出:代码拆分(advancedChunks / codeSplitting)、chunk 命名等
|
|
991
|
+
...userOutput,
|
|
992
|
+
// dir 始终由 Nasti 掌管 —— 下方 HTML 改写依赖固定的产物目录,故放在最后强制生效
|
|
993
|
+
dir: outDir
|
|
938
994
|
});
|
|
939
995
|
await bundle.close();
|
|
940
996
|
await pluginContainer.buildEnd();
|
|
@@ -1603,7 +1659,7 @@ function rewriteImports(code, config, filePath) {
|
|
|
1603
1659
|
const baseSpec = suffix ? spec.slice(0, -suffix.length) : spec;
|
|
1604
1660
|
for (const [key, value] of aliasEntries) {
|
|
1605
1661
|
if (baseSpec === key || baseSpec.startsWith(key + "/")) {
|
|
1606
|
-
const aliasBase =
|
|
1662
|
+
const aliasBase = resolveAliasTarget2(value, root);
|
|
1607
1663
|
const sub = baseSpec.slice(key.length).replace(/^\//, "");
|
|
1608
1664
|
const target = sub ? path10.join(aliasBase, sub) : aliasBase;
|
|
1609
1665
|
const resolved = tryResolveDiskPath(target);
|
|
@@ -1634,7 +1690,7 @@ function rewriteImports(code, config, filePath) {
|
|
|
1634
1690
|
(_m, q, s) => `import(${q}${transformSpec(s)}${q})`
|
|
1635
1691
|
);
|
|
1636
1692
|
}
|
|
1637
|
-
function
|
|
1693
|
+
function resolveAliasTarget2(value, root) {
|
|
1638
1694
|
if (path10.isAbsolute(value) && fs8.existsSync(value)) return value;
|
|
1639
1695
|
if (value.startsWith("/")) return path10.join(root, value.slice(1));
|
|
1640
1696
|
return path10.resolve(root, value);
|
|
@@ -2059,7 +2115,7 @@ async function createServer(inlineConfig = {}) {
|
|
|
2059
2115
|
const localUrl = `http://localhost:${actualPort}`;
|
|
2060
2116
|
const networkUrl = host === "0.0.0.0" ? `http://${getNetworkAddress()}:${actualPort}` : null;
|
|
2061
2117
|
console.log();
|
|
2062
|
-
console.log(pc3.cyan(" nasti dev server") + pc3.dim(` v${"1.
|
|
2118
|
+
console.log(pc3.cyan(" nasti dev server") + pc3.dim(` v${"1.7.0"}`));
|
|
2063
2119
|
console.log();
|
|
2064
2120
|
console.log(` ${pc3.green(">")} Local: ${pc3.cyan(localUrl)}`);
|
|
2065
2121
|
if (networkUrl) {
|
|
@@ -2183,7 +2239,7 @@ async function buildElectron(inlineConfig = {}) {
|
|
|
2183
2239
|
const config = await resolveConfig({ ...inlineConfig, target: "electron" }, "build");
|
|
2184
2240
|
const startTime = performance.now();
|
|
2185
2241
|
assertElectronVersion(config);
|
|
2186
|
-
console.log(pc2.cyan("\n\u26A1 nasti build (electron)") + pc2.dim(` v${"1.
|
|
2242
|
+
console.log(pc2.cyan("\n\u26A1 nasti build (electron)") + pc2.dim(` v${"1.7.0"}`));
|
|
2187
2243
|
console.log(pc2.dim(` root: ${config.root}`));
|
|
2188
2244
|
console.log(pc2.dim(` mode: ${config.mode}`));
|
|
2189
2245
|
console.log(pc2.dim(` target: electron (\u2265 ${config.electron.minVersion})`));
|
|
@@ -2261,19 +2317,23 @@ async function bundleNode(config, entry, opts) {
|
|
|
2261
2317
|
return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
|
|
2262
2318
|
}
|
|
2263
2319
|
};
|
|
2320
|
+
const { output: userOutput, transform: userTransform, ...restInputOptions } = config.build.rolldownOptions;
|
|
2321
|
+
const mergedDefine = { ...userTransform?.define ?? {}, ...envDefine };
|
|
2264
2322
|
const bundle = await rolldown2({
|
|
2323
|
+
...restInputOptions,
|
|
2265
2324
|
input: entry,
|
|
2266
|
-
define: envDefine,
|
|
2267
2325
|
platform: "node",
|
|
2268
|
-
|
|
2269
|
-
|
|
2326
|
+
transform: { ...userTransform, define: mergedDefine },
|
|
2327
|
+
plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
|
|
2270
2328
|
});
|
|
2271
2329
|
fs7.mkdirSync(path9.dirname(opts.outFile), { recursive: true });
|
|
2272
2330
|
await bundle.write({
|
|
2273
|
-
file: opts.outFile,
|
|
2274
|
-
format: opts.format === "cjs" ? "cjs" : "esm",
|
|
2275
2331
|
sourcemap: !!config.build.sourcemap,
|
|
2276
2332
|
minify: !!config.build.minify,
|
|
2333
|
+
// 允许用户微调 output;但主进程 / preload 的单文件约束由下方键强制保证
|
|
2334
|
+
...userOutput,
|
|
2335
|
+
file: opts.outFile,
|
|
2336
|
+
format: opts.format === "cjs" ? "cjs" : "esm",
|
|
2277
2337
|
codeSplitting: false
|
|
2278
2338
|
});
|
|
2279
2339
|
await bundle.close();
|
|
@@ -2330,7 +2390,7 @@ async function startElectronDev(inlineConfig = {}) {
|
|
|
2330
2390
|
const { noSpawn, ...rest } = inlineConfig;
|
|
2331
2391
|
const config = await resolveConfig({ ...rest, target: "electron" }, "serve");
|
|
2332
2392
|
warnElectronVersion(config);
|
|
2333
|
-
console.log(pc4.cyan("\n\u26A1 nasti electron dev") + pc4.dim(` v${"1.
|
|
2393
|
+
console.log(pc4.cyan("\n\u26A1 nasti electron dev") + pc4.dim(` v${"1.7.0"}`));
|
|
2334
2394
|
const { createServer: createServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
2335
2395
|
const server = await createServer2({ ...rest, target: "electron" });
|
|
2336
2396
|
await server.listen();
|