sora 24.02.26 → 25.1.25
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +22 -22
- data/CHANGELOG.md +5 -5
- data/CODE_OF_CONDUCT.md +84 -84
- data/LICENSE.txt +21 -21
- data/README.ja.md +69 -0
- data/README.md +48 -20
- data/Rakefile +12 -8
- data/cgi-bin/get.rb +13 -1
- data/cgi-bin/main.rb +47 -11
- data/docs/get-ipaddr.png +0 -0
- data/docs/screenshot.png +0 -0
- data/lib/sora/version.rb +5 -5
- data/lib/sora.rb +3 -2
- data/package.json +18 -0
- data/sig/sora.rbs +4 -4
- data/src/index.ts +374 -0
- data/src/style.css +157 -0
- data/tsconfig.json +11 -0
- data/webpack.config.js +25 -0
- data/www/bundle.js +1346 -0
- data/www/index.html +30 -35
- data/yarn.lock +1073 -0
- metadata +12 -2
data/src/index.ts
ADDED
@@ -0,0 +1,374 @@
|
|
1
|
+
require('./style.css')
|
2
|
+
import { fluentProgress, fluentListbox, fluentOption, fluentButton, fluentCard, fluentTextField, fluentSwitch, provideFluentDesignSystem, Listbox, TextArea, Button } from '@fluentui/web-components'
|
3
|
+
import i18next from 'i18next'
|
4
|
+
|
5
|
+
provideFluentDesignSystem()
|
6
|
+
.register(
|
7
|
+
fluentProgress(),
|
8
|
+
fluentListbox(),
|
9
|
+
fluentOption(),
|
10
|
+
fluentButton(),
|
11
|
+
fluentCard(),
|
12
|
+
fluentTextField(),
|
13
|
+
fluentSwitch()
|
14
|
+
)
|
15
|
+
|
16
|
+
const setAttribute = (id: string) => {
|
17
|
+
const element = document.getElementById(id)
|
18
|
+
if(element != null) element.setAttribute("placeholder", i18next.t(id))
|
19
|
+
}
|
20
|
+
|
21
|
+
const setText = (id: string) => {
|
22
|
+
const element = document.getElementById(id)
|
23
|
+
if(element != null) element.textContent = i18next.t(id)
|
24
|
+
}
|
25
|
+
|
26
|
+
i18next.init({
|
27
|
+
lng: 'ja', // if you're using a language detector, do not define the lng option
|
28
|
+
debug: true,
|
29
|
+
resources: {
|
30
|
+
en: {
|
31
|
+
translation: {
|
32
|
+
"title-sora": "sora",
|
33
|
+
"input-text": "Input text...",
|
34
|
+
"select-file": "Select a file...",
|
35
|
+
"send-data": "Send",
|
36
|
+
"remove-file": "Remove a file",
|
37
|
+
"remove-text": "Remove a text",
|
38
|
+
"copy-text": "Copy a text",
|
39
|
+
"open-file": "Open a file",
|
40
|
+
"download-file": "Download a file"
|
41
|
+
}
|
42
|
+
},
|
43
|
+
ja: {
|
44
|
+
translation: {
|
45
|
+
"title-sora": "ソラ",
|
46
|
+
"input-text": "テキストを入力...",
|
47
|
+
"select-file": "ファイルの選択",
|
48
|
+
"send-data": "送信",
|
49
|
+
"remove-file": "ファイルの削除",
|
50
|
+
"remove-text": "テキストの削除",
|
51
|
+
"copy-text": "テキストのコピー",
|
52
|
+
"open-file": "ファイルを開く",
|
53
|
+
"download-file": "ファイルのダウンロード"
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}, () => {
|
58
|
+
setText("title-sora")
|
59
|
+
setAttribute("input-text")
|
60
|
+
setText("select-file")
|
61
|
+
setText("send-data")
|
62
|
+
setText("remove-file")
|
63
|
+
setText("remove-text")
|
64
|
+
setText("copy-text")
|
65
|
+
setText("open-file")
|
66
|
+
setText("download-file")
|
67
|
+
})
|
68
|
+
|
69
|
+
const progress = (isOn: boolean) => {
|
70
|
+
const progressBar = document.getElementById("progress")
|
71
|
+
if(isOn)
|
72
|
+
progressBar.classList.remove("hidden-progress")
|
73
|
+
else
|
74
|
+
progressBar.classList.add("hidden-progress")
|
75
|
+
}
|
76
|
+
|
77
|
+
// 与えられたオブジェクトのキーと値をエンコードして、HTMLフォームのクエリ文字列として連結
|
78
|
+
const encodeHTMLForm = (data: Record<string, string>) => {
|
79
|
+
const params: string[] = []
|
80
|
+
for (const name in data) {
|
81
|
+
const value = data[name]
|
82
|
+
const param = encodeURIComponent(name) + '=' + encodeURIComponent(value)
|
83
|
+
params.push(param)
|
84
|
+
}
|
85
|
+
return params.join('&').replace(/%20/g, '+')
|
86
|
+
}
|
87
|
+
|
88
|
+
const getFiles = () => {
|
89
|
+
const xhr = new XMLHttpRequest()
|
90
|
+
xhr.open("POST", "/cgi-bin")
|
91
|
+
xhr.send(encodeHTMLForm({ "files": "" }))
|
92
|
+
xhr.onreadystatechange = () => {
|
93
|
+
let texts = JSON.parse(xhr.responseText || "null")
|
94
|
+
if (texts == null)
|
95
|
+
return
|
96
|
+
|
97
|
+
// const fileListUl = document.getElementById('file-list-ul')
|
98
|
+
// fileListUl.innerHTML = ""
|
99
|
+
|
100
|
+
// const fileList = document.getElementById('file-list')
|
101
|
+
// fileList.style.display = texts.length == 0 ? "none" : "block"
|
102
|
+
// fileListWrap.style.display = texts.length == 0 ? "none" : "block"
|
103
|
+
|
104
|
+
for(const fileName of texts){
|
105
|
+
const div = document.createElement('li')
|
106
|
+
div.textContent = fileName
|
107
|
+
|
108
|
+
div.addEventListener('contextmenu', (e) => {
|
109
|
+
e.preventDefault()
|
110
|
+
contextmenuFile.style.left = e.pageX + 'px'
|
111
|
+
contextmenuFile.style.top = e.pageY + 'px'
|
112
|
+
contextmenuFile.style.display = 'block'
|
113
|
+
fileTarget = e.target
|
114
|
+
})
|
115
|
+
div.addEventListener('click', () => {
|
116
|
+
contextmenuFile.style.display = 'none'
|
117
|
+
location.href = `/get?filename=${fileName}`
|
118
|
+
})
|
119
|
+
|
120
|
+
// fileListUl.append(div)
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
// ファイルアップロード
|
126
|
+
const selectFileButtonPre = document.getElementById("select-file-button-pre")
|
127
|
+
const selectFile = document.getElementById("select-file")
|
128
|
+
if(selectFile != null && selectFileButtonPre != null){
|
129
|
+
selectFile.addEventListener("click", () => selectFileButtonPre.click())
|
130
|
+
selectFileButtonPre.addEventListener("change", () => {
|
131
|
+
const form: HTMLFormElement = document.getElementById("upload-form") as HTMLFormElement
|
132
|
+
const formData: FormData = new FormData(form)
|
133
|
+
|
134
|
+
const xhr = new XMLHttpRequest()
|
135
|
+
xhr.open("POST", "/cgi-bin")
|
136
|
+
xhr.upload.addEventListener('loadstart', () => {
|
137
|
+
console.log("Upload: start")
|
138
|
+
})
|
139
|
+
|
140
|
+
xhr.upload.addEventListener('load', () => {
|
141
|
+
// アップロード正常終了
|
142
|
+
console.log('Upload: done')
|
143
|
+
getFiles()
|
144
|
+
refresh()
|
145
|
+
})
|
146
|
+
|
147
|
+
progress(true)
|
148
|
+
xhr.send(formData)
|
149
|
+
})
|
150
|
+
}
|
151
|
+
|
152
|
+
// 子要素を削除
|
153
|
+
const removeChild = (element: HTMLElement) => {
|
154
|
+
const children: HTMLCollection = element.children
|
155
|
+
for(const child of Array.from(children)){
|
156
|
+
child.remove()
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
const b64ToUtf8 = (str: string): string => {
|
161
|
+
return decodeURIComponent(atob(str));
|
162
|
+
}
|
163
|
+
|
164
|
+
// リストを更新
|
165
|
+
const showList = (data: any, files: string[]) => {
|
166
|
+
const contents = document.getElementById("contents")
|
167
|
+
removeChild(contents)
|
168
|
+
|
169
|
+
// テキスト一覧を追加
|
170
|
+
for (const content of data) {
|
171
|
+
const text = b64ToUtf8(content.content)
|
172
|
+
const uuid = content.uuid
|
173
|
+
|
174
|
+
const fluentOpt = document.createElement("fluent-option")
|
175
|
+
fluentOpt.textContent = text
|
176
|
+
fluentOpt.setAttribute("value", uuid)
|
177
|
+
fluentOpt.setAttribute("type", "text")
|
178
|
+
|
179
|
+
fluentOpt.addEventListener("click", () => {
|
180
|
+
const items = document.getElementById("items")
|
181
|
+
items.classList.add("visible")
|
182
|
+
items.classList.remove("type-file")
|
183
|
+
items.classList.add("type-text")
|
184
|
+
|
185
|
+
const textData = document.getElementById("text-title")
|
186
|
+
textData.textContent = fluentOpt.textContent
|
187
|
+
|
188
|
+
const textContent = document.getElementById("text-content")
|
189
|
+
textContent.textContent = fluentOpt.textContent
|
190
|
+
})
|
191
|
+
|
192
|
+
contents.appendChild(fluentOpt)
|
193
|
+
}
|
194
|
+
|
195
|
+
// ファイル一覧を追加
|
196
|
+
for (const text of files){
|
197
|
+
const fluentOpt = document.createElement("fluent-option")
|
198
|
+
fluentOpt.textContent = text
|
199
|
+
fluentOpt.setAttribute("type", "file")
|
200
|
+
|
201
|
+
fluentOpt.addEventListener("click", () => {
|
202
|
+
const items = document.getElementById("items")
|
203
|
+
items.classList.add("visible")
|
204
|
+
items.classList.add("type-file")
|
205
|
+
items.classList.remove("type-text")
|
206
|
+
|
207
|
+
const textData = document.getElementById("file-title")
|
208
|
+
textData.textContent = fluentOpt.textContent
|
209
|
+
})
|
210
|
+
contents.appendChild(fluentOpt)
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
const refresh = (visibleDetails: boolean = false) => {
|
215
|
+
// 一覧を表示
|
216
|
+
fetch('/cgi-bin?data=')
|
217
|
+
.then((response) => response.json())
|
218
|
+
.then((data) => {
|
219
|
+
// showList(data)
|
220
|
+
fetch('/cgi-bin?files')
|
221
|
+
.then((response) => response.json())
|
222
|
+
.then((files) => {
|
223
|
+
showList(data, files)
|
224
|
+
progress(false)
|
225
|
+
if(visibleDetails) {
|
226
|
+
const items = document.getElementById("items")
|
227
|
+
items.classList.remove("visible")
|
228
|
+
}
|
229
|
+
}
|
230
|
+
)
|
231
|
+
}
|
232
|
+
)
|
233
|
+
}
|
234
|
+
|
235
|
+
refresh()
|
236
|
+
|
237
|
+
|
238
|
+
const utf8ToB64 = (str: string): string => {
|
239
|
+
return btoa(encodeURIComponent(str))
|
240
|
+
}
|
241
|
+
|
242
|
+
// データ送信
|
243
|
+
document.getElementById("send-data").addEventListener("click", () => {
|
244
|
+
progress(true)
|
245
|
+
const text: string = (document.getElementById("input-text") as HTMLInputElement).value
|
246
|
+
const base64Encoded: string = utf8ToB64(text);
|
247
|
+
fetch('/cgi-bin?data=' + base64Encoded)
|
248
|
+
.then((response) => response.json())
|
249
|
+
.then((data) => {
|
250
|
+
// showList(data)
|
251
|
+
fetch('/cgi-bin?files')
|
252
|
+
.then((response) => response.json())
|
253
|
+
.then((files) => {
|
254
|
+
showList(data, files)
|
255
|
+
progress(false)
|
256
|
+
}
|
257
|
+
)
|
258
|
+
}
|
259
|
+
)
|
260
|
+
})
|
261
|
+
|
262
|
+
/* テキストのコピーがクリックされたとき */
|
263
|
+
const copyText = document.getElementById("copy-text") as Button
|
264
|
+
copyText.addEventListener("click", () => {
|
265
|
+
const contents = document.getElementById("contents") as Listbox
|
266
|
+
if(contents.selectedIndex != -1){
|
267
|
+
// console.log(contents)
|
268
|
+
const selected = contents.selectedOptions[0]
|
269
|
+
selected.textContent
|
270
|
+
|
271
|
+
const textarea: HTMLTextAreaElement = document.createElement("textarea")
|
272
|
+
textarea.value = selected.textContent
|
273
|
+
document.body.appendChild(textarea)
|
274
|
+
textarea.select()
|
275
|
+
document.execCommand("copy")
|
276
|
+
document.body.removeChild(textarea)
|
277
|
+
}
|
278
|
+
})
|
279
|
+
|
280
|
+
/* テキストの削除がクリックされたとき */
|
281
|
+
const removeText = document.getElementById("remove-text") as Button
|
282
|
+
removeText.addEventListener("click", () => {
|
283
|
+
progress(true)
|
284
|
+
const contents = document.getElementById("contents") as Listbox
|
285
|
+
let uuid: string
|
286
|
+
if(contents.selectedIndex != -1){
|
287
|
+
const selected = contents.selectedOptions[0]
|
288
|
+
uuid = selected.value
|
289
|
+
}else{
|
290
|
+
return
|
291
|
+
}
|
292
|
+
|
293
|
+
fetch('/cgi-bin?delete=' + uuid)
|
294
|
+
.then((response) => response.json())
|
295
|
+
.then((_data) => {
|
296
|
+
refresh(true)
|
297
|
+
}
|
298
|
+
)
|
299
|
+
})
|
300
|
+
|
301
|
+
/* ファイルの削除がクリックされたとき */
|
302
|
+
const removeFile = document.getElementById("remove-file") as Button
|
303
|
+
removeFile.addEventListener("click", () => {
|
304
|
+
progress(true)
|
305
|
+
const contents = document.getElementById("contents") as Listbox
|
306
|
+
let fileName: string
|
307
|
+
if(contents.selectedIndex != -1){
|
308
|
+
const selected = contents.selectedOptions[0]
|
309
|
+
fileName = selected.textContent
|
310
|
+
}else{
|
311
|
+
return
|
312
|
+
}
|
313
|
+
|
314
|
+
fetch('/cgi-bin?removefile=' + fileName)
|
315
|
+
.then((response) => response.json())
|
316
|
+
.then((_data) => {
|
317
|
+
refresh(true)
|
318
|
+
}
|
319
|
+
)
|
320
|
+
refresh()
|
321
|
+
})
|
322
|
+
|
323
|
+
const downloadFile = (url: string, filename: string): void => {
|
324
|
+
"use strict"
|
325
|
+
|
326
|
+
// XMLHttpRequestオブジェクトを作成する
|
327
|
+
const xhr: XMLHttpRequest = new XMLHttpRequest()
|
328
|
+
xhr.open("GET", url, true)
|
329
|
+
xhr.responseType = "blob" // Blobオブジェクトとしてダウンロードする
|
330
|
+
xhr.onload = (): void => {
|
331
|
+
// ダウンロード完了後の処理を定義する
|
332
|
+
const blob: Blob = xhr.response
|
333
|
+
// Blobオブジェクトを指すURLオブジェクトを作る
|
334
|
+
const objectURL: string = window.URL.createObjectURL(blob)
|
335
|
+
// リンク(<a>要素)を生成し、JavaScriptからクリックする
|
336
|
+
const link: HTMLAnchorElement = document.createElement("a")
|
337
|
+
document.body.appendChild(link)
|
338
|
+
link.href = objectURL
|
339
|
+
link.download = filename
|
340
|
+
link.click()
|
341
|
+
document.body.removeChild(link)
|
342
|
+
}
|
343
|
+
// XMLHttpRequestオブジェクトの通信を開始する
|
344
|
+
xhr.send()
|
345
|
+
}
|
346
|
+
|
347
|
+
/* ファイルを開く */
|
348
|
+
const openFile = document.getElementById("open-file") as Button
|
349
|
+
openFile.addEventListener("click", () => {
|
350
|
+
const contents = document.getElementById("contents") as Listbox
|
351
|
+
let fileName: string
|
352
|
+
if(contents.selectedIndex != -1){
|
353
|
+
const selected = contents.selectedOptions[0]
|
354
|
+
fileName = selected.textContent
|
355
|
+
window.open(`/get?filename=${fileName}`, '_blank')
|
356
|
+
}else{
|
357
|
+
return
|
358
|
+
}
|
359
|
+
})
|
360
|
+
|
361
|
+
/* ファイルをダウンロード */
|
362
|
+
const downloadFileE = document.getElementById("download-file") as Button
|
363
|
+
downloadFileE.addEventListener("click", () => {
|
364
|
+
const contents = document.getElementById("contents") as Listbox
|
365
|
+
let fileName: string
|
366
|
+
if(contents.selectedIndex != -1){
|
367
|
+
const selected = contents.selectedOptions[0]
|
368
|
+
fileName = selected.textContent
|
369
|
+
downloadFile(`/get?filename=${fileName}`, fileName)
|
370
|
+
}else{
|
371
|
+
return
|
372
|
+
}
|
373
|
+
})
|
374
|
+
|
data/src/style.css
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
body {
|
2
|
+
/* background-color: green; */
|
3
|
+
height: 100vh;
|
4
|
+
margin: 0;
|
5
|
+
}
|
6
|
+
|
7
|
+
main {
|
8
|
+
padding: 8px;
|
9
|
+
display: grid;
|
10
|
+
grid-template-rows: auto 1fr auto;
|
11
|
+
height: calc(100% - 19px);
|
12
|
+
gap: 8px;
|
13
|
+
}
|
14
|
+
|
15
|
+
#items {
|
16
|
+
grid-row: 2 / 3;
|
17
|
+
display: grid;
|
18
|
+
grid-template-columns: 1fr auto;
|
19
|
+
transition: 1s;
|
20
|
+
}
|
21
|
+
|
22
|
+
#items.visible {
|
23
|
+
gap: 8px;
|
24
|
+
}
|
25
|
+
|
26
|
+
#items #contents {
|
27
|
+
grid-column: 1 / 2;
|
28
|
+
overflow-x: auto;
|
29
|
+
height: calc(100vh - 66px);
|
30
|
+
}
|
31
|
+
|
32
|
+
#items #details {
|
33
|
+
padding: 0;
|
34
|
+
width: 0;
|
35
|
+
border: unset;
|
36
|
+
box-shadow: unset;
|
37
|
+
}
|
38
|
+
|
39
|
+
#items.visible #details {
|
40
|
+
padding: 8px;
|
41
|
+
width: 480px;
|
42
|
+
border: solid 1px #F3F3F3;
|
43
|
+
}
|
44
|
+
|
45
|
+
#items.type-text #details .file-details {
|
46
|
+
display: none;
|
47
|
+
}
|
48
|
+
|
49
|
+
#items.type-file #details .text-details {
|
50
|
+
display: none;
|
51
|
+
}
|
52
|
+
|
53
|
+
#items.type-file #details .file-details {
|
54
|
+
display: grid;
|
55
|
+
grid-template-rows: auto 1fr auto auto;
|
56
|
+
gap: 8px;
|
57
|
+
height: 100%;
|
58
|
+
}
|
59
|
+
|
60
|
+
#items.type-file #details .file-details #file-title {
|
61
|
+
grid-row: 1 / 2;
|
62
|
+
margin: 8px 4px 0 4px;
|
63
|
+
}
|
64
|
+
|
65
|
+
#items.type-file #details .file-details #operation-file {
|
66
|
+
grid-row: 3 / 4;
|
67
|
+
display: grid;
|
68
|
+
grid-template-columns: 1fr 1fr;
|
69
|
+
gap: 8px;
|
70
|
+
}
|
71
|
+
|
72
|
+
#items.type-file #details .file-details #remove-file {
|
73
|
+
grid-row: 4 / 5;
|
74
|
+
}
|
75
|
+
|
76
|
+
/* テキスト */
|
77
|
+
#items.type-text #details .text-details {
|
78
|
+
display: grid;
|
79
|
+
grid-template-rows: auto auto auto auto;
|
80
|
+
gap: 8px;
|
81
|
+
}
|
82
|
+
|
83
|
+
#items.type-text #details .text-details #text-title {
|
84
|
+
grid-row: 1 / 2;
|
85
|
+
white-space: nowrap;
|
86
|
+
text-overflow: ellipsis;
|
87
|
+
overflow-x: hidden;
|
88
|
+
margin: 8px 4px 0 4px;
|
89
|
+
user-select: none;
|
90
|
+
height: 36px;
|
91
|
+
}
|
92
|
+
|
93
|
+
#items.type-text #details .text-details #text-content {
|
94
|
+
grid-row: 2 / 3;
|
95
|
+
margin: 0 4px;
|
96
|
+
height: calc(100vh - 217px);
|
97
|
+
overflow-y: auto;
|
98
|
+
}
|
99
|
+
|
100
|
+
#items.type-text #details .text-details #remove-text {
|
101
|
+
grid-row: 4 / 5;
|
102
|
+
}
|
103
|
+
/* ここまで */
|
104
|
+
|
105
|
+
|
106
|
+
#input-content{
|
107
|
+
grid-row: 3 / 4;
|
108
|
+
display: grid;
|
109
|
+
grid-template-columns: 1fr auto auto;
|
110
|
+
gap: 8px;
|
111
|
+
}
|
112
|
+
|
113
|
+
#contents fluent-option {
|
114
|
+
flex-shrink: 0;
|
115
|
+
margin: 2px 4px;
|
116
|
+
}
|
117
|
+
|
118
|
+
/* 進捗バー */
|
119
|
+
#progress.hidden-progress {
|
120
|
+
visibility: hidden;
|
121
|
+
}
|
122
|
+
|
123
|
+
@media only screen and (max-width: 768px) {
|
124
|
+
main {
|
125
|
+
display: block;
|
126
|
+
}
|
127
|
+
|
128
|
+
#items {
|
129
|
+
grid-template-columns: unset;
|
130
|
+
/* grid-template-rows: auto auto; */
|
131
|
+
display: block;
|
132
|
+
}
|
133
|
+
|
134
|
+
#items #contents {
|
135
|
+
height: unset;
|
136
|
+
margin-top: 8px;
|
137
|
+
width: calc(100vw - 16px);
|
138
|
+
}
|
139
|
+
|
140
|
+
#items #details {
|
141
|
+
height: 0;
|
142
|
+
}
|
143
|
+
|
144
|
+
#items.visible #details {
|
145
|
+
width: unset;
|
146
|
+
height: auto;
|
147
|
+
margin-top: 8px;
|
148
|
+
}
|
149
|
+
|
150
|
+
#items.type-text #details .text-details #text-content {
|
151
|
+
height: unset;
|
152
|
+
}
|
153
|
+
|
154
|
+
#input-content {
|
155
|
+
margin-top: 8px;
|
156
|
+
}
|
157
|
+
}
|
data/tsconfig.json
ADDED
data/webpack.config.js
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
const path = require('path');
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
entry: './src/index.ts',
|
5
|
+
module: {
|
6
|
+
rules: [
|
7
|
+
{
|
8
|
+
test: /\.tsx?$/,
|
9
|
+
use: 'ts-loader',
|
10
|
+
exclude: /node_modules/,
|
11
|
+
},
|
12
|
+
{
|
13
|
+
test: /\.css$/i,
|
14
|
+
use: ["style-loader", "css-loader"],
|
15
|
+
},
|
16
|
+
],
|
17
|
+
},
|
18
|
+
resolve: {
|
19
|
+
extensions: ['.tsx', '.ts', '.js'],
|
20
|
+
},
|
21
|
+
output: {
|
22
|
+
filename: 'bundle.js',
|
23
|
+
path: path.resolve(__dirname, 'dist'),
|
24
|
+
},
|
25
|
+
}
|