@simplysm/sd-cli 12.5.17 → 12.5.18

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 (34) hide show
  1. package/dist/build-tools/SdCliIndexFileGenerator.js +2 -2
  2. package/dist/build-tools/SdCliIndexFileGenerator.js.map +1 -1
  3. package/dist/build-tools/SdLinter.js +5 -2
  4. package/dist/build-tools/SdLinter.js.map +1 -1
  5. package/dist/build-tools/SdReactBundler.d.ts +24 -0
  6. package/dist/build-tools/SdReactBundler.js +294 -0
  7. package/dist/build-tools/SdReactBundler.js.map +1 -0
  8. package/dist/build-tools/SdReactBundlerContext.d.ts +14 -0
  9. package/dist/build-tools/SdReactBundlerContext.js +59 -0
  10. package/dist/build-tools/SdReactBundlerContext.js.map +1 -0
  11. package/dist/build-tools/SdTsCompiler.js +274 -226
  12. package/dist/build-tools/SdTsCompiler.js.map +1 -1
  13. package/dist/builders/SdCliClientBuilder.js +41 -17
  14. package/dist/builders/SdCliClientBuilder.js.map +1 -1
  15. package/dist/bundle-plugins/sdReactPlugin.d.ts +15 -0
  16. package/dist/bundle-plugins/sdReactPlugin.js +116 -0
  17. package/dist/bundle-plugins/sdReactPlugin.js.map +1 -0
  18. package/dist/entry/SdCliProject.js +2 -2
  19. package/dist/entry/SdCliProject.js.map +1 -1
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/index.js.map +1 -1
  23. package/package.json +11 -11
  24. package/src/build-tools/SdCliIndexFileGenerator.ts +2 -2
  25. package/src/build-tools/SdLinter.ts +8 -2
  26. package/src/build-tools/SdReactBundler.ts +370 -0
  27. package/src/build-tools/SdReactBundlerContext.ts +71 -0
  28. package/src/build-tools/SdTsCompiler.ts +227 -112
  29. package/src/builders/SdCliClientBuilder.ts +52 -27
  30. package/src/bundle-plugins/sdReactPlugin.ts +160 -0
  31. package/src/entry/SdCliProject.ts +6 -2
  32. package/src/index.ts +3 -0
  33. package/tsconfig.json +5 -11
  34. package/eslint.config.js +0 -1
@@ -0,0 +1,370 @@
1
+ import path from "path";
2
+ import { FsUtil, Logger, PathUtil } from "@simplysm/sd-core-node";
3
+ import { fileURLToPath } from "url";
4
+ import nodeStdLibBrowser from "node-stdlib-browser";
5
+ import nodeStdLibBrowserPlugin from "node-stdlib-browser/helpers/esbuild/plugin";
6
+ import { INpmConfig, ISdCliClientBuilderCordovaConfig, ISdCliPackageBuildResult } from "../commons";
7
+ import ts from "typescript";
8
+ import { SdReactBundlerContext } from "./SdReactBundlerContext";
9
+ import { IReactPluginResultCache, sdReactPlugin } from "../bundle-plugins/sdReactPlugin";
10
+ import { transformSupportedBrowsersToTargets } from "@angular/build/src/tools/esbuild/utils";
11
+ import browserslist from "browserslist";
12
+ import { glob } from "glob";
13
+ import { generateSW } from "workbox-build";
14
+
15
+ export class SdReactBundler {
16
+ readonly #logger = Logger.get(["simplysm", "sd-cli", "SdNgBundler"]);
17
+
18
+ readonly #modifiedFileSet = new Set<string>();
19
+ readonly #compileResultCache: IReactPluginResultCache = {
20
+ affectedFileSet: new Set<string>(),
21
+ watchFileSet: new Set<string>()
22
+ };
23
+ #contexts: SdReactBundlerContext[] | undefined;
24
+
25
+ readonly #outputCache = new Map<string, string | number>();
26
+
27
+ readonly #opt: IOptions;
28
+
29
+ readonly #pkgNpmConf: INpmConfig;
30
+ readonly #indexFilePath: string;
31
+ readonly #tsConfigFilePath: string;
32
+ readonly #browserTarget: string[];
33
+
34
+ public constructor(opt: IOptions) {
35
+ this.#opt = opt;
36
+ this.#pkgNpmConf = FsUtil.readJson(path.resolve(opt.pkgPath, "package.json"));
37
+ this.#indexFilePath = path.resolve(opt.pkgPath, "src/index.tsx");
38
+ this.#tsConfigFilePath = path.resolve(opt.pkgPath, "tsconfig.json");
39
+ this.#browserTarget = transformSupportedBrowsersToTargets(browserslist(["Chrome > 78"]));
40
+ }
41
+
42
+ public markForChanges(filePaths: string[]): void {
43
+ for (const filePath of filePaths) {
44
+ this.#modifiedFileSet.add(path.normalize(filePath));
45
+ }
46
+ }
47
+
48
+ public async bundleAsync(): Promise<{
49
+ program?: ts.Program;
50
+ watchFileSet: Set<string>;
51
+ affectedFileSet: Set<string>;
52
+ results: ISdCliPackageBuildResult[];
53
+ }> {
54
+ this.#debug(`get contexts...`);
55
+
56
+ if (!this.#contexts) {
57
+ this.#contexts = [
58
+ this._getAppContext(),
59
+ ...(this.#opt.builderType === "electron" ? [this._getElectronMainContext()] : [])
60
+ ];
61
+ }
62
+
63
+ this.#debug(`build...`);
64
+
65
+ const bundlingResults = await this.#contexts.mapAsync(async (ctx, i) => await ctx.bundleAsync());
66
+
67
+ //-- results
68
+ const results = bundlingResults.mapMany((bundlingResult) => bundlingResult.results);
69
+
70
+ this.#debug(`convert result...`);
71
+
72
+ const outputFiles: IReactOutputFile[] = bundlingResults
73
+ .mapMany((item) => item.outputFiles ?? [])
74
+ .map((item) => ({
75
+ relPath: path.relative(this.#opt.pkgPath, item.path),
76
+ contents: item.contents
77
+ }));
78
+
79
+ // cordova empty
80
+ if (this.#opt.builderType === "cordova" && this.#opt.cordovaConfig?.plugins) {
81
+ outputFiles.push({ relPath: "cordova-empty.js", contents: "export default {};" });
82
+ }
83
+
84
+ // index.html
85
+ let indexHtml = await FsUtil.readFileAsync(path.resolve(this.#opt.pkgPath, "src/index.html"));
86
+ indexHtml = indexHtml.replace(/<\/head>/, "<script type=\"module\" src=\"index.js\"></script></head>");
87
+ if (!this.#opt.dev) {
88
+ indexHtml = indexHtml.replace(/<\/head>/, "<script src=\"registerSW.js\"></script></head>");
89
+ }
90
+ if (this.#opt.builderType === "cordova") {
91
+ indexHtml = indexHtml.replace(
92
+ /(.*)<\/head>/,
93
+ "<script type=\"module\" src=\"cordova-entry.js\"></script>\n$1</head>"
94
+ );
95
+ }
96
+ if (outputFiles.some((item) => item.relPath === "index.css")) {
97
+ indexHtml = indexHtml.replace(/(.*)<\/head>/, "<link rel=\"stylesheet\" href=\"index.css\">\n$1</head>");
98
+ }
99
+
100
+ outputFiles.push({
101
+ relPath: "index.html",
102
+ contents: indexHtml
103
+ });
104
+
105
+ // copy assets
106
+ outputFiles.push(
107
+ ...this.#copyAssets("src", "favicon.ico"),
108
+ ...this.#copyAssets("src", "manifest.webmanifest"),
109
+ ...this.#copyAssets("src/assets", "**/*", "assets"),
110
+ ...(this.#opt.dev && this.#opt.builderType === "cordova"
111
+ ? Object.keys(this.#opt.cordovaConfig?.platform ?? { browser: {} }).mapMany((platform) => [
112
+ ...this.#copyAssets(
113
+ `.cordova/platforms/${platform}/platform_www/plugins`,
114
+ "**/*",
115
+ `cordova-${platform}/plugins`
116
+ ),
117
+ ...this.#copyAssets(`.cordova/platforms/${platform}/platform_www`, "cordova.js", `cordova-${platform}`),
118
+ ...this.#copyAssets(
119
+ `.cordova/platforms/${platform}/platform_www`,
120
+ "cordova_plugins.js",
121
+ `cordova-${platform}`
122
+ ),
123
+ ...this.#copyAssets(`.cordova/platforms/${platform}/www`, "config.xml", `cordova-${platform}`)
124
+ ])
125
+ : [])
126
+ );
127
+
128
+ // TODO: extract 3rdpartylicenses
129
+
130
+ //-- write
131
+ this.#debug(`write output files...(${outputFiles.length})`);
132
+
133
+ for (const outputFile of outputFiles) {
134
+ const distFilePath = path.resolve(this.#opt.outputPath, outputFile.relPath);
135
+ const prev = this.#outputCache.get(distFilePath);
136
+ if (prev !== Buffer.from(outputFile.contents).toString("base64")) {
137
+ await FsUtil.writeFileAsync(distFilePath, outputFile.contents);
138
+ this.#outputCache.set(distFilePath, Buffer.from(outputFile.contents).toString("base64"));
139
+ }
140
+ }
141
+
142
+ // service worker
143
+ if (!this.#opt.dev) {
144
+ try {
145
+ const swResult = await generateSW({
146
+ globDirectory: path.resolve(this.#opt.pkgPath, "dist"),
147
+ globPatterns: ["**/*"],
148
+ globIgnores: ["3rdpartylicenses.txt", "*.js.map"],
149
+ globStrict: true,
150
+ swDest: path.resolve(this.#opt.pkgPath, "dist", "sw.js")
151
+ });
152
+ swResult.warnings.map((msg) => {
153
+ results.push({
154
+ filePath: undefined,
155
+ line: undefined,
156
+ char: undefined,
157
+ code: undefined,
158
+ severity: "warning",
159
+ message: `(gen-sw) ${msg}`,
160
+ type: "build"
161
+ });
162
+ });
163
+
164
+ await FsUtil.writeFileAsync(
165
+ path.resolve(this.#opt.pkgPath, "dist", "registerSW.js"),
166
+ `
167
+ if ('serviceWorker' in navigator) {
168
+ window.addEventListener('load', () => {
169
+ navigator.serviceWorker.register('sw.js')
170
+ })
171
+ }`.trim()
172
+ );
173
+ }
174
+ catch (err) {
175
+ results.push({
176
+ filePath: undefined,
177
+ line: undefined,
178
+ char: undefined,
179
+ code: undefined,
180
+ severity: "error",
181
+ message: `(gen-sw) ${err.toString()}`,
182
+ type: "build"
183
+ });
184
+ }
185
+ }
186
+
187
+ this.#debug(`번들링중 영향받은 파일`, Array.from(this.#compileResultCache.affectedFileSet!));
188
+
189
+ return {
190
+ program: this.#compileResultCache.program,
191
+ watchFileSet: new Set(this.#compileResultCache.watchFileSet),
192
+ affectedFileSet: this.#compileResultCache.affectedFileSet!,
193
+ results
194
+ };
195
+ }
196
+
197
+ #copyAssets(srcDir: string, globPath: string, distDir?: string): IReactOutputFile[] {
198
+ const result: IReactOutputFile[] = [];
199
+ const srcGlobPath = path.resolve(this.#opt.pkgPath, srcDir, globPath);
200
+ const srcPaths = glob.sync(srcGlobPath);
201
+ for (const srcPath of srcPaths) {
202
+ if (!FsUtil.exists(srcPath)) continue;
203
+ result.push({
204
+ relPath: path.join(
205
+ ...[distDir, path.relative(path.resolve(this.#opt.pkgPath, srcDir), srcPath)].filterExists()
206
+ ),
207
+ contents: FsUtil.readFile(srcPath)
208
+ });
209
+ }
210
+
211
+ return result;
212
+ }
213
+
214
+ private _getAppContext() {
215
+ return new SdReactBundlerContext(this.#opt.pkgPath, {
216
+ absWorkingDir: this.#opt.pkgPath,
217
+ bundle: true,
218
+ keepNames: true,
219
+ format: "esm",
220
+ assetNames: "media/[name]",
221
+ conditions: ["es2020", "es2015", "module"],
222
+ resolveExtensions: [".js", ".mjs", ".cjs", ".ts", ".tsx"],
223
+ metafile: true,
224
+ legalComments: this.#opt.dev ? "eof" : "none",
225
+ logLevel: "silent",
226
+ minifyIdentifiers: !this.#opt.dev,
227
+ minifySyntax: !this.#opt.dev,
228
+ minifyWhitespace: !this.#opt.dev,
229
+ pure: ["forwardRef"],
230
+ outdir: this.#opt.pkgPath,
231
+ outExtension: undefined,
232
+ sourcemap: true, //this.#opt.dev,
233
+ splitting: true,
234
+ chunkNames: "[name]-[hash]",
235
+ tsconfig: this.#tsConfigFilePath,
236
+ write: false,
237
+ preserveSymlinks: false,
238
+ define: {
239
+ ...(!this.#opt.dev ? { ngDevMode: "false" } : {}),
240
+ "ngJitMode": "false",
241
+ "global": "global",
242
+ "process": "process",
243
+ "Buffer": "Buffer",
244
+ "process.env.SD_VERSION": JSON.stringify(this.#pkgNpmConf.version),
245
+ "process.env.NODE_ENV": JSON.stringify(this.#opt.dev ? "development" : "production"),
246
+ ...(this.#opt.env
247
+ ? Object.keys(this.#opt.env).toObject(
248
+ (key) => `process.env.${key}`,
249
+ (key) => JSON.stringify(this.#opt.env![key])
250
+ )
251
+ : {})
252
+ },
253
+ platform: "browser",
254
+ mainFields: ["es2020", "es2015", "browser", "module", "main"],
255
+ entryNames: "[name]",
256
+ entryPoints: {
257
+ index: this.#indexFilePath
258
+ },
259
+ external: ["electron"],
260
+ target: this.#browserTarget,
261
+ supported: { "async-await": false, "object-rest-spread": false },
262
+ loader: {
263
+ ".png": "file",
264
+ ".jpeg": "file",
265
+ ".jpg": "file",
266
+ ".jfif": "file",
267
+ ".gif": "file",
268
+ ".svg": "file",
269
+ ".woff": "file",
270
+ ".woff2": "file",
271
+ ".ttf": "file",
272
+ ".ttc": "file",
273
+ ".eot": "file",
274
+ ".ico": "file",
275
+ ".otf": "file",
276
+ ".csv": "file",
277
+ ".xlsx": "file",
278
+ ".xls": "file",
279
+ ".pptx": "file",
280
+ ".ppt": "file",
281
+ ".docx": "file",
282
+ ".doc": "file",
283
+ ".zip": "file",
284
+ ".pfx": "file",
285
+ ".pkl": "file",
286
+ ".mp3": "file",
287
+ ".ogg": "file"
288
+ },
289
+ inject: [PathUtil.posix(fileURLToPath(import.meta.resolve("node-stdlib-browser/helpers/esbuild/shim")))],
290
+ plugins: [
291
+ ...(this.#opt.builderType === "cordova" && this.#opt.cordovaConfig?.plugins
292
+ ? [
293
+ {
294
+ name: "cordova:plugin-empty",
295
+ setup: ({ onResolve }) => {
296
+ onResolve({ filter: new RegExp("(" + this.#opt.cordovaConfig!.plugins!.join("|") + ")") }, () => {
297
+ return {
298
+ path: `./cordova-empty.js`,
299
+ external: true
300
+ };
301
+ });
302
+ }
303
+ }
304
+ ]
305
+ : []),
306
+ sdReactPlugin({
307
+ modifiedFileSet: this.#modifiedFileSet,
308
+ dev: this.#opt.dev,
309
+ pkgPath: this.#opt.pkgPath,
310
+ result: this.#compileResultCache
311
+ }),
312
+ nodeStdLibBrowserPlugin(nodeStdLibBrowser)
313
+ ]
314
+ });
315
+ }
316
+
317
+ private _getElectronMainContext() {
318
+ return new SdReactBundlerContext(this.#opt.pkgPath, {
319
+ absWorkingDir: this.#opt.pkgPath,
320
+ bundle: true,
321
+ entryNames: "[name]",
322
+ assetNames: "media/[name]",
323
+ conditions: ["es2020", "es2015", "module"],
324
+ resolveExtensions: [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"],
325
+ metafile: true,
326
+ legalComments: this.#opt.dev ? "eof" : "none",
327
+ logLevel: "silent",
328
+ minify: !this.#opt.dev,
329
+ outdir: this.#opt.pkgPath,
330
+ sourcemap: true, //this.#opt.dev,
331
+ tsconfig: this.#tsConfigFilePath,
332
+ write: false,
333
+ preserveSymlinks: false,
334
+ external: ["electron"],
335
+ define: {
336
+ ...(!this.#opt.dev ? { ngDevMode: "false" } : {}),
337
+ "process.env.SD_VERSION": JSON.stringify(this.#pkgNpmConf.version),
338
+ "process.env.NODE_ENV": JSON.stringify(this.#opt.dev ? "development" : "production"),
339
+ ...(this.#opt.env
340
+ ? Object.keys(this.#opt.env).toObject(
341
+ (key) => `process.env.${key}`,
342
+ (key) => JSON.stringify(this.#opt.env![key])
343
+ )
344
+ : {})
345
+ },
346
+ platform: "node",
347
+ entryPoints: {
348
+ "electron-main": path.resolve(this.#opt.pkgPath, "src/electron-main.ts")
349
+ }
350
+ });
351
+ }
352
+
353
+ #debug(...msg: any[]): void {
354
+ this.#logger.debug(`[${path.basename(this.#opt.pkgPath)}]`, ...msg);
355
+ }
356
+ }
357
+
358
+ interface IOptions {
359
+ dev: boolean;
360
+ outputPath: string;
361
+ pkgPath: string;
362
+ builderType: string;
363
+ env: Record<string, string> | undefined;
364
+ cordovaConfig: ISdCliClientBuilderCordovaConfig | undefined;
365
+ }
366
+
367
+ interface IReactOutputFile {
368
+ relPath: string;
369
+ contents: Uint8Array | string;
370
+ }
@@ -0,0 +1,71 @@
1
+ import esbuild from "esbuild";
2
+ import path from "path";
3
+ import { ISdCliPackageBuildResult } from "../commons";
4
+ import { Logger } from "@simplysm/sd-core-node";
5
+
6
+ export class SdReactBundlerContext {
7
+ readonly #logger = Logger.get(["simplysm", "sd-cli", "SdReactBundlerContext"]);
8
+
9
+ private _context?: esbuild.BuildContext;
10
+
11
+ public constructor(
12
+ private readonly _pkgPath: string,
13
+ private readonly _esbuildOptions: esbuild.BuildOptions,
14
+ ) {}
15
+
16
+ public async bundleAsync() {
17
+ if (this._context == null) {
18
+ this._context = await esbuild.context(this._esbuildOptions);
19
+ }
20
+
21
+ let buildResult: esbuild.BuildResult;
22
+
23
+ try {
24
+ this.#debug(`rebuild...`);
25
+ buildResult = await this._context.rebuild();
26
+ this.#debug(`rebuild completed`);
27
+ } catch (err) {
28
+ if ("warnings" in err || "errors" in err) {
29
+ buildResult = err;
30
+ } else {
31
+ throw err;
32
+ }
33
+ }
34
+
35
+ this.#debug(`convert results...`);
36
+
37
+ const results = [
38
+ ...buildResult.warnings.map((warn) => ({
39
+ filePath: warn.location?.file !== undefined ? path.resolve(this._pkgPath, warn.location.file) : undefined,
40
+ line: warn.location?.line,
41
+ char: warn.location?.column,
42
+ code: warn.text.slice(0, warn.text.indexOf(":")),
43
+ severity: "warning",
44
+ message: `${warn.pluginName ? `(${warn.pluginName}) ` : ""} ${warn.text.slice(warn.text.indexOf(":") + 1)}`,
45
+ type: "build",
46
+ })),
47
+ ...buildResult.errors.map((err) => ({
48
+ filePath: err.location?.file !== undefined ? path.resolve(this._pkgPath, err.location.file) : undefined,
49
+ line: err.location?.line,
50
+ char: err.location?.column !== undefined ? err.location.column + 1 : undefined,
51
+ code: err.text.slice(0, err.text.indexOf(":")),
52
+ severity: "error",
53
+ message: `${err.pluginName ? `(${err.pluginName}) ` : ""} ${err.text.slice(err.text.indexOf(":") + 1)}`,
54
+ type: "build",
55
+ })),
56
+ ] as ISdCliPackageBuildResult[];
57
+
58
+ return {
59
+ results,
60
+ outputFiles: buildResult.outputFiles,
61
+ metafile: buildResult.metafile,
62
+ };
63
+ }
64
+
65
+ #debug(...msg: any[]): void {
66
+ this.#logger.debug(
67
+ `[${path.basename(this._pkgPath)}] (${Object.keys(this._esbuildOptions.entryPoints as Record<string, any>).join(", ")})`,
68
+ ...msg,
69
+ );
70
+ }
71
+ }