@dxtmisha/scripts 0.7.9 → 0.7.11

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.
@@ -0,0 +1,375 @@
1
+ import {
2
+ type Browser,
3
+ type EvaluateFunc,
4
+ type HTTPResponse,
5
+ launch,
6
+ type Page,
7
+ type PuppeteerLifeCycleEvent
8
+ } from 'puppeteer'
9
+ import { sleep } from '@dxtmisha/functional-basic'
10
+
11
+ import {
12
+ type ScreenshotMetrics,
13
+ type ScreenshotOptions,
14
+ SCREENSHOT_ARGS,
15
+ SCREENSHOT_FORMAT,
16
+ SCREENSHOT_HEIGHTS,
17
+ SCREENSHOT_TIMEOUT,
18
+ SCREENSHOT_QUALITY,
19
+ SCREENSHOT_WAIT_UNTIL,
20
+ SCREENSHOT_WIDTHS
21
+ } from '../types/screenshotTypes'
22
+
23
+ /**
24
+ * Class for managing browser instances and taking screenshots using Puppeteer.
25
+ *
26
+ * Класс для управления экземплярами браузера и создания скриншотов с помощью Puppeteer.
27
+ */
28
+ export class BrowserItem {
29
+ /** Browser instance / Экземпляр браузера */
30
+ protected browser?: Browser
31
+
32
+ /** Page instance / Экземпляр страницы */
33
+ protected page?: Page
34
+
35
+ /** HTTP Response from page loading / HTTP-ответ при загрузке страницы */
36
+ protected response?: HTTPResponse
37
+
38
+ /** Total scrollable width of the loaded page / Полная прокручиваемая ширина загруженной страницы */
39
+ protected pageWidth: number = -1
40
+
41
+ /** Total scrollable height of the loaded page / Полная прокручиваемая высота загруженной страницы */
42
+ protected pageHeight: number = -1
43
+
44
+ /**
45
+ * Constructor
46
+ * @param url component path / путь к компоненту
47
+ * @param options additional options for capture / дополнительные опции захвата
48
+ */
49
+ constructor(
50
+ protected url: string,
51
+ protected readonly options: ScreenshotOptions = {}
52
+ ) {
53
+ }
54
+
55
+ /** Additional arguments for browser launch / Дополнительные аргументы для запуска браузера */
56
+ get args(): string[] {
57
+ return this.options.args ?? SCREENSHOT_ARGS
58
+ }
59
+
60
+ /** Condition to wait for when navigating to the page / Условие, которое нужно дождаться при переходе на страницу */
61
+ get waitUntil(): PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[] {
62
+ return this.options.waitUntil ?? SCREENSHOT_WAIT_UNTIL
63
+ }
64
+
65
+ /** Timeout for navigating to the page / Таймаут для перехода на страницу */
66
+ get timeout(): number {
67
+ return this.options.timeout ?? SCREENSHOT_TIMEOUT
68
+ }
69
+
70
+ /** Image format for screenshots / Формат изображения для скриншотов */
71
+ get format(): ScreenshotOptions['format'] {
72
+ return this.options.format ?? SCREENSHOT_FORMAT
73
+ }
74
+
75
+ /** Viewport width for the initial capture / Ширина порта просмотра для начального захвата */
76
+ get width(): number {
77
+ return this.options.width ?? SCREENSHOT_WIDTHS
78
+ }
79
+
80
+ /** Viewport height for the initial capture and page-by-page slicing / Высота порта просмотра для начального захвата и постраничной нарезки */
81
+ get height(): number {
82
+ return this.options.height ?? SCREENSHOT_HEIGHTS
83
+ }
84
+
85
+ /** Quality for JPEG/WebP formats (0-100) / Качество для форматов JPEG/WebP (0-100) */
86
+ get quality(): number {
87
+ return this.options.quality ?? SCREENSHOT_QUALITY
88
+ }
89
+
90
+ /** Whether to capture the whole scrollable page at once or in slices / Захватывать ли всю прокручиваемую страницу сразу или по частям */
91
+ get fullPage(): boolean {
92
+ return this.options.fullPage ?? false
93
+ }
94
+
95
+ /** File extension based on the selected format / Расширение файла на основе выбранного формата */
96
+ get extension(): string {
97
+ const format = this.format
98
+
99
+ if (format === 'jpeg') {
100
+ return '.jpg'
101
+ }
102
+
103
+ return `.${format}`
104
+ }
105
+
106
+ /**
107
+ * launches or returns the browser instance.
108
+ *
109
+ * Запускает или возвращает экземпляр браузера.
110
+ * @returns browser instance / экземпляр браузера
111
+ */
112
+ async getBrowser(): Promise<Browser> {
113
+ if (!this.browser) {
114
+ this.browser = await launch({
115
+ headless: true,
116
+ args: this.args
117
+ })
118
+ }
119
+
120
+ return this.browser
121
+ }
122
+
123
+ /**
124
+ * Creates or returns the page instance.
125
+ *
126
+ * Создает или возвращает экземпляр страницы.
127
+ * @returns page instance / экземпляр страницы
128
+ */
129
+ async getPage(): Promise<Page> {
130
+ if (!this.page) {
131
+ this.page = await (await this.getBrowser()).newPage()
132
+
133
+ await this.page.setViewport({
134
+ width: this.width,
135
+ height: this.height
136
+ })
137
+
138
+ this.page.on(
139
+ 'console',
140
+ msg => this.toConsole(`Log: ${msg.text()}`)
141
+ )
142
+
143
+ this.page.on(
144
+ 'pageerror',
145
+ err => this.toConsole(`Error: ${(err as any)?.message}`)
146
+ )
147
+ }
148
+
149
+ return this.page
150
+ }
151
+
152
+ /**
153
+ * Navigates to the URL and returns the HTTP response.
154
+ *
155
+ * Переходит по URL и возвращает HTTP-ответ.
156
+ * @returns HTTP response or undefined / HTTP-ответ или undefined
157
+ */
158
+ async getResponse(): Promise<HTTPResponse | undefined> {
159
+ if (!this.response) {
160
+ const response = await (await this.getPage()).goto(this.url, {
161
+ waitUntil: this.waitUntil,
162
+ timeout: this.timeout
163
+ })
164
+
165
+ if (
166
+ response
167
+ && response.ok()
168
+ ) {
169
+ this.response = response
170
+ } else {
171
+ this.toConsole(
172
+ `Failed to load page: ${this.url}. Status: ${response?.status()}`
173
+ )
174
+ }
175
+ }
176
+
177
+ return this.response
178
+ }
179
+
180
+ /**
181
+ * Retrieves the HTML content of the page.
182
+ *
183
+ * Извлекает HTML-содержимое страницы.
184
+ * @returns html content string / HTML-содержимое
185
+ */
186
+ async getDom(): Promise<string> {
187
+ return await (await this.getPage()).content()
188
+ }
189
+
190
+ /**
191
+ * Extracts scrollable dimensions of the page.
192
+ *
193
+ * Извлекает прокручиваемые размеры страницы.
194
+ * @returns object with width and height / объект с шириной и высотой
195
+ */
196
+ async getMetrics(): Promise<ScreenshotMetrics> {
197
+ if (
198
+ await this.getResponse()
199
+ && (
200
+ this.pageWidth === -1
201
+ || this.pageHeight === -1
202
+ )
203
+ ) {
204
+ const metrics = await this.evaluate(() => {
205
+ return {
206
+ height: document.documentElement.scrollHeight,
207
+ width: document.documentElement.scrollWidth
208
+ }
209
+ })
210
+
211
+ if (metrics) {
212
+ this.pageWidth = metrics.width
213
+ this.pageHeight = metrics.height
214
+ }
215
+ }
216
+
217
+ return {
218
+ height: this.pageHeight,
219
+ width: this.pageWidth
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Evaluates a function in the browser context.
225
+ *
226
+ * Выполняет функцию в контексте браузера.
227
+ * @param pageFunction function to evaluate / функция для выполнения
228
+ * @param args arguments for the function / аргументы для функции
229
+ * @returns result of evaluation / результат выполнения
230
+ */
231
+ async evaluate<
232
+ Params extends unknown[],
233
+ Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
234
+ >(
235
+ pageFunction: Func | string,
236
+ ...args: Params
237
+ ): Promise<Awaited<ReturnType<Func>> | undefined> {
238
+ return await (await this.getPage()).evaluate<Params, Func>(
239
+ pageFunction,
240
+ ...args
241
+ )
242
+ }
243
+
244
+ /**
245
+ * Captures screenshots based on the fullPage option.
246
+ *
247
+ * Создает скриншоты в зависимости от опции fullPage.
248
+ * @param outputPath base path for saving / базовый путь для сохранения
249
+ * @returns this instance / этот экземпляр
250
+ */
251
+ async screenshot(outputPath: string): Promise<this> {
252
+ if (this.fullPage) {
253
+ await this.screenshotFull(outputPath)
254
+ } else {
255
+ await this.screenshotPages(outputPath)
256
+ }
257
+
258
+ return this
259
+ }
260
+
261
+ /**
262
+ * Resets internal page and response states.
263
+ *
264
+ * Сбрасывает состояние страницы и ответа.
265
+ * @returns this instance / этот экземпляр
266
+ */
267
+ async reset(): Promise<this> {
268
+ this.page = undefined
269
+ this.pageWidth = -1
270
+ this.pageHeight = -1
271
+
272
+ this.response = undefined
273
+
274
+ return this
275
+ }
276
+
277
+ /**
278
+ * Formats the final file path based on current state and index.
279
+ *
280
+ * Форматирует итоговый путь к файлу на основе состояния и индекса.
281
+ * @param outputPath original output path / исходный путь сохранения
282
+ * @param index current page index / текущий индекс страницы
283
+ * @param max total pages count / общее количество страниц
284
+ * @returns formatted path string / отформатированная строка пути
285
+ */
286
+ protected getOutputPath(
287
+ outputPath: string,
288
+ index: number = 1,
289
+ max: number = 1
290
+ ): string {
291
+ if (this.fullPage) {
292
+ return outputPath + this.extension
293
+ }
294
+
295
+ return outputPath + `-${max}_${index}` + this.extension
296
+ }
297
+
298
+ /**
299
+ * Internal wrapper for Puppeteer's screenshot method.
300
+ *
301
+ * Внутренняя обертка для метода скриншота Puppeteer.
302
+ * @param path specific file path / путь к конкретному файлу
303
+ * @returns this instance / этот экземпляр
304
+ */
305
+ protected async saveScreenshot(path: string): Promise<this> {
306
+ const type = this.format
307
+ const quality = type === 'png' ? undefined : this.quality
308
+
309
+ await (await this.getPage()).screenshot({
310
+ path,
311
+ type,
312
+ quality,
313
+ fullPage: this.fullPage
314
+ })
315
+
316
+ return this
317
+ }
318
+
319
+ /**
320
+ * Iterates through page slices and saves multiple screenshots.
321
+ *
322
+ * Проходит по частям страницы и сохраняет несколько скриншотов.
323
+ * @param outputPath base output path / базовый путь сохранения
324
+ * @returns this instance / этот экземпляр
325
+ */
326
+ protected async screenshotPages(outputPath: string): Promise<this> {
327
+ const {
328
+ height: pageHeight
329
+ } = await this.getMetrics()
330
+
331
+ const viewportHeight = this.height
332
+ const pages = Math.ceil(pageHeight / viewportHeight)
333
+
334
+ this.toConsole(`Page height: ${pageHeight}px, viewport height: ${viewportHeight}px, total pages: ${pages}`)
335
+
336
+ for (let i = 0; i < pages; i++) {
337
+ const currentY = i * viewportHeight
338
+ const fileOutputPath = this.getOutputPath(outputPath, i + 1, pages)
339
+
340
+ await this.evaluate(y => window.scrollTo(0, y), currentY)
341
+ await sleep(320)
342
+ await this.saveScreenshot(fileOutputPath)
343
+
344
+ this.toConsole(`Saved screenshot page ${i + 1}/${pages} to ${fileOutputPath}`)
345
+ }
346
+
347
+ return this
348
+ }
349
+
350
+ /**
351
+ * Captures a single full page screenshot.
352
+ *
353
+ * Создает один скриншот всей страницы.
354
+ * @param outputPath output path / путь сохранения
355
+ * @returns this instance / этот экземпляр
356
+ */
357
+ protected async screenshotFull(outputPath: string): Promise<this> {
358
+ const outputPathFull = this.getOutputPath(outputPath)
359
+ await this.saveScreenshot(outputPathFull)
360
+
361
+ this.toConsole(`Saved full page screenshot to ${outputPathFull}`)
362
+
363
+ return this
364
+ }
365
+
366
+ /**
367
+ * Logs text with a "Browser" prefix.
368
+ *
369
+ * Выводит текст в консоль с префиксом "Browser".
370
+ * @param text text to log / текст для вывода
371
+ */
372
+ protected toConsole(text: string): void {
373
+ console.log('Browser', text)
374
+ }
375
+ }
@@ -0,0 +1,34 @@
1
+ import { FigmaApi } from '../FigmaApi'
2
+ import { PropertiesConfig } from '../Properties/PropertiesConfig.ts'
3
+
4
+ export class DesignFigma {
5
+ protected readonly api: FigmaApi
6
+
7
+ constructor(
8
+ protected readonly fileKey: string,
9
+ protected readonly nodeId?: string,
10
+ protected readonly token: string = PropertiesConfig.getFigmaToken()
11
+ ) {
12
+ this.api = new FigmaApi(token, fileKey, nodeId)
13
+ }
14
+
15
+ async make(): Promise<void> {
16
+ console.log('Design Figma', await this.initImage())
17
+ }
18
+
19
+ async initImage(): Promise<Record<string, string> | undefined> {
20
+ const image = await this.api.fileImages({
21
+ ids: this.nodeId as string,
22
+ format: 'svg',
23
+ svg_outline_text: false,
24
+ svg_include_id: true,
25
+ svg_include_node_id: true
26
+ })
27
+
28
+ if (image?.err) {
29
+ return undefined
30
+ }
31
+
32
+ return image?.images
33
+ }
34
+ }
@@ -215,7 +215,7 @@ export class DesignReplace {
215
215
  && !this.isNoMark(mark, name)
216
216
  ) {
217
217
  const typesString = !constructor && !types.match(/string|boolean/) && types.match(/'/)
218
- ? `${types}` // string |
218
+ ? `string | ${types}`
219
219
  : types
220
220
 
221
221
  templates.push(`${name}?: ${typesString}`)
@@ -0,0 +1,123 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { ServerStorage } from '@dxtmisha/functional-basic'
3
+ import { BrowserItem } from '../BrowserItem'
4
+ import { PropertiesFile } from '../Properties/PropertiesFile.ts'
5
+
6
+ /**
7
+ * Class for automatic capturing of screenshots by running dev server.
8
+ *
9
+ * Класс для автоматического захвата скриншотов путем запуска сервера разработки.
10
+ */
11
+ export class DesignScreenshot {
12
+ /** indicates if screenshot process is running / указывает, запущен ли процесс создания скриншота */
13
+ protected isReading: boolean = false
14
+
15
+ /** server url / URL сервера */
16
+ protected url?: string
17
+
18
+ /**
19
+ * Constructor
20
+ * @param file output path / путь к файлу
21
+ */
22
+ constructor(
23
+ protected readonly file: string = './ai-screenshot/screenshot'
24
+ ) {
25
+ ServerStorage.setErrorStatus(true)
26
+ }
27
+
28
+ /**
29
+ * starts the screenshot process.
30
+ *
31
+ * Запускает процесс создания скриншота.
32
+ */
33
+ async make() {
34
+ console.info('Screenshot')
35
+
36
+ PropertiesFile.createDir(PropertiesFile.getPathDir(this.file + '.file'))
37
+ this.makeServer()
38
+ }
39
+
40
+ /**
41
+ * screenshot trigger listener.
42
+ *
43
+ * Слушатель триггера для создания скриншота.
44
+ * @returns capture success status / статус успешного захвата
45
+ */
46
+ protected readonly listener = async (): Promise<boolean> => {
47
+ if (
48
+ this.url
49
+ && !this.isReading
50
+ ) {
51
+ this.isReading = true
52
+
53
+ console.info('URL', this.url)
54
+
55
+ const browser = new BrowserItem(this.url, { height: 1024 * 12 })
56
+ await browser.screenshot(this.file)
57
+
58
+ return true
59
+ }
60
+
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * spawns development server.
66
+ *
67
+ * Запускает сервер разработки.
68
+ * @returns this instance / этот экземпляр
69
+ */
70
+ protected makeServer(): this {
71
+ const server = spawn(
72
+ 'npm',
73
+ ['run', 'dev'],
74
+ {
75
+ shell: true,
76
+ stdio: 'pipe'
77
+ }
78
+ )
79
+
80
+ server.stdout?.on('data', (data) => {
81
+ console.log('Server', 'start')
82
+
83
+ const dataString = data.toString()
84
+ this.makeUrl(dataString)
85
+
86
+ if (this.url) {
87
+ console.info('Server', 'init')
88
+
89
+ this.listener()
90
+ .then((success: boolean) => {
91
+ if (success) {
92
+ server.kill()
93
+
94
+ console.info('Server', 'kill')
95
+ }
96
+ })
97
+ }
98
+ })
99
+
100
+ server.stderr?.on('data', (data) => {
101
+ console.error(`Server Error: ${data.toString()}`)
102
+ })
103
+
104
+ return this
105
+ }
106
+
107
+ /**
108
+ * extracts server url from console output.
109
+ *
110
+ * Извлекает URL сервера из консольного вывода.
111
+ * @param data command output / вывод команды
112
+ * @returns this instance / этот экземпляр
113
+ */
114
+ makeUrl(data: string): this {
115
+ const match = data.match(/(https?:\/\/localhost\S+)/)
116
+
117
+ if (match) {
118
+ this.url = match[1]
119
+ }
120
+
121
+ return this
122
+ }
123
+ }