tsnv 0.0.0-dev.20260119180902 → 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
@@ -2,7 +2,9 @@
2
2
 
3
3
  # tsnv
4
4
 
5
- Modern bundler for React Native libraries - fast, platform-aware, zero-config<br>
5
+ <img alt="preview" src="./preview.png">
6
+
7
+ Modern build toolkit for React Native libraries<br>
6
8
  (powered by [Rolldown](https://rolldown.rs))
7
9
 
8
10
  </div>
@@ -14,7 +16,7 @@ Modern bundler for React Native libraries - fast, platform-aware, zero-config<br
14
16
 
15
17
  - **Fast** - Powered by [Rolldown](https://rolldown.rs), a Rust-based bundler
16
18
  - **Platform-aware** - Automatic handling of platform-specific modules (`.android.ts`, `.ios.ts`, `.native.ts`)
17
- - **Dual format** - Supports both CommonJS and ESM output
19
+ - **Assets** - Built-in support for assets (images, fonts, and other static files)
18
20
  - **TypeScript** - First-class TypeScript support with automatic `.d.ts` generation
19
21
  - **Zero-config** - Sensible defaults that just work
20
22
  - **Yarn PnP** - Works seamlessly with Yarn Plug'n'Play
@@ -44,7 +46,6 @@ That's it. tsnv works out of the box with sensible defaults:
44
46
 
45
47
  - Source directory: `src`
46
48
  - Output directory: `dist`
47
- - Format: ESM
48
49
  - TypeScript declarations: enabled
49
50
 
50
51
  ### Custom Configuration (Optional)
@@ -55,7 +56,6 @@ If you need to customize the build, create a `tsnv.config.ts`:
55
56
  import { defineConfig } from 'tsnv';
56
57
 
57
58
  export default defineConfig({
58
- format: ['esm', 'cjs'],
59
59
  sourcemap: true,
60
60
  });
61
61
  ```
@@ -74,9 +74,6 @@ export default defineConfig({
74
74
  // Output directory
75
75
  outDir: 'dist',
76
76
 
77
- // Output format: 'esm', 'cjs', or ['esm', 'cjs']
78
- format: 'esm',
79
-
80
77
  // Generate TypeScript declaration files
81
78
  dts: true,
82
79
 
@@ -89,6 +86,9 @@ export default defineConfig({
89
86
  // Asset file extensions (Metro defaults)
90
87
  assetExtensions: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'psd', 'svg', 'webp' /* ... */],
91
88
 
89
+ // The directory where asset files will be written.
90
+ assetsDir: '_assets',
91
+
92
92
  // Files to exclude from the build
93
93
  exclude: /__(?:tests?|fixtures?|mocks?)__/,
94
94
 
@@ -113,48 +113,20 @@ export default defineConfig({
113
113
 
114
114
  ## Output Structure
115
115
 
116
- ### ESM only (`format: 'esm'`)
117
-
118
116
  ```
119
117
  dist/
118
+ │ # JavaScript
120
119
  ├── index.js
121
120
  ├── greeting.android.js
122
121
  ├── greeting.ios.js
123
- └── types/
124
- ├── index.d.ts
125
- ├── greeting.android.d.ts
126
- └── greeting.ios.d.ts
127
- ```
128
-
129
- ### CommonJS only (`format: 'cjs'`)
130
-
131
- ```
132
- dist/
133
- ├── index.js
134
- ├── greeting.android.js
135
- ├── greeting.ios.js
136
- └── types/
137
- ├── index.d.ts
138
- ├── greeting.android.d.ts
139
- └── greeting.ios.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.android.d.ts
157
- └── greeting.ios.d.ts
122
+
123
+ │ # Types
124
+ ├── index.d.ts
125
+ ├── greeting.d.ts
126
+
127
+ │ # Assets
128
+ └── _assets/
129
+ └── (files)
158
130
  ```
159
131
 
160
132
  ## License
package/dist/config.d.cts CHANGED
@@ -1,93 +1,106 @@
1
+ import { TsConfigJson } from "get-tsconfig";
1
2
  import { OutputOptions } from "rolldown";
2
3
 
3
- //#region src/types.d.ts
4
- type Format = 'esm' | 'cjs';
5
- //#endregion
6
4
  //#region src/config/types.d.ts
7
5
  interface Config {
8
6
  /**
9
- * Defaults to `'src'`
10
- */
7
+ * Defaults to `'src'`
8
+ */
11
9
  source?: string;
12
10
  /**
13
- * The directory where output files will be written.
14
- *
15
- * Defaults to `'dist'`
16
- */
11
+ * The directory where output files will be written.
12
+ *
13
+ * Defaults to `'dist'`
14
+ */
17
15
  outDir?: string;
18
16
  /**
19
- * Files to exclude from the build.
20
- *
21
- * Defaults to `/__(?:tests?|fixtures?|mocks?)__/`
22
- */
17
+ * Files to exclude from the build.
18
+ *
19
+ * Defaults to `/__(?:tests?|fixtures?|mocks?)__/`
20
+ */
23
21
  exclude?: RegExp;
24
22
  /**
25
- * Expected format of generated code.
26
- *
27
- * Defaults to `'esm'`
28
- */
29
- format?: Format | Format[];
30
- /**
31
- * Specifiers to resolve platform specific modules.
32
- *
33
- * Defaults to `['android', 'ios', 'native']`
34
- */
23
+ * Specifiers to resolve platform specific modules.
24
+ *
25
+ * Defaults to `['android', 'ios', 'native']`
26
+ */
35
27
  specifiers?: string[];
36
28
  /**
37
- * Source files extensions.
38
- *
39
- * Defaults to `['ts', 'tsx', 'js', 'jsx', 'json']`
40
- */
29
+ * Source files extensions.
30
+ *
31
+ * Defaults to `['ts', 'tsx', 'js', 'jsx', 'json']`
32
+ */
41
33
  sourceExtensions?: string[];
42
34
  /**
43
- * Asset files extensions.
44
- *
45
- * 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)
46
- */
35
+ * Asset files extensions.
36
+ *
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)
38
+ */
47
39
  assetExtensions?: string[];
48
40
  /**
49
- * Enables generation of TypeScript declaration files (.d.ts).
50
- *
51
- * Defaults to `true`
52
- */
41
+ * The directory where asset files will be written.
42
+ *
43
+ * Defaults to `'_assets'`
44
+ */
45
+ assetsDir?: string;
46
+ /**
47
+ * Enables generation of TypeScript declaration files (.d.ts).
48
+ *
49
+ * Defaults to `true`
50
+ */
53
51
  dts?: boolean;
54
52
  /**
55
- * Generate source map files.
56
- */
53
+ * Generate source map files.
54
+ */
57
55
  sourcemap?: OutputOptions['sourcemap'];
58
56
  /**
59
- * Code to prepend to the beginning of each output chunk.
60
- */
57
+ * Code to prepend to the beginning of each output chunk.
58
+ */
61
59
  banner?: OutputOptions['banner'];
62
60
  /**
63
- * Code to append to the end of each output chunk.
64
- */
61
+ * Code to append to the end of each output chunk.
62
+ */
65
63
  footer?: OutputOptions['footer'];
66
64
  /**
67
- * Code to prepend inside the wrapper function (after banner, before actual code).
68
- */
65
+ * Code to prepend inside the wrapper function (after banner, before actual code).
66
+ */
69
67
  intro?: OutputOptions['intro'];
70
68
  /**
71
- * Code to append inside the wrapper function (after actual code, before footer).
72
- */
69
+ * Code to append inside the wrapper function (after actual code, before footer).
70
+ */
73
71
  outro?: OutputOptions['outro'];
74
72
  /**
75
- * Clean output directory before build.
76
- *
77
- * Defaults to `true`
78
- */
73
+ * Clean output directory before build.
74
+ *
75
+ * Defaults to `true`
76
+ */
79
77
  clean?: boolean;
80
78
  /**
81
- * Experimental configuration.
82
- */
79
+ * The path to the tsconfig.json file.
80
+ *
81
+ * If set to `false`, the plugin will ignore any `tsconfig.json` file.
82
+ * You can still specify `compilerOptions` directly in the options.
83
+ *
84
+ * Defaults to `'tsconfig.json'`
85
+ */
86
+ tsconfig?: string;
87
+ /**
88
+ * Pass a raw `tsconfig.json` object directly to the plugin.
89
+ *
90
+ * @see https://www.typescriptlang.org/tsconfig
91
+ */
92
+ tsconfigRaw?: Omit<TsConfigJson, 'compilerOptions'>;
93
+ /**
94
+ * Experimental configuration.
95
+ */
83
96
  experimental?: ExperimentalConfig;
84
97
  }
85
98
  interface ExperimentalConfig {
86
99
  /**
87
- * Whether to use the tsgo compiler.
88
- *
89
- * To use this option, make sure `@typescript/native-preview` is installed as a dependency.
90
- */
100
+ * Whether to use the tsgo compiler.
101
+ *
102
+ * To use this option, make sure `@typescript/native-preview` is installed as a dependency.
103
+ */
91
104
  tsgo?: boolean;
92
105
  }
93
106
  //#endregion
package/dist/config.d.ts CHANGED
@@ -1,93 +1,106 @@
1
+ import { TsConfigJson } from "get-tsconfig";
1
2
  import { OutputOptions } from "rolldown";
2
3
 
3
- //#region src/types.d.ts
4
- type Format = 'esm' | 'cjs';
5
- //#endregion
6
4
  //#region src/config/types.d.ts
7
5
  interface Config {
8
6
  /**
9
- * Defaults to `'src'`
10
- */
7
+ * Defaults to `'src'`
8
+ */
11
9
  source?: string;
12
10
  /**
13
- * The directory where output files will be written.
14
- *
15
- * Defaults to `'dist'`
16
- */
11
+ * The directory where output files will be written.
12
+ *
13
+ * Defaults to `'dist'`
14
+ */
17
15
  outDir?: string;
18
16
  /**
19
- * Files to exclude from the build.
20
- *
21
- * Defaults to `/__(?:tests?|fixtures?|mocks?)__/`
22
- */
17
+ * Files to exclude from the build.
18
+ *
19
+ * Defaults to `/__(?:tests?|fixtures?|mocks?)__/`
20
+ */
23
21
  exclude?: RegExp;
24
22
  /**
25
- * Expected format of generated code.
26
- *
27
- * Defaults to `'esm'`
28
- */
29
- format?: Format | Format[];
30
- /**
31
- * Specifiers to resolve platform specific modules.
32
- *
33
- * Defaults to `['android', 'ios', 'native']`
34
- */
23
+ * Specifiers to resolve platform specific modules.
24
+ *
25
+ * Defaults to `['android', 'ios', 'native']`
26
+ */
35
27
  specifiers?: string[];
36
28
  /**
37
- * Source files extensions.
38
- *
39
- * Defaults to `['ts', 'tsx', 'js', 'jsx', 'json']`
40
- */
29
+ * Source files extensions.
30
+ *
31
+ * Defaults to `['ts', 'tsx', 'js', 'jsx', 'json']`
32
+ */
41
33
  sourceExtensions?: string[];
42
34
  /**
43
- * Asset files extensions.
44
- *
45
- * 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)
46
- */
35
+ * Asset files extensions.
36
+ *
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)
38
+ */
47
39
  assetExtensions?: string[];
48
40
  /**
49
- * Enables generation of TypeScript declaration files (.d.ts).
50
- *
51
- * Defaults to `true`
52
- */
41
+ * The directory where asset files will be written.
42
+ *
43
+ * Defaults to `'_assets'`
44
+ */
45
+ assetsDir?: string;
46
+ /**
47
+ * Enables generation of TypeScript declaration files (.d.ts).
48
+ *
49
+ * Defaults to `true`
50
+ */
53
51
  dts?: boolean;
54
52
  /**
55
- * Generate source map files.
56
- */
53
+ * Generate source map files.
54
+ */
57
55
  sourcemap?: OutputOptions['sourcemap'];
58
56
  /**
59
- * Code to prepend to the beginning of each output chunk.
60
- */
57
+ * Code to prepend to the beginning of each output chunk.
58
+ */
61
59
  banner?: OutputOptions['banner'];
62
60
  /**
63
- * Code to append to the end of each output chunk.
64
- */
61
+ * Code to append to the end of each output chunk.
62
+ */
65
63
  footer?: OutputOptions['footer'];
66
64
  /**
67
- * Code to prepend inside the wrapper function (after banner, before actual code).
68
- */
65
+ * Code to prepend inside the wrapper function (after banner, before actual code).
66
+ */
69
67
  intro?: OutputOptions['intro'];
70
68
  /**
71
- * Code to append inside the wrapper function (after actual code, before footer).
72
- */
69
+ * Code to append inside the wrapper function (after actual code, before footer).
70
+ */
73
71
  outro?: OutputOptions['outro'];
74
72
  /**
75
- * Clean output directory before build.
76
- *
77
- * Defaults to `true`
78
- */
73
+ * Clean output directory before build.
74
+ *
75
+ * Defaults to `true`
76
+ */
79
77
  clean?: boolean;
80
78
  /**
81
- * Experimental configuration.
82
- */
79
+ * The path to the tsconfig.json file.
80
+ *
81
+ * If set to `false`, the plugin will ignore any `tsconfig.json` file.
82
+ * You can still specify `compilerOptions` directly in the options.
83
+ *
84
+ * Defaults to `'tsconfig.json'`
85
+ */
86
+ tsconfig?: string;
87
+ /**
88
+ * Pass a raw `tsconfig.json` object directly to the plugin.
89
+ *
90
+ * @see https://www.typescriptlang.org/tsconfig
91
+ */
92
+ tsconfigRaw?: Omit<TsConfigJson, 'compilerOptions'>;
93
+ /**
94
+ * Experimental configuration.
95
+ */
83
96
  experimental?: ExperimentalConfig;
84
97
  }
85
98
  interface ExperimentalConfig {
86
99
  /**
87
- * Whether to use the tsgo compiler.
88
- *
89
- * To use this option, make sure `@typescript/native-preview` is installed as a dependency.
90
- */
100
+ * Whether to use the tsgo compiler.
101
+ *
102
+ * To use this option, make sure `@typescript/native-preview` is installed as a dependency.
103
+ */
91
104
  tsgo?: boolean;
92
105
  }
93
106
  //#endregion
package/dist/index.js CHANGED
@@ -1,10 +1,15 @@
1
+ import path from "path";
1
2
  import { loadConfig } from "c12";
3
+ import pc from "picocolors";
4
+ import { VERSION, build } from "rolldown";
2
5
  import { createDebug } from "obug";
3
6
  import fs, { globSync } from "node:fs";
7
+ import path$1 from "node:path";
4
8
  import * as pkg from "empathic/package";
5
- import { build } from "rolldown";
6
- import path from "node:path";
9
+ import { assert } from "es-toolkit";
7
10
  import { dts } from "rolldown-plugin-dts";
11
+ import { promisify } from "node:util";
12
+ import { brotliCompress, gzip } from "node:zlib";
8
13
 
9
14
  //#region src/common.ts
10
15
  const debug$3 = createDebug("tsnv:common");
@@ -15,7 +20,6 @@ const DEFAULT_CONFIG = {
15
20
  source: "src",
16
21
  outDir: "dist",
17
22
  exclude: /__(?:tests?|fixtures?|mocks?)__/,
18
- format: "esm",
19
23
  specifiers: [
20
24
  "android",
21
25
  "ios",
@@ -58,43 +62,30 @@ const DEFAULT_CONFIG = {
58
62
  "ttf",
59
63
  "zip"
60
64
  ],
65
+ assetsDir: "_assets",
61
66
  dts: true,
62
67
  clean: true
63
68
  };
64
69
 
65
- //#endregion
66
- //#region ../../../.yarn/berry/cache/es-toolkit-npm-1.43.0-b8f13c51d9-10c0.zip/node_modules/es-toolkit/dist/util/invariant.mjs
67
- function invariant(condition, message) {
68
- if (condition) return;
69
- if (typeof message === "string") throw new Error(message);
70
- throw message;
71
- }
72
-
73
70
  //#endregion
74
71
  //#region src/context.ts
75
- async function resolveContext(cwd) {
72
+ async function resolveContext(cwd, config) {
76
73
  const packageJsonPath = pkg.up({ cwd });
77
- invariant(packageJsonPath, "could not find package.json");
74
+ assert(packageJsonPath, "could not find package.json");
78
75
  const rawPackageJson = await fs.promises.readFile(packageJsonPath, "utf-8");
79
- const packageJson = JSON.parse(rawPackageJson);
80
76
  return {
81
77
  cwd,
82
- packageJson,
83
- packageType: resolvePackageType(packageJson)
78
+ packageJson: JSON.parse(rawPackageJson),
79
+ outdir: path$1.resolve(cwd, config.outDir),
80
+ source: path$1.resolve(cwd, config.source)
84
81
  };
85
82
  }
86
- function resolvePackageType(packageJson) {
87
- switch (packageJson.type) {
88
- case "module": return "esm";
89
- default: return "cjs";
90
- }
91
- }
92
83
 
93
84
  //#endregion
94
85
  //#region src/utils/path.ts
95
- async function hasPlatformSpecificModule(id, importer, config$1) {
96
- const specifiers = config$1.specifiers;
97
- const resolveDir = path.dirname(importer);
86
+ async function hasPlatformSpecificModule(id, importer, config) {
87
+ const specifiers = config.specifiers;
88
+ const resolveDir = path$1.dirname(importer);
98
89
  let fileList = hasPlatformSpecificModule.cache.get(resolveDir);
99
90
  if (fileList == null) {
100
91
  fileList = (await fs.promises.readdir(resolveDir, {
@@ -106,7 +97,7 @@ async function hasPlatformSpecificModule(id, importer, config$1) {
106
97
  return specifiers.some((specifier) => fileList.includes(`${basenameWithoutExtension(id)}.${specifier}`));
107
98
  }
108
99
  function basenameWithoutExtension(id) {
109
- return path.basename(id, path.extname(id));
100
+ return path$1.basename(id, path$1.extname(id));
110
101
  }
111
102
  hasPlatformSpecificModule.cache = /* @__PURE__ */ new Map();
112
103
  /**
@@ -115,7 +106,7 @@ hasPlatformSpecificModule.cache = /* @__PURE__ */ new Map();
115
106
  * In the case of platform-specific modules, a prefix such as `android.js` or `ios.js` is added before the module name.
116
107
  * If the standard module specification, which requires the full file path to be specified, is followed, platform-specific modules cannot be found.
117
108
  *
118
- * Therefore, the `.js` extension is used regardless of whether the module is ESM or CJS.
109
+ * Therefore, the `.js` extension is used.
119
110
  */
120
111
  function resolveFilename() {
121
112
  return `[name].js`;
@@ -125,15 +116,15 @@ function isDts(filename) {
125
116
  }
126
117
  function removePlatformSpecificExtension(filename, extensions, specifiers) {
127
118
  const extPattern = extensions.map((extension) => extension.replace(".", "")).join("|");
128
- const regex = /* @__PURE__ */ new RegExp(`\\.(${specifiers.join("|")})\\.(${extPattern})$`);
119
+ const regex = new RegExp(`\\.(${specifiers.join("|")})\\.(${extPattern})$`);
129
120
  return filename.replace(regex, ".$2");
130
121
  }
131
- function getUniquePlatformSpecificFiles(files$1, extensions, specifiers) {
122
+ function getUniquePlatformSpecificFiles(files, extensions, specifiers) {
132
123
  const extPattern = extensions.map((e) => e.replace(".", "")).join("|");
133
- const regex = /* @__PURE__ */ new RegExp(`\\.(${specifiers.join("|")})\\.(${extPattern})$`);
124
+ const regex = new RegExp(`\\.(${specifiers.join("|")})\\.(${extPattern})$`);
134
125
  const seen = /* @__PURE__ */ new Set();
135
126
  const result = [];
136
- for (const file of files$1) {
127
+ for (const file of files) {
137
128
  const baseName = file.replace(regex, ".$2");
138
129
  if (seen.has(baseName)) continue;
139
130
  seen.add(baseName);
@@ -142,15 +133,123 @@ function getUniquePlatformSpecificFiles(files$1, extensions, specifiers) {
142
133
  return result;
143
134
  }
144
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
+
145
242
  //#endregion
146
243
  //#region src/rolldown/plugins/dts.ts
147
244
  const debug$2 = createDebug("tsnv:dts");
148
- function dts$1(config$1) {
149
- if (!config$1.dts) return null;
245
+ function dts$1(config) {
246
+ if (!config.dts) return null;
150
247
  const dtsExtension = ["d"];
151
248
  return [dts({
152
249
  emitDtsOnly: true,
153
- tsgo: config$1.experimental?.tsgo
250
+ tsconfig: config.tsconfig,
251
+ tsconfigRaw: config.tsconfigRaw,
252
+ tsgo: config.experimental?.tsgo
154
253
  }), {
155
254
  name: "tsnv:dts-renamer",
156
255
  outputOptions(options) {
@@ -158,7 +257,7 @@ function dts$1(config$1) {
158
257
  ...options,
159
258
  entryFileNames(chunkInfo) {
160
259
  if (chunkInfo.name.endsWith(".d")) {
161
- const newChunkName = `${removePlatformSpecificExtension(chunkInfo.name, dtsExtension, config$1.specifiers)}.ts`;
260
+ const newChunkName = `${removePlatformSpecificExtension(chunkInfo.name, dtsExtension, config.specifiers)}.ts`;
162
261
  debug$2(`renaming ${chunkInfo.name} to ${newChunkName}`);
163
262
  return newChunkName;
164
263
  } else return chunkInfo.name;
@@ -172,8 +271,8 @@ function dts$1(config$1) {
172
271
  //#region src/rolldown/plugins/external.ts
173
272
  const PLUGIN_NAME = "tsnv:external";
174
273
  const debug$1 = createDebug(PLUGIN_NAME);
175
- function external(context$1) {
176
- const productionDependencies = Array.from(new Set([...Object.keys(context$1.packageJson.dependencies ?? {}), ...Object.keys(context$1.packageJson.peerDependencies ?? {})]));
274
+ function external(context) {
275
+ const productionDependencies = Array.from(new Set([...Object.keys(context.packageJson.dependencies ?? {}), ...Object.keys(context.packageJson.peerDependencies ?? {})]));
177
276
  debug$1("production dependencies", productionDependencies);
178
277
  return {
179
278
  name: PLUGIN_NAME,
@@ -183,16 +282,16 @@ function external(context$1) {
183
282
  id,
184
283
  external: true
185
284
  };
186
- const extname = path.extname(id);
285
+ const extname = path$1.extname(id).slice(1);
187
286
  if (extname) {
188
- if (context$1.config.assetExtensions.includes(extname)) return {
287
+ if (context.config.assetExtensions.includes(extname)) return {
189
288
  id,
190
289
  external: true
191
290
  };
192
- if (!context$1.config.sourceExtensions.includes(extname)) throw new Error(`Unsupported file extension: ${extname}`);
291
+ if (!context.config.sourceExtensions.includes(extname)) throw new Error(`Unsupported file extension: ${extname}`);
193
292
  }
194
293
  const resolved = await this.resolve(id, importer, extraOptions);
195
- if (resolved == null && await hasPlatformSpecificModule(id, importer, context$1.config)) return {
294
+ if (resolved == null && await hasPlatformSpecificModule(id, importer, context.config)) return {
196
295
  id,
197
296
  external: true,
198
297
  moduleSideEffects: true
@@ -205,16 +304,77 @@ function isPackageImportSource(packageName, id) {
205
304
  return id === packageName || id.startsWith(`${packageName}/`);
206
305
  }
207
306
 
307
+ //#endregion
308
+ //#region src/utils/fs.ts
309
+ const gzipAsync = promisify(gzip);
310
+ const brotliCompressAsync = promisify(brotliCompress);
311
+ function collectFiles(config) {
312
+ const files = globSync(`**/*.{${config.sourceExtensions.join(",")}}`, {
313
+ cwd: config.source,
314
+ exclude: (filename) => config.exclude.test(filename) || isDts(filename)
315
+ });
316
+ if (files.length === 0) throw new Error(`No files found in ${path$1.resolve(config.source)}`);
317
+ return files.map((file) => path$1.join(config.source, file));
318
+ }
319
+ async function calcSize(chunk) {
320
+ const content = chunk.type === "chunk" ? chunk.code : chunk.source;
321
+ const raw = Buffer.byteLength(content, "utf8");
322
+ const gzip = (await gzipAsync(content)).length;
323
+ return {
324
+ filename: chunk.fileName,
325
+ dts: chunk.fileName.endsWith(".d.ts"),
326
+ raw,
327
+ rawText: formatBytes(raw),
328
+ gzip,
329
+ gzipText: formatBytes(gzip)
330
+ };
331
+ }
332
+ function formatBytes(bytes) {
333
+ return `${(bytes / 1e3).toFixed(2)} kB`;
334
+ }
335
+
336
+ //#endregion
337
+ //#region src/rolldown/plugins/report.ts
338
+ const noop = (text) => text;
339
+ function report(options) {
340
+ const { cwd, format } = options;
341
+ const formatLabel = (() => {
342
+ switch (format) {
343
+ case "esm": return pc.blue(`[ESM]`);
344
+ case "dts": return pc.green(`[DTS]`);
345
+ }
346
+ })();
347
+ return {
348
+ name: "tsnv:report",
349
+ async writeBundle(outputOptions, bundle) {
350
+ const outDir = path$1.relative(cwd, outputOptions.dir ? path$1.resolve(outputOptions.dir) : path$1.dirname(outputOptions.file));
351
+ const sizes = [];
352
+ for (const chunk of Object.values(bundle)) {
353
+ const size = await calcSize(chunk);
354
+ sizes.push(size);
355
+ }
356
+ let totalRaw = 0;
357
+ for (const size of sizes) totalRaw += size.raw;
358
+ for (const size of sizes) {
359
+ const filenameColor = size.dts ? pc.green : noop;
360
+ const filename = path$1.normalize(size.filename);
361
+ console.log(formatLabel, pc.dim(outDir + path$1.sep) + filenameColor(filename), pc.dim(size.rawText), pc.dim(`(gzip ${size.gzipText})`));
362
+ }
363
+ console.log(formatLabel, `${sizes.length} files, total: ${formatBytes(totalRaw)}`);
364
+ }
365
+ };
366
+ }
367
+
208
368
  //#endregion
209
369
  //#region src/rolldown/build-options.ts
210
- function resolveBuildOptions(context$1, options) {
370
+ function resolveBuildOptions(context, options) {
211
371
  const pluginContext = {
212
- ...context$1,
372
+ ...context,
213
373
  config: options.config
214
374
  };
215
375
  const baseOptions = {
216
376
  input: options.files,
217
- plugins: [external(pluginContext)],
377
+ transform: { jsx: "react-jsx" },
218
378
  output: {
219
379
  banner: options.config.banner,
220
380
  footer: options.config.footer,
@@ -222,36 +382,41 @@ function resolveBuildOptions(context$1, options) {
222
382
  outro: options.config.outro,
223
383
  sourcemap: options.config.sourcemap,
224
384
  preserveModulesRoot: options.config.source,
225
- preserveModules: true
385
+ cleanDir: options.config.clean,
386
+ preserveModules: true,
387
+ polyfillRequire: false
226
388
  }
227
389
  };
228
- let formats;
229
- if (Array.isArray(options.config.format)) formats = options.config.format;
230
- else formats = [options.config.format];
231
- const uniqueFormats = Array.from(new Set(formats));
232
- const isSingleFormat = uniqueFormats.length === 1;
233
390
  const filename = resolveFilename();
234
- const resolvedBuildOptions = uniqueFormats.map((format) => {
235
- return {
236
- ...baseOptions,
237
- output: {
238
- ...baseOptions.output,
239
- dir: isSingleFormat ? options.config.outDir : path.join(options.config.outDir, format),
240
- cleanDir: options.config.clean,
241
- format,
242
- entryFileNames: filename,
243
- chunkFileNames: filename
244
- }
245
- };
246
- });
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
+ }];
247
405
  if (options.config.dts) resolvedBuildOptions.push({
248
406
  ...baseOptions,
249
407
  input: getUniquePlatformSpecificFiles(options.files, options.config.sourceExtensions, options.config.specifiers),
250
- plugins: [...Array.isArray(baseOptions.plugins) ? baseOptions.plugins : [baseOptions.plugins], dts$1(options.config)],
408
+ plugins: [
409
+ ...getBasePlugins(pluginContext),
410
+ report({
411
+ cwd: options.cwd,
412
+ format: "dts"
413
+ }),
414
+ dts$1(options.config)
415
+ ],
251
416
  output: {
252
417
  ...baseOptions.output,
253
- cleanDir: options.config.clean,
254
- dir: path.join(options.config.outDir, "types"),
418
+ cleanDir: false,
419
+ dir: options.config.outDir,
255
420
  format: "esm",
256
421
  entryFileNames: filename,
257
422
  chunkFileNames: filename
@@ -259,42 +424,80 @@ function resolveBuildOptions(context$1, options) {
259
424
  });
260
425
  return resolvedBuildOptions;
261
426
  }
427
+ function getBasePlugins(pluginContext) {
428
+ return [
429
+ blockRequire(),
430
+ collectAsset(pluginContext),
431
+ external(pluginContext)
432
+ ];
433
+ }
262
434
 
263
435
  //#endregion
264
436
  //#region src/rolldown/index.ts
265
437
  const debug = createDebug("tsnv:build");
266
- async function build$1(context$1, options) {
267
- const buildOptions = resolveBuildOptions(context$1, options);
438
+ async function build$1(context, options) {
439
+ const buildOptions = resolveBuildOptions(context, options);
268
440
  debug("Resolved build options", buildOptions);
269
441
  for (const buildOption of buildOptions) await build(buildOption);
270
442
  }
271
443
 
272
444
  //#endregion
273
- //#region src/utils/fs.ts
274
- function collectFiles(config$1) {
275
- const files$1 = globSync(`**/*.{${config$1.sourceExtensions.join(",")}}`, {
276
- cwd: config$1.source,
277
- exclude: (filename) => config$1.exclude.test(filename) || isDts(filename)
278
- });
279
- if (files$1.length === 0) throw new Error(`No files found in ${path.resolve(config$1.source)}`);
280
- return files$1.map((file) => path.join(config$1.source, file));
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;
281
460
  }
282
461
 
283
462
  //#endregion
284
463
  //#region src/index.ts
285
- debug$3("Loading config...");
286
- const { config } = await loadConfig({
287
- configFile: "tsnv.config",
288
- defaultConfig: DEFAULT_CONFIG
289
- });
290
- debug$3("Config loaded", config);
291
- const context = await resolveContext(process.cwd());
292
- debug$3("Resolved context", context);
293
- const files = await collectFiles(config);
294
- debug$3("Collected files", files);
295
- await build$1(context, {
296
- files,
297
- config
464
+ const version = `v0.0.0-dev.20260203190732`;
465
+ async function main() {
466
+ console.log(`tsnv ${pc.dim(version)} powered by rolldown ${pc.dim(VERSION)}`);
467
+ debug$3("Loading config...");
468
+ const cwd = process.cwd();
469
+ const { config, configFile } = await loadConfig({
470
+ cwd,
471
+ configFile: "tsnv.config",
472
+ defaultConfig: DEFAULT_CONFIG
473
+ });
474
+ debug$3("Config loaded", config);
475
+ console.log(`Config File: ${pc.underline(configFile)}`);
476
+ console.log(`Source Path: ${pc.blue(path.resolve(config.source))}`);
477
+ const context = await resolveContext(cwd, config);
478
+ debug$3("Resolved context", context);
479
+ const files = await collectFiles(config);
480
+ console.log(`Collected files: ${pc.dim(files.length)}`);
481
+ console.log("Build start");
482
+ const startedAt = performance.now();
483
+ await build$1(context, {
484
+ cwd,
485
+ files,
486
+ config
487
+ });
488
+ const endedAt = performance.now();
489
+ flushAssets(context);
490
+ const duration = `${Math.floor(endedAt - startedAt)}ms`;
491
+ console.log(`Build completed in ${pc.green(duration)}`);
492
+ }
493
+ await main().catch((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" : ""}`));
500
+ process.exit(1);
298
501
  });
299
502
 
300
503
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsnv",
3
- "version": "0.0.0-dev.20260119180902",
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>",
@@ -10,6 +10,9 @@
10
10
  "directory": "."
11
11
  },
12
12
  "bin": "bin.js",
13
+ "workspaces": [
14
+ "example"
15
+ ],
13
16
  "files": [
14
17
  "bin.js",
15
18
  "dist"
@@ -40,6 +43,7 @@
40
43
  "fmt": "oxfmt",
41
44
  "test": "vitest --run",
42
45
  "test:e2e": "vitest --run --config vitest.e2e.config.ts",
46
+ "test:all": "yarn test && yarn test:e2e",
43
47
  "build": "tsdown",
44
48
  "scripts:version": ".scripts/version.sh",
45
49
  "scripts:publish": ".scripts/publish.sh"
@@ -47,19 +51,20 @@
47
51
  "dependencies": {
48
52
  "c12": "^3.3.3",
49
53
  "empathic": "^2.0.0",
54
+ "es-toolkit": "^1.44.0",
55
+ "get-tsconfig": "^4.13.1",
50
56
  "obug": "^2.1.1",
51
- "rolldown": "1.0.0-beta.60",
52
- "rolldown-plugin-dts": "0.21.2",
53
- "tsdown": "0.20.0-beta.3"
57
+ "picocolors": "^1.1.1",
58
+ "rolldown": "1.0.0-rc.2",
59
+ "rolldown-plugin-dts": "0.21.8"
54
60
  },
55
61
  "devDependencies": {
56
62
  "@changesets/cli": "^2.29.8",
57
63
  "@types/node": "^24.10.1",
58
- "es-toolkit": "^1.43.0",
59
- "oxfmt": "^0.24.0",
60
- "oxlint": "^1.39.0",
61
- "oxlint-tsgolint": "^0.11.1",
62
- "picocolors": "^1.1.1",
64
+ "oxfmt": "^0.27.0",
65
+ "oxlint": "^1.42.0",
66
+ "oxlint-tsgolint": "^0.11.4",
67
+ "tsdown": "^0.20.1",
63
68
  "tsx": "^4.21.0",
64
69
  "typescript": "^5.9.3",
65
70
  "vitest": "^4.0.17",