modular-library 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [alfredosalzillo]
4
+ patreon: # Replace with a single Patreon username
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: # Replace with a single Ko-fi username
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ otechie: # Replace with a single Otechie username
12
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -0,0 +1,9 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: npm
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ time: "04:00"
8
+ open-pull-requests-limit: 10
9
+ target-branch: dev
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ node-version: [22, 23, 24, 25]
14
+
15
+ steps:
16
+ - name: Checkout repository
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ cache: npm
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Build
29
+ run: npm run build
30
+
31
+ - name: Test
32
+ run: npm test
package/AGENTS.md ADDED
@@ -0,0 +1,58 @@
1
+ # AGENTS.md
2
+
3
+ This file provides context and instructions for AI coding agents working on this project.
4
+
5
+ ## Setup Commands
6
+
7
+ - Install dependencies: `npm install`
8
+ - Build the project: `npm run build`
9
+ - Run tests: `npm test`
10
+ - Run lint checks: `npm run lint`
11
+
12
+ ## Technology Stack
13
+
14
+ - **TypeScript**: Use TypeScript for all code changes. Follow the existing configuration in `tsconfig.json`.
15
+ - Keep this `AGENTS.md` technology stack section up to date whenever the project tech stack changes.
16
+
17
+ ## Development Workflow
18
+
19
+ 1. **Keep changes minimal**: Implement the smallest safe change that resolves the issue.
20
+ 2. **Build**: Before submitting, ensure the project compiles by running `npm run build`.
21
+ 3. **Validate**: Run relevant tests with `npm test` when behavior changes.
22
+ 4. **Code Style**: Follow the existing style and naming patterns in the codebase.
23
+ 5. **GitHub Workflows**: To test workflows locally, use [`act`](https://nektosact.com/).
24
+
25
+ ## Temporary Files
26
+
27
+ - **Temporary Directory**: Always place temporary files in the `.ai-tmp` directory.
28
+
29
+ ## Commit Message Guidelines
30
+
31
+ When creating commits, use **Conventional Commits**.
32
+
33
+ ### Format
34
+
35
+ ```text
36
+ <type>(<scope>): <description>
37
+
38
+ [optional body]
39
+
40
+ [optional footer(s)]
41
+ ```
42
+
43
+ ### Types
44
+
45
+ - `feat`: A new feature (minor version update).
46
+ - `fix`: A bug fix (patch version update).
47
+ - `docs`: Documentation changes only.
48
+ - `style`: Changes that do not affect the meaning of the code (white-space, formatting, etc).
49
+ - `refactor`: A code change that neither fixes a bug nor adds a feature.
50
+ - `perf`: A code change that improves performance.
51
+ - `test`: Adding missing tests or correcting existing tests.
52
+ - `build`: Changes that affect the build system or external dependencies.
53
+ - `ci`: Changes to CI configuration files and scripts.
54
+ - `chore`: Other changes that don't modify src or test files.
55
+
56
+ ### Breaking Changes
57
+
58
+ Breaking changes must be indicated by a `!` after the type/scope or by including `BREAKING CHANGE:` in the footer.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # modular-library
2
+
3
+ `modular-library` is a utility library for generating modular libraries.
4
+
5
+ ## What is a modular library?
6
+
7
+ A **modular library** is a library split into small, focused modules instead of one large, monolithic package.
8
+
9
+ This approach helps you:
10
+
11
+ - keep each part of the library easier to understand and maintain,
12
+ - reuse modules independently across projects,
13
+ - reduce coupling between features,
14
+ - improve scalability as the codebase grows,
15
+ - be tree-shakeable so consumers only bundle what they import,
16
+ - expose clear entry points for each public feature or component.
17
+
18
+ Typical examples of modular libraries include:
19
+
20
+ - UI libraries exposing per-component entries (for example `@acme/ui/button`, `@acme/ui/modal`),
21
+ - utility libraries exposing per-domain modules (for example `@acme/utils/date`, `@acme/utils/string`),
22
+ - SDKs exposing feature-based modules (for example `@acme/sdk/auth`, `@acme/sdk/storage`).
23
+
24
+ With `modular-library`, the goal is to make the creation of this kind of modular architecture faster and more consistent.
25
+
26
+ ## Installation
27
+
28
+ > **Node.js requirement:** This library supports only Node.js `22` or newer.
29
+
30
+ ```bash
31
+ npm install modular-library
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ `modular-library` provides dedicated plugins for `vite`, `rollup`, and `rolldown`.
37
+
38
+ ### Vite
39
+
40
+ ```ts
41
+ // vite.config.ts
42
+ import { defineConfig } from "vite";
43
+ import modularLibrary from "modular-library/vite";
44
+
45
+ export default defineConfig({
46
+ plugins: [modularLibrary()],
47
+ build: {
48
+ lib: {
49
+ entry: ["src/**/*.ts"],
50
+ formats: ["es"],
51
+ },
52
+ },
53
+ });
54
+ ```
55
+
56
+ ### Rollup
57
+
58
+ ```ts
59
+ // rollup.config.ts
60
+ import modularLibrary from "modular-library/rollup";
61
+
62
+ export default {
63
+ input: ["src/**/*.ts"],
64
+ output: {
65
+ dir: "dist",
66
+ format: "es",
67
+ },
68
+ plugins: [modularLibrary()],
69
+ };
70
+ ```
71
+
72
+ ### Rolldown
73
+
74
+ ```ts
75
+ // rolldown.config.ts
76
+ import modularLibrary from "modular-library/rolldown";
77
+
78
+ export default {
79
+ input: ["src/**/*.ts"],
80
+ output: {
81
+ dir: "dist",
82
+ format: "es",
83
+ },
84
+ plugins: [modularLibrary()],
85
+ };
86
+ ```
87
+
88
+ ### Options
89
+
90
+ All plugin variants support the same options:
91
+
92
+ - `relative` (default: `"src/"`): base path removed from generated output keys.
93
+ - `glob`: [`fs.globSync`](https://nodejs.org/api/fs.html#fsglobsyncpattern-options) options.
94
+ - `transformOutputPath(outputPath, inputPath)`: customize each generated output path.
95
+
96
+ Example with options:
97
+
98
+ ```ts
99
+ import modularLibrary from "modular-library/rollup";
100
+
101
+ export default {
102
+ input: ["src/**/*.ts"],
103
+ output: {
104
+ dir: "dist",
105
+ format: "es",
106
+ },
107
+ plugins: [
108
+ modularLibrary({
109
+ relative: "src/",
110
+ transformOutputPath: (outputPath) => `modules/${outputPath}`,
111
+ }),
112
+ ],
113
+ };
114
+ ```
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ npm install
120
+ npm run build
121
+ npm test
122
+ ```
123
+
124
+ To test GitHub workflows locally, you can use [`act`](https://nektosact.com/).
125
+
126
+ ## License
127
+
128
+ ISC
package/biome.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": true,
10
+ "includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "space",
15
+ "indentWidth": 2
16
+ },
17
+ "javascript": {
18
+ "formatter": {
19
+ "quoteStyle": "double"
20
+ }
21
+ },
22
+ "linter": {
23
+ "enabled": true,
24
+ "rules": {
25
+ "recommended": true
26
+ }
27
+ },
28
+ "assist": {
29
+ "actions": {
30
+ "source": {
31
+ "organizeImports": {
32
+ "level": "on",
33
+ "options": {
34
+ "groups": [":PACKAGE:", ":PATH:"]
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "modular-library",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "exports": {
6
+ "./rollup": {
7
+ "types": "./dist/rollup/index.d.ts",
8
+ "import": "./dist/rollup/index.es.js",
9
+ "require": "./dist/rollup/index.cjs.js"
10
+ },
11
+ "./rolldown": {
12
+ "types": "./dist/rolldown/index.d.ts",
13
+ "import": "./dist/rolldown/index.es.js",
14
+ "require": "./dist/rolldown/index.cjs.js"
15
+ },
16
+ "./vite": {
17
+ "types": "./dist/vite/index.d.ts",
18
+ "import": "./dist/vite/index.es.js",
19
+ "require": "./dist/vite/index.cjs.js"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "build": "vite build",
24
+ "lint": "biome check .",
25
+ "format": "biome format --write .",
26
+ "test": "vitest run --coverage"
27
+ },
28
+ "keywords": [
29
+ "modular-library",
30
+ "vite-plugin",
31
+ "rollup-plugin",
32
+ "rolldown-plugin",
33
+ "multi-entry",
34
+ "multi-input",
35
+ "tree-shaking",
36
+ "typescript",
37
+ "bundler"
38
+ ],
39
+ "author": "alfredo salzillo <alfredosalzillo93@gmail.com>",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/alfredosalzillo/modular-library.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/alfredosalzillo/modular-library/issues"
47
+ },
48
+ "funding": {
49
+ "url": "https://github.com/sponsors/alfredosalzillo"
50
+ },
51
+ "homepage": "https://github.com/alfredosalzillo/modular-library#readme",
52
+ "description": "Plugins for Vite, Rollup, and Rolldown to build modular multi-entry TypeScript libraries.",
53
+ "devDependencies": {
54
+ "@biomejs/biome": "^2.4.11",
55
+ "@rollup/plugin-json": "^6.1.0",
56
+ "@types/node": "^25.6.0",
57
+ "@vitejs/plugin-react": "^6.0.1",
58
+ "@vitest/coverage-v8": "^4.1.4",
59
+ "rolldown": "^1.0.0-rc.15",
60
+ "rollup": "^4.60.1",
61
+ "typescript": "^6.0.2",
62
+ "unplugin-dts": "^1.0.0-beta.6",
63
+ "vite": "^8.0.8",
64
+ "vitest": "^4.1.4"
65
+ },
66
+ "volta": {
67
+ "node": "25.9.0"
68
+ },
69
+ "engines": {
70
+ "node": ">=22.0.0"
71
+ }
72
+ }
@@ -0,0 +1,59 @@
1
+ import { globSync, type GlobOptionsWithoutFileTypes } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const isString = (value: unknown): value is string => typeof value === "string";
5
+ const outputFileName = (filePath: string) =>
6
+ filePath.replace(/\.[^/.]+$/, "").replace(/\\/g, "/");
7
+
8
+ export type CreateEntryInput = string | string[] | Record<string, string>;
9
+ type CreateEntriesGlobOptions = GlobOptionsWithoutFileTypes;
10
+
11
+ export type CreateEntriesOptions = {
12
+ glob?: CreateEntriesGlobOptions;
13
+ relative?: string;
14
+ transformOutputPath?: (path: string, fileName: string) => string;
15
+ };
16
+
17
+ const defaultOptions = {
18
+ relative: `src/`,
19
+ };
20
+ const createEntries = (
21
+ input: CreateEntryInput,
22
+ options?: CreateEntriesOptions,
23
+ ) => {
24
+ // flat to enable input to be a string or an array
25
+ const inputs = [input].flat();
26
+ // separate globs inputs string from others to enable input to be a mixed array too
27
+ const globs = inputs.filter((value) => isString(value));
28
+ const others = inputs.filter((value) => !isString(value));
29
+ const normalizedGlobs = globs.map((glob) => glob.replace(/\\/g, "/"));
30
+
31
+ // get files from the strings and return as entries Object
32
+ const entries = globSync(normalizedGlobs, {
33
+ ...options?.glob,
34
+ withFileTypes: false,
35
+ }).map((name) => {
36
+ const filePath = path.relative(
37
+ options?.relative ?? defaultOptions.relative,
38
+ name,
39
+ );
40
+ const isRelative = !filePath.startsWith(`../`);
41
+ const relativeFilePath = isRelative ? filePath : path.relative(`./`, name);
42
+ return [
43
+ outputFileName(
44
+ options?.transformOutputPath
45
+ ? options.transformOutputPath(relativeFilePath, name)
46
+ : relativeFilePath,
47
+ ),
48
+ name,
49
+ ];
50
+ });
51
+ return Object.assign(
52
+ {},
53
+ Object.fromEntries(entries),
54
+ // add no globs input to the result
55
+ ...others,
56
+ );
57
+ };
58
+
59
+ export default createEntries;
@@ -0,0 +1,2 @@
1
+ export * from "./plugin";
2
+ export { default } from "./plugin";
@@ -0,0 +1,129 @@
1
+ import type { InputOptions } from "rolldown";
2
+ import modularLibrary from "./plugin";
3
+
4
+ const expectedOutput = ["fixture/input1", "fixture/input2"].sort();
5
+
6
+ const applyOptions = (
7
+ options: InputOptions,
8
+ pluginOptions?: Parameters<typeof modularLibrary>[0],
9
+ ) => {
10
+ const plugin = modularLibrary(pluginOptions);
11
+ if (!plugin.options) {
12
+ throw new Error("options hook is required");
13
+ }
14
+
15
+ const context = {
16
+ warn: vi.fn(),
17
+ };
18
+
19
+ const result = plugin.options.call(context as never, options);
20
+ return {
21
+ result,
22
+ warn: context.warn,
23
+ };
24
+ };
25
+
26
+ describe("modular-library/rolldown", () => {
27
+ it("should have name modular-library/rolldown", () => {
28
+ const plugin = modularLibrary({ relative: "./test" });
29
+ expect(plugin.name).toBe("modular-library/rolldown");
30
+ });
31
+
32
+ it("should resolve glob", () => {
33
+ const { result } = applyOptions(
34
+ {
35
+ input: ["test/fixture/**/*.js"],
36
+ },
37
+ { relative: "./test" },
38
+ );
39
+
40
+ expect(Object.keys(result.input as Record<string, string>).sort()).toEqual(
41
+ expectedOutput,
42
+ );
43
+ });
44
+
45
+ it("should accept a simple string as input", () => {
46
+ const { result } = applyOptions(
47
+ {
48
+ input: "test/fixture/**/*.js",
49
+ },
50
+ { relative: "./test" },
51
+ );
52
+
53
+ expect(Object.keys(result.input as Record<string, string>).sort()).toEqual(
54
+ expectedOutput,
55
+ );
56
+ });
57
+
58
+ it("should accept an array of strings as input", () => {
59
+ const { result } = applyOptions(
60
+ {
61
+ input: ["test/fixture/**/*.js"],
62
+ },
63
+ { relative: "./test" },
64
+ );
65
+
66
+ expect(Object.keys(result.input as Record<string, string>).sort()).toEqual(
67
+ expectedOutput,
68
+ );
69
+ });
70
+
71
+ it("should remove unresolved glob", () => {
72
+ const { result } = applyOptions(
73
+ {
74
+ input: ["test/fixture/**/*.js", "/not-found/file.js"],
75
+ },
76
+ { relative: "./test" },
77
+ );
78
+
79
+ expect(Object.keys(result.input as Record<string, string>).sort()).toEqual(
80
+ expectedOutput,
81
+ );
82
+ });
83
+
84
+ it("should resolve relative to src as default", () => {
85
+ const { result: outputFilesWithNoOptions } = applyOptions({
86
+ input: ["test/fixture/**/*.js"],
87
+ });
88
+ const { result: outputFilesWithNoRelativeOption } = applyOptions(
89
+ {
90
+ input: ["./test/fixture/**/*.js"],
91
+ },
92
+ {},
93
+ );
94
+
95
+ expect(
96
+ Object.keys(
97
+ outputFilesWithNoOptions.input as Record<string, string>,
98
+ ).sort(),
99
+ ).toEqual(["test/fixture/input1", "test/fixture/input2"]);
100
+ expect(
101
+ Object.keys(
102
+ outputFilesWithNoRelativeOption.input as Record<string, string>,
103
+ ).sort(),
104
+ ).toEqual(["test/fixture/input1", "test/fixture/input2"]);
105
+ });
106
+
107
+ it("should resolve output with transformOutputPath option", () => {
108
+ const { result } = applyOptions(
109
+ {
110
+ input: ["test/fixture/**/*.js"],
111
+ },
112
+ {
113
+ transformOutputPath: (output) => `dest/${output.split("/").at(-1)}`,
114
+ },
115
+ );
116
+
117
+ expect(Object.keys(result.input as Record<string, string>).sort()).toEqual([
118
+ "dest/input1",
119
+ "dest/input2",
120
+ ]);
121
+ });
122
+
123
+ it("should warn when input is missing", () => {
124
+ const { result, warn } = applyOptions({}, { relative: "./test" });
125
+
126
+ expect(warn).toHaveBeenCalledWith("At least one input is required");
127
+ expect(result).toEqual({});
128
+ });
129
+ });
@@ -0,0 +1,34 @@
1
+ import type { Plugin } from "rolldown";
2
+ import createEntries, { type CreateEntriesOptions } from "@/createEntries";
3
+
4
+ const pluginName = "modular-library/rolldown";
5
+
6
+ export type RolldownModularLibraryOptions = CreateEntriesOptions;
7
+
8
+ /**
9
+ * rolldownModularLibrary is a rolldown plugin to use multiple entry points and preserve the directory
10
+ * structure in the dist folder
11
+ */
12
+ const rolldownModularLibrary = (
13
+ options?: RolldownModularLibraryOptions,
14
+ ): Plugin => {
15
+ return {
16
+ name: pluginName,
17
+ options(conf) {
18
+ if (!conf.input) {
19
+ if (this.warn) {
20
+ this.warn("At least one input is required");
21
+ }
22
+ return conf;
23
+ }
24
+
25
+ const input = createEntries(conf.input, options);
26
+ return {
27
+ ...conf,
28
+ input,
29
+ };
30
+ },
31
+ };
32
+ };
33
+
34
+ export default rolldownModularLibrary;
@@ -0,0 +1,2 @@
1
+ export * from "./plugin";
2
+ export { default } from "./plugin";
@@ -0,0 +1,114 @@
1
+ import { rollup, RollupOptions } from "rollup";
2
+ import importJson from "@rollup/plugin-json";
3
+ import path from "node:path";
4
+ import modularLibrary from "./plugin";
5
+
6
+ const expectedOutput = ["fixture/input1.js", "fixture/input2.js"].sort();
7
+
8
+ const externalDependencies = ["node:fs", "path"];
9
+
10
+ describe.each([
11
+ ["rollup 4", rollup],
12
+ ])("modular-library/rollup using %s", (_, rollup) => {
13
+ const generateBundle = (options: RollupOptions) =>
14
+ rollup(options).then((bundle) =>
15
+ bundle.generate({
16
+ format: "cjs",
17
+ }),
18
+ );
19
+
20
+ const generateOutputFileNames = (options: RollupOptions) =>
21
+ generateBundle(options).then(({ output }) =>
22
+ output.map((module) => module.fileName).sort(),
23
+ );
24
+
25
+ it("should have name modular-library/rollup", async () => {
26
+ const plugin = modularLibrary({ relative: "./test" });
27
+ expect("name" in plugin).toBeTruthy();
28
+ expect(plugin.name).toBe("modular-library/rollup");
29
+ });
30
+ it("should resolve glob", async () => {
31
+ const outputFiles = await generateOutputFileNames({
32
+ input: ["test/fixture/**/*.js"],
33
+ plugins: [modularLibrary({ relative: "./test" })],
34
+ });
35
+ expect(outputFiles).toEqual(expectedOutput);
36
+ });
37
+ it("should accept a simple string as input", async () => {
38
+ const outputFiles = await generateOutputFileNames({
39
+ input: "test/fixture/**/*.js",
40
+ plugins: [modularLibrary({ relative: "./test" })],
41
+ });
42
+ expect(outputFiles).toEqual(expectedOutput);
43
+ });
44
+ it("should accept an array of strings as input", async () => {
45
+ const outputFiles = await generateOutputFileNames({
46
+ input: ["test/fixture/**/*.js"],
47
+ plugins: [modularLibrary({ relative: "./test" })],
48
+ });
49
+ expect(outputFiles).toEqual(expectedOutput);
50
+ });
51
+ it("should remove unresolved glob", async () => {
52
+ const outputFiles = await generateOutputFileNames({
53
+ input: ["test/fixture/**/*.js", "/not-found/file.js"],
54
+ plugins: [modularLibrary({ relative: "./test" })],
55
+ });
56
+ expect(outputFiles).toEqual(expectedOutput);
57
+ });
58
+ it("should preserve no string entries", async () => {
59
+ const bundle = generateBundle({
60
+ // @ts-expect-error
61
+ input: [
62
+ "test/fixture/**/*.js",
63
+ {
64
+ test: "path/to/test.js",
65
+ },
66
+ ],
67
+ plugins: [modularLibrary({ relative: "./test" })],
68
+ });
69
+ await expect(bundle).rejects.toThrow(/^Could not resolve entry module/);
70
+ });
71
+ it('should resolve relative to "src" as default', async () => {
72
+ const outputFilesWithNoOptions = await generateOutputFileNames({
73
+ input: ["test/fixture/**/*.js"],
74
+ plugins: [modularLibrary(), importJson()],
75
+ external: externalDependencies,
76
+ });
77
+ const outputFilesWithNoRelativeOption = await generateOutputFileNames({
78
+ input: ["./test/fixture/**/*.js"],
79
+ plugins: [modularLibrary({}), importJson()],
80
+ external: externalDependencies,
81
+ });
82
+ expect(outputFilesWithNoOptions).toEqual([
83
+ "test/fixture/input1.js",
84
+ "test/fixture/input2.js",
85
+ ]);
86
+ expect(outputFilesWithNoRelativeOption).toEqual([
87
+ "test/fixture/input1.js",
88
+ "test/fixture/input2.js",
89
+ ]);
90
+ });
91
+ it('should resolve non relative to "relative" options path to root', async () => {
92
+ const outputFiles = await generateOutputFileNames({
93
+ input: ["test/fixture/**/*.js"],
94
+ plugins: [modularLibrary(), importJson()],
95
+ external: ["node:fs", "path"],
96
+ });
97
+ expect(outputFiles).toEqual([
98
+ "test/fixture/input1.js",
99
+ "test/fixture/input2.js",
100
+ ]);
101
+ });
102
+ it('should resolve output to "dist" directory', async () => {
103
+ const outputFiles = await generateOutputFileNames({
104
+ input: ["test/fixture/**/*.js"],
105
+ plugins: [
106
+ modularLibrary({
107
+ transformOutputPath: (output) => `dest/${path.basename(output)}`,
108
+ }),
109
+ ],
110
+ external: ["node:fs", "path"],
111
+ });
112
+ expect(outputFiles).toEqual(["dest/input1.js", "dest/input2.js"]);
113
+ });
114
+ });
@@ -0,0 +1,34 @@
1
+ import type { Plugin } from "rollup";
2
+ import createEntries, { CreateEntriesOptions } from "@/createEntries";
3
+
4
+ const pluginName = "modular-library/rollup";
5
+
6
+ export type RollupModularLibraryOptions = CreateEntriesOptions;
7
+
8
+ /**
9
+ * modularLibrary is a rollup plugin to use multiple entry point and preserve the directory
10
+ * structure in the dist folder
11
+ * */
12
+ const rollupModularLibrary = (
13
+ options?: RollupModularLibraryOptions,
14
+ ): Plugin => {
15
+ return {
16
+ name: pluginName,
17
+ buildStart() {},
18
+ options(conf) {
19
+ if (!conf.input) {
20
+ if (this.warn) {
21
+ this.warn("At least one input is required");
22
+ }
23
+ return conf;
24
+ }
25
+ const input = createEntries(conf.input, options);
26
+ return {
27
+ ...conf,
28
+ input,
29
+ };
30
+ },
31
+ };
32
+ };
33
+
34
+ export default rollupModularLibrary;
@@ -0,0 +1,2 @@
1
+ export * from "./plugin";
2
+ export { default } from "./plugin";
@@ -0,0 +1,192 @@
1
+ import type { UserConfig } from "vite";
2
+ import modularLibrary from "./plugin";
3
+
4
+ const expectedOutput = ["fixture/input1", "fixture/input2"].sort();
5
+
6
+ const applyConfig = (
7
+ config: UserConfig,
8
+ pluginOptions?: Parameters<typeof modularLibrary>[0],
9
+ ) => {
10
+ const plugin = modularLibrary(pluginOptions);
11
+ if (!plugin.config) {
12
+ throw new Error("config hook is required");
13
+ }
14
+
15
+ const context = {
16
+ warn: vi.fn(),
17
+ };
18
+
19
+ // @ts-expect-error
20
+ const result = plugin.config.call(context as never, config);
21
+ return {
22
+ result,
23
+ warn: context.warn,
24
+ };
25
+ };
26
+
27
+ describe("modular-library/vite", () => {
28
+ it("should have name modular-library/vite", () => {
29
+ const plugin = modularLibrary({ relative: "./test" });
30
+ expect(plugin.name).toBe("modular-library/vite");
31
+ expect(plugin.apply).toBe("build");
32
+ });
33
+
34
+ it("should warn when build.lib is missing", () => {
35
+ const { result, warn } = applyConfig({});
36
+
37
+ expect(warn).toHaveBeenCalledWith("The build.lib option is required");
38
+ expect(result).toEqual({});
39
+ });
40
+
41
+ it("should warn when build.lib.entry is missing", () => {
42
+ const { result, warn } = applyConfig({
43
+ build: {
44
+ // @ts-expect-error
45
+ lib: {},
46
+ },
47
+ });
48
+
49
+ expect(warn).toHaveBeenCalledWith("At least one entry is required");
50
+ expect(result).toEqual({
51
+ build: {
52
+ lib: {},
53
+ },
54
+ });
55
+ });
56
+
57
+ it("should resolve glob", () => {
58
+ const { result } = applyConfig(
59
+ {
60
+ build: {
61
+ lib: {
62
+ entry: ["test/fixture/**/*.js"],
63
+ },
64
+ },
65
+ },
66
+ { relative: "./test" },
67
+ );
68
+
69
+ expect(
70
+ Object.keys(
71
+ (result.build?.lib as { entry: Record<string, string> }).entry,
72
+ ).sort(),
73
+ ).toEqual(expectedOutput);
74
+ });
75
+
76
+ it("should accept a simple string as input", () => {
77
+ const { result } = applyConfig(
78
+ {
79
+ build: {
80
+ lib: {
81
+ entry: "test/fixture/**/*.js",
82
+ },
83
+ },
84
+ },
85
+ { relative: "./test" },
86
+ );
87
+
88
+ expect(
89
+ Object.keys(
90
+ (result.build?.lib as { entry: Record<string, string> }).entry,
91
+ ).sort(),
92
+ ).toEqual(expectedOutput);
93
+ });
94
+
95
+ it("should accept an array of strings as input", () => {
96
+ const { result } = applyConfig(
97
+ {
98
+ build: {
99
+ lib: {
100
+ entry: ["test/fixture/**/*.js"],
101
+ },
102
+ },
103
+ },
104
+ { relative: "./test" },
105
+ );
106
+
107
+ expect(
108
+ Object.keys(
109
+ (result.build?.lib as { entry: Record<string, string> }).entry,
110
+ ).sort(),
111
+ ).toEqual(expectedOutput);
112
+ });
113
+
114
+ it("should remove unresolved glob", () => {
115
+ const { result } = applyConfig(
116
+ {
117
+ build: {
118
+ lib: {
119
+ entry: ["test/fixture/**/*.js", "/not-found/file.js"],
120
+ },
121
+ },
122
+ },
123
+ { relative: "./test" },
124
+ );
125
+
126
+ expect(
127
+ Object.keys(
128
+ (result.build?.lib as { entry: Record<string, string> }).entry,
129
+ ).sort(),
130
+ ).toEqual(expectedOutput);
131
+ });
132
+
133
+ it("should resolve relative to src as default", () => {
134
+ const { result: outputFilesWithNoOptions } = applyConfig({
135
+ build: {
136
+ lib: {
137
+ entry: ["test/fixture/**/*.js"],
138
+ },
139
+ },
140
+ });
141
+ const { result: outputFilesWithNoRelativeOption } = applyConfig(
142
+ {
143
+ build: {
144
+ lib: {
145
+ entry: ["./test/fixture/**/*.js"],
146
+ },
147
+ },
148
+ },
149
+ {},
150
+ );
151
+
152
+ expect(
153
+ Object.keys(
154
+ (
155
+ outputFilesWithNoOptions.build?.lib as {
156
+ entry: Record<string, string>;
157
+ }
158
+ ).entry,
159
+ ).sort(),
160
+ ).toEqual(["test/fixture/input1", "test/fixture/input2"]);
161
+ expect(
162
+ Object.keys(
163
+ (
164
+ outputFilesWithNoRelativeOption.build?.lib as {
165
+ entry: Record<string, string>;
166
+ }
167
+ ).entry,
168
+ ).sort(),
169
+ ).toEqual(["test/fixture/input1", "test/fixture/input2"]);
170
+ });
171
+
172
+ it("should resolve output with transformOutputPath option", () => {
173
+ const { result } = applyConfig(
174
+ {
175
+ build: {
176
+ lib: {
177
+ entry: ["test/fixture/**/*.js"],
178
+ },
179
+ },
180
+ },
181
+ {
182
+ transformOutputPath: (output) => `dest/${output.split("/").at(-1)}`,
183
+ },
184
+ );
185
+
186
+ expect(
187
+ Object.keys(
188
+ (result.build?.lib as { entry: Record<string, string> }).entry,
189
+ ).sort(),
190
+ ).toEqual(["dest/input1", "dest/input2"]);
191
+ });
192
+ });
@@ -0,0 +1,40 @@
1
+ import type { Plugin } from "vite";
2
+ import createEntries, { type CreateEntriesOptions } from "@/createEntries";
3
+
4
+ const pluginName = "modular-library/vite";
5
+
6
+ export type ViteModularLibraryOptions = CreateEntriesOptions;
7
+
8
+ /**
9
+ * viteModularLibrary is a vite plugin to use multiple entry points and preserve the directory
10
+ * structure in the dist folder
11
+ */
12
+ const viteModularLibrary = (options?: ViteModularLibraryOptions): Plugin => {
13
+ return {
14
+ name: pluginName,
15
+ apply: "build",
16
+ config(config) {
17
+ if (!config.build?.lib) {
18
+ if (this.warn) {
19
+ this.warn("The build.lib option is required");
20
+ }
21
+ return config;
22
+ }
23
+ const entry = config.build?.lib?.entry;
24
+
25
+ if (!entry) {
26
+ if (this.warn) {
27
+ this.warn("At least one entry is required");
28
+ }
29
+
30
+ return config;
31
+ }
32
+
33
+ config.build.lib.entry = createEntries(entry, options);
34
+
35
+ return config;
36
+ },
37
+ };
38
+ };
39
+
40
+ export default viteModularLibrary;
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export function doNothing() {}
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export function doNothing() {}
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "skipLibCheck": true,
13
+ "baseUrl": ".",
14
+ "paths": {
15
+ "@/*": ["src/*"]
16
+ }
17
+ },
18
+ "include": ["src", "node_modules/vitest/globals.d.ts"]
19
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import dts from "unplugin-dts/vite";
3
+ import { defineConfig } from "vite";
4
+ import { resolve } from "node:path";
5
+
6
+ export default defineConfig({
7
+ plugins: [
8
+ react(),
9
+ dts({
10
+ copyDtsFiles: true,
11
+ outDirs: ["dist"],
12
+ exclude: ["src/**/*.test.ts"],
13
+ beforeWriteFile: (filePath, content) => ({
14
+ filePath: filePath.replace(/([\\/])dist\1src\1/, "$1dist$1"),
15
+ content,
16
+ }),
17
+ }),
18
+ ],
19
+ build: {
20
+ lib: {
21
+ entry: {
22
+ "rolldown/index": resolve(__dirname, "src/rolldown/index.ts"),
23
+ "rollup/index": resolve(__dirname, "src/rollup/index.ts"),
24
+ "vite/index": resolve(__dirname, "src/vite/index.ts"),
25
+ },
26
+ formats: ["es", "cjs"],
27
+ fileName: (format, entryName) => `${entryName}.${format}.js`,
28
+ },
29
+ rolldownOptions: {
30
+ output: {
31
+ chunkFileNames: "chunks/[name].js",
32
+ },
33
+ external: ["node:path", "node:fs"],
34
+ },
35
+ },
36
+ });
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { resolve } from "node:path";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ "@": resolve(__dirname, "src"),
8
+ },
9
+ },
10
+ test: {
11
+ environment: "node",
12
+ globals: true,
13
+ coverage: {
14
+ provider: "v8",
15
+ reportsDirectory: "./coverage",
16
+ },
17
+ },
18
+ });