|
1 | | -import MiniCssExtractPlugin from 'mini-css-extract-plugin' |
2 | | -import path from 'pathe' |
3 | | -import { compile, getErrors, getMemfsCompiler5, getWarnings, readAssets } from 'webpack-build-utils' |
4 | | -import utwm from '@/webpack' |
5 | | - |
6 | | -const context = path.resolve(__dirname, 'fixtures/webpack-repo') |
7 | | -describe('webpack build', () => { |
8 | | - it('common', async () => { |
9 | | - const compiler = getMemfsCompiler5({ |
10 | | - mode: 'production', |
11 | | - entry: path.resolve(context, './src/index.js'), |
12 | | - context, |
13 | | - plugins: [new MiniCssExtractPlugin()], |
14 | | - output: { |
15 | | - path: path.resolve(context, './dist'), |
16 | | - filename: 'index.js', |
| 1 | +import type { Mock } from 'vitest' |
| 2 | +import { describe, expect, it, beforeEach, vi } from 'vitest' |
| 3 | +import factory from '@/core/factory' |
| 4 | + |
| 5 | +const { mockCtx, mockCssHandler, mockHtmlHandler, mockJsHandler } = vi.hoisted(() => { |
| 6 | + return { |
| 7 | + mockCtx: { |
| 8 | + options: { |
| 9 | + sources: {}, |
17 | 10 | }, |
18 | | - module: { |
19 | | - rules: [ |
20 | | - { |
21 | | - test: /\.css$/i, |
22 | | - type: 'javascript/auto', |
23 | | - use: [ |
24 | | - MiniCssExtractPlugin.loader, |
25 | | - { |
26 | | - loader: 'css-loader', |
27 | | - options: { |
28 | | - url: false, |
29 | | - }, |
30 | | - }, |
31 | | - 'postcss-loader', |
32 | | - ], |
33 | | - }, |
34 | | - ], |
| 11 | + initConfig: vi.fn(), |
| 12 | + replaceMap: new Map(), |
| 13 | + classGenerator: { |
| 14 | + generateClassName: vi.fn(), |
35 | 15 | }, |
36 | | - }) |
37 | | - const stats = await compile(compiler) |
38 | | - |
39 | | - // get all Assets as Record<string,string> |
40 | | - const assets = readAssets(compiler, stats) |
41 | | - const cssAssetName = Object.keys(assets).find(name => name.endsWith('.css')) |
42 | | - expect(cssAssetName).toBeDefined() |
43 | | - expect(typeof assets[cssAssetName!]).toBe('string') |
44 | | - expect(assets[cssAssetName!].length).toBeGreaterThan(0) |
45 | | - expect(assets['index.js']).toBeDefined() |
46 | | - // get all error |
47 | | - expect(getErrors(stats)).toEqual([]) |
48 | | - // get all warnings |
49 | | - expect(getWarnings(stats)).toEqual([]) |
| 16 | + addToUsedBy: vi.fn(), |
| 17 | + isPreserveClass: vi.fn(() => false), |
| 18 | + isPreserveFunction: vi.fn(() => false), |
| 19 | + addPreserveClass: vi.fn(), |
| 20 | + dump: vi.fn(), |
| 21 | + }, |
| 22 | + mockCssHandler: vi.fn(async (source: string) => ({ code: `/*handled*/${source}` })), |
| 23 | + mockHtmlHandler: vi.fn(() => ({ code: '<html></html>' })), |
| 24 | + mockJsHandler: vi.fn(() => ({ code: 'export {}' })), |
| 25 | + } |
| 26 | +}) |
| 27 | + |
| 28 | +vi.mock('@tailwindcss-mangle/core', () => { |
| 29 | + const Context = vi.fn(function ContextMock() { |
| 30 | + return mockCtx |
50 | 31 | }) |
| 32 | + return { |
| 33 | + Context, |
| 34 | + cssHandler: mockCssHandler, |
| 35 | + htmlHandler: mockHtmlHandler, |
| 36 | + jsHandler: mockJsHandler, |
| 37 | + } |
| 38 | +}) |
| 39 | + |
| 40 | +import { Context } from '@tailwindcss-mangle/core' |
| 41 | + |
| 42 | +function getLatestCtx() { |
| 43 | + return (Context as unknown as Mock).mock.results.at(-1)?.value ?? mockCtx |
| 44 | +} |
| 45 | + |
| 46 | +vi.mock('@/utils', async (importOriginal) => { |
| 47 | + const actual = await importOriginal<typeof import('@/utils')>() |
| 48 | + return { |
| 49 | + ...actual, |
| 50 | + getGroupedEntries: vi.fn((entries: [string, any][]) => { |
| 51 | + const css = entries.filter(([name]) => name.endsWith('.css')) |
| 52 | + const js = entries.filter(([name]) => name.endsWith('.js')) |
| 53 | + const html = entries.filter(([name]) => name.endsWith('.html')) |
| 54 | + return { css, js, html } |
| 55 | + }), |
| 56 | + } |
| 57 | +}) |
51 | 58 |
|
52 | | - it.skip('with plugin', async () => { |
53 | | - const compiler = getMemfsCompiler5({ |
54 | | - mode: 'production', |
55 | | - entry: path.resolve(context, './src/index.js'), |
56 | | - context, |
57 | | - plugins: [new MiniCssExtractPlugin()], |
58 | | - output: { |
59 | | - path: path.resolve(context, './dist'), |
60 | | - filename: 'index.js', |
| 59 | +function createSyncHook<TArgs extends any[] = any[]>() { |
| 60 | + const taps: Array<(...args: TArgs) => any> = [] |
| 61 | + return { |
| 62 | + tap(_name: string, fn: (...args: TArgs) => any) { |
| 63 | + taps.push(fn) |
| 64 | + }, |
| 65 | + call(...args: TArgs) { |
| 66 | + taps.forEach(fn => fn(...args)) |
| 67 | + }, |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +function createAsyncHook<TArgs extends any[] = any[]>() { |
| 72 | + const taps: Array<(...args: TArgs) => Promise<any>> = [] |
| 73 | + return { |
| 74 | + tapPromise(_opts: any, fn: (...args: TArgs) => Promise<any>) { |
| 75 | + taps.push(fn) |
| 76 | + }, |
| 77 | + async promise(...args: TArgs) { |
| 78 | + for (const fn of taps) { |
| 79 | + await fn(...args) |
| 80 | + } |
| 81 | + }, |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +function createFakeCompiler() { |
| 86 | + const compilationHook = createSyncHook<any[]>() |
| 87 | + const loaderHook = createSyncHook<any[]>() |
| 88 | + const processAssetsHook = createAsyncHook<any[]>() |
| 89 | + |
| 90 | + const updates: Array<[string, any]> = [] |
| 91 | + |
| 92 | + const compiler = { |
| 93 | + webpack: { |
| 94 | + NormalModule: { |
| 95 | + getCompilationHooks: () => ({ |
| 96 | + loader: loaderHook, |
| 97 | + }), |
61 | 98 | }, |
62 | | - module: { |
63 | | - rules: [ |
64 | | - { |
65 | | - test: /\.css$/i, |
66 | | - type: 'javascript/auto', |
67 | | - use: [ |
68 | | - MiniCssExtractPlugin.loader, |
69 | | - { |
70 | | - loader: 'css-loader', |
71 | | - options: { |
72 | | - url: false, |
73 | | - }, |
74 | | - }, |
75 | | - 'postcss-loader', |
76 | | - ], |
77 | | - }, |
78 | | - ], |
| 99 | + Compilation: { |
| 100 | + PROCESS_ASSETS_STAGE_SUMMARIZE: 0, |
79 | 101 | }, |
80 | | - }) |
81 | | - |
82 | | - utwm({ |
83 | | - registry: { |
84 | | - file: path.resolve(context, '.tw-patch/tw-class-list.json'), |
| 102 | + sources: { |
| 103 | + ConcatSource: class { |
| 104 | + code: string |
| 105 | + constructor(code: string) { |
| 106 | + this.code = code |
| 107 | + } |
| 108 | + toString() { |
| 109 | + return this.code |
| 110 | + } |
| 111 | + source() { |
| 112 | + return this.code |
| 113 | + } |
| 114 | + }, |
85 | 115 | }, |
86 | | - }).apply(compiler) |
87 | | - const stats = await compile(compiler) |
88 | | - |
89 | | - // get all Assets as Record<string,string> |
90 | | - const assets = readAssets(compiler, stats) |
91 | | - const cssAssetName = Object.keys(assets).find(name => name.endsWith('.css')) |
92 | | - expect(cssAssetName).toBeDefined() |
93 | | - expect(assets[cssAssetName!]).toContain('.tw-') |
94 | | - expect(assets['index.js']).toBeDefined() |
95 | | - // get all error |
96 | | - expect(getErrors(stats)).toEqual([]) |
97 | | - // get all warnings |
98 | | - expect(getWarnings(stats)).toEqual([]) |
| 116 | + }, |
| 117 | + hooks: { |
| 118 | + compilation: compilationHook, |
| 119 | + }, |
| 120 | + triggerCompilation(initial: Record<string, any> = {}) { |
| 121 | + const compilation = { |
| 122 | + hooks: { |
| 123 | + processAssets: processAssetsHook, |
| 124 | + }, |
| 125 | + updateAsset: vi.fn((...args) => { |
| 126 | + updates.push(args as unknown as [string, any]) |
| 127 | + }), |
| 128 | + ...initial, |
| 129 | + } |
| 130 | + compilationHook.call(compilation) |
| 131 | + return { compilation, loaderHook, processAssetsHook, updates } |
| 132 | + }, |
| 133 | + } |
| 134 | + |
| 135 | + return compiler |
| 136 | +} |
| 137 | + |
| 138 | +describe('webpack plugin integration (unit)', () => { |
| 139 | + beforeEach(() => { |
| 140 | + vi.clearAllMocks() |
| 141 | + mockCtx.replaceMap = new Map() |
99 | 142 | }) |
100 | 143 |
|
101 | | - // webpack({}, (err, stats) => { |
102 | | - // if (err || stats.hasErrors()) { |
103 | | - // // ... |
104 | | - // } |
105 | | - // // Done processing |
106 | | - // }); |
| 144 | + it('injects the webpack loader before postcss-loader', () => { |
| 145 | + const compiler = createFakeCompiler() |
| 146 | + const [, mainPlugin, postPlugin] = factory() as any[] |
| 147 | + mainPlugin.webpack?.(compiler as any) |
| 148 | + postPlugin.webpack?.(compiler as any) |
| 149 | + |
| 150 | + const { loaderHook } = compiler.triggerCompilation() |
| 151 | + const module = { |
| 152 | + loaders: [ |
| 153 | + { loader: 'style-loader' }, |
| 154 | + { loader: 'postcss-loader' }, |
| 155 | + ], |
| 156 | + } |
| 157 | + |
| 158 | + loaderHook.call({}, module) |
| 159 | + |
| 160 | + expect(module.loaders).toHaveLength(3) |
| 161 | + const inserted = module.loaders[1] |
| 162 | + expect(String(inserted.loader)).toContain('loader.cjs') |
| 163 | + expect(inserted.options?.ctx).toBe(getLatestCtx()) |
| 164 | + }) |
| 165 | + |
| 166 | + it('skips injection when postcss-loader is missing', () => { |
| 167 | + const compiler = createFakeCompiler() |
| 168 | + const [, mainPlugin, postPlugin] = factory() as any[] |
| 169 | + mainPlugin.webpack?.(compiler as any) |
| 170 | + postPlugin.webpack?.(compiler as any) |
| 171 | + |
| 172 | + const { loaderHook } = compiler.triggerCompilation() |
| 173 | + const module = { |
| 174 | + loaders: [{ loader: 'css-loader' }], |
| 175 | + } |
| 176 | + |
| 177 | + loaderHook.call({}, module) |
| 178 | + |
| 179 | + expect(module.loaders).toHaveLength(1) |
| 180 | + }) |
| 181 | + |
| 182 | + it('transforms css assets during processAssets', async () => { |
| 183 | + const compiler = createFakeCompiler() |
| 184 | + const [, mainPlugin, postPlugin] = factory() as any[] |
| 185 | + mainPlugin.webpack?.(compiler as any) |
| 186 | + postPlugin.webpack?.(compiler as any) |
| 187 | + |
| 188 | + const { compilation, processAssetsHook, updates } = compiler.triggerCompilation() |
| 189 | + const assets = { |
| 190 | + 'style.css': { |
| 191 | + source: () => ({ |
| 192 | + toString: () => 'body { color: red; }', |
| 193 | + }), |
| 194 | + }, |
| 195 | + 'index.js': { |
| 196 | + source: () => ({ |
| 197 | + toString: () => 'console.log("noop")', |
| 198 | + }), |
| 199 | + }, |
| 200 | + } |
| 201 | + |
| 202 | + await processAssetsHook.promise(assets) |
| 203 | + |
| 204 | + expect(mockCssHandler).toHaveBeenCalledTimes(1) |
| 205 | + expect(mockCssHandler).toHaveBeenCalledWith('body { color: red; }', { |
| 206 | + id: 'style.css', |
| 207 | + ctx: getLatestCtx(), |
| 208 | + }) |
| 209 | + expect(compilation.updateAsset).toHaveBeenCalledTimes(1) |
| 210 | + expect(updates[0][0]).toBe('style.css') |
| 211 | + expect(String(updates[0][1])).toContain('/*handled*/body { color: red; }') |
| 212 | + }) |
107 | 213 | }) |
0 commit comments