@mirta/rollup 0.3.5 → 0.4.1

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,1030 @@
1
+ import ts$1 from '@rollup/plugin-typescript';
2
+ import nodeResolve from '@rollup/plugin-node-resolve';
3
+ import commonjs from '@rollup/plugin-commonjs';
4
+ import replace from '@rollup/plugin-replace';
5
+ import copy from 'rollup-plugin-copy';
6
+ import dts from 'rollup-plugin-dts';
7
+ import { A as AstTransformError, d as del, N as NpmBuildError } from './errors.mjs';
8
+ import ts from 'typescript';
9
+ import nodePath, { basename, dirname } from 'node:path';
10
+ import { readPackage, toPosix } from '@mirta/package';
11
+ import { ensureCompactArray } from '@mirta/basics/array';
12
+
13
+ /**
14
+ * Удаляет расширение файла `.ts`, `.d.ts` или `.js`.
15
+ *
16
+ * @param fileName - Полное имя файла
17
+ * @returns Имя файла без расширения
18
+ *
19
+ * @since 0.3.5
20
+ *
21
+ **/
22
+ const removeFileExtension = (fileName) => fileName.replace(/\.(?:d\.)?(?:[cm]?[tj]s)$/i, '');
23
+ /**
24
+ * Находит общий префикс двух путей.
25
+ *
26
+ * @param a - Первый путь
27
+ * @param b - Второй путь
28
+ * @returns Общий префикс
29
+ *
30
+ * @since 0.3.5
31
+ *
32
+ **/
33
+ function getCommonPrefix(a, b) {
34
+ const aParts = nodePath.normalize(a).split(nodePath.sep);
35
+ const bParts = nodePath.normalize(b).split(nodePath.sep);
36
+ const minLength = Math.min(aParts.length, bParts.length);
37
+ let i = 0;
38
+ while (i < minLength && aParts[i] === bParts[i]) {
39
+ i++;
40
+ }
41
+ return aParts.slice(0, i).join(nodePath.sep);
42
+ }
43
+ /**
44
+ * Проверяет, является ли указанный файл частью проекта.
45
+ *
46
+ * Файл считается проектным, если он:
47
+ * - Не находится в директории `node_modules`;
48
+ * - Расположен внутри указанной корневой директории.
49
+ *
50
+ * @param fileName - Полный путь к проверяемому файлу.
51
+ * @param rootDir - Корневая директория проекта.
52
+ * @returns `true`, если файл принадлежит проекту, иначе false.
53
+ *
54
+ * @since 0.3.5
55
+ *
56
+ **/
57
+ function isProjectFile(fileName, rootDir) {
58
+ if (fileName.includes('node_modules'))
59
+ return false;
60
+ if (nodePath.relative(rootDir, fileName).startsWith('..'))
61
+ return false;
62
+ return true;
63
+ }
64
+ /**
65
+ * Вычисляет относительный путь от директории исходного файла к целевому файлу,
66
+ * добавляет имя выходного файла и нормализует путь для совместимости с POSIX-системами.
67
+ *
68
+ * @param sourceFilePath - Полный путь к исходному файлу.
69
+ * @param targetFilePath - Полный путь к целевому файлу.
70
+ * @param outputName - Имя выходного файла, который будет добавлен к относительному пути.
71
+ * @returns Нормализованный относительный путь с именем выходного файла.
72
+ *
73
+ * @example
74
+ *
75
+ * ```ts
76
+ * const sourceFilePath = '/project/src/index.ts'
77
+ * const targetFilePath = '/project/src/utils/index.ts'
78
+ * const outputName = 'utils.d.ts'
79
+ *
80
+ * getRelativeOutputPath(sourceFilePath, targetFilePath, outputName) // Возвращает './utils.d.ts'
81
+ *
82
+ * ```
83
+ * @since 0.3.5
84
+ *
85
+ **/
86
+ function getRelativeOutputPath(sourceFilePath, targetFilePath, outputFileName) {
87
+ // Шаг 1: Получаем директорию исходного файла.
88
+ const sourceDir = nodePath
89
+ .dirname(sourceFilePath);
90
+ // Шаг 2: Вычисляем относительный путь от директории исходного файла до целевого файла.
91
+ const relativeDir = nodePath
92
+ .dirname(nodePath.relative(sourceDir, targetFilePath));
93
+ // Шаг 3: Объединяем относительную директорию с именем выходного файла.
94
+ let relativePath = nodePath
95
+ .join(relativeDir, outputFileName);
96
+ relativePath = relativePath
97
+ .split(nodePath.sep)
98
+ .join(nodePath.posix.sep);
99
+ // Шаг 4: Гарантируем, что путь является относительным.
100
+ if (!relativePath.startsWith('.'))
101
+ relativePath = `./${relativePath}`;
102
+ return relativePath;
103
+ }
104
+ /**
105
+ * Определяет корневую директорию файла на основе конфигурации.
106
+ *
107
+ * @param context - Базовый контекст
108
+ * @param sourceFile - Обрабатываемый файл
109
+ * @returns Путь к корню проекта
110
+ * @throws Ошибка, если файлы не найдены
111
+ *
112
+ * @since 0.3.5
113
+ *
114
+ **/
115
+ function getRootDir(context, sourceFile) {
116
+ const { compilerOptions, program } = context;
117
+ // Если rootDirs указаны — ищем, к какому корню относится файл.
118
+ if (compilerOptions.rootDirs?.length) {
119
+ const sortedRoots = [...compilerOptions.rootDirs]
120
+ .sort((a, b) => b.length - a.length);
121
+ const normalizedFile = nodePath.resolve(sourceFile.fileName);
122
+ for (const rootDir of sortedRoots) {
123
+ const normalizedRoot = nodePath.resolve(rootDir);
124
+ if (normalizedFile.startsWith(normalizedRoot + nodePath.sep))
125
+ return rootDir;
126
+ }
127
+ }
128
+ // Если rootDir указан — используем его.
129
+ if (compilerOptions.rootDir)
130
+ return compilerOptions.rootDir;
131
+ // Иначе находим общий корень для всех файлов.
132
+ const fileNames = program.getRootFileNames();
133
+ if (!fileNames.length)
134
+ throw AstTransformError.get('noRootFiles');
135
+ let commonPrefix = nodePath.dirname(fileNames[0]);
136
+ for (const fileName of fileNames.slice(1)) {
137
+ commonPrefix = getCommonPrefix(commonPrefix, fileName);
138
+ }
139
+ return commonPrefix;
140
+ }
141
+
142
+ /**
143
+ * Перечисление типов индексных файлов.
144
+ * Используется для классификации модулей в зависимости от их отношения к файлам `index`.
145
+ *
146
+ * @since 0.3.5
147
+ *
148
+ **/
149
+ var IndexType;
150
+ (function (IndexType) {
151
+ /** Не индексный файл. */
152
+ IndexType["NonIndex"] = "non-index";
153
+ /** Явный индекс (например, `import './index'`). */
154
+ IndexType["Explicit"] = "explicit";
155
+ /** Неявный индекс (например, `import './dir'`). */
156
+ IndexType["Implicit"] = "implicit";
157
+ /** Индекс внутри пакета (например, import 'package/index'). */
158
+ IndexType["ImplicitPackage"] = "implicit-package";
159
+ })(IndexType || (IndexType = {}));
160
+
161
+ /**
162
+ * Проверяет безопасность пути модуля, блокируя подозрительные символы.
163
+ *
164
+ * Эта функция проверяет, не содержит ли путь модуля недопустимых символов,
165
+ * таких как `:` (используется в URL) или `~` (ссылка на домашнюю директорию),
166
+ * которые могут представлять риск безопасности.
167
+ *
168
+ * @param path - Путь модуля для проверки.
169
+ *
170
+ * @throws {AstTransformError} Если обнаружены запрещённые символы.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * assertPathIsValid('./utils') // OK
175
+ * assertPathIsValid('http://malicious.com') // Выбросит ошибку
176
+ *
177
+ * ```
178
+ * @since 0.3.5
179
+ *
180
+ **/
181
+ function assertPathIsValid(path) {
182
+ if (path.includes(':') || path.includes('~'))
183
+ throw AstTransformError.get('invalidChars', path);
184
+ }
185
+
186
+ /**
187
+ * Создает кэш файлов по имени без расширения.
188
+ *
189
+ * @param program - Программа TypeScript
190
+ * @returns Карта: ключ - имя файла без расширения, значение - объект файла
191
+ *
192
+ * @since 0.3.5
193
+ *
194
+ **/
195
+ function createSourceFilesCache(program) {
196
+ return new Map(program.getSourceFiles().map(sourceFile => [
197
+ removeFileExtension(sourceFile.fileName),
198
+ sourceFile,
199
+ ]));
200
+ }
201
+ /**
202
+ * Получает файл из программы или создает его при необходимости.
203
+ * Использует кэш для повышения производительности при повторных запросах.
204
+ *
205
+ * @param context - Контекст трансформера, содержащий ссылку на программу и кэш файлов.
206
+ * @param fileName - Полный путь к файлу, который необходимо найти или создать.
207
+ * @returns Экземпляр {@link ts.SourceFile} для существующего или созданного файла.
208
+ *
209
+ * @since 0.3.5
210
+ *
211
+ **/
212
+ function resolveSourceFile(context, fileName) {
213
+ const { program, compilerOptions } = context;
214
+ let result = program.getSourceFile(fileName);
215
+ if (result)
216
+ return result;
217
+ // Если кэш уже создан, используем его. Иначе создаем новый.
218
+ const sourceFilesCache = context.sourceFilesCache ??= createSourceFilesCache(program);
219
+ const normalizedFileName = removeFileExtension(fileName);
220
+ // Попытка найти файл в кэше.
221
+ result = sourceFilesCache.get(normalizedFileName);
222
+ if (!result) {
223
+ result = ts.createSourceFile(fileName, '', compilerOptions.target ?? ts.ScriptTarget.ESNext, false);
224
+ sourceFilesCache.set(normalizedFileName, result);
225
+ }
226
+ return result;
227
+ }
228
+
229
+ /**
230
+ * Анализирует путь модуля и определяет его тип (явный, неявный, пакетный или обычный).
231
+ * Собирает метаданные о модуле для дальнейшей обработки путей импорта.
232
+ *
233
+ * @param path - Путь модуля из исходного кода (например, './dir' или 'package/index').
234
+ * @param resolvedModule - Объект, содержащий информацию о разрешённом модуле из TypeScript.
235
+ * @returns Объект с детализацией пути, включая тип индекса, имя файла, расширение и директорию.
236
+ *
237
+ * @since 0.3.5
238
+ *
239
+ **/
240
+ function getPathDetails(path, resolvedModule) {
241
+ const { resolvedFileName, packageId } = resolvedModule;
242
+ const implicitPackagePath = packageId?.subModuleName;
243
+ // Указывает, является ли модуль частью пакета (например, 'package/index').
244
+ const isPackage = !!implicitPackagePath;
245
+ // Базовые данные разрешённого файла
246
+ const resolvedBaseName = nodePath.basename(isPackage
247
+ ? implicitPackagePath
248
+ : resolvedFileName);
249
+ const resolvedBaseNameNoExtension = resolvedBaseName
250
+ ? removeFileExtension(resolvedBaseName)
251
+ : undefined;
252
+ // const resolvedExtension = resolvedBaseName
253
+ // ? nodePath.extname(resolvedFileName)
254
+ // : undefined
255
+ // Базовые данные оригинального модуля
256
+ let baseName = isPackage
257
+ ? undefined
258
+ : nodePath.basename(path);
259
+ let baseNameNoExtension = baseName
260
+ ? removeFileExtension(baseName)
261
+ : undefined;
262
+ let extName = baseName
263
+ ? nodePath.extname(path)
264
+ : undefined;
265
+ // Если имя оригинального модуля совпадает с разрешённым, убираем расширение.
266
+ if (resolvedBaseNameNoExtension
267
+ && baseName
268
+ && resolvedBaseNameNoExtension === baseName) {
269
+ baseNameNoExtension = baseName;
270
+ extName = undefined;
271
+ }
272
+ let indexType;
273
+ if (isPackage) {
274
+ // Модуль внутри пакета (например, import 'package/index').
275
+ indexType = IndexType.ImplicitPackage;
276
+ }
277
+ else if (baseNameNoExtension === 'index' && resolvedBaseNameNoExtension === 'index') {
278
+ // Явный импорт файла index (например, import './dir/index').
279
+ indexType = IndexType.Explicit;
280
+ }
281
+ else if (baseNameNoExtension !== 'index' && resolvedBaseNameNoExtension === 'index') {
282
+ // Неявный импорт index (например, import './dir').
283
+ indexType = IndexType.Implicit;
284
+ }
285
+ else {
286
+ // Обычный файл, не связанный с index.
287
+ indexType = IndexType.NonIndex;
288
+ }
289
+ // Для неявных индексов убирает лишние поля оригинального
290
+ // модуля, чтобы не отображать index и расширения.
291
+ //
292
+ if (indexType === IndexType.Implicit) {
293
+ baseName = undefined;
294
+ baseNameNoExtension = undefined;
295
+ extName = undefined;
296
+ }
297
+ return {
298
+ // baseName,
299
+ // baseNameNoExtension,
300
+ extName,
301
+ // resolvedBaseName,
302
+ resolvedBaseNameNoExtension,
303
+ // resolvedExtension,
304
+ // resolvedDir: isPackage
305
+ // ? removeSuffix(resolvedFileName, `/${implicitPackageIndex}`)
306
+ // : nodePath.dirname(resolvedFileName),
307
+ indexType,
308
+ // implicitPackagePath,
309
+ // resolvedFileName,
310
+ };
311
+ }
312
+ /**
313
+ * Генерирует новый относительный путь для импорта модуля, исключая расширения и индексные файлы.
314
+ * Проверяет безопасность пути и убеждается, что файл находится внутри корня проекта.
315
+ *
316
+ * @param context - Контекст трансформера, содержащий информацию о текущем файле и конфигурации.
317
+ * @param oldPath - Путь модуля из исходного импорта (например, './utils/index').
318
+ * @returns Относительный путь без расширения и индекса, или `undefined`, если модуль недопустим.
319
+ *
320
+ * @since 0.3.5
321
+ *
322
+ **/
323
+ function resolveNewModulePath(context, oldPath) {
324
+ assertPathIsValid(oldPath);
325
+ const {
326
+ // Текущий обрабатываемый файл.
327
+ sourceFile: currentSourceFile,
328
+ // Актуальная конфигурация TypeScript.
329
+ compilerOptions, } = context;
330
+ // Получаем модуль импортированного файла.
331
+ //
332
+ const { resolvedModule: importedModule } = ts.resolveModuleName(oldPath, currentSourceFile.fileName, compilerOptions, ts.sys);
333
+ if (!importedModule)
334
+ throw AstTransformError.get('moduleNotFound', oldPath, currentSourceFile.fileName);
335
+ if (!isProjectFile(importedModule.resolvedFileName, context.rootDir))
336
+ return null;
337
+ // Получает детали пути импортированного модуля.
338
+ const pathDetails = getPathDetails(oldPath, importedModule);
339
+ const { indexType, resolvedBaseNameNoExtension, extName } = pathDetails;
340
+ let outputBaseName = resolvedBaseNameNoExtension ?? '';
341
+ if (indexType === IndexType.Implicit && outputBaseName.endsWith('index'))
342
+ outputBaseName = outputBaseName.slice(0, -5);
343
+ if (outputBaseName && extName)
344
+ outputBaseName = `${outputBaseName}${extName}`;
345
+ // Получает исходный файл импортированного модуля.
346
+ const importedSourceFile = resolveSourceFile(context, importedModule.resolvedFileName);
347
+ const newPath = getRelativeOutputPath(currentSourceFile.fileName, importedSourceFile.fileName, outputBaseName);
348
+ return newPath;
349
+ }
350
+
351
+ const aliasPattern = /^[@#]/;
352
+ /**
353
+ * Обновляет спецификатор модуля в узле импорта или экспорта.
354
+ *
355
+ * @param factory - Фабрика для создания новых узлов AST.
356
+ * @param node - Узел импорта или экспорта.
357
+ * @param newModuleSpecifier - Новый относительный путь.
358
+ * @returns Обновлённый узел.
359
+ *
360
+ * @since 0.3.5
361
+ *
362
+ **/
363
+ function updateModuleSpecifier(factory, node, newModuleSpecifier) {
364
+ if (ts.isImportDeclaration(node)) {
365
+ return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(newModuleSpecifier), node.attributes);
366
+ }
367
+ else if (ts.isExportDeclaration(node)) {
368
+ return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(newModuleSpecifier), node.attributes);
369
+ }
370
+ return node;
371
+ }
372
+ /**
373
+ * Рекурсивно обходит дочерние узлы AST с текущим визитором.
374
+ */
375
+ function visitChildren(context, node) {
376
+ return ts.visitEachChild(node, context.getVisitor(), context.transformationContext);
377
+ }
378
+ /**
379
+ * Визитор для обработки узлов AST, связанных с импортами.
380
+ *
381
+ * Обновляет пути модулей, начинающиеся с `#` или `@`, в файлах объявлений TypeScript.
382
+ *
383
+ * @param this - Контекст трансформера, предоставляющий инструменты для работы с AST.
384
+ * @param node - Текущий узел AST, который проверяется и, при необходимости, обновляется.
385
+ * @returns Обновленный узел AST или результат рекурсивного обхода дочерних узлов.
386
+ *
387
+ * @example
388
+ * ```ts
389
+ * // Исходный узел импорта:
390
+ * import { foo } from '#utils/index'
391
+ *
392
+ * // После обработки:
393
+ * import { foo } from './utils'
394
+ *
395
+ * ```
396
+ * @since 0.3.5
397
+ *
398
+ **/
399
+ function nodeVisitor(node) {
400
+ if (!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node))
401
+ return visitChildren(this, node);
402
+ const moduleSpecifier = node.moduleSpecifier;
403
+ if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier))
404
+ return visitChildren(this, node);
405
+ // Извлекаем путь импорта/экспорта (указывается в кавычках после from).
406
+ const { text: oldPath } = moduleSpecifier;
407
+ // Проверяем, относится ли спецификатор к алиасу проекта (`#`) или кастомному алиасу (`@`).
408
+ // Такие спецификаторы требуют пересчета в относительный путь.
409
+ //
410
+ if (!aliasPattern.test(oldPath))
411
+ return visitChildren(this, node);
412
+ const cachedNewPath = this.pathsCache.get(oldPath);
413
+ const newPath = cachedNewPath === undefined
414
+ ? resolveNewModulePath(this, oldPath)
415
+ : cachedNewPath;
416
+ if (cachedNewPath === undefined)
417
+ this.pathsCache.set(oldPath, newPath);
418
+ if (!newPath)
419
+ return node;
420
+ // Обновляем узел импорта новым значением пути.
421
+ // Если путь не удалось преобразовать, возвращаем исходный узел.
422
+ //
423
+ return updateModuleSpecifier(this.factory, node, newPath);
424
+ }
425
+
426
+ /**
427
+ * Фабрика трансформеров для Rollup, предназначенная для обработки файлов объявлений TypeScript (`.d.ts`).
428
+ * Создаёт трансформер, который модифицирует пути импортов в декларациях, исключая расширения и индексные файлы.
429
+ *
430
+ * @param program - Экземпляр программы TypeScript, предоставляющий доступ к всем файлам проекта.
431
+ * @returns Фабрика трансформеров, применяющаяся ко всем `.d.ts`-файлам.
432
+ *
433
+ * @since 0.3.5
434
+ *
435
+ **/
436
+ function dtsAliasTransformerFactory(program) {
437
+ return (context) => {
438
+ const compilerOptions = program.getCompilerOptions();
439
+ /**
440
+ * Базовый контекст для работы с AST-трансформером.
441
+ * Содержит общие параметры и ссылки на программу TypeScript.
442
+ *
443
+ **/
444
+ const visitorContextBase = {
445
+ compilerOptions,
446
+ program,
447
+ factory: context.factory,
448
+ transformationContext: context,
449
+ };
450
+ return (sourceFile) => {
451
+ /**
452
+ * Обработка происходит только для файлов объявлений (`.d.ts`).
453
+ * Обычные файлы (`*.ts`) игнорируются.
454
+ *
455
+ **/
456
+ if (!sourceFile.isDeclarationFile)
457
+ return sourceFile;
458
+ /**
459
+ * Контекст визитора, расширенный информацией о текущем файле и корне проекта.
460
+ * Используется для передачи данных в `nodeVisitor`.
461
+ *
462
+ **/
463
+ const visitorContext = {
464
+ ...visitorContextBase,
465
+ sourceFile,
466
+ rootDir: getRootDir(visitorContextBase, sourceFile),
467
+ pathsCache: new Map(),
468
+ getVisitor() {
469
+ return nodeVisitor.bind(this);
470
+ },
471
+ };
472
+ /**
473
+ * Рекурсивный обход AST с применением визитора.
474
+ * Обрабатывает все узлы файла, начиная с корня.
475
+ *
476
+ **/
477
+ return ts.visitEachChild(sourceFile, visitorContext.getVisitor(), context);
478
+ };
479
+ };
480
+ }
481
+
482
+ /**
483
+ * Экспортируемый трансформер для Rollup, предназначенный для обработки файлов объявлений TypeScript (`.d.ts`).
484
+ * Используется в фазе `afterDeclarations`, чтобы корректировать пути импортов после генерации типов.
485
+ *
486
+ * @returns Объект трансформера, который может быть передан в конфигурацию Rollup.
487
+ *
488
+ * @example
489
+ * Пример использования в конфигурации Rollup:
490
+ * ```ts
491
+ * import { dtsAlias } from '#ast/dts-alias';
492
+ *
493
+ * export default {
494
+ * plugins: [
495
+ * typescript({
496
+ * transformers: {
497
+ * afterDeclarations: [
498
+ * dtsAlias()
499
+ * ]
500
+ * }
501
+ * })
502
+ * ]
503
+ * }
504
+ *
505
+ * ```
506
+ * @since 0.3.5
507
+ *
508
+ **/
509
+ const dtsAlias = () => ({
510
+ type: 'program',
511
+ factory: dtsAliasTransformerFactory,
512
+ });
513
+
514
+ /**
515
+ * Создаёт предикат для проверки, следует ли считать модуль внешним по набору правил.
516
+ *
517
+ * @param cwd - рабочая директория. Пути за её пределами считаются внешними.
518
+ * @param externals - список функций или паттернов (строки, RegExp или массивы таких значений), определяющих внешние модули
519
+ * @returns `true` если модуль считается внешним, `false` в противном случае
520
+ */
521
+ function createExternalFilter(cwd, ...externals) {
522
+ /**
523
+ * Функция-предикат для определения внешнего модуля
524
+ *
525
+ * @param target Целевой путь/имя модуля
526
+ * @param importer Имя файла, который импортирует модуль (если доступно)
527
+ * @param isResolved Флаг, указывающий, был ли модуль успешно разрешён
528
+ * @returns `true`, если модуль внешний, иначе false
529
+ */
530
+ return (target, importer, isResolved) => {
531
+ for (const external of externals) {
532
+ // Шаг 1: Пользовательская функция
533
+ if (typeof external === 'function') {
534
+ if (external(target, importer, isResolved))
535
+ return true;
536
+ }
537
+ else {
538
+ // Шаг 2: Массив паттернов
539
+ const isExternal = ensureCompactArray(external).some((item) => {
540
+ if (item instanceof RegExp)
541
+ return item.test(target);
542
+ return item === target;
543
+ });
544
+ if (isExternal)
545
+ return true;
546
+ }
547
+ }
548
+ // Шаг 3: Путь вне cwd (только для обнаруженных модулей)
549
+ if (isResolved && cwd && nodePath.isAbsolute(target)) {
550
+ const relativePath = nodePath.relative(cwd, target);
551
+ // Если путь вне cwd, отмечаем его как внешний
552
+ if (relativePath.startsWith('..') || nodePath.isAbsolute(relativePath)) {
553
+ return true;
554
+ }
555
+ }
556
+ // Шаг 4: По умолчанию — внутренний
557
+ return false;
558
+ };
559
+ }
560
+
561
+ /**
562
+ * @file Генерирует конфигурации Rollup на основе `package.json`.
563
+ *
564
+ * Главная функция — `definePackageConfig`. Она:
565
+ * - Читает `exports` и сопоставляет с `input`
566
+ * - Генерирует ESM-бандл и, при необходимости, `.d.ts`
567
+ * - Поддерживает запуск из корня монорепозитория (через `cwd`)
568
+ *
569
+ * Используется для сборки пакетов в монорепозитории.
570
+ *
571
+ * @since 0.3.0
572
+ *
573
+ **/
574
+ const outDir = 'dist';
575
+ const outDirDts = `${outDir}/dts`;
576
+ /**
577
+ * Удаляет префикс каталога вывода (`./${outDir}/`) из пути.
578
+ * @param path Путь к файлу.
579
+ * @returns Нормализованный путь.
580
+ *
581
+ * @since 0.3.5
582
+ *
583
+ **/
584
+ function sliceDistPrefix(path) {
585
+ const prefix = `./${outDir}/`;
586
+ return path.startsWith(prefix)
587
+ ? path.slice(prefix.length)
588
+ : path;
589
+ }
590
+ /**
591
+ * Нормализует входные данные в массив строк.
592
+ *
593
+ * @param input Входные данные (строка, массив или объект).
594
+ * @returns Массив путей к входным файлам.
595
+ * @throws {NpmBuildError} Если входная конфигурация пуста.
596
+ *
597
+ * @since 0.3.4
598
+ *
599
+ **/
600
+ function normalizeInput(input) {
601
+ const inputs = [];
602
+ if (typeof input === 'string') {
603
+ inputs.push(input);
604
+ }
605
+ else if (Array.isArray(input)) {
606
+ inputs.push(...input);
607
+ }
608
+ else if (typeof input === 'object') {
609
+ inputs.push(...Object.values(input));
610
+ }
611
+ if (inputs.length === 0)
612
+ throw NpmBuildError.get('inputEmpty');
613
+ return inputs;
614
+ }
615
+ /**
616
+ * Проверяет, является ли объект условной записью экспорта.
617
+ *
618
+ * @param source Объект для проверки.
619
+ * @returns `true`, если объект содержит поле `import`.
620
+ *
621
+ * @since 0.3.5
622
+ *
623
+ **/
624
+ function isConditionalEntry(source) {
625
+ return 'import' in source;
626
+ }
627
+ /**
628
+ * Обрабатывает условную запись экспорта.
629
+ *
630
+ * @param source Условная запись.
631
+ * @returns Объект с полями entry и types.
632
+ *
633
+ * @since 0.3.5
634
+ *
635
+ **/
636
+ function processConditionalEntry(source) {
637
+ const result = {};
638
+ if (source.import) {
639
+ if (typeof source.import === 'string') {
640
+ // Путь точки входа определён, типизация отсутствует.
641
+ result.entry = source.import;
642
+ }
643
+ else {
644
+ result.entry = source.import.default;
645
+ result.types = source.import.types;
646
+ }
647
+ }
648
+ return result;
649
+ }
650
+ /**
651
+ * Проверяет наличие точки входа для типизации.
652
+ *
653
+ * @param entry Путь к точке входа.
654
+ * @param types Путь к файлу типов.
655
+ * @throws {NpmBuildError} Если отсутствует точка входа.
656
+ *
657
+ * @since 0.3.5
658
+ *
659
+ **/
660
+ function assertTypesHaveEntry(entry, types) {
661
+ if (types && !entry)
662
+ throw NpmBuildError.get('exportTypesOnly', types);
663
+ }
664
+ /**
665
+ * Нормализует поле `exports` из package.json в словарь точек входа.
666
+ *
667
+ * @param exportsField Значение поля `exports`.
668
+ * @returns Словарь точек входа с метаданными.
669
+ * @throws {NpmBuildError} Если конфигурация экспорта отсутствует или некорректна.
670
+ *
671
+ * @since 0.3.5
672
+ *
673
+ **/
674
+ function normalizeExports(exportsField) {
675
+ if (!exportsField)
676
+ throw NpmBuildError.get('exportEmpty');
677
+ if (Array.isArray(exportsField))
678
+ throw NpmBuildError.get('exportDisallowArrayType');
679
+ const result = {};
680
+ if (typeof exportsField === 'string') {
681
+ result[exportsField] = {};
682
+ return result;
683
+ }
684
+ if (isConditionalEntry(exportsField)) {
685
+ const { entry, types } = processConditionalEntry(exportsField);
686
+ assertTypesHaveEntry(entry, types);
687
+ if (entry)
688
+ result[entry] = types
689
+ ? { dtsOutputFile: types }
690
+ : {};
691
+ return result;
692
+ }
693
+ for (const [key, value] of Object.entries(exportsField)) {
694
+ if (!value)
695
+ continue;
696
+ if (!key.startsWith('.'))
697
+ throw NpmBuildError.get('exportMustStartWithDot', key);
698
+ let entry, types;
699
+ if (typeof value === 'string') {
700
+ // Путь точки входа определён, типизация отсутствует.
701
+ entry = value;
702
+ }
703
+ else if (isConditionalEntry(value)) {
704
+ ({ entry, types } = processConditionalEntry(value));
705
+ }
706
+ else {
707
+ entry = value.default;
708
+ types = value.types;
709
+ }
710
+ assertTypesHaveEntry(entry, types);
711
+ if (entry)
712
+ result[entry] = types
713
+ ? { dtsOutputFile: types }
714
+ : {};
715
+ }
716
+ return result;
717
+ }
718
+ /**
719
+ * Сопоставляет входные файлы (`src/*.ts`) с выходными (`./dist/*.mjs`)
720
+ * на основе поля `exports` из `package.json`.
721
+ *
722
+ * @param inputs Локальные пути (`src/index.ts`)
723
+ * @param normalizedExports Словарь `exportPath → { dtsOutputFile }`
724
+ * @param skipExports Пропустить проверку (для `bin`-пакетов)
725
+ * @returns Словарь `input → { outputFile, dtsSourceFile, dtsOutputFile }`
726
+ *
727
+ * @throws NpmBuildError если:
728
+ * - `input` не начинается с `src/`
729
+ * - `input` не имеет соответствия в `exports`
730
+ * - `exports` не имеет соответствия в `input`
731
+ * - `types` указан без `import`/`default`
732
+ *
733
+ * @since 0.3.4
734
+ *
735
+ **/
736
+ function createInputBindings(inputs, normalizedExports, skipExports) {
737
+ // Извлекает относительный путь после префикса `src/` (без расширения).
738
+ const filePattern = /^(?:.*\/)?src\/(.*)\.[jt]s$/;
739
+ const result = {};
740
+ const usedExports = new Set();
741
+ const producingOutputs = new Set();
742
+ for (const input of inputs) {
743
+ if (!input.startsWith('src/'))
744
+ throw NpmBuildError.get('inputPathRequiresPrefix', input, 'src/');
745
+ const match = filePattern.exec(input);
746
+ if (!match)
747
+ throw NpmBuildError.get('inputFileExtensionNotSupported', input);
748
+ const outputFile = `${match[1]}.mjs`;
749
+ if (producingOutputs.has(outputFile))
750
+ throw NpmBuildError.get('inputGeneratesDuplicateOutput', outputFile);
751
+ producingOutputs.add(outputFile);
752
+ const exportEntry = `./${outDir}/${outputFile}`;
753
+ usedExports.add(exportEntry);
754
+ const descriptor = normalizedExports[exportEntry];
755
+ // Проверяет наличие ключа в словаре экспорта (при необходимости).
756
+ if (!descriptor && !skipExports)
757
+ throw NpmBuildError.get('inputHasNoExport', input, exportEntry);
758
+ result[input] = {
759
+ outputFile,
760
+ dtsSourceFile: `${outDirDts}/${match[1]}.d.ts`,
761
+ dtsOutputFile: descriptor?.dtsOutputFile,
762
+ };
763
+ }
764
+ for (const key of Object.keys(normalizedExports)) {
765
+ // Выявляет незадействованные ключи в словаре экспорта (обратная проверка).
766
+ if (!usedExports.has(key))
767
+ throw NpmBuildError.get('exportHasNoInput', key);
768
+ }
769
+ return result;
770
+ }
771
+ /**
772
+ * Создаёт отображение между промежуточными файлами типов `.d.ts` и их финальными выходными путями.
773
+ * Используется для настройки `entryFileNames` в конфигурации Rollup для `.d.ts`-бандла.
774
+ *
775
+ * @param inputBindings Словарь связей входных файлов с выходными путями.
776
+ * @returns Словарь отображений: ключ — промежуточный путь `.d.ts` (например, `dist/dts/index.d.ts`),
777
+ * значение — финальное имя файла (например, `index.d.mts`).
778
+ * @example
779
+ * ```ts
780
+ * const mappings = createDtsMappings({
781
+ * 'src/index.ts': {
782
+ * outputFile: 'index.mjs',
783
+ * dtsSourceFile: 'dist/dts/index.d.ts',
784
+ * dtsOutputFile: './dist/index.d.mts'
785
+ * }
786
+ * })
787
+ * // Возвращает: { 'dist/dts/index.d.ts': 'index.d.mts' }
788
+ * ```
789
+ * @since 0.4.0
790
+ *
791
+ **/
792
+ function createDtsMappings(inputBindings) {
793
+ const mappings = {};
794
+ for (const binding of Object.values(inputBindings)) {
795
+ if (binding?.dtsOutputFile)
796
+ mappings[binding.dtsSourceFile] = sliceDistPrefix(binding.dtsOutputFile);
797
+ }
798
+ return mappings;
799
+ }
800
+ /**
801
+ * Возвращает относительный путь от корня монорепозитория до пакета с завершающим слешем.
802
+ *
803
+ * Используется для:
804
+ * - Преобразования локальных путей (`src/index.ts`) → глобальные (`packages/name/src/index.ts`)
805
+ * - Формирования входов для `dts`-сборки (`packages/name/dist/dts/index.d.ts`)
806
+ *
807
+ * @param cwdRoot Корень монорепозитория (где запущен Rollup)
808
+ * @param cwdPackage Директория пакета
809
+ * @returns Относительный путь вида `'packages/mirta-rollup/'`, или `''`, если это корень
810
+ *
811
+ * @example
812
+ *
813
+ * ```ts
814
+ * getPackagePrefix('/repo', '/repo/packages/core') → 'packages/core/'
815
+ *
816
+ * ```
817
+ *
818
+ * @since 0.4.0
819
+ *
820
+ **/
821
+ function getPackagePrefix(cwdRoot, cwdPackage) {
822
+ const packagePrefix = toPosix(nodePath.relative(cwdRoot, cwdPackage));
823
+ return packagePrefix
824
+ ? `${packagePrefix}/`
825
+ : '';
826
+ }
827
+ // Проверка TypeScript выполняется только для первой конфигурации.
828
+ let hasTsChecked = false;
829
+ /**
830
+ * Создаёт конфигурации Rollup для пакета на основе его `package.json`.
831
+ *
832
+ * Поддерживает:
833
+ * - ESM-бандл (обязательно)
834
+ * - `.d.ts`-бандл (если в `exports` указаны `types`)
835
+ * - Режим запуска из корня монорепозитория (через `packagePrefix`)
836
+ * - Проверки соответствия `input` ↔ `exports`
837
+ *
838
+ * @param options Настройки сборки
839
+ * @returns Массив конфигураций Rollup
840
+ *
841
+ * @example
842
+ *
843
+ * ```ts
844
+ * definePackageConfig({
845
+ * cwd: '/repo/packages/my-package',
846
+ * input: 'src/index.ts',
847
+ * })
848
+ *
849
+ * ```
850
+ * @since 0.3.0
851
+ *
852
+ **/
853
+ function definePackageConfig(options = {}) {
854
+ // Реальная директория запуска может отличаться от директории пакета `cwd`.
855
+ const cwdRoot = process.cwd();
856
+ const { cwd = cwdRoot, input = 'src/index.ts', external = [], plugins, skipExports = false, } = options;
857
+ const packagePrefix = getPackagePrefix(cwdRoot, cwd);
858
+ const outDirPath = nodePath.join(cwd, outDir);
859
+ const outDirDtsPath = nodePath.join(cwd, outDirDts);
860
+ const pkgPath = nodePath.resolve(cwd, 'package.json');
861
+ const externalFilter = createExternalFilter(cwd, [
862
+ /node_modules/,
863
+ pkgPath, // Для предотвращения встраивания `package.json` в бандл
864
+ ], external);
865
+ const normalizedInput = normalizeInput(input);
866
+ const { exports: exports$1 = {} } = readPackage(pkgPath);
867
+ const normalizedExports = !skipExports
868
+ ? normalizeExports(exports$1)
869
+ : {};
870
+ const inputBindings = createInputBindings(normalizedInput, normalizedExports, skipExports);
871
+ const dtsMappings = createDtsMappings(inputBindings);
872
+ const dtsInputs = Object.keys(dtsMappings)
873
+ .map(item => `${packagePrefix}${item}`);
874
+ const rollupConfigs = [
875
+ createBuildConfig('mjs', {
876
+ cwd,
877
+ input: normalizedInput.map(input => `${packagePrefix}${input}`),
878
+ external: externalFilter,
879
+ emitDeclarations: dtsInputs.length > 0,
880
+ plugins,
881
+ outPath: outDirPath,
882
+ outPathDts: outDirDtsPath,
883
+ output: {
884
+ dir: outDirPath,
885
+ format: 'es',
886
+ importAttributesKey: 'with',
887
+ entryFileNames(chunk) {
888
+ if (chunk.facadeModuleId) {
889
+ const localPath = nodePath
890
+ .relative(cwd, chunk.facadeModuleId)
891
+ .replaceAll(nodePath.sep, nodePath.posix.sep);
892
+ const binding = inputBindings[localPath];
893
+ if (binding)
894
+ return binding.outputFile;
895
+ }
896
+ return `${chunk.name}.mjs`;
897
+ },
898
+ chunkFileNames(chunk) {
899
+ // Для чанков с названием `index` использует имя родительской директории
900
+ // вместо порядкового номера (`index.mjs`, `index2.mjs`, `index3.mjs`)
901
+ //
902
+ if (chunk.name === 'index' && chunk.facadeModuleId)
903
+ return `${basename(dirname(chunk.facadeModuleId))}.mjs`;
904
+ return `${chunk.name}.mjs`;
905
+ },
906
+ },
907
+ }),
908
+ ];
909
+ if (dtsInputs.length > 0) {
910
+ rollupConfigs.push({
911
+ input: dtsInputs,
912
+ external: externalFilter,
913
+ plugins: [
914
+ nodeResolve(),
915
+ commonjs(),
916
+ dts(),
917
+ del({
918
+ targets: outDirDtsPath,
919
+ hook: 'closeBundle',
920
+ }),
921
+ ],
922
+ output: {
923
+ dir: outDirPath,
924
+ format: 'es',
925
+ entryFileNames(chunk) {
926
+ if (chunk.facadeModuleId) {
927
+ const localPath = nodePath
928
+ .relative(cwd, chunk.facadeModuleId)
929
+ .replaceAll(nodePath.sep, nodePath.posix.sep);
930
+ if (dtsMappings[localPath])
931
+ return dtsMappings[localPath];
932
+ }
933
+ return `${chunk.name}.mts`;
934
+ },
935
+ },
936
+ });
937
+ }
938
+ return rollupConfigs;
939
+ }
940
+ /**
941
+ * Создаёт конфигурацию сборки Rollup.
942
+ *
943
+ * @param buildName Имя сборки.
944
+ * @param options Параметры сборки.
945
+ * @returns Конфигурация Rollup.
946
+ *
947
+ * @since 0.3.0
948
+ *
949
+ **/
950
+ function createBuildConfig(buildName, options) {
951
+ const { cwd, external, input, emitDeclarations, plugins = [], outPath, outPathDts, output } = options;
952
+ output.sourcemap = !!process.env.SOURCE_MAP;
953
+ output.externalLiveBindings = false;
954
+ process.env.NODE_ENV === 'production';
955
+ const tsPlugin = ts$1({
956
+ tsconfig: nodePath.resolve(cwd, './tsconfig.build.json'),
957
+ compilerOptions: {
958
+ noCheck: hasTsChecked,
959
+ outDir: outPath,
960
+ declaration: emitDeclarations,
961
+ declarationDir: emitDeclarations ? outPathDts : void 0,
962
+ },
963
+ exclude: [
964
+ 'packages/*/tests',
965
+ ],
966
+ transformers: {
967
+ afterDeclarations: [
968
+ dtsAlias(),
969
+ ],
970
+ },
971
+ });
972
+ // При запуске команды build, проверки TS и генерация определений
973
+ // выполняются единожды - для первой конфигурации.
974
+ hasTsChecked = true;
975
+ const assetsSrc = toPosix(nodePath.join(cwd, 'public/*'));
976
+ return {
977
+ input,
978
+ external,
979
+ plugins: [
980
+ // Очистка директории dist перед сборкой
981
+ del({
982
+ targets: outPath,
983
+ }),
984
+ tsPlugin,
985
+ createReplacePlugin(),
986
+ nodeResolve(),
987
+ commonjs(),
988
+ ...plugins,
989
+ copy({
990
+ targets: [
991
+ { src: assetsSrc, dest: outPath },
992
+ ],
993
+ }),
994
+ ],
995
+ output,
996
+ };
997
+ }
998
+ /**
999
+ * Создаёт плагин замены значений.
1000
+ *
1001
+ * @param isProduction Признак production-сборки.
1002
+ * @param isBundlerEsmBuild Признак сборки для bundler ESM.
1003
+ * @param isNodeBuild Признак сборки для Node.js.
1004
+ * @returns Плагин замены.
1005
+ *
1006
+ * @since 0.3.0
1007
+ *
1008
+ **/
1009
+ function createReplacePlugin(isProduction, isBundlerEsmBuild, isNodeBuild) {
1010
+ const replacements = {
1011
+ // Preserve to be handled by bundlers
1012
+ __DEV__: `(process.env.NODE_ENV !== 'production')`
1013
+ ,
1014
+ __TEST__: `(process.env.NODE_ENV === 'test')`
1015
+ ,
1016
+ };
1017
+ // Allow inline overrides like
1018
+ // __DEV__=true pnpm build
1019
+ Object.keys(replacements).forEach((key) => {
1020
+ if (key in process.env)
1021
+ replacements[key] = process.env[key];
1022
+ });
1023
+ return replace({
1024
+ preventAssignment: true,
1025
+ values: replacements,
1026
+ delimiters: ['\\b', '\\b(?![\\.\\:])'],
1027
+ });
1028
+ }
1029
+
1030
+ export { definePackageConfig as d };