tsnv 0.0.0-dev.20260201114010 → 0.0.0-dev.20260203190732

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/README.md CHANGED
@@ -16,7 +16,7 @@ Modern build toolkit for React Native libraries<br>
16
16
 
17
17
  - **Fast** - Powered by [Rolldown](https://rolldown.rs), a Rust-based bundler
18
18
  - **Platform-aware** - Automatic handling of platform-specific modules (`.android.ts`, `.ios.ts`, `.native.ts`)
19
- - **Dual format** - Supports both CommonJS and ESM output
19
+ - **Assets** - Built-in support for assets (images, fonts, and other static files)
20
20
  - **TypeScript** - First-class TypeScript support with automatic `.d.ts` generation
21
21
  - **Zero-config** - Sensible defaults that just work
22
22
  - **Yarn PnP** - Works seamlessly with Yarn Plug'n'Play
@@ -46,7 +46,6 @@ That's it. tsnv works out of the box with sensible defaults:
46
46
 
47
47
  - Source directory: `src`
48
48
  - Output directory: `dist`
49
- - Format: ESM
50
49
  - TypeScript declarations: enabled
51
50
 
52
51
  ### Custom Configuration (Optional)
@@ -57,7 +56,6 @@ If you need to customize the build, create a `tsnv.config.ts`:
57
56
  import { defineConfig } from 'tsnv';
58
57
 
59
58
  export default defineConfig({
60
- format: ['esm', 'cjs'],
61
59
  sourcemap: true,
62
60
  });
63
61
  ```
@@ -76,9 +74,6 @@ export default defineConfig({
76
74
  // Output directory
77
75
  outDir: 'dist',
78
76
 
79
- // Output format: 'esm', 'cjs', or ['esm', 'cjs']
80
- format: 'esm',
81
-
82
77
  // Generate TypeScript declaration files
83
78
  dts: true,
84
79
 
@@ -91,6 +86,9 @@ export default defineConfig({
91
86
  // Asset file extensions (Metro defaults)
92
87
  assetExtensions: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'psd', 'svg', 'webp' /* ... */],
93
88
 
89
+ // The directory where asset files will be written.
90
+ assetsDir: '_assets',
91
+
94
92
  // Files to exclude from the build
95
93
  exclude: /__(?:tests?|fixtures?|mocks?)__/,
96
94
 
@@ -115,45 +113,20 @@ export default defineConfig({
115
113
 
116
114
  ## Output Structure
117
115
 
118
- ### ESM only (`format: 'esm'`)
119
-
120
- ```
121
- dist/
122
- ├── index.js
123
- ├── greeting.android.js
124
- ├── greeting.ios.js
125
- └── types/
126
- ├── index.d.ts
127
- └── greeting.d.ts
128
- ```
129
-
130
- ### CommonJS only (`format: 'cjs'`)
131
-
132
116
  ```
133
117
  dist/
118
+ │ # JavaScript
134
119
  ├── index.js
135
120
  ├── greeting.android.js
136
121
  ├── greeting.ios.js
137
- └── types/
138
- ├── index.d.ts
139
- └── greeting.d.ts
140
- ```
141
-
142
- ### Dual format (`format: ['esm', 'cjs']`)
143
-
144
- ```
145
- dist/
146
- ├── esm/
147
- │ ├── index.js
148
- │ ├── greeting.android.js
149
- │ └── greeting.ios.js
150
- ├── cjs/
151
- │ ├── index.js
152
- │ ├── greeting.android.js
153
- │ └── greeting.ios.js
154
- └── types/
155
- ├── index.d.ts
156
- └── greeting.d.ts
122
+
123
+ │ # Types
124
+ ├── index.d.ts
125
+ ├── greeting.d.ts
126
+
127
+ │ # Assets
128
+ └── _assets/
129
+ └── (files)
157
130
  ```
158
131
 
159
132
  ## License
package/dist/config.d.cts CHANGED
@@ -1,9 +1,6 @@
1
1
  import { TsConfigJson } from "get-tsconfig";
2
2
  import { OutputOptions } from "rolldown";
3
3
 
4
- //#region src/types.d.ts
5
- type Format = 'esm' | 'cjs';
6
- //#endregion
7
4
  //#region src/config/types.d.ts
8
5
  interface Config {
9
6
  /**
@@ -22,12 +19,6 @@ interface Config {
22
19
  * Defaults to `/__(?:tests?|fixtures?|mocks?)__/`
23
20
  */
24
21
  exclude?: RegExp;
25
- /**
26
- * Expected format of generated code.
27
- *
28
- * Defaults to `'esm'`
29
- */
30
- format?: Format | Format[];
31
22
  /**
32
23
  * Specifiers to resolve platform specific modules.
33
24
  *
@@ -46,6 +37,12 @@ interface Config {
46
37
  * Default to following extensions: [Metro's default asset extensions](https://github.com/facebook/metro/blob/v0.83.3/packages/metro-config/src/defaults/defaults.js)
47
38
  */
48
39
  assetExtensions?: string[];
40
+ /**
41
+ * The directory where asset files will be written.
42
+ *
43
+ * Defaults to `'_assets'`
44
+ */
45
+ assetsDir?: string;
49
46
  /**
50
47
  * Enables generation of TypeScript declaration files (.d.ts).
51
48
  *
package/dist/config.d.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  import { TsConfigJson } from "get-tsconfig";
2
2
  import { OutputOptions } from "rolldown";
3
3
 
4
- //#region src/types.d.ts
5
- type Format = 'esm' | 'cjs';
6
- //#endregion
7
4
  //#region src/config/types.d.ts
8
5
  interface Config {
9
6
  /**
@@ -22,12 +19,6 @@ interface Config {
22
19
  * Defaults to `/__(?:tests?|fixtures?|mocks?)__/`
23
20
  */
24
21
  exclude?: RegExp;
25
- /**
26
- * Expected format of generated code.
27
- *
28
- * Defaults to `'esm'`
29
- */
30
- format?: Format | Format[];
31
22
  /**
32
23
  * Specifiers to resolve platform specific modules.
33
24
  *
@@ -46,6 +37,12 @@ interface Config {
46
37
  * Default to following extensions: [Metro's default asset extensions](https://github.com/facebook/metro/blob/v0.83.3/packages/metro-config/src/defaults/defaults.js)
47
38
  */
48
39
  assetExtensions?: string[];
40
+ /**
41
+ * The directory where asset files will be written.
42
+ *
43
+ * Defaults to `'_assets'`
44
+ */
45
+ assetsDir?: string;
49
46
  /**
50
47
  * Enables generation of TypeScript declaration files (.d.ts).
51
48
  *
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ import pc from "picocolors";
4
4
  import { VERSION, build } from "rolldown";
5
5
  import { createDebug } from "obug";
6
6
  import fs, { globSync } from "node:fs";
7
+ import path$1 from "node:path";
7
8
  import * as pkg from "empathic/package";
8
9
  import { assert } from "es-toolkit";
9
- import path$1 from "node:path";
10
10
  import { dts } from "rolldown-plugin-dts";
11
11
  import { promisify } from "node:util";
12
12
  import { brotliCompress, gzip } from "node:zlib";
@@ -20,7 +20,6 @@ const DEFAULT_CONFIG = {
20
20
  source: "src",
21
21
  outDir: "dist",
22
22
  exclude: /__(?:tests?|fixtures?|mocks?)__/,
23
- format: "esm",
24
23
  specifiers: [
25
24
  "android",
26
25
  "ios",
@@ -63,29 +62,24 @@ const DEFAULT_CONFIG = {
63
62
  "ttf",
64
63
  "zip"
65
64
  ],
65
+ assetsDir: "_assets",
66
66
  dts: true,
67
67
  clean: true
68
68
  };
69
69
 
70
70
  //#endregion
71
71
  //#region src/context.ts
72
- async function resolveContext(cwd) {
72
+ async function resolveContext(cwd, config) {
73
73
  const packageJsonPath = pkg.up({ cwd });
74
74
  assert(packageJsonPath, "could not find package.json");
75
75
  const rawPackageJson = await fs.promises.readFile(packageJsonPath, "utf-8");
76
- const packageJson = JSON.parse(rawPackageJson);
77
76
  return {
78
77
  cwd,
79
- packageJson,
80
- packageType: resolvePackageType(packageJson)
78
+ packageJson: JSON.parse(rawPackageJson),
79
+ outdir: path$1.resolve(cwd, config.outDir),
80
+ source: path$1.resolve(cwd, config.source)
81
81
  };
82
82
  }
83
- function resolvePackageType(packageJson) {
84
- switch (packageJson.type) {
85
- case "module": return "esm";
86
- default: return "cjs";
87
- }
88
- }
89
83
 
90
84
  //#endregion
91
85
  //#region src/utils/path.ts
@@ -112,7 +106,7 @@ hasPlatformSpecificModule.cache = /* @__PURE__ */ new Map();
112
106
  * In the case of platform-specific modules, a prefix such as `android.js` or `ios.js` is added before the module name.
113
107
  * If the standard module specification, which requires the full file path to be specified, is followed, platform-specific modules cannot be found.
114
108
  *
115
- * Therefore, the `.js` extension is used regardless of whether the module is ESM or CJS.
109
+ * Therefore, the `.js` extension is used.
116
110
  */
117
111
  function resolveFilename() {
118
112
  return `[name].js`;
@@ -139,6 +133,112 @@ function getUniquePlatformSpecificFiles(files, extensions, specifiers) {
139
133
  return result;
140
134
  }
141
135
 
136
+ //#endregion
137
+ //#region src/rolldown/plugins/block-require.ts
138
+ function blockRequire() {
139
+ return {
140
+ name: "tsnv:block-require",
141
+ resolveId(id, importer, extraOptions) {
142
+ if (extraOptions.kind === "require-call") throw new Error([
143
+ "CommonJS require call expressions are not allowed.",
144
+ "Please use import statements instead.",
145
+ "",
146
+ `require('${id}') at '${importer ?? "<unknown file>"}'`
147
+ ].join("\n"));
148
+ }
149
+ };
150
+ }
151
+
152
+ //#endregion
153
+ //#region src/utils/asset.ts
154
+ const assets = /* @__PURE__ */ new Map();
155
+ function addAsset(key, value) {
156
+ assets.set(key, value);
157
+ }
158
+ function flushAssets(context) {
159
+ let count = 0;
160
+ const label = pc.yellow("[AST]");
161
+ for (const [key, value] of assets.entries()) {
162
+ const destination = path$1.join(context.outdir, value);
163
+ const dirname = path$1.dirname(destination);
164
+ const filename = path$1.basename(destination);
165
+ if (!fs.existsSync(dirname)) fs.mkdirSync(dirname, { recursive: true });
166
+ fs.copyFileSync(key, destination);
167
+ console.log(label, pc.dim(path$1.relative(context.cwd, dirname) + path$1.sep) + pc.yellow(filename));
168
+ count++;
169
+ }
170
+ console.log(label, `${count} files`);
171
+ assets.clear();
172
+ }
173
+ const SCALE_PATTERN = "@(\\d+\\.?\\d*)x";
174
+ function collectAssets(context, assetPath) {
175
+ const dirname = path$1.dirname(assetPath);
176
+ const extension = path$1.extname(assetPath);
177
+ const baseName = stripAllSuffixes(context, path$1.basename(assetPath, extension));
178
+ const platformPattern = context.config.specifiers.map((p) => `\\.${p}`).join("|");
179
+ const assetRegExp = new RegExp(`^${escapeRegExp(baseName)}(${SCALE_PATTERN})?(${platformPattern})?${escapeRegExp(extension)}$`);
180
+ const files = fs.readdirSync(dirname, { withFileTypes: true });
181
+ const matchedFiles = [];
182
+ for (const file of files) if (file.isFile() && assetRegExp.test(file.name)) matchedFiles.push(path$1.join(dirname, file.name));
183
+ return {
184
+ baseName,
185
+ extension,
186
+ files: matchedFiles
187
+ };
188
+ }
189
+ function stripAllSuffixes(context, basename) {
190
+ const platformPattern = context.config.specifiers.map((p) => `\\.${p}`).join("|");
191
+ return basename.replace(new RegExp(`(${SCALE_PATTERN})?(${platformPattern})?$`), "");
192
+ }
193
+ function escapeRegExp(str) {
194
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
195
+ }
196
+
197
+ //#endregion
198
+ //#region src/rolldown/plugins/collect-asset.ts
199
+ function collectAsset(context) {
200
+ return {
201
+ name: "tsnv:collect-asset",
202
+ resolveId(id, importer) {
203
+ const extname = path$1.extname(id).slice(1);
204
+ if (extname) {
205
+ if (context.config.assetExtensions.includes(extname)) {
206
+ const resolveDir = importer ? path$1.dirname(importer) : context.cwd;
207
+ const assetPath = path$1.resolve(resolveDir, id);
208
+ const collectedAssets = collectAssets(context, assetPath);
209
+ this.debug(`Found asset: ${id} (at: ${importer ?? "<unknown>"})`);
210
+ let virtualDir = "";
211
+ for (const file of collectedAssets.files) {
212
+ const virtualPath = resolveVirtualAssetPath(context, file);
213
+ if (!virtualDir) virtualDir = path$1.dirname(virtualPath);
214
+ addAsset(file, virtualPath);
215
+ }
216
+ if (collectedAssets.files.length === 0) {
217
+ this.warn(`No assets found for ${id}`);
218
+ return {
219
+ id,
220
+ external: true
221
+ };
222
+ } else {
223
+ assert(virtualDir, "virtual asset directory not found");
224
+ const basename = path$1.basename(assetPath);
225
+ const toRootRelativePath = path$1.relative(resolveDir, context.source);
226
+ const virtualAssetPath = path$1.join(toRootRelativePath, virtualDir, basename);
227
+ return {
228
+ id: virtualAssetPath.startsWith(".") ? virtualAssetPath : `.${path$1.sep}${virtualAssetPath}`,
229
+ external: true
230
+ };
231
+ }
232
+ }
233
+ }
234
+ }
235
+ };
236
+ }
237
+ function resolveVirtualAssetPath(context, asset) {
238
+ const assetRelativePath = path$1.relative(context.cwd, asset);
239
+ return path$1.join(context.config.assetsDir, assetRelativePath);
240
+ }
241
+
142
242
  //#endregion
143
243
  //#region src/rolldown/plugins/dts.ts
144
244
  const debug$2 = createDebug("tsnv:dts");
@@ -241,7 +341,6 @@ function report(options) {
241
341
  const formatLabel = (() => {
242
342
  switch (format) {
243
343
  case "esm": return pc.blue(`[ESM]`);
244
- case "cjs": return pc.yellow(`[CJS]`);
245
344
  case "dts": return pc.green(`[DTS]`);
246
345
  }
247
346
  })();
@@ -275,6 +374,7 @@ function resolveBuildOptions(context, options) {
275
374
  };
276
375
  const baseOptions = {
277
376
  input: options.files,
377
+ transform: { jsx: "react-jsx" },
278
378
  output: {
279
379
  banner: options.config.banner,
280
380
  footer: options.config.footer,
@@ -282,37 +382,31 @@ function resolveBuildOptions(context, options) {
282
382
  outro: options.config.outro,
283
383
  sourcemap: options.config.sourcemap,
284
384
  preserveModulesRoot: options.config.source,
285
- preserveModules: true
385
+ cleanDir: options.config.clean,
386
+ preserveModules: true,
387
+ polyfillRequire: false
286
388
  }
287
389
  };
288
- let formats;
289
- if (Array.isArray(options.config.format)) formats = options.config.format;
290
- else formats = [options.config.format];
291
- const uniqueFormats = Array.from(new Set(formats));
292
- const isSingleFormat = uniqueFormats.length === 1;
293
390
  const filename = resolveFilename();
294
- const resolvedBuildOptions = uniqueFormats.map((format) => {
295
- return {
296
- ...baseOptions,
297
- plugins: [external(pluginContext), report({
298
- cwd: options.cwd,
299
- format
300
- })],
301
- output: {
302
- ...baseOptions.output,
303
- dir: isSingleFormat ? options.config.outDir : path$1.join(options.config.outDir, format),
304
- cleanDir: options.config.clean,
305
- format,
306
- entryFileNames: filename,
307
- chunkFileNames: filename
308
- }
309
- };
310
- });
391
+ const resolvedBuildOptions = [{
392
+ ...baseOptions,
393
+ plugins: [...getBasePlugins(pluginContext), report({
394
+ cwd: options.cwd,
395
+ format: "esm"
396
+ })],
397
+ output: {
398
+ ...baseOptions.output,
399
+ format: "esm",
400
+ dir: options.config.outDir,
401
+ entryFileNames: filename,
402
+ chunkFileNames: filename
403
+ }
404
+ }];
311
405
  if (options.config.dts) resolvedBuildOptions.push({
312
406
  ...baseOptions,
313
407
  input: getUniquePlatformSpecificFiles(options.files, options.config.sourceExtensions, options.config.specifiers),
314
408
  plugins: [
315
- external(pluginContext),
409
+ ...getBasePlugins(pluginContext),
316
410
  report({
317
411
  cwd: options.cwd,
318
412
  format: "dts"
@@ -321,8 +415,8 @@ function resolveBuildOptions(context, options) {
321
415
  ],
322
416
  output: {
323
417
  ...baseOptions.output,
324
- cleanDir: options.config.clean,
325
- dir: path$1.join(options.config.outDir, "types"),
418
+ cleanDir: false,
419
+ dir: options.config.outDir,
326
420
  format: "esm",
327
421
  entryFileNames: filename,
328
422
  chunkFileNames: filename
@@ -330,6 +424,13 @@ function resolveBuildOptions(context, options) {
330
424
  });
331
425
  return resolvedBuildOptions;
332
426
  }
427
+ function getBasePlugins(pluginContext) {
428
+ return [
429
+ blockRequire(),
430
+ collectAsset(pluginContext),
431
+ external(pluginContext)
432
+ ];
433
+ }
333
434
 
334
435
  //#endregion
335
436
  //#region src/rolldown/index.ts
@@ -340,9 +441,27 @@ async function build$1(context, options) {
340
441
  for (const buildOption of buildOptions) await build(buildOption);
341
442
  }
342
443
 
444
+ //#endregion
445
+ //#region src/utils/log.ts
446
+ function withBoundary(title, text) {
447
+ return [
448
+ `╭─ ${title}`,
449
+ ...text.split("\n").map((line) => {
450
+ return `│ ${line}`;
451
+ }),
452
+ "╰─ ·"
453
+ ].join("\n");
454
+ }
455
+
456
+ //#endregion
457
+ //#region src/utils/rolldown.ts
458
+ function getBindingErrors(reason) {
459
+ if (reason instanceof Error) return reason.errors;
460
+ }
461
+
343
462
  //#endregion
344
463
  //#region src/index.ts
345
- const version = `v0.0.0-dev.20260201114010`;
464
+ const version = `v0.0.0-dev.20260203190732`;
346
465
  async function main() {
347
466
  console.log(`tsnv ${pc.dim(version)} powered by rolldown ${pc.dim(VERSION)}`);
348
467
  debug$3("Loading config...");
@@ -355,7 +474,7 @@ async function main() {
355
474
  debug$3("Config loaded", config);
356
475
  console.log(`Config File: ${pc.underline(configFile)}`);
357
476
  console.log(`Source Path: ${pc.blue(path.resolve(config.source))}`);
358
- const context = await resolveContext(process.cwd());
477
+ const context = await resolveContext(cwd, config);
359
478
  debug$3("Resolved context", context);
360
479
  const files = await collectFiles(config);
361
480
  console.log(`Collected files: ${pc.dim(files.length)}`);
@@ -367,13 +486,17 @@ async function main() {
367
486
  config
368
487
  });
369
488
  const endedAt = performance.now();
489
+ flushAssets(context);
370
490
  const duration = `${Math.floor(endedAt - startedAt)}ms`;
371
491
  console.log(`Build completed in ${pc.green(duration)}`);
372
492
  }
373
493
  await main().catch((reason) => {
374
- console.error(pc.red(`Build failed`));
375
- console.error();
376
- console.error(reason);
494
+ const errors = getBindingErrors(reason) ?? [reason];
495
+ console.error("");
496
+ errors.forEach((error, index) => {
497
+ console.error(withBoundary(pc.red(`Error #${index + 1}`), error.message) + "\n");
498
+ });
499
+ console.error(pc.red(`Build failed with ${errors.length} error${errors.length > 1 ? "s" : ""}`));
377
500
  process.exit(1);
378
501
  });
379
502
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsnv",
3
- "version": "0.0.0-dev.20260201114010",
3
+ "version": "0.0.0-dev.20260203190732",
4
4
  "description": "Modern build toolkit for React Native libraries",
5
5
  "license": "MIT",
6
6
  "author": "Geunhyeok Lee <dev.ghlee@gmail.com>",