@devup-api/webpack-plugin 0.1.0

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 ADDED
@@ -0,0 +1,24 @@
1
+ # @devup-api/webpack-plugin
2
+
3
+ devup API plugin for Webpack
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @devup-api/webpack-plugin
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import devupApiWebpackPlugin from '@devup-api/webpack-plugin';
15
+ import webpack from 'webpack';
16
+
17
+ const config: webpack.Configuration = {
18
+ plugins: [
19
+ new devupApiWebpackPlugin({
20
+ // options
21
+ }),
22
+ ],
23
+ };
24
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/plugin.test.ts"],"names":[],"mappings":""}
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ var{defineProperty:F,getOwnPropertyNames:Q,getOwnPropertyDescriptor:R}=Object,S=Object.prototype.hasOwnProperty;var K=new WeakMap,V=(x)=>{var W=K.get(x),q;if(W)return W;if(W=F({},"__esModule",{value:!0}),x&&typeof x==="object"||typeof x==="function")Q(x).map((z)=>!S.call(W,z)&&F(W,z,{get:()=>x[z],enumerable:!(q=R(x,z))||q.enumerable}));return K.set(x,W),W};var X=(x,W)=>{for(var q in W)F(x,q,{get:W[q],enumerable:!0,configurable:!0,set:(z)=>W[q]=()=>z})};var Y={};X(Y,{devupApiWebpackPlugin:()=>G,default:()=>G});module.exports=V(Y);var L=require("node:path"),C=require("@devup-api/generator"),B=require("@devup-api/utils"),N=require("webpack");class G{options;initialized=!1;constructor(x){this.options=x||{}}apply(x){x.hooks.beforeCompile.tapAsync("devup-api",async(W,q)=>{if(this.initialized){q();return}try{this.initialized=!0;let z=await B.createTmpDirAsync(this.options?.tempDir),H=await B.readOpenapiAsync(this.options?.openapiFile);await B.writeInterfaceAsync(L.join(z,"api.d.ts"),C.generateInterface(H,this.options));let J=C.createUrlMap(H,this.options),E={};if(J)E["process.env.DEVUP_API_URL_MAP"]=JSON.stringify(JSON.stringify(J));if(Object.keys(E).length>0)new N.DefinePlugin(E).apply(x);q()}catch(z){this.initialized=!1,q(z)}})}}
@@ -0,0 +1,3 @@
1
+ export * from './plugin';
2
+ export { devupApiWebpackPlugin as default } from './plugin';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,qBAAqB,IAAI,OAAO,EAAE,MAAM,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{join as F}from"node:path";import{createUrlMap as G,generateInterface as H}from"@devup-api/generator";import{createTmpDirAsync as J,readOpenapiAsync as K,writeInterfaceAsync as L}from"@devup-api/utils";import{DefinePlugin as N}from"webpack";class E{options;initialized=!1;constructor(x){this.options=x||{}}apply(x){x.hooks.beforeCompile.tapAsync("devup-api",async(Q,W)=>{if(this.initialized){W();return}try{this.initialized=!0;let q=await J(this.options?.tempDir),B=await K(this.options?.openapiFile);await L(F(q,"api.d.ts"),H(B,this.options));let C=G(B,this.options),z={};if(C)z["process.env.DEVUP_API_URL_MAP"]=JSON.stringify(JSON.stringify(C));if(Object.keys(z).length>0)new N(z).apply(x);W()}catch(q){this.initialized=!1,W(q)}})}}export{E as devupApiWebpackPlugin,E as default};
@@ -0,0 +1,9 @@
1
+ import type { DevupApiOptions } from '@devup-api/core';
2
+ import type { Compiler } from 'webpack';
3
+ export declare class devupApiWebpackPlugin {
4
+ options: DevupApiOptions;
5
+ initialized: boolean;
6
+ constructor(options?: DevupApiOptions);
7
+ apply(compiler: Compiler): void;
8
+ }
9
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAOtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAGvC,qBAAa,qBAAqB;IAChC,OAAO,EAAE,eAAe,CAAA;IACxB,WAAW,UAAQ;gBAEP,OAAO,CAAC,EAAE,eAAe;IAIrC,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;CA6ChC"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@devup-api/webpack-plugin",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/index.js",
8
+ "require": "./dist/index.cjs",
9
+ "types": "./dist/index.d.ts"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "dependencies": {
19
+ "@devup-api/utils": "0.1.0",
20
+ "@devup-api/core": "0.1.0",
21
+ "@devup-api/generator": "0.1.0"
22
+ },
23
+ "peerDependencies": {
24
+ "webpack": "*",
25
+ "@devup-api/core": "*"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^24.10",
29
+ "@types/webpack": "^5.28",
30
+ "typescript": "^5.9"
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ import { expect, test } from 'bun:test'
2
+ import * as indexModule from '../index'
3
+
4
+ test('index.ts exports', () => {
5
+ expect({ ...indexModule }).toEqual({
6
+ devupApiWebpackPlugin: expect.any(Function),
7
+ default: expect.any(Function),
8
+ })
9
+ })
@@ -0,0 +1,263 @@
1
+ import { beforeEach, expect, mock, spyOn, test } from 'bun:test'
2
+ import { join } from 'node:path'
3
+ import type { DevupApiOptions } from '@devup-api/core'
4
+ import * as generator from '@devup-api/generator'
5
+ import * as utils from '@devup-api/utils'
6
+ import type { Compiler } from 'webpack'
7
+ import { DefinePlugin } from 'webpack'
8
+ import { devupApiWebpackPlugin } from '../plugin'
9
+
10
+ let mockCreateTmpDirAsync: ReturnType<typeof spyOn>
11
+ let mockReadOpenapiAsync: ReturnType<typeof spyOn>
12
+ let mockWriteInterfaceAsync: ReturnType<typeof spyOn>
13
+ let mockCreateUrlMap: ReturnType<typeof spyOn>
14
+ let mockGenerateInterface: ReturnType<typeof spyOn>
15
+
16
+ const mockSchema = {
17
+ openapi: '3.1.0',
18
+ paths: {
19
+ '/users': {
20
+ get: {
21
+ operationId: 'getUsers',
22
+ responses: {
23
+ '200': {
24
+ content: {
25
+ 'application/json': {
26
+ schema: {
27
+ type: 'array',
28
+ items: { type: 'string' },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ },
37
+ } as const
38
+
39
+ const mockUrlMap = {
40
+ getUsers: {
41
+ method: 'GET' as const,
42
+ url: '/users',
43
+ },
44
+ '/users': {
45
+ method: 'GET' as const,
46
+ url: '/users',
47
+ },
48
+ }
49
+
50
+ const mockInterfaceContent = 'export interface Test {}'
51
+
52
+ const createMockCompiler = (): Compiler & {
53
+ _storedCallback?: (params: unknown, cb: (error?: Error) => void) => void
54
+ } => {
55
+ const storedCallback: {
56
+ callback?: (params: unknown, cb: (error?: Error) => void) => void
57
+ } = {}
58
+ const tapAsyncMock = mock(
59
+ (
60
+ _name: string,
61
+ callback: (params: unknown, cb: (error?: Error) => void) => void,
62
+ ) => {
63
+ storedCallback.callback = callback
64
+ },
65
+ )
66
+ const hooks = {
67
+ beforeCompile: {
68
+ tapAsync: tapAsyncMock,
69
+ },
70
+ }
71
+ const compiler = {
72
+ hooks,
73
+ } as unknown as Compiler & {
74
+ _storedCallback?: (params: unknown, cb: (error?: Error) => void) => void
75
+ }
76
+ Object.defineProperty(compiler, '_storedCallback', {
77
+ get() {
78
+ return storedCallback.callback
79
+ },
80
+ enumerable: true,
81
+ configurable: true,
82
+ })
83
+ return compiler
84
+ }
85
+
86
+ beforeEach(() => {
87
+ mockCreateTmpDirAsync = spyOn(utils, 'createTmpDirAsync').mockResolvedValue(
88
+ 'df',
89
+ )
90
+ mockReadOpenapiAsync = spyOn(utils, 'readOpenapiAsync').mockResolvedValue(
91
+ mockSchema as never,
92
+ )
93
+ mockWriteInterfaceAsync = spyOn(
94
+ utils,
95
+ 'writeInterfaceAsync',
96
+ ).mockResolvedValue(undefined)
97
+ mockCreateUrlMap = spyOn(generator, 'createUrlMap').mockReturnValue(
98
+ mockUrlMap as never,
99
+ )
100
+ mockGenerateInterface = spyOn(generator, 'generateInterface').mockReturnValue(
101
+ mockInterfaceContent,
102
+ )
103
+ mockCreateTmpDirAsync.mockClear()
104
+ mockReadOpenapiAsync.mockClear()
105
+ mockWriteInterfaceAsync.mockClear()
106
+ mockCreateUrlMap.mockClear()
107
+ mockGenerateInterface.mockClear()
108
+ })
109
+
110
+ test('devupApiWebpackPlugin constructor initializes with default options', () => {
111
+ const plugin = new devupApiWebpackPlugin()
112
+ expect(plugin.options).toEqual({})
113
+ expect(plugin.initialized).toBe(false)
114
+ })
115
+
116
+ test.each([
117
+ [{ tempDir: 'custom-dir' }],
118
+ [{ openapiFile: 'custom-openapi.json' }],
119
+ [
120
+ {
121
+ tempDir: 'custom-dir',
122
+ openapiFile: 'custom-openapi.json',
123
+ convertCase: 'snake' as const,
124
+ },
125
+ ],
126
+ ] as const)('devupApiWebpackPlugin constructor initializes with options: %s', (options: DevupApiOptions) => {
127
+ const plugin = new devupApiWebpackPlugin(options)
128
+ expect(plugin.options).toEqual(options)
129
+ expect(plugin.initialized).toBe(false)
130
+ })
131
+
132
+ test('devupApiWebpackPlugin apply method registers beforeCompile hook', () => {
133
+ const plugin = new devupApiWebpackPlugin()
134
+ const compiler = createMockCompiler()
135
+ plugin.apply(compiler)
136
+ expect(compiler.hooks.beforeCompile.tapAsync).toHaveBeenCalledWith(
137
+ 'devup-api',
138
+ expect.any(Function),
139
+ )
140
+ expect(compiler._storedCallback).toBeDefined()
141
+ })
142
+
143
+ test.each([
144
+ [undefined],
145
+ [{ tempDir: 'custom-dir' }],
146
+ [{ openapiFile: 'custom-openapi.json' }],
147
+ [
148
+ {
149
+ tempDir: 'custom-dir',
150
+ openapiFile: 'custom-openapi.json',
151
+ convertCase: 'pascal' as const,
152
+ },
153
+ ],
154
+ ] as const)('devupApiWebpackPlugin beforeCompile hook executes correctly: %s', async (options:
155
+ | DevupApiOptions
156
+ | undefined) => {
157
+ const plugin = new devupApiWebpackPlugin(options)
158
+ const compiler = createMockCompiler()
159
+ const definePluginApplySpy = spyOn(
160
+ DefinePlugin.prototype,
161
+ 'apply',
162
+ ).mockImplementation(() => {})
163
+ plugin.apply(compiler)
164
+
165
+ const callback = compiler._storedCallback
166
+ expect(callback).toBeDefined()
167
+
168
+ const mockCallback = mock(() => {})
169
+ await callback?.(null, mockCallback)
170
+
171
+ expect(mockCreateTmpDirAsync).toHaveBeenCalledWith(options?.tempDir)
172
+ expect(mockReadOpenapiAsync).toHaveBeenCalledWith(options?.openapiFile)
173
+ expect(mockGenerateInterface).toHaveBeenCalledWith(mockSchema, options || {})
174
+ expect(mockWriteInterfaceAsync).toHaveBeenCalledWith(
175
+ join('df', 'api.d.ts'),
176
+ mockInterfaceContent,
177
+ )
178
+ expect(mockCreateUrlMap).toHaveBeenCalledWith(mockSchema, options || {})
179
+ expect(definePluginApplySpy).toHaveBeenCalledWith(compiler)
180
+ expect(mockCallback).toHaveBeenCalled()
181
+ expect(plugin.initialized).toBe(true)
182
+ definePluginApplySpy.mockRestore()
183
+ })
184
+
185
+ test('devupApiWebpackPlugin beforeCompile hook does not add DefinePlugin when urlMap is null', async () => {
186
+ mockCreateUrlMap.mockReturnValueOnce(null as never)
187
+ const plugin = new devupApiWebpackPlugin()
188
+ const compiler = createMockCompiler()
189
+ const definePluginApplySpy = spyOn(
190
+ DefinePlugin.prototype,
191
+ 'apply',
192
+ ).mockImplementation(() => {})
193
+ plugin.apply(compiler)
194
+
195
+ const callback = compiler._storedCallback
196
+
197
+ const mockCallback = mock(() => {})
198
+ await callback?.(null, mockCallback)
199
+
200
+ expect(definePluginApplySpy).not.toHaveBeenCalled()
201
+ expect(mockCallback).toHaveBeenCalled()
202
+ definePluginApplySpy.mockRestore()
203
+ })
204
+
205
+ test('devupApiWebpackPlugin beforeCompile hook does not add DefinePlugin when urlMap is undefined', async () => {
206
+ mockCreateUrlMap.mockReturnValueOnce(undefined as never)
207
+ const plugin = new devupApiWebpackPlugin()
208
+ const compiler = createMockCompiler()
209
+ const definePluginApplySpy = spyOn(
210
+ DefinePlugin.prototype,
211
+ 'apply',
212
+ ).mockImplementation(() => {})
213
+ plugin.apply(compiler)
214
+
215
+ const callback = compiler._storedCallback
216
+
217
+ const mockCallback = mock(() => {})
218
+ await callback?.(null, mockCallback)
219
+
220
+ expect(definePluginApplySpy).not.toHaveBeenCalled()
221
+ expect(mockCallback).toHaveBeenCalled()
222
+ definePluginApplySpy.mockRestore()
223
+ })
224
+
225
+ test('devupApiWebpackPlugin beforeCompile hook only runs once when called multiple times', async () => {
226
+ const plugin = new devupApiWebpackPlugin()
227
+ const compiler = createMockCompiler()
228
+ plugin.apply(compiler)
229
+
230
+ const callback = compiler._storedCallback
231
+
232
+ const mockCallback1 = mock(() => {})
233
+ const mockCallback2 = mock(() => {})
234
+
235
+ await Promise.all([
236
+ callback?.(null, mockCallback1),
237
+ callback?.(null, mockCallback2),
238
+ ])
239
+
240
+ expect(mockCreateTmpDirAsync).toHaveBeenCalledTimes(1)
241
+ expect(mockReadOpenapiAsync).toHaveBeenCalledTimes(1)
242
+ expect(mockGenerateInterface).toHaveBeenCalledTimes(1)
243
+ expect(mockWriteInterfaceAsync).toHaveBeenCalledTimes(1)
244
+ expect(mockCreateUrlMap).toHaveBeenCalledTimes(1)
245
+ expect(mockCallback1).toHaveBeenCalled()
246
+ expect(mockCallback2).toHaveBeenCalled()
247
+ })
248
+
249
+ test('devupApiWebpackPlugin beforeCompile hook handles errors correctly', async () => {
250
+ const error = new Error('Test error')
251
+ mockCreateTmpDirAsync.mockRejectedValueOnce(error)
252
+ const plugin = new devupApiWebpackPlugin()
253
+ const compiler = createMockCompiler()
254
+ plugin.apply(compiler)
255
+
256
+ const callback = compiler._storedCallback
257
+
258
+ const mockCallback = mock(() => {})
259
+ await callback?.(null, mockCallback)
260
+
261
+ expect(mockCallback).toHaveBeenCalledWith(error)
262
+ expect(plugin.initialized).toBe(false)
263
+ })
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './plugin'
2
+ export { devupApiWebpackPlugin as default } from './plugin'
package/src/plugin.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { join } from 'node:path'
2
+ import type { DevupApiOptions } from '@devup-api/core'
3
+ import { createUrlMap, generateInterface } from '@devup-api/generator'
4
+ import {
5
+ createTmpDirAsync,
6
+ readOpenapiAsync,
7
+ writeInterfaceAsync,
8
+ } from '@devup-api/utils'
9
+ import type { Compiler } from 'webpack'
10
+ import { DefinePlugin } from 'webpack'
11
+
12
+ export class devupApiWebpackPlugin {
13
+ options: DevupApiOptions
14
+ initialized = false
15
+
16
+ constructor(options?: DevupApiOptions) {
17
+ this.options = options || {}
18
+ }
19
+
20
+ apply(compiler: Compiler): void {
21
+ // Perform async operations before compilation
22
+ compiler.hooks.beforeCompile.tapAsync(
23
+ 'devup-api',
24
+ async (_params, callback) => {
25
+ // Guard: only run once
26
+ if (this.initialized) {
27
+ callback()
28
+ return
29
+ }
30
+
31
+ try {
32
+ this.initialized = true
33
+
34
+ const tempDir = await createTmpDirAsync(this.options?.tempDir)
35
+ const schema = await readOpenapiAsync(this.options?.openapiFile)
36
+
37
+ // Generate interface file
38
+ await writeInterfaceAsync(
39
+ join(tempDir, 'api.d.ts'),
40
+ generateInterface(schema, this.options),
41
+ )
42
+
43
+ // Create urlMap and set environment variable
44
+ const urlMap = createUrlMap(schema, this.options)
45
+ const define: Record<string, string> = {}
46
+ if (urlMap) {
47
+ define['process.env.DEVUP_API_URL_MAP'] = JSON.stringify(
48
+ JSON.stringify(urlMap),
49
+ )
50
+ }
51
+
52
+ // Add DefinePlugin to webpack configuration
53
+ if (Object.keys(define).length > 0) {
54
+ new DefinePlugin(define).apply(compiler)
55
+ }
56
+
57
+ callback()
58
+ } catch (error) {
59
+ this.initialized = false
60
+ callback(error as Error)
61
+ }
62
+ },
63
+ )
64
+ }
65
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "verbatimModuleSyntax": true,
14
+ "emitDeclarationOnly": true,
15
+
16
+ // Best practices
17
+ "strict": true,
18
+ "skipLibCheck": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noUncheckedIndexedAccess": true,
21
+ "noImplicitOverride": true,
22
+
23
+ // Some stricter flags (disabled by default)
24
+ "noUnusedLocals": false,
25
+ "noUnusedParameters": false,
26
+ "noPropertyAccessFromIndexSignature": false,
27
+ "declaration": true,
28
+ "declarationMap": true,
29
+ "outDir": "dist",
30
+ "rootDir": "src"
31
+ },
32
+ "include": ["src/**/*.ts"],
33
+ "exclude": ["dist", "node_modules"]
34
+ }