@mirta/rollup 0.3.4 → 0.3.5
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/configs/index.d.mts +19 -0
- package/dist/configs/index.mjs +47 -0
- package/dist/index.d.mts +56 -12
- package/dist/index.mjs +19 -514
- package/dist/runtime.mjs +1571 -0
- package/package.json +21 -5
package/dist/runtime.mjs
ADDED
|
@@ -0,0 +1,1571 @@
|
|
|
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 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
|
+
import { getBabelOutputPlugin } from '@rollup/plugin-babel';
|
|
13
|
+
import { deleteAsync } from 'del';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
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
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Абсолютный путь к результирующему каталогу.
|
|
1244
|
+
const outputDir$1 = path.join(process.cwd(), 'dist');
|
|
1245
|
+
const modulesDir = path.join(outputDir$1, 'wb-rules-modules');
|
|
1246
|
+
// Расположение обрабатываемого чанка.
|
|
1247
|
+
const getChunkDir = (fileName) => path.dirname(path.join(outputDir$1, fileName));
|
|
1248
|
+
// Шаблон для отлова конструкций require.
|
|
1249
|
+
const patternRequire = /require\(['"]([^'"]+)'\)/g;
|
|
1250
|
+
/**
|
|
1251
|
+
* Плагин Rollup, перестраивающий пути импорта модулей
|
|
1252
|
+
* относительно каталога wb-rules-modules.
|
|
1253
|
+
* */
|
|
1254
|
+
function wbRulesImports() {
|
|
1255
|
+
return {
|
|
1256
|
+
name: 'wbRulesImports',
|
|
1257
|
+
// Выполняется перед записью содержимого
|
|
1258
|
+
// в результирующий файл.
|
|
1259
|
+
renderChunk(code, chunk) {
|
|
1260
|
+
// Виртуальный каталог не обрабатываем.
|
|
1261
|
+
if (chunk.fileName.startsWith('_virtual'))
|
|
1262
|
+
return;
|
|
1263
|
+
const magicString = new MagicString(code);
|
|
1264
|
+
let hasReplacements = false;
|
|
1265
|
+
let start;
|
|
1266
|
+
let end;
|
|
1267
|
+
const chunkDir = getChunkDir(chunk.fileName);
|
|
1268
|
+
// Преобразует путь подключения модуля в формат,
|
|
1269
|
+
// поддерживаемый контроллером Wirenboard.
|
|
1270
|
+
function rebaseRequire(match) {
|
|
1271
|
+
// Начальная позиция оригинального вхождения
|
|
1272
|
+
start = match.index;
|
|
1273
|
+
// Конечная позиция оригинального вхождения
|
|
1274
|
+
end = start + match[0].length;
|
|
1275
|
+
// Преобразует путь подключаемого модуля в абсолютный,
|
|
1276
|
+
// опираясь на каталог с текущим обрабатываемым файлом.
|
|
1277
|
+
const requireAbsolutePath = path.resolve(chunkDir, match[1]);
|
|
1278
|
+
// Если абсолютный путь начинается с каталога модулей...
|
|
1279
|
+
if (requireAbsolutePath.startsWith(modulesDir)) {
|
|
1280
|
+
const parsed = path.parse(path.relative(modulesDir, requireAbsolutePath));
|
|
1281
|
+
const rebased = path
|
|
1282
|
+
.join(parsed.dir, parsed.name)
|
|
1283
|
+
.replaceAll(path.sep, path.posix.sep);
|
|
1284
|
+
// Удаляет расширение файла для полного соответствия
|
|
1285
|
+
// принципам импорта модулей wb-rules.
|
|
1286
|
+
const replacement = match[0]
|
|
1287
|
+
.replace(match[1], rebased);
|
|
1288
|
+
magicString.overwrite(start, end, replacement);
|
|
1289
|
+
// Признак произведённой замены.
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
// Строит полный список конструкций require
|
|
1294
|
+
// в обрабатываемом файле.
|
|
1295
|
+
const matches = [...code.matchAll(patternRequire)];
|
|
1296
|
+
for (const match of matches) {
|
|
1297
|
+
if (rebaseRequire(match))
|
|
1298
|
+
hasReplacements = true;
|
|
1299
|
+
}
|
|
1300
|
+
// Если никаких замен не было, то файл пропускается.
|
|
1301
|
+
if (!hasReplacements)
|
|
1302
|
+
return null;
|
|
1303
|
+
return {
|
|
1304
|
+
code: magicString.toString(),
|
|
1305
|
+
};
|
|
1306
|
+
},
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
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
|
|
1349
|
+
*
|
|
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
|
+
* Поиск пакета по имени чанка
|
|
1378
|
+
*
|
|
1379
|
+
* @param context - Контекст монорепозитория
|
|
1380
|
+
* @param chunkName - Имя чанка (обычно путь к файлу)
|
|
1381
|
+
* @returns PackageDefinition | undefined - Найденный пакет или undefined
|
|
1382
|
+
*
|
|
1383
|
+
* @since 0.3.5
|
|
1384
|
+
*
|
|
1385
|
+
**/
|
|
1386
|
+
function findMonorepoPackageByChunkName(context, chunkName) {
|
|
1387
|
+
for (const pkg of context.packages) {
|
|
1388
|
+
if (chunkName.startsWith(pkg.workspacePath)) {
|
|
1389
|
+
return pkg;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Преобразует путь к чанку в путь внутри node_modules
|
|
1395
|
+
*
|
|
1396
|
+
* @param chunkName - Полный путь к чанку
|
|
1397
|
+
* @param pkgDefinition - Определение пакета
|
|
1398
|
+
* @returns Строка в формате node_modules/<package-name>/<relative-path>
|
|
1399
|
+
* @throws {WorkspaceError} Если имя пакета не указано
|
|
1400
|
+
*
|
|
1401
|
+
* @since 0.3.5
|
|
1402
|
+
*
|
|
1403
|
+
**/
|
|
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));
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const packagesPattern = /(.*)node_modules[\\/]@?(.+)[\\/](.+)?/;
|
|
1411
|
+
const entryMatchers = {
|
|
1412
|
+
'wb-rules': /(?:src[\\/])?wb-rules[\\/](.*)/,
|
|
1413
|
+
'wb-rules-modules': /(?:src[\\/])?wb-rules-modules[\\/](.*)/,
|
|
1414
|
+
};
|
|
1415
|
+
/**
|
|
1416
|
+
* Парсит путь к исходному файлу и возвращает имя модуля формата `wb-rules-modules/...`.
|
|
1417
|
+
* Используется для обработки путей внутри node_modules.
|
|
1418
|
+
*
|
|
1419
|
+
* @param sourcePath - путь к исходному файлу
|
|
1420
|
+
* @returns Строка с именем модуля или undefined
|
|
1421
|
+
*
|
|
1422
|
+
* @since 0.3.2
|
|
1423
|
+
*
|
|
1424
|
+
**/
|
|
1425
|
+
function tryGetPackageEntryPath(sourcePath) {
|
|
1426
|
+
sourcePath = sourcePath.replaceAll(nodePath.sep, nodePath.posix.sep);
|
|
1427
|
+
const pathParts = [];
|
|
1428
|
+
do {
|
|
1429
|
+
const match = packagesPattern.exec(sourcePath);
|
|
1430
|
+
if (!match)
|
|
1431
|
+
break;
|
|
1432
|
+
if (match[3])
|
|
1433
|
+
pathParts.unshift(match[3]);
|
|
1434
|
+
pathParts.unshift('packages/' + match[2].replace(/\/dist$/, ''));
|
|
1435
|
+
sourcePath = match[1];
|
|
1436
|
+
} while (sourcePath);
|
|
1437
|
+
if (pathParts.length)
|
|
1438
|
+
return `wb-rules-modules/${pathParts.join('/')}.js`;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Определяет имя входного файла для типов `wb-rules` и `wb-rules-modules`.
|
|
1442
|
+
*
|
|
1443
|
+
* @param sourcePath - путь к исходному файлу
|
|
1444
|
+
* @param type - тип модуля (wb-rules или wb-rules-modules)
|
|
1445
|
+
* @returns Строка с именем файла или undefined
|
|
1446
|
+
*
|
|
1447
|
+
* @since 0.3.5
|
|
1448
|
+
*
|
|
1449
|
+
**/
|
|
1450
|
+
function tryGetEntryPath(sourcePath, type) {
|
|
1451
|
+
const match = entryMatchers[type].exec(sourcePath);
|
|
1452
|
+
if (!match)
|
|
1453
|
+
return;
|
|
1454
|
+
return `${type}/${match[1]}.js`;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Проверяет различные сценарии и возвращает корректный путь выходного файла.
|
|
1458
|
+
*
|
|
1459
|
+
* @param filePath - Исходный путь к файлу.
|
|
1460
|
+
* @returns Строка с корректным путём.
|
|
1461
|
+
*
|
|
1462
|
+
* @since 0.3.0
|
|
1463
|
+
*
|
|
1464
|
+
**/
|
|
1465
|
+
function getEntryPath(filePath) {
|
|
1466
|
+
if (filePath.startsWith('_virtual'))
|
|
1467
|
+
return filePath;
|
|
1468
|
+
return tryGetPackageEntryPath(filePath)
|
|
1469
|
+
?? tryGetEntryPath(filePath, 'wb-rules-modules')
|
|
1470
|
+
?? tryGetEntryPath(filePath, 'wb-rules')
|
|
1471
|
+
// None of the above matched.
|
|
1472
|
+
?? filePath;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const env = process.env.NODE_ENV;
|
|
1476
|
+
const isProduction = env === 'production';
|
|
1477
|
+
const outputDir = {
|
|
1478
|
+
es5: 'dist/es5',
|
|
1479
|
+
};
|
|
1480
|
+
/**
|
|
1481
|
+
* Основная функция, возвращающая конфигурацию Rollup.
|
|
1482
|
+
* Обрабатывает входные файлы, плагины и настройку выходных путей.
|
|
1483
|
+
*
|
|
1484
|
+
* @param options - опции конфигурации
|
|
1485
|
+
* @returns Объект RollupOptions
|
|
1486
|
+
*
|
|
1487
|
+
* @since 0.3.0
|
|
1488
|
+
*
|
|
1489
|
+
**/
|
|
1490
|
+
async function defineRuntimeConfig(options = {}) {
|
|
1491
|
+
const { cwd = process.cwd(), external, dotenv: dotenvOptions = {}, plugins = [], } = options;
|
|
1492
|
+
const monorepoContext = await getMonorepoContextAsync(cwd);
|
|
1493
|
+
const defaultPlugins = [
|
|
1494
|
+
// Очистка директории dist перед сборкой
|
|
1495
|
+
del({
|
|
1496
|
+
targets: 'dist/*',
|
|
1497
|
+
}),
|
|
1498
|
+
// Поддержка множественных входных файлов
|
|
1499
|
+
multi({
|
|
1500
|
+
exclude: ['src/wb-rules/*.disabled.[jt]s'],
|
|
1501
|
+
preserveModules: true,
|
|
1502
|
+
}),
|
|
1503
|
+
// Поиск зависимостей в node_modules
|
|
1504
|
+
nodeResolve(),
|
|
1505
|
+
// Транспиляция TypeScript
|
|
1506
|
+
ts$2({ clean: true }),
|
|
1507
|
+
// Обработка импортов для wb-rules
|
|
1508
|
+
wbRulesImports(),
|
|
1509
|
+
// Загрузка переменных окружения
|
|
1510
|
+
dotenv(dotenvOptions),
|
|
1511
|
+
// Замена условных флагов в коде
|
|
1512
|
+
replace({
|
|
1513
|
+
preventAssignment: true,
|
|
1514
|
+
values: {
|
|
1515
|
+
__DEV__: JSON.stringify(!isProduction),
|
|
1516
|
+
// Автоматически меняется в процессе тестирования
|
|
1517
|
+
__TEST__: 'false',
|
|
1518
|
+
},
|
|
1519
|
+
}),
|
|
1520
|
+
// Транспиляция через Babel
|
|
1521
|
+
getBabelOutputPlugin({
|
|
1522
|
+
presets: ['@babel/preset-env'],
|
|
1523
|
+
plugins: [
|
|
1524
|
+
'@babel/plugin-transform-spread',
|
|
1525
|
+
'array-includes',
|
|
1526
|
+
],
|
|
1527
|
+
}),
|
|
1528
|
+
// Очистка виртуальных файлов после сборки
|
|
1529
|
+
del({
|
|
1530
|
+
targets: 'dist/*/_virtual',
|
|
1531
|
+
hook: 'closeBundle',
|
|
1532
|
+
}),
|
|
1533
|
+
];
|
|
1534
|
+
return {
|
|
1535
|
+
input: 'src/wb-rules/*.[jt]s',
|
|
1536
|
+
external,
|
|
1537
|
+
plugins: [
|
|
1538
|
+
...defaultPlugins,
|
|
1539
|
+
...plugins,
|
|
1540
|
+
],
|
|
1541
|
+
output: {
|
|
1542
|
+
format: 'cjs',
|
|
1543
|
+
strict: false,
|
|
1544
|
+
dir: outputDir.es5,
|
|
1545
|
+
preserveModules: true,
|
|
1546
|
+
entryFileNames(chunkInfo) {
|
|
1547
|
+
let chunkName = chunkInfo.name;
|
|
1548
|
+
// Адаптация путей при сборке в монорепозитории.
|
|
1549
|
+
if (monorepoContext) {
|
|
1550
|
+
const { rootDir } = monorepoContext;
|
|
1551
|
+
const absolutePath = nodePath.resolve(rootDir, chunkInfo.name);
|
|
1552
|
+
if (absolutePath.startsWith(cwd)) {
|
|
1553
|
+
// Путь в текущем проекте, не требует встраивания отдельным пакетом.
|
|
1554
|
+
chunkName = nodePath
|
|
1555
|
+
.relative(cwd, absolutePath)
|
|
1556
|
+
.replaceAll(nodePath.sep, nodePath.posix.sep);
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
// Ищем пакет монорепозитория, в котором находится указанный путь.
|
|
1560
|
+
const pkgDefinition = findMonorepoPackageByChunkName(monorepoContext, chunkName);
|
|
1561
|
+
if (pkgDefinition)
|
|
1562
|
+
chunkName = mapChunkToPackage(chunkName, pkgDefinition);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
return getEntryPath(chunkName);
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
export { defineRuntimeConfig as a, definePackageConfig as d };
|