@mirta/rollup 0.3.5 → 0.4.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/dist/runtime.mjs CHANGED
@@ -1,1250 +1,20 @@
1
- import ts$1 from '@rollup/plugin-typescript';
1
+ import multi from '@rollup/plugin-multi-entry';
2
2
  import nodeResolve from '@rollup/plugin-node-resolve';
3
- import commonjs from '@rollup/plugin-commonjs';
3
+ import ts from 'rollup-plugin-typescript2';
4
4
  import replace from '@rollup/plugin-replace';
5
- import copy from 'rollup-plugin-copy';
6
- import dts from 'rollup-plugin-dts';
7
- import ts from 'typescript';
8
- import nodePath from 'node:path';
9
- import multi from '@rollup/plugin-multi-entry';
10
- import ts$2 from 'rollup-plugin-typescript2';
11
- import dotenv from '@dotenv-run/rollup';
12
5
  import { getBabelOutputPlugin } from '@rollup/plugin-babel';
13
- import { deleteAsync } from 'del';
6
+ import { loadEnvReplacements } from '@mirta/env-loader';
7
+ import { resolveMonorepoContextAsync } from '@mirta/workspace';
8
+ import { B as BuildError, d as del } from './errors.mjs';
14
9
  import path from 'path';
15
10
  import MagicString from 'magic-string';
16
- import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
17
- import { findWorkspacePackages } from '@pnpm/workspace.find-packages';
18
- import { readFileSync } from 'fs';
19
-
20
- function del(options = {}) {
21
- const { hook = 'buildStart', runOnce = false, targets = [], verbose = false, } = options;
22
- let isDeleted = false;
23
- return {
24
- name: 'del',
25
- [hook]: async () => {
26
- if (runOnce && isDeleted)
27
- return;
28
- const paths = await deleteAsync(targets, options);
29
- if (verbose || options.dryRun) {
30
- const message = options.dryRun
31
- ? `Expected files and folders to be deleted: ${paths.length.toString()}`
32
- : `Deleted files and folders: ${paths.length.toString()}`;
33
- console.log(message);
34
- if (paths.length)
35
- paths.forEach((path) => {
36
- console.log(path);
37
- });
38
- }
39
- isDeleted = true;
40
- },
41
- };
42
- }
43
-
44
- /**
45
- * Класс ошибки для обработки проблем с монорепозиторием,
46
- * расширяющий стандартный Error.
47
- *
48
- * @since 0.3.5
49
- *
50
- **/
51
- class WorkspaceError extends Error {
52
- /**
53
- * Приватный конструктор для создания экземпляра ошибки.
54
- *
55
- * @param message - Сообщение об ошибке.
56
- * @param scope - Область, к которой относится ошибка (по умолчанию '@mirta/rollup').
57
- *
58
- **/
59
- constructor(message, scope = '@mirta/rollup') {
60
- super(`[${scope}] ${message}`);
61
- this.name = 'WorkspaceError';
62
- if ('captureStackTrace' in Error)
63
- // eslint-disable-next-line @typescript-eslint/unbound-method
64
- Error.captureStackTrace(this, WorkspaceError.get);
65
- }
66
- static codeMappings = {
67
- noPackageName: (packagePath) => `Package with path "${packagePath}" missing required 'name' field in package.json`,
68
- noWorkspaces: () => 'No workspaces configured in root package.json',
69
- };
70
- static get(code, ...args) {
71
- const messageFn = this.codeMappings[code];
72
- const message = messageFn(...args);
73
- return new WorkspaceError(message);
74
- }
75
- }
76
- /**
77
- * Класс ошибки для обработки проблем с менеджерами пакетов,
78
- * расширяющий стандартный Error.
79
- *
80
- * @since 0.3.5
81
- *
82
- **/
83
- class PackageManagerError extends Error {
84
- /**
85
- * Приватный конструктор для создания экземпляра ошибки.
86
- *
87
- * @param message - Сообщение об ошибке.
88
- * @param scope - Область, к которой относится ошибка (по умолчанию '@mirta/rollup').
89
- *
90
- **/
91
- constructor(message, scope = '@mirta/rollup') {
92
- super(`[${scope}] ${message}`);
93
- this.name = 'PackageManagerError';
94
- if ('captureStackTrace' in Error)
95
- // eslint-disable-next-line @typescript-eslint/unbound-method
96
- Error.captureStackTrace(this, PackageManagerError.get);
97
- }
98
- static codeMappings = {
99
- pnpmOnly: () => 'PNPM is required for building. Other package managers are not supported at this time',
100
- };
101
- static get(code, ...args) {
102
- const messageFn = this.codeMappings[code];
103
- const message = messageFn(...args);
104
- return new PackageManagerError(message);
105
- }
106
- }
107
- /**
108
- * Класс ошибки для обработки проблем с файлами, расширяющий стандартный Error.
109
- *
110
- * @since 0.3.5
111
- *
112
- **/
113
- class FileError extends Error {
114
- /**
115
- * Приватный конструктор для создания экземпляра ошибки.
116
- *
117
- * @param message - Сообщение об ошибке.
118
- * @param scope - Область, к которой относится ошибка (по умолчанию '@mirta/rollup').
119
- *
120
- **/
121
- constructor(message, scope = '@mirta/rollup') {
122
- super(`[${scope}] ${message}`);
123
- this.name = 'FileError';
124
- if ('captureStackTrace' in Error)
125
- // eslint-disable-next-line @typescript-eslint/unbound-method
126
- Error.captureStackTrace(this, FileError.get);
127
- }
128
- /** Карта кодов ошибок с соответствующими сообщениями. */
129
- static codeMappings = {
130
- /** Ошибка, возникающая при отсутствии файла в указанном расположении. */
131
- notFound: (filePath) => `File not found: "${filePath}"`,
132
- /** Ошибка, возникающая при отсутствии доступа к указанному файлу. */
133
- noAccess: (filePath) => `No access to file "${filePath}"`,
134
- /** Ошибка, возникающая при невалидном JSON в файле. */
135
- invalidJson: (filePath, message) => `Invalid JSON in file "${filePath}": ${message}`,
136
- /** Ошибка парсинга, возникающая по неуточненным причинам. */
137
- failedToParse: (filePath, message) => `Failed to parse "${nodePath.basename(filePath)}": ${message}`,
138
- };
139
- /**
140
- * Статический метод для получения экземпляра ошибки по коду.
141
- *
142
- * @template T - Тип ключа из codeMappings
143
- * @param code - Код ошибки
144
- * @param args - Аргументы для формирования сообщения
145
- * @returns Экземпляр {@link FileError}
146
- *
147
- **/
148
- static get(code, ...args) {
149
- const messageFn = this.codeMappings[code];
150
- const message = messageFn(...args);
151
- return new FileError(message);
152
- }
153
- }
154
- /**
155
- * Класс ошибки сборки под NPM, расширяющий стандартный Error.
156
- *
157
- * @since 0.3.5
158
- *
159
- **/
160
- class NpmBuildError extends Error {
161
- /**
162
- * Приватный конструктор для создания экземпляра ошибки.
163
- *
164
- * @param message - Сообщение об ошибке
165
- * @param scope - Область действия ошибки (по умолчанию '@mirta/rollup NPM')
166
- *
167
- **/
168
- constructor(message, scope = '@mirta/rollup NPM') {
169
- super(`[${scope}] ${message}`);
170
- this.name = 'NpmBuildError';
171
- if ('captureStackTrace' in Error)
172
- // eslint-disable-next-line @typescript-eslint/unbound-method
173
- Error.captureStackTrace(this, NpmBuildError.get);
174
- }
175
- /** Карта кодов ошибок с соответствующими сообщениями. */
176
- static codeMappings = {
177
- /** Ошибка, возникающая когда конфигурация input-файлов Rollup пуста. */
178
- inputEmpty: () => 'Rollup Config: Input configuration cannot be empty',
179
- /** Ошибка, возникающая когда input-файл не начинается с требуемого префикса. */
180
- inputPathRequiresPrefix: (input, prefix) => `Rollup Config: Input path "${input}" must start with required prefix "${prefix}"`,
181
- /** Ошибка, возникающая когда input-файл имеет недопустимое расширение. */
182
- inputFileExtensionNotSupported: (input) => `Rollup Config: Unsupported input "${input}". Please use valid JS or TS file extension`,
183
- /** Ошибка, возникающая из-за дублирования выходного файла несколькими input-файлами. */
184
- inputGeneratesDuplicateOutput: (outputFile) => `Rollup Config: Duplicate output file "${outputFile}" produced by multiple inputs. Ensure each input maps to a unique export path`,
185
- /** Ошибка, возникающая когда input-файл не ассоциирован с экспортом в package.json. */
186
- inputHasNoExport: (input, entry) => `Rollup Config: The input file "${input}" is not associated with corresponding export "${entry}"`,
187
- /** Ошибка, возникающая при отсутствии экспорта в package.json. */
188
- exportEmpty: () => 'Package Config: Missing export configuration. Please define the "exports" field',
189
- /** Ошибка, возникающая при экспорте типов без указания default-импорта. */
190
- exportTypesOnly: (types) => `Package Config: Export contains only types "${types}" without specifying a default import in package.json`,
191
- /** Ошибка, возникающая при отсутствии соответствия с input-файлом конфигурации Rollup. */
192
- exportHasNoInput: (entry) => `Package Config: Export "${entry}" has no corresponding input file in Rollup configuration`,
193
- /** Ошибка, возникающая при использовании массива в качестве значения exports. */
194
- exportDisallowArrayType: () => 'Package Config: The field "exports" must be either a string or an object, but found an array',
195
- /** Ошибка, возникающая при отсутствии точки в начале пути экспорта. */
196
- exportMustStartWithDot: (key) => `Package Config: Invalid export path "${key}", it must start with "."`,
197
- };
198
- /**
199
- * Статический метод для получения экземпляра ошибки по коду.
200
- *
201
- * @template T - Тип ключа из codeMappings
202
- * @param code - Код ошибки
203
- * @param args - Аргументы для формирования сообщения
204
- * @returns Экземпляр {@link NpmBuildError}
205
- *
206
- **/
207
- static get(code, ...args) {
208
- const messageFn = this.codeMappings[code];
209
- const message = messageFn(...args);
210
- return new NpmBuildError(message);
211
- }
212
- }
213
- /**
214
- * Класс ошибки трансформации AST, расширяющий стандартный Error.
215
- *
216
- * @since 0.3.5
217
- *
218
- **/
219
- class AstTransformError extends Error {
220
- /**
221
- * Приватный конструктор для создания экземпляра ошибки.
222
- *
223
- * @param message - Сообщение об ошибке
224
- * @param scope - Область действия ошибки (по умолчанию '@mirta/rollup AST')
225
- *
226
- **/
227
- constructor(message, scope = '@mirta/rollup AST') {
228
- super(`[${scope}] ${message}`);
229
- this.name = 'AstTransformError';
230
- if ('captureStackTrace' in Error)
231
- // eslint-disable-next-line @typescript-eslint/unbound-method
232
- Error.captureStackTrace(this, AstTransformError.get);
233
- }
234
- /** Карта кодов ошибок с соответствующими сообщениями. */
235
- static codeMappings = {
236
- /** Ошибка, возникающая при отсутствии root-файлов в проекте. */
237
- noRootFilesInProject: () => 'No root files found in the project. Check your TypeScript configuration (tsconfig.json)',
238
- /** Ошибка, возникающая при отсутствии модуля для указанного спецификатора. */
239
- moduleNotFound: (modulePath, sourceFileName) => `Module "${modulePath}" not found in "${sourceFileName}"`,
240
- /** Ошибка, возникающая когда modulePath содержит недопустимые символы. */
241
- invalidPathFormat: (modulePath) => `Invalid format of module path: "${modulePath}"`,
242
- };
243
- /**
244
- * Статический метод для получения экземпляра ошибки по коду.
245
- *
246
- * @template T - Тип ключа из codeMappings
247
- * @param code - Код ошибки
248
- * @param args - Аргументы для формирования сообщения
249
- * @returns Экземпляр {@link AstTransformError}
250
- *
251
- **/
252
- static get(code, ...args) {
253
- const messageFn = this.codeMappings[code];
254
- const message = messageFn(...args);
255
- return new AstTransformError(message);
256
- }
257
- }
258
-
259
- /**
260
- * Удаляет расширение файла `.ts`, `.d.ts` или `.js`.
261
- *
262
- * @param fileName - Полное имя файла
263
- * @returns Имя файла без расширения
264
- *
265
- * @since 0.3.5
266
- *
267
- **/
268
- const removeFileExtension = (fileName) => fileName.replace(/\.(?:d\.)?(?:[cm]?[tj]s)$/i, '');
269
- /**
270
- * Находит общий префикс двух путей.
271
- *
272
- * @param a - Первый путь
273
- * @param b - Второй путь
274
- * @returns Общий префикс
275
- *
276
- * @since 0.3.5
277
- *
278
- **/
279
- function getCommonPrefix(a, b) {
280
- const aParts = nodePath.normalize(a).split(nodePath.sep);
281
- const bParts = nodePath.normalize(b).split(nodePath.sep);
282
- const minLength = Math.min(aParts.length, bParts.length);
283
- let i = 0;
284
- while (i < minLength && aParts[i] === bParts[i]) {
285
- i++;
286
- }
287
- return aParts.slice(0, i).join(nodePath.sep);
288
- }
289
- /**
290
- * Проверяет, является ли указанный файл частью проекта.
291
- *
292
- * Файл считается проектным, если он:
293
- * - Не находится в директории `node_modules`;
294
- * - Расположен внутри указанной корневой директории.
295
- *
296
- * @param fileName - Полный путь к проверяемому файлу.
297
- * @param rootDir - Корневая директория проекта.
298
- * @returns `true`, если файл принадлежит проекту, иначе false.
299
- *
300
- * @since 0.3.5
301
- *
302
- **/
303
- function isProjectFile(fileName, rootDir) {
304
- if (fileName.includes('node_modules'))
305
- return false;
306
- if (nodePath.relative(rootDir, fileName).startsWith('..'))
307
- return false;
308
- return true;
309
- }
310
- /**
311
- * Вычисляет относительный путь от директории исходного файла к целевому файлу,
312
- * добавляет имя выходного файла и нормализует путь для совместимости с POSIX-системами.
313
- *
314
- * @param sourceFilePath - Полный путь к исходному файлу.
315
- * @param targetFilePath - Полный путь к целевому файлу.
316
- * @param outputName - Имя выходного файла, который будет добавлен к относительному пути.
317
- * @returns Нормализованный относительный путь с именем выходного файла.
318
- *
319
- * @example
320
- *
321
- * ```ts
322
- * const sourceFilePath = '/project/src/index.ts'
323
- * const targetFilePath = '/project/src/utils/index.ts'
324
- * const outputName = 'utils.d.ts'
325
- *
326
- * getRelativeOutputPath(sourceFilePath, targetFilePath, outputName) // Возвращает './utils.d.ts'
327
- *
328
- * ```
329
- * @since 0.3.5
330
- *
331
- **/
332
- function getRelativeOutputPath(sourceFilePath, targetFilePath, outputFileName) {
333
- // Шаг 1: Получаем директорию исходного файла.
334
- const sourceDir = nodePath
335
- .dirname(sourceFilePath);
336
- // Шаг 2: Вычисляем относительный путь от директории исходного файла до целевого файла.
337
- const relativeDir = nodePath
338
- .dirname(nodePath.relative(sourceDir, targetFilePath));
339
- // Шаг 3: Объединяем относительную директорию с именем выходного файла.
340
- let relativePath = nodePath
341
- .join(relativeDir, outputFileName);
342
- relativePath = relativePath
343
- .split(nodePath.sep)
344
- .join(nodePath.posix.sep);
345
- // Шаг 4: Гарантируем, что путь является относительным.
346
- if (!relativePath.startsWith('.'))
347
- relativePath = `./${relativePath}`;
348
- return relativePath;
349
- }
350
- /**
351
- * Определяет корневую директорию файла на основе конфигурации.
352
- *
353
- * @param context - Базовый контекст
354
- * @param sourceFile - Обрабатываемый файл
355
- * @returns Путь к корню проекта
356
- * @throws Ошибка, если файлы не найдены
357
- *
358
- * @since 0.3.5
359
- *
360
- **/
361
- function getRootDir(context, sourceFile) {
362
- const { compilerOptions, program } = context;
363
- // Если rootDirs указаны — ищем, к какому корню относится файл.
364
- if (compilerOptions.rootDirs?.length) {
365
- const sortedRoots = [...compilerOptions.rootDirs]
366
- .sort((a, b) => b.length - a.length);
367
- const normalizedFile = nodePath.resolve(sourceFile.fileName);
368
- for (const rootDir of sortedRoots) {
369
- const normalizedRoot = nodePath.resolve(rootDir);
370
- if (normalizedFile.startsWith(normalizedRoot + nodePath.sep))
371
- return rootDir;
372
- }
373
- }
374
- // Если rootDir указан — используем его.
375
- if (compilerOptions.rootDir)
376
- return compilerOptions.rootDir;
377
- // Иначе находим общий корень для всех файлов.
378
- const fileNames = program.getRootFileNames();
379
- if (!fileNames.length)
380
- throw AstTransformError.get('noRootFilesInProject');
381
- let commonPrefix = nodePath.dirname(fileNames[0]);
382
- for (const fileName of fileNames.slice(1)) {
383
- commonPrefix = getCommonPrefix(commonPrefix, fileName);
384
- }
385
- return commonPrefix;
386
- }
387
-
388
- /**
389
- * Перечисление типов индексных файлов.
390
- * Используется для классификации модулей в зависимости от их отношения к файлам `index`.
391
- *
392
- * @since 0.3.5
393
- *
394
- **/
395
- var IndexType;
396
- (function (IndexType) {
397
- /** Не индексный файл. */
398
- IndexType["NonIndex"] = "non-index";
399
- /** Явный индекс (например, `import './index'`). */
400
- IndexType["Explicit"] = "explicit";
401
- /** Неявный индекс (например, `import './dir'`). */
402
- IndexType["Implicit"] = "implicit";
403
- /** Индекс внутри пакета (например, import 'package/index'). */
404
- IndexType["ImplicitPackage"] = "implicit-package";
405
- })(IndexType || (IndexType = {}));
406
-
407
- /**
408
- * Проверяет безопасность пути модуля, блокируя подозрительные символы.
409
- *
410
- * Эта функция проверяет, не содержит ли путь модуля недопустимых символов,
411
- * таких как `:` (используется в URL) или `~` (ссылка на домашнюю директорию),
412
- * которые могут представлять риск безопасности.
413
- *
414
- * @param path - Путь модуля для проверки.
415
- *
416
- * @throws {AstTransformError} Если обнаружены запрещённые символы.
417
- *
418
- * @example
419
- * ```ts
420
- * assertPathIsValid('./utils') // OK
421
- * assertPathIsValid('http://malicious.com') // Выбросит ошибку
422
- *
423
- * ```
424
- * @since 0.3.5
425
- *
426
- **/
427
- function assertPathIsValid(path) {
428
- if (path.includes(':') || path.includes('~'))
429
- throw AstTransformError.get('invalidPathFormat', path);
430
- }
431
-
432
- /**
433
- * Создает кэш файлов по имени без расширения.
434
- *
435
- * @param program - Программа TypeScript
436
- * @returns Карта: ключ - имя файла без расширения, значение - объект файла
437
- *
438
- * @since 0.3.5
439
- *
440
- **/
441
- function createSourceFilesCache(program) {
442
- return new Map(program.getSourceFiles().map(sourceFile => [
443
- removeFileExtension(sourceFile.fileName),
444
- sourceFile,
445
- ]));
446
- }
447
- /**
448
- * Получает файл из программы или создает его при необходимости.
449
- * Использует кэш для повышения производительности при повторных запросах.
450
- *
451
- * @param context - Контекст трансформера, содержащий ссылку на программу и кэш файлов.
452
- * @param fileName - Полный путь к файлу, который необходимо найти или создать.
453
- * @returns Экземпляр {@link ts.SourceFile} для существующего или созданного файла.
454
- *
455
- * @since 0.3.5
456
- *
457
- **/
458
- function resolveSourceFile(context, fileName) {
459
- const { program, compilerOptions } = context;
460
- let result = program.getSourceFile(fileName);
461
- if (result)
462
- return result;
463
- // Если кэш уже создан, используем его. Иначе создаем новый.
464
- const sourceFilesCache = context.sourceFilesCache ??= createSourceFilesCache(program);
465
- const normalizedFileName = removeFileExtension(fileName);
466
- // Попытка найти файл в кэше.
467
- result = sourceFilesCache.get(normalizedFileName);
468
- if (!result) {
469
- result = ts.createSourceFile(fileName, '', compilerOptions.target ?? ts.ScriptTarget.ESNext, false);
470
- sourceFilesCache.set(normalizedFileName, result);
471
- }
472
- return result;
473
- }
474
-
475
- /**
476
- * Анализирует путь модуля и определяет его тип (явный, неявный, пакетный или обычный).
477
- * Собирает метаданные о модуле для дальнейшей обработки путей импорта.
478
- *
479
- * @param path - Путь модуля из исходного кода (например, './dir' или 'package/index').
480
- * @param resolvedModule - Объект, содержащий информацию о разрешённом модуле из TypeScript.
481
- * @returns Объект с детализацией пути, включая тип индекса, имя файла, расширение и директорию.
482
- *
483
- * @since 0.3.5
484
- *
485
- **/
486
- function getPathDetails(path, resolvedModule) {
487
- const { resolvedFileName, packageId } = resolvedModule;
488
- const implicitPackagePath = packageId?.subModuleName;
489
- // Указывает, является ли модуль частью пакета (например, 'package/index').
490
- const isPackage = !!implicitPackagePath;
491
- // Базовые данные разрешённого файла
492
- const resolvedBaseName = nodePath.basename(isPackage
493
- ? implicitPackagePath
494
- : resolvedFileName);
495
- const resolvedBaseNameNoExtension = resolvedBaseName
496
- ? removeFileExtension(resolvedBaseName)
497
- : undefined;
498
- // const resolvedExtension = resolvedBaseName
499
- // ? nodePath.extname(resolvedFileName)
500
- // : undefined
501
- // Базовые данные оригинального модуля
502
- let baseName = isPackage
503
- ? undefined
504
- : nodePath.basename(path);
505
- let baseNameNoExtension = baseName
506
- ? removeFileExtension(baseName)
507
- : undefined;
508
- let extName = baseName
509
- ? nodePath.extname(path)
510
- : undefined;
511
- // Если имя оригинального модуля совпадает с разрешённым, убираем расширение.
512
- if (resolvedBaseNameNoExtension
513
- && baseName
514
- && resolvedBaseNameNoExtension === baseName) {
515
- baseNameNoExtension = baseName;
516
- extName = undefined;
517
- }
518
- let indexType;
519
- if (isPackage) {
520
- // Модуль внутри пакета (например, import 'package/index').
521
- indexType = IndexType.ImplicitPackage;
522
- }
523
- else if (baseNameNoExtension === 'index' && resolvedBaseNameNoExtension === 'index') {
524
- // Явный импорт файла index (например, import './dir/index').
525
- indexType = IndexType.Explicit;
526
- }
527
- else if (baseNameNoExtension !== 'index' && resolvedBaseNameNoExtension === 'index') {
528
- // Неявный импорт index (например, import './dir').
529
- indexType = IndexType.Implicit;
530
- }
531
- else {
532
- // Обычный файл, не связанный с index.
533
- indexType = IndexType.NonIndex;
534
- }
535
- // Для неявных индексов убирает лишние поля оригинального
536
- // модуля, чтобы не отображать index и расширения.
537
- //
538
- if (indexType === IndexType.Implicit) {
539
- baseName = undefined;
540
- baseNameNoExtension = undefined;
541
- extName = undefined;
542
- }
543
- return {
544
- // baseName,
545
- // baseNameNoExtension,
546
- extName,
547
- // resolvedBaseName,
548
- resolvedBaseNameNoExtension,
549
- // resolvedExtension,
550
- // resolvedDir: isPackage
551
- // ? removeSuffix(resolvedFileName, `/${implicitPackageIndex}`)
552
- // : nodePath.dirname(resolvedFileName),
553
- indexType,
554
- // implicitPackagePath,
555
- // resolvedFileName,
556
- };
557
- }
558
- /**
559
- * Генерирует новый относительный путь для импорта модуля, исключая расширения и индексные файлы.
560
- * Проверяет безопасность пути и убеждается, что файл находится внутри корня проекта.
561
- *
562
- * @param context - Контекст трансформера, содержащий информацию о текущем файле и конфигурации.
563
- * @param oldPath - Путь модуля из исходного импорта (например, './utils/index').
564
- * @returns Относительный путь без расширения и индекса, или `undefined`, если модуль недопустим.
565
- *
566
- * @since 0.3.5
567
- *
568
- **/
569
- function resolveNewModulePath(context, oldPath) {
570
- assertPathIsValid(oldPath);
571
- const {
572
- // Текущий обрабатываемый файл.
573
- sourceFile: currentSourceFile,
574
- // Актуальная конфигурация TypeScript.
575
- compilerOptions, } = context;
576
- // Получаем модуль импортированного файла.
577
- //
578
- const { resolvedModule: importedModule } = ts.resolveModuleName(oldPath, currentSourceFile.fileName, compilerOptions, ts.sys);
579
- if (!importedModule)
580
- throw AstTransformError.get('moduleNotFound', oldPath, currentSourceFile.fileName);
581
- if (!isProjectFile(importedModule.resolvedFileName, context.rootDir))
582
- return null;
583
- // Получает детали пути импортированного модуля.
584
- const pathDetails = getPathDetails(oldPath, importedModule);
585
- const { indexType, resolvedBaseNameNoExtension, extName } = pathDetails;
586
- let outputBaseName = resolvedBaseNameNoExtension ?? '';
587
- if (indexType === IndexType.Implicit && outputBaseName.endsWith('index'))
588
- outputBaseName = outputBaseName.slice(0, -5);
589
- if (outputBaseName && extName)
590
- outputBaseName = `${outputBaseName}${extName}`;
591
- // Получает исходный файл импортированного модуля.
592
- const importedSourceFile = resolveSourceFile(context, importedModule.resolvedFileName);
593
- const newPath = getRelativeOutputPath(currentSourceFile.fileName, importedSourceFile.fileName, outputBaseName);
594
- return newPath;
595
- }
596
-
597
- const aliasPattern = /^[@#]/;
598
- /**
599
- * Обновляет спецификатор модуля в узле импорта или экспорта.
600
- *
601
- * @param factory - Фабрика для создания новых узлов AST.
602
- * @param node - Узел импорта или экспорта.
603
- * @param newModuleSpecifier - Новый относительный путь.
604
- * @returns Обновлённый узел.
605
- *
606
- * @since 0.3.5
607
- *
608
- **/
609
- function updateModuleSpecifier(factory, node, newModuleSpecifier) {
610
- if (ts.isImportDeclaration(node)) {
611
- return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(newModuleSpecifier), node.attributes);
612
- }
613
- else if (ts.isExportDeclaration(node)) {
614
- return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(newModuleSpecifier), node.attributes);
615
- }
616
- return node;
617
- }
618
- /**
619
- * Рекурсивно обходит дочерние узлы AST с текущим визитором.
620
- */
621
- function visitChildren(context, node) {
622
- return ts.visitEachChild(node, context.getVisitor(), context.transformationContext);
623
- }
624
- /**
625
- * Визитор для обработки узлов AST, связанных с импортами.
626
- *
627
- * Обновляет пути модулей, начинающиеся с `#` или `@`, в файлах объявлений TypeScript.
628
- *
629
- * @param this - Контекст трансформера, предоставляющий инструменты для работы с AST.
630
- * @param node - Текущий узел AST, который проверяется и, при необходимости, обновляется.
631
- * @returns Обновленный узел AST или результат рекурсивного обхода дочерних узлов.
632
- *
633
- * @example
634
- * ```ts
635
- * // Исходный узел импорта:
636
- * import { foo } from '#utils/index'
637
- *
638
- * // После обработки:
639
- * import { foo } from './utils'
640
- *
641
- * ```
642
- * @since 0.3.5
643
- *
644
- **/
645
- function nodeVisitor(node) {
646
- if (!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node))
647
- return visitChildren(this, node);
648
- const moduleSpecifier = node.moduleSpecifier;
649
- if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier))
650
- return visitChildren(this, node);
651
- // Извлекаем путь импорта/экспорта (указывается в кавычках после from).
652
- const { text: oldPath } = moduleSpecifier;
653
- // Проверяем, относится ли спецификатор к алиасу проекта (`#`) или кастомному алиасу (`@`).
654
- // Такие спецификаторы требуют пересчета в относительный путь.
655
- //
656
- if (!aliasPattern.test(oldPath))
657
- return visitChildren(this, node);
658
- const cachedNewPath = this.pathsCache.get(oldPath);
659
- const newPath = cachedNewPath === undefined
660
- ? resolveNewModulePath(this, oldPath)
661
- : cachedNewPath;
662
- if (cachedNewPath === undefined)
663
- this.pathsCache.set(oldPath, newPath);
664
- if (!newPath)
665
- return node;
666
- // Обновляем узел импорта новым значением пути.
667
- // Если путь не удалось преобразовать, возвращаем исходный узел.
668
- //
669
- return updateModuleSpecifier(this.factory, node, newPath);
670
- }
671
-
672
- /**
673
- * Фабрика трансформеров для Rollup, предназначенная для обработки файлов объявлений TypeScript (`.d.ts`).
674
- * Создаёт трансформер, который модифицирует пути импортов в декларациях, исключая расширения и индексные файлы.
675
- *
676
- * @param program - Экземпляр программы TypeScript, предоставляющий доступ к всем файлам проекта.
677
- * @returns Фабрика трансформеров, применяющаяся ко всем `.d.ts`-файлам.
678
- *
679
- * @since 0.3.5
680
- *
681
- **/
682
- function dtsAliasTransformerFactory(program) {
683
- return (context) => {
684
- const compilerOptions = program.getCompilerOptions();
685
- /**
686
- * Базовый контекст для работы с AST-трансформером.
687
- * Содержит общие параметры и ссылки на программу TypeScript.
688
- *
689
- **/
690
- const visitorContextBase = {
691
- compilerOptions,
692
- program,
693
- factory: context.factory,
694
- transformationContext: context,
695
- };
696
- return (sourceFile) => {
697
- /**
698
- * Обработка происходит только для файлов объявлений (`.d.ts`).
699
- * Обычные файлы (`*.ts`) игнорируются.
700
- *
701
- **/
702
- if (!sourceFile.isDeclarationFile)
703
- return sourceFile;
704
- /**
705
- * Контекст визитора, расширенный информацией о текущем файле и корне проекта.
706
- * Используется для передачи данных в `nodeVisitor`.
707
- *
708
- **/
709
- const visitorContext = {
710
- ...visitorContextBase,
711
- sourceFile,
712
- rootDir: getRootDir(visitorContextBase, sourceFile),
713
- pathsCache: new Map(),
714
- getVisitor() {
715
- return nodeVisitor.bind(this);
716
- },
717
- };
718
- /**
719
- * Рекурсивный обход AST с применением визитора.
720
- * Обрабатывает все узлы файла, начиная с корня.
721
- *
722
- **/
723
- return ts.visitEachChild(sourceFile, visitorContext.getVisitor(), context);
724
- };
725
- };
726
- }
727
-
728
- /**
729
- * Экспортируемый трансформер для Rollup, предназначенный для обработки файлов объявлений TypeScript (`.d.ts`).
730
- * Используется в фазе `afterDeclarations`, чтобы корректировать пути импортов после генерации типов.
731
- *
732
- * @returns Объект трансформера, который может быть передан в конфигурацию Rollup.
733
- *
734
- * @example
735
- * Пример использования в конфигурации Rollup:
736
- * ```ts
737
- * import { dtsAlias } from '#ast/dts-alias';
738
- *
739
- * export default {
740
- * plugins: [
741
- * typescript({
742
- * transformers: {
743
- * afterDeclarations: [
744
- * dtsAlias()
745
- * ]
746
- * }
747
- * })
748
- * ]
749
- * }
750
- *
751
- * ```
752
- * @since 0.3.5
753
- *
754
- **/
755
- const dtsAlias = () => ({
756
- type: 'program',
757
- factory: dtsAliasTransformerFactory,
758
- });
759
-
760
- /**
761
- * Синхронно парсит файл `package.json` и возвращает его содержимое в виде
762
- * типизированного объекта. Обрабатывает распространённые ошибки
763
- * файловой системы и синтаксические ошибки JSON с помощью пользовательских исключений.
764
- *
765
- * @param filePath - Абсолютный или относительный путь к файлу `package.json`.
766
- * @returns Объект типа {@link Package}, представляющий данные из файла.
767
- * @throws {FileError} При ошибке обработки файла.
768
- *
769
- * @since 0.3.5
770
- *
771
- **/
772
- function parsePackageJson(filePath) {
773
- try {
774
- const content = readFileSync(filePath, 'utf-8');
775
- return JSON.parse(content);
776
- }
777
- catch (e) {
778
- if (e instanceof SyntaxError)
779
- throw FileError.get('invalidJson', filePath, e.message);
780
- if (e instanceof Error) {
781
- if ('code' in e) {
782
- switch (e.code) {
783
- case 'ENOENT':
784
- throw FileError.get('notFound', filePath);
785
- case 'EACCES':
786
- case 'EPERM':
787
- throw FileError.get('noAccess', filePath);
788
- }
789
- }
790
- throw FileError.get('failedToParse', filePath, e.message);
791
- }
792
- throw FileError.get('failedToParse', filePath, String(e));
793
- }
794
- }
795
-
796
- /**
797
- * Преобразует входные данные в массив, исключая "ложные" значения (`false`, `null`, `undefined`).
798
- *
799
- * @param input - Входные данные, которые могут быть:
800
- * - массивом элементов типа `TItem | false | null | undefined`;
801
- * - отдельным элементом типа `TItem | false | null | undefined`.
802
- * @returns Массив элементов типа `TItem`, содержащий только "истинные" значения.
803
- *
804
- * @example
805
- *
806
- * ```ts
807
- * ensureCompactArray([1, null, 2]) // [1, 2]
808
- *
809
- * ensureCompactArray(undefined) // []
810
- *
811
- * ensureCompactArray('test') // ['test']
812
- *
813
- * ```
814
- * @since 0.3.5
815
- *
816
- **/
817
- function ensureCompactArray(input) {
818
- if (Array.isArray(input))
819
- return input.filter(Boolean);
820
- if (input)
821
- return [input];
822
- return [];
823
- }
824
-
825
- /**
826
- * Создаёт фильтр для определения внешних модулей на основе указанных правил.
827
- *
828
- * @param cwd Рабочая директория проекта (используется для проверки относительных путей)
829
- * @param externals Набор паттернов или функций для определения внешних модулей
830
- * @returns Функция-предикат, которая принимает параметры модуля и возвращает true,
831
- * если модуль должен быть считаться внешним
832
- *
833
- * @returns Функция-предикат `(target: string, importer: string | undefined, isResolved: boolean): boolean`,
834
- * которая возвращает `true`, если модуль считается внешним.
835
- *
836
- * @since 0.3.5
837
- *
838
- **/
839
- function createExternalFilter(cwd, ...externals) {
840
- /**
841
- * Функция-предикат для определения внешнего модуля
842
- *
843
- * @param target Целевой путь/имя модуля
844
- * @param importer Имя файла, который импортирует модуль (если доступно)
845
- * @param isResolved Флаг, указывающий, был ли модуль успешно разрешён
846
- * @returns `true`, если модуль внешний, иначе false
847
- */
848
- return (target, importer, isResolved) => {
849
- for (const external of externals) {
850
- // Шаг 1: Пользовательская функция
851
- if (typeof external === 'function') {
852
- if (external(target, importer, isResolved))
853
- return true;
854
- }
855
- else {
856
- // Шаг 2: Массив паттернов
857
- const isExternal = ensureCompactArray(external).some((item) => {
858
- if (item instanceof RegExp)
859
- return item.test(target);
860
- return item === target;
861
- });
862
- if (isExternal)
863
- return true;
864
- }
865
- }
866
- // Шаг 3: Путь вне cwd (только для обнаруженных модулей)
867
- if (isResolved && cwd && nodePath.isAbsolute(target)) {
868
- const relativePath = nodePath.relative(cwd, target);
869
- // Если путь вне cwd, отмечаем его как внешний
870
- if (relativePath.startsWith('..') || nodePath.isAbsolute(relativePath)) {
871
- if ((process.env.NODE_ENV !== 'production'))
872
- console.debug(`Skipping non-project "${relativePath}"`);
873
- return true;
874
- }
875
- }
876
- // Шаг 4: По умолчанию — внутренний
877
- return false;
878
- };
879
- }
880
-
881
- const dtsOutputDir = 'dist/dts';
882
- /**
883
- * Удаляет префикс './dist/' из пути.
884
- * @param path Путь к файлу.
885
- * @returns Нормализованный путь.
886
- *
887
- * @since 0.3.5
888
- *
889
- **/
890
- function sliceDistPrefix(path) {
891
- return path.startsWith('./dist/')
892
- ? path.slice(7)
893
- : path;
894
- }
895
- /**
896
- * Нормализует входные данные в массив строк.
897
- *
898
- * @param input Входные данные (строка, массив или объект).
899
- * @returns Массив путей к входным файлам.
900
- * @throws {NpmBuildError} Если входная конфигурация пуста.
901
- *
902
- * @since 0.3.4
903
- *
904
- **/
905
- function normalizeInput(input) {
906
- const inputs = [];
907
- // Нормализация входных данных.
908
- // Строка, массив или объект преобразуются в единый массив строк inputs.
909
- if (typeof input === 'string') {
910
- inputs.push(input);
911
- }
912
- else if (Array.isArray(input)) {
913
- inputs.push(...input);
914
- }
915
- else if (typeof input === 'object') {
916
- inputs.push(...Object.values(input));
917
- }
918
- if (inputs.length === 0)
919
- throw NpmBuildError.get('inputEmpty');
920
- return inputs;
921
- }
922
- /**
923
- * Проверяет, является ли объект условной записью экспорта.
924
- *
925
- * @param source Объект для проверки.
926
- * @returns `true`, если объект содержит поле `import`.
927
- *
928
- * @since 0.3.5
929
- *
930
- **/
931
- function isConditionalEntry(source) {
932
- return 'import' in source;
933
- }
934
- /**
935
- * Обрабатывает условную запись экспорта.
936
- *
937
- * @param source Условная запись.
938
- * @returns Объект с полями entry и types.
939
- *
940
- * @since 0.3.5
941
- *
942
- **/
943
- function processConditionalEntry(source) {
944
- const result = {};
945
- if (source.import) {
946
- if (typeof source.import === 'string') {
947
- // Путь точки входа определён, типизация отсутствует.
948
- result.entry = source.import;
949
- }
950
- else {
951
- result.entry = source.import.default;
952
- result.types = source.import.types;
953
- }
954
- }
955
- return result;
956
- }
957
- /**
958
- * Проверяет наличие точки входа для типизации.
959
- *
960
- * @param entry Путь к точке входа.
961
- * @param types Путь к файлу типов.
962
- * @throws {NpmBuildError} Если отсутствует точка входа.
963
- *
964
- * @since 0.3.5
965
- *
966
- **/
967
- function assertTypesHaveEntry(entry, types) {
968
- if (types && !entry)
969
- throw NpmBuildError.get('exportTypesOnly', types);
970
- }
971
- /**
972
- * Нормализует поле `exports` из package.json в словарь точек входа.
973
- *
974
- * @param exportsField Значение поля `exports`.
975
- * @returns Словарь точек входа с метаданными.
976
- * @throws {NpmBuildError} Если конфигурация экспорта отсутствует или некорректна.
977
- *
978
- * @since 0.3.5
979
- *
980
- **/
981
- function normalizeExports(exportsField) {
982
- if (!exportsField)
983
- throw NpmBuildError.get('exportEmpty');
984
- if (Array.isArray(exportsField))
985
- throw NpmBuildError.get('exportDisallowArrayType');
986
- const result = {};
987
- if (typeof exportsField === 'string') {
988
- result[exportsField] = {};
989
- return result;
990
- }
991
- if (isConditionalEntry(exportsField)) {
992
- const { entry, types } = processConditionalEntry(exportsField);
993
- assertTypesHaveEntry(entry, types);
994
- if (entry)
995
- result[entry] = types
996
- ? { dtsOutputFile: types }
997
- : {};
998
- return result;
999
- }
1000
- for (const [key, value] of Object.entries(exportsField)) {
1001
- if (!value)
1002
- continue;
1003
- if (!key.startsWith('.'))
1004
- throw NpmBuildError.get('exportMustStartWithDot', key);
1005
- let entry, types;
1006
- if (typeof value === 'string') {
1007
- // Путь точки входа определён, типизация отсутствует.
1008
- entry = value;
1009
- }
1010
- else if (isConditionalEntry(value)) {
1011
- ({ entry, types } = processConditionalEntry(value));
1012
- }
1013
- else {
1014
- entry = value.default;
1015
- types = value.types;
1016
- }
1017
- assertTypesHaveEntry(entry, types);
1018
- if (entry)
1019
- result[entry] = types
1020
- ? { dtsOutputFile: types }
1021
- : {};
1022
- }
1023
- return result;
1024
- }
1025
- /**
1026
- * Создаёт отображение между входными файлами и выходными путями.
1027
- *
1028
- * @param inputs Массив исходных файлов.
1029
- * @param normalizedExports Нормализованные дескрипторы экспорта.
1030
- * @param skipExports Позволяет пропустить проверку экспорта.
1031
- * @returns Словарь связей вход-выход.
1032
- * @throws {NpmBuildError} Если входной файл не связан с экспортом.
1033
- *
1034
- * @since 0.3.4
1035
- *
1036
- **/
1037
- function getInputBindings(inputs, normalizedExports, skipExports) {
1038
- const bodyPattern = /^src\/(.*)\.[jt]s$/;
1039
- const result = {};
1040
- const usedExports = new Set();
1041
- const producingOutputs = new Set();
1042
- for (const input of inputs) {
1043
- if (!input.startsWith('src/'))
1044
- throw NpmBuildError.get('inputPathRequiresPrefix', input, 'src/');
1045
- const match = bodyPattern.exec(input);
1046
- if (!match)
1047
- throw NpmBuildError.get('inputFileExtensionNotSupported', input);
1048
- const outputFile = `${match[1]}.mjs`;
1049
- if (producingOutputs.has(outputFile))
1050
- throw NpmBuildError.get('inputGeneratesDuplicateOutput', outputFile);
1051
- producingOutputs.add(outputFile);
1052
- const exportEntry = `./dist/${outputFile}`;
1053
- usedExports.add(exportEntry);
1054
- const descriptor = normalizedExports[exportEntry];
1055
- // Проверяем наличие ключа в словаре экспорта (при необходимости).
1056
- if (!descriptor && !skipExports)
1057
- throw NpmBuildError.get('inputHasNoExport', input, exportEntry);
1058
- result[input] = {
1059
- outputFile,
1060
- dtsSourceFile: `${dtsOutputDir}/${match[1]}.d.ts`,
1061
- dtsOutputFile: descriptor?.dtsOutputFile,
1062
- };
1063
- }
1064
- for (const key of Object.keys(normalizedExports)) {
1065
- // Выявляем незадействованные ключи в словаре экспорта (обратная проверка).
1066
- if (!usedExports.has(key))
1067
- throw NpmBuildError.get('exportHasNoInput', key);
1068
- }
1069
- return result;
1070
- }
1071
- // Проверка TypeScript выполняется только для первой конфигурации.
1072
- let hasTsChecked = false;
1073
- /**
1074
- * Определяет конфигурацию сборки на основе package.json.
1075
- *
1076
- * @param options Опции конфигурации Rollup.
1077
- * @returns Массив конфигураций Rollup.
1078
- *
1079
- * @since 0.3.0
1080
- *
1081
- **/
1082
- function definePackageConfig(options = {}) {
1083
- const { cwd = process.cwd(), input = 'src/index.ts', external = [], plugins, skipExports = false, } = options;
1084
- const pkgPath = nodePath.resolve(cwd, 'package.json');
1085
- const externalFilter = createExternalFilter(cwd, [
1086
- /node_modules/,
1087
- pkgPath,
1088
- ], external);
1089
- const { exports = {} } = parsePackageJson(pkgPath);
1090
- const normalizedExports = !skipExports
1091
- ? normalizeExports(exports)
1092
- : {};
1093
- const inputBindings = getInputBindings(normalizeInput(input), normalizedExports, skipExports);
1094
- // Создаёт отображение между файлами типов `.d.ts` и их выходными путями
1095
- const dtsMappings = Object.values(inputBindings)
1096
- .reduce((mappings, nextValue) => {
1097
- if (nextValue?.dtsOutputFile)
1098
- mappings[nextValue.dtsSourceFile] = sliceDistPrefix(nextValue.dtsOutputFile);
1099
- return mappings;
1100
- }, {});
1101
- const dtsInputs = Object.keys(dtsMappings);
1102
- const rollupConfigs = [
1103
- createBuildConfig('mjs', {
1104
- cwd,
1105
- input,
1106
- external: externalFilter,
1107
- emitDeclarations: dtsInputs.length > 0,
1108
- plugins,
1109
- output: {
1110
- dir: 'dist/',
1111
- format: 'es',
1112
- importAttributesKey: 'with',
1113
- entryFileNames(chunk) {
1114
- if (chunk.facadeModuleId) {
1115
- const localPath = nodePath
1116
- .relative(cwd, chunk.facadeModuleId)
1117
- .replaceAll(nodePath.sep, nodePath.posix.sep);
1118
- const binding = inputBindings[localPath];
1119
- if (binding)
1120
- return binding.outputFile;
1121
- }
1122
- return `${chunk.name}.mjs`;
1123
- },
1124
- chunkFileNames: '[name].mjs',
1125
- },
1126
- }),
1127
- ];
1128
- if (dtsInputs.length > 0) {
1129
- rollupConfigs.push({
1130
- input: dtsInputs,
1131
- external: externalFilter,
1132
- plugins: [
1133
- nodeResolve(),
1134
- commonjs(),
1135
- dts(),
1136
- del({
1137
- targets: [dtsOutputDir],
1138
- hook: 'closeBundle',
1139
- }),
1140
- ],
1141
- output: {
1142
- dir: 'dist/',
1143
- format: 'es',
1144
- entryFileNames(chunk) {
1145
- if (chunk.facadeModuleId) {
1146
- const localPath = nodePath
1147
- .relative(cwd, chunk.facadeModuleId)
1148
- .replaceAll(nodePath.sep, nodePath.posix.sep);
1149
- if (dtsMappings[localPath])
1150
- return dtsMappings[localPath];
1151
- }
1152
- return `${chunk.name}.mts`;
1153
- },
1154
- },
1155
- });
1156
- }
1157
- return rollupConfigs;
1158
- }
1159
- /**
1160
- * Создаёт конфигурацию сборки Rollup.
1161
- *
1162
- * @param buildName Имя сборки.
1163
- * @param options Параметры сборки.
1164
- * @returns Конфигурация Rollup.
1165
- *
1166
- * @since 0.3.0
1167
- *
1168
- **/
1169
- function createBuildConfig(buildName, options) {
1170
- const { cwd, external, input, emitDeclarations, plugins = [], output } = options;
1171
- output.sourcemap = !!process.env.SOURCE_MAP;
1172
- output.externalLiveBindings = false;
1173
- process.env.NODE_ENV === 'production';
1174
- const tsPlugin = ts$1({
1175
- tsconfig: nodePath.resolve(cwd, './tsconfig.build.json'),
1176
- compilerOptions: {
1177
- noCheck: hasTsChecked,
1178
- declaration: emitDeclarations,
1179
- declarationDir: emitDeclarations ? dtsOutputDir : void 0,
1180
- },
1181
- exclude: [
1182
- 'packages/*/tests',
1183
- ],
1184
- transformers: {
1185
- afterDeclarations: [
1186
- dtsAlias(),
1187
- ],
1188
- },
1189
- });
1190
- // При запуске команды build, проверки TS и генерация определений
1191
- // выполняются единожды - для первой конфигурации.
1192
- hasTsChecked = true;
1193
- return {
1194
- input,
1195
- external,
1196
- plugins: [
1197
- tsPlugin,
1198
- createReplacePlugin(),
1199
- nodeResolve(),
1200
- commonjs(),
1201
- ...plugins,
1202
- copy({
1203
- targets: [
1204
- { src: 'public/*', dest: 'dist' },
1205
- ],
1206
- }),
1207
- ],
1208
- output,
1209
- };
1210
- }
1211
- /**
1212
- * Создаёт плагин замены значений.
1213
- *
1214
- * @param isProduction Признак production-сборки.
1215
- * @param isBundlerEsmBuild Признак сборки для bundler ESM.
1216
- * @param isNodeBuild Признак сборки для Node.js.
1217
- * @returns Плагин замены.
1218
- *
1219
- * @since 0.3.0
1220
- *
1221
- **/
1222
- function createReplacePlugin(isProduction, isBundlerEsmBuild, isNodeBuild) {
1223
- const replacements = {
1224
- // Preserve to be handled by bundlers
1225
- __DEV__: `(process.env.NODE_ENV !== 'production')`
1226
- ,
1227
- __TEST__: `(process.env.NODE_ENV === 'test')`
1228
- ,
1229
- };
1230
- // Allow inline overrides like
1231
- // __DEV__=true pnpm build
1232
- Object.keys(replacements).forEach((key) => {
1233
- if (key in process.env)
1234
- replacements[key] = process.env[key];
1235
- });
1236
- return replace({
1237
- preventAssignment: true,
1238
- values: replacements,
1239
- delimiters: ['\\b', '\\b(?![\\.\\:])'],
1240
- });
1241
- }
11
+ import nodePath from 'node:path';
1242
12
 
1243
13
  // Абсолютный путь к результирующему каталогу.
1244
- const outputDir$1 = path.join(process.cwd(), 'dist');
1245
- const modulesDir = path.join(outputDir$1, 'wb-rules-modules');
14
+ const outputDir = path.join(process.cwd(), 'dist');
15
+ const modulesDir = path.join(outputDir, 'wb-rules-modules');
1246
16
  // Расположение обрабатываемого чанка.
1247
- const getChunkDir = (fileName) => path.dirname(path.join(outputDir$1, fileName));
17
+ const getChunkDir = (fileName) => path.dirname(path.join(outputDir, fileName));
1248
18
  // Шаблон для отлова конструкций require.
1249
19
  const patternRequire = /require\(['"]([^'"]+)'\)/g;
1250
20
  /**
@@ -1308,103 +78,52 @@ function wbRulesImports() {
1308
78
  }
1309
79
 
1310
80
  /**
1311
- * Определяет корень монорепозитория по текущей директории.
1312
- *
1313
- * @param cwd - Текущая рабочая директория для поиска
1314
- * @returns Promise<string | undefined> - Абсолютный путь к корню или undefined
1315
- * @throws {PackageManagerError} Если используется неподдерживаемый менеджер пакетов
1316
- *
1317
- * @remarks
1318
- * Поддерживает только PNPM в текущей реализации.
1319
- *
1320
- * @since 0.3.5
1321
- *
1322
- **/
1323
- async function findMonorepoDirAsync(cwd) {
1324
- if (process.env.PNPM_HOME)
1325
- return await findWorkspaceDir(cwd);
1326
- // TODO: реализовать поддержку остальных пакетных менеджеров.
1327
- throw PackageManagerError.get('pnpmOnly');
1328
- }
1329
- /**
1330
- * Формирует относительный путь в формате posix.
1331
- *
1332
- * @param workspaceDir - Корневая директория монорепозитория
1333
- * @param packageRootDir - Директория конкретного пакета
1334
- * @returns Стандартизированный относительный путь с завершающим слешем
1335
- *
1336
- **/
1337
- function getWorkspacePath(workspaceDir, packageRootDir) {
1338
- return nodePath.relative(workspaceDir, packageRootDir)
1339
- .replaceAll(nodePath.sep, nodePath.posix.sep) + '/';
1340
- }
1341
- /**
1342
- * Получает полную информацию о структуре монорепозитория.
1343
- *
1344
- * @param cwd - Текущая рабочая директория для поиска
1345
- * @returns Объект контекста {@link MonorepoContext} или undefined
1346
- * @throws {WorkspaceError} Если проект определён как монорепозиторий и отсутствует секция `workspaces` в package.json
1347
- *
1348
- * @since 0.3.5
81
+ * Находит пакет, которому принадлежит указанный чанк.
1349
82
  *
1350
- **/
1351
- async function getMonorepoContextAsync(cwd) {
1352
- const monorepoDir = await findMonorepoDirAsync(cwd);
1353
- if (monorepoDir) {
1354
- const pkg = parsePackageJson(`${monorepoDir}/package.json`);
1355
- if (!pkg.workspaces)
1356
- throw WorkspaceError.get('noWorkspaces');
1357
- const packages = await findWorkspacePackages(monorepoDir, {
1358
- patterns: pkg.workspaces,
1359
- });
1360
- const context = {
1361
- rootDir: monorepoDir,
1362
- packages: packages
1363
- // Не рассматриваем корневой каталог в качестве пакета.
1364
- .filter(x => x.rootDir !== monorepoDir)
1365
- // Сортируем по длине пути к корню монорепозитория (самые длинные первыми).
1366
- .sort((a, b) => b.rootDir.length - a.rootDir.length)
1367
- // Создаём массив объектов с информацией о пакетах монорепозитория.
1368
- .map(item => ({
1369
- workspacePath: getWorkspacePath(monorepoDir, item.rootDir),
1370
- name: item.manifest.name,
1371
- })),
1372
- };
1373
- return context;
1374
- }
1375
- }
1376
- /**
1377
- * Поиск пакета по имени чанка
83
+ * Поиск выполняется по префиксу пути: первый пакет, чей `workspacePath`
84
+ * является началом `chunkName`, считается владельцем.
1378
85
  *
1379
- * @param context - Контекст монорепозитория
1380
- * @param chunkName - Имя чанка (обычно путь к файлу)
1381
- * @returns PackageDefinition | undefined - Найденный пакет или undefined
86
+ * @param context - Контекст монорепозитория.
87
+ * @param chunkName - Имя чанка, предоставляемое Rollup (`chunk.name`).
88
+ * @returns Объект {@link PackageDefinition} или `undefined`, если пакет не найден.
1382
89
  *
1383
- * @since 0.3.5
90
+ * @since 0.4.0
1384
91
  *
1385
92
  **/
1386
- function findMonorepoPackageByChunkName(context, chunkName) {
93
+ function findPackageByChunkName(context, chunkName) {
1387
94
  for (const pkg of context.packages) {
1388
- if (chunkName.startsWith(pkg.workspacePath)) {
95
+ if (chunkName.startsWith(pkg.workspacePath + '/'))
1389
96
  return pkg;
1390
- }
1391
97
  }
1392
98
  }
99
+
1393
100
  /**
1394
- * Преобразует путь к чанку в путь внутри node_modules
101
+ * Преобразует имя чанка в путь, имитирующий установленный пакет в `node_modules`.
1395
102
  *
1396
- * @param chunkName - Полный путь к чанку
1397
- * @param pkgDefinition - Определение пакета
1398
- * @returns Строка в формате node_modules/<package-name>/<relative-path>
1399
- * @throws {WorkspaceError} Если имя пакета не указано
103
+ * Позволяет обрабатывать импорты из других пакетов монорепозитория
104
+ * наравне с установленными зависимостями.
1400
105
  *
1401
- * @since 0.3.5
106
+ * @param chunkName - Путь к файлу от корня монорепозитория в формате POSIX (и без расширения).
107
+ * @param pkgDefinition - Пакет, которому принадлежит файл.
108
+ * @returns Виртуальный путь вида `node_modules/<имя-пакета>/<относительный-путь>`.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * toVirtualModulePath('packages/mirta-home/dist/heater', {
113
+ * name: '@mirta/home',
114
+ * workspacePath: 'packages/mirta-home'
115
+ * })
116
+ * // → 'node_modules/@mirta/home/dist/heater'
117
+ * ```
118
+ * @since 0.4.0
1402
119
  *
1403
120
  **/
1404
- function mapChunkToPackage(chunkName, pkgDefinition) {
1405
- if (!pkgDefinition.name)
1406
- throw WorkspaceError.get('noPackageName', pkgDefinition.workspacePath);
1407
- return 'node_modules/'.concat(pkgDefinition.name, '/', nodePath.posix.relative(pkgDefinition.workspacePath, chunkName));
121
+ function toVirtualModulePath(chunkName, pkgDefinition) {
122
+ const relativePath = nodePath.posix.relative(pkgDefinition.workspacePath, chunkName);
123
+ // Проверяем, что путь не выходит за пределы пакета.
124
+ if (relativePath.startsWith('..'))
125
+ throw BuildError.get('chunkOutsidePackage', chunkName, pkgDefinition.name, pkgDefinition.workspacePath);
126
+ return `node_modules/${pkgDefinition.name}/${relativePath}`;
1408
127
  }
1409
128
 
1410
129
  const packagesPattern = /(.*)node_modules[\\/]@?(.+)[\\/](.+)?/;
@@ -1472,28 +191,23 @@ function getEntryPath(filePath) {
1472
191
  ?? filePath;
1473
192
  }
1474
193
 
1475
- const env = process.env.NODE_ENV;
1476
- const isProduction = env === 'production';
1477
- const outputDir = {
1478
- es5: 'dist/es5',
1479
- };
194
+ const mode = process.env.NODE_ENV ?? 'development';
195
+ const isProduction = mode === 'production';
196
+ const outDir = 'dist/es5';
1480
197
  /**
1481
- * Основная функция, возвращающая конфигурацию Rollup.
1482
- * Обрабатывает входные файлы, плагины и настройку выходных путей.
1483
- *
1484
- * @param options - опции конфигурации
1485
- * @returns Объект RollupOptions
198
+ * Собирает Rollup-конфигурацию для сборки runtime-кода проекта с учётом монорепозитория и подстановки переменных окружения.
1486
199
  *
200
+ * @param options - Параметры: `cwd` — рабочая директория проекта; `external` — список внешних зависимостей; `envLoader` — опции загрузчика окружения; `plugins` — дополнительные Rollup-плагины
201
+ * @returns Сконфигурированный объект RollupOptions, готовый для сборки в каталог `dist/es5`
1487
202
  * @since 0.3.0
1488
- *
1489
- **/
203
+ */
1490
204
  async function defineRuntimeConfig(options = {}) {
1491
- const { cwd = process.cwd(), external, dotenv: dotenvOptions = {}, plugins = [], } = options;
1492
- const monorepoContext = await getMonorepoContextAsync(cwd);
205
+ const { cwd = process.cwd(), external, envLoader: envLoaderOptions, plugins = [], } = options;
206
+ const monorepoContext = await resolveMonorepoContextAsync(cwd);
1493
207
  const defaultPlugins = [
1494
208
  // Очистка директории dist перед сборкой
1495
209
  del({
1496
- targets: 'dist/*',
210
+ targets: outDir,
1497
211
  }),
1498
212
  // Поддержка множественных входных файлов
1499
213
  multi({
@@ -1503,15 +217,21 @@ async function defineRuntimeConfig(options = {}) {
1503
217
  // Поиск зависимостей в node_modules
1504
218
  nodeResolve(),
1505
219
  // Транспиляция TypeScript
1506
- ts$2({ clean: true }),
220
+ ts({ clean: true }),
1507
221
  // Обработка импортов для wb-rules
1508
222
  wbRulesImports(),
1509
- // Загрузка переменных окружения
1510
- dotenv(dotenvOptions),
1511
- // Замена условных флагов в коде
223
+ // Подстановка переменных окружения
1512
224
  replace({
1513
225
  preventAssignment: true,
1514
226
  values: {
227
+ // Загрузка переменных окружения
228
+ ...loadEnvReplacements({
229
+ ...envLoaderOptions,
230
+ mode,
231
+ cwd,
232
+ rootDir: monorepoContext.rootDir,
233
+ }),
234
+ // Признак сборки в режиме разработки
1515
235
  __DEV__: JSON.stringify(!isProduction),
1516
236
  // Автоматически меняется в процессе тестирования
1517
237
  __TEST__: 'false',
@@ -1527,7 +247,7 @@ async function defineRuntimeConfig(options = {}) {
1527
247
  }),
1528
248
  // Очистка виртуальных файлов после сборки
1529
249
  del({
1530
- targets: 'dist/*/_virtual',
250
+ targets: `${outDir}/_virtual`,
1531
251
  hook: 'closeBundle',
1532
252
  }),
1533
253
  ];
@@ -1541,14 +261,19 @@ async function defineRuntimeConfig(options = {}) {
1541
261
  output: {
1542
262
  format: 'cjs',
1543
263
  strict: false,
1544
- dir: outputDir.es5,
264
+ dir: outDir,
1545
265
  preserveModules: true,
1546
- entryFileNames(chunkInfo) {
1547
- let chunkName = chunkInfo.name;
266
+ entryFileNames(chunk) {
267
+ // Относительный путь от корня репозитория.
268
+ let chunkName = chunk.name;
1548
269
  // Адаптация путей при сборке в монорепозитории.
1549
- if (monorepoContext) {
270
+ if (monorepoContext.packages.length !== 0) {
1550
271
  const { rootDir } = monorepoContext;
1551
- const absolutePath = nodePath.resolve(rootDir, chunkInfo.name);
272
+ // Преобразуем chunkName (относительный путь от корня монорепозитория)
273
+ // в абсолютный путь для корректного сравнения с cwd (абсолютным путём
274
+ // к текущему пакету).
275
+ //
276
+ const absolutePath = nodePath.resolve(rootDir, chunkName);
1552
277
  if (absolutePath.startsWith(cwd)) {
1553
278
  // Путь в текущем проекте, не требует встраивания отдельным пакетом.
1554
279
  chunkName = nodePath
@@ -1557,9 +282,9 @@ async function defineRuntimeConfig(options = {}) {
1557
282
  }
1558
283
  else {
1559
284
  // Ищем пакет монорепозитория, в котором находится указанный путь.
1560
- const pkgDefinition = findMonorepoPackageByChunkName(monorepoContext, chunkName);
285
+ const pkgDefinition = findPackageByChunkName(monorepoContext, chunkName);
1561
286
  if (pkgDefinition)
1562
- chunkName = mapChunkToPackage(chunkName, pkgDefinition);
287
+ chunkName = toVirtualModulePath(chunkName, pkgDefinition);
1563
288
  }
1564
289
  }
1565
290
  return getEntryPath(chunkName);
@@ -1568,4 +293,4 @@ async function defineRuntimeConfig(options = {}) {
1568
293
  };
1569
294
  }
1570
295
 
1571
- export { defineRuntimeConfig as a, definePackageConfig as d };
296
+ export { defineRuntimeConfig as d };