-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8d55448
commit 2bf1786
Showing
9 changed files
with
449 additions
and
223 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import fs from 'node:fs' | ||
import fsp from 'node:fs/promises' | ||
import path from 'node:path' | ||
import process from 'node:process' | ||
import fg from 'fast-glob' | ||
import { createFilter } from '@rollup/pluginutils' | ||
import color from 'picocolors' | ||
import { toArray } from '@pengzhanbo/utils' | ||
import type { ServerBuildOption } from '../types' | ||
import { lookupFile, normalizePath, packageDir } from './utils' | ||
import type { ResolvePluginOptions } from './resolvePluginOptions' | ||
import { transformWithRspack } from './createRspackCompiler' | ||
|
||
export async function buildMockServer( | ||
options: ResolvePluginOptions, | ||
outputDir: string, | ||
) { | ||
const entryFile = path.resolve(process.cwd(), 'node_modules/.cache/mock-server/mock-server.ts') | ||
const mockFileList = await getMockFileList(options) | ||
await writeMockEntryFile(entryFile, mockFileList, options.cwd) | ||
const { code, externals } = await transformWithRspack({ | ||
entryFile, | ||
cwd: options.cwd, | ||
plugins: options.plugins, | ||
alias: options.alias, | ||
}) | ||
await fsp.unlink(entryFile) | ||
const outputList: { filename: string, source: string }[] = [ | ||
{ filename: 'mock-data.js', source: code }, | ||
{ filename: 'index.js', source: generatorServerEntryCode(options) }, | ||
{ filename: 'package.json', source: generatePackageJson(options, externals) }, | ||
] | ||
const dist = path.resolve(outputDir, (options.build as ServerBuildOption).dist!) | ||
options.logger.info( | ||
`${color.green('✓')} generate mock server in ${color.cyan(path.relative(process.cwd(), dist))}`, | ||
) | ||
if (!fs.existsSync(dist)) { | ||
await fsp.mkdir(dist, { recursive: true }) | ||
} | ||
for (const { filename, source } of outputList) { | ||
await fsp.writeFile(path.join(dist, filename), source, 'utf8') | ||
const sourceSize = (source.length / 1024).toFixed(2) | ||
const space = filename.length < 24 ? ' '.repeat(24 - filename.length) : '' | ||
options.logger.info(` ${color.green(filename)}${space}${color.bold(color.dim(`${sourceSize} kB`))}`) | ||
} | ||
} | ||
|
||
function generatePackageJson(options: ResolvePluginOptions, externals: string[]): string { | ||
const deps = getHostDependencies(options.cwd) | ||
const { name, version } = getPluginPackageInfo() | ||
const mockPkg = { | ||
name: 'mock-server', | ||
type: 'module', | ||
scripts: { | ||
start: 'node index.js', | ||
}, | ||
dependencies: { | ||
connect: '^3.7.0', | ||
[name]: `^${version}`, | ||
cors: '^2.8.5', | ||
} as Record<string, string>, | ||
} | ||
externals.forEach((dep) => { | ||
mockPkg.dependencies[dep] = deps[dep] || 'latest' | ||
}) | ||
return JSON.stringify(mockPkg, null, 2) | ||
} | ||
|
||
function generatorServerEntryCode({ | ||
proxies, | ||
wsPrefix, | ||
cookiesOptions, | ||
bodyParserOptions, | ||
priority, | ||
build, | ||
}: ResolvePluginOptions): string { | ||
const { serverPort, log } = build as ServerBuildOption | ||
return `import { createServer } from 'node:http'; | ||
import connect from 'connect'; | ||
import corsMiddleware from 'cors'; | ||
import { | ||
baseMiddleware, | ||
createLogger, | ||
mockWebSocket, | ||
transformMockData, | ||
transformRawData | ||
} from 'rspack-plugin-mock'; | ||
import rawData from './mock-data.js'; | ||
const app = connect(); | ||
const server = createServer(app); | ||
const logger = createLogger('mock-server', '${log}'); | ||
const proxies = ${JSON.stringify(proxies)}; | ||
const wsProxies = ${JSON.stringify(toArray(wsPrefix))}; | ||
const cookiesOptions = ${JSON.stringify(cookiesOptions)}; | ||
const bodyParserOptions = ${JSON.stringify(bodyParserOptions)}; | ||
const priority = ${JSON.stringify(priority)}; | ||
const data = { mockData: transformMockData(transformRawData(rawData)) }; | ||
mockWebSocket(data, server, { wsProxies, cookiesOptions, logger }); | ||
app.use(corsMiddleware()); | ||
app.use(baseMiddleware(data, { | ||
formidableOptions: { multiples: true }, | ||
proxies, | ||
priority, | ||
cookiesOptions, | ||
bodyParserOptions, | ||
logger, | ||
})); | ||
server.listen(${serverPort}); | ||
console.log('listen: http://localhost:${serverPort}'); | ||
` | ||
} | ||
|
||
async function getMockFileList({ cwd, include, exclude }: { | ||
cwd: string | ||
include: string | string[] | ||
exclude: string | string[] | ||
}): Promise<string[]> { | ||
const filter = createFilter(include, exclude, { resolve: false }) | ||
return await fg(include, { cwd }).then(files => files.filter(filter)) | ||
} | ||
|
||
export async function writeMockEntryFile(entryFile: string, files: string[], cwd: string) { | ||
const importers: string[] = [] | ||
const exporters: string[] = [] | ||
for (const [index, filepath] of files.entries()) { | ||
const file = normalizePath(path.join(cwd, filepath)) | ||
importers.push(`import * as m${index} from '${file}'`) | ||
exporters.push(`[m${index}, '${filepath}']`) | ||
} | ||
const code = `${importers.join('\n')}\n\nexport default [\n ${exporters.join(',\n ')}\n]` | ||
const dirname = path.dirname(entryFile) | ||
|
||
if (!fs.existsSync(dirname)) { | ||
await fsp.mkdir(dirname, { recursive: true }) | ||
} | ||
await fsp.writeFile(entryFile, code, 'utf8') | ||
} | ||
|
||
function getPluginPackageInfo() { | ||
let pkg = {} as Record<string, any> | ||
try { | ||
const filepath = path.join(packageDir, '../package.json') | ||
if (fs.existsSync(filepath)) { | ||
pkg = JSON.parse(fs.readFileSync(filepath, 'utf8')) | ||
} | ||
} | ||
catch {} | ||
|
||
return { | ||
name: pkg.name || 'rspack-plugin-mock', | ||
version: pkg.version || 'latest', | ||
} | ||
} | ||
|
||
function getHostDependencies(context: string): Record<string, string> { | ||
let pkg = {} as Record<string, any> | ||
try { | ||
const content = lookupFile(context, ['package.json']) | ||
if (content) | ||
pkg = JSON.parse(content) | ||
} | ||
catch {} | ||
return { ...pkg.dependencies, ...pkg.devDependencies } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import path from 'node:path' | ||
import type { Compiler, RspackOptions, RspackPluginInstance } from '@rspack/core' | ||
import * as rspackCore from '@rspack/core' | ||
import color from 'picocolors' | ||
import isCore from 'is-core-module' | ||
import { packageDir, vfs } from './utils' | ||
|
||
export interface CompilerOptions { | ||
cwd: string | ||
isEsm?: boolean | ||
entryFile: string | ||
plugins: RspackPluginInstance[] | ||
alias?: Record<string, false | string | (string | false)[]> | ||
watch?: boolean | ||
} | ||
|
||
export function createCompiler( | ||
options: CompilerOptions, | ||
callback: (result: { code: string, externals: string[] }) => Promise<void> | void, | ||
): Compiler | null { | ||
const rspackOptions = resolveRspackOptions(options) | ||
const isWatch = rspackOptions.watch === true | ||
|
||
async function handler(err: Error | null, stats?: rspackCore.Stats) { | ||
const name = '[rspack:mock]' | ||
const logError = stats?.compilation.getLogger(name).error | ||
|| ((...args: string[]) => console.error(color.red(name), ...args)) | ||
|
||
if (err) { | ||
logError(err.stack || err) | ||
if ('details' in err) { | ||
logError(err.details) | ||
} | ||
return | ||
} | ||
|
||
if (stats?.hasErrors()) { | ||
const info = stats.toJson() | ||
logError(info.errors) | ||
} | ||
|
||
const code = vfs.readFileSync('/output.js', 'utf-8') as string | ||
const externals: string[] = [] | ||
|
||
if (!isWatch) { | ||
const modules = stats?.toJson().modules || [] | ||
const aliasList = Object.keys(options.alias || {}).map(key => key.replace(/\$$/g, '')) | ||
for (const { name } of modules) { | ||
if (name?.startsWith('external')) { | ||
const packageName = normalizePackageName(name) | ||
if (!isCore(packageName) && !aliasList.includes(packageName)) | ||
externals.push(normalizePackageName(name)) | ||
} | ||
} | ||
} | ||
|
||
await callback({ code, externals }) | ||
} | ||
|
||
const compiler = rspackCore.rspack(rspackOptions, isWatch ? handler : undefined) | ||
|
||
if (compiler) | ||
compiler.outputFileSystem = vfs | ||
|
||
if (!isWatch) { | ||
compiler?.run(async (...args) => { | ||
await handler(...args) | ||
compiler!.close(() => {}) | ||
}) | ||
} | ||
return compiler | ||
} | ||
|
||
export function transformWithRspack(options: Omit<CompilerOptions, 'watch'>): Promise<{ code: string, externals: string[] }> { | ||
return new Promise((resolve) => { | ||
createCompiler({ ...options, watch: false }, (result) => { | ||
resolve(result) | ||
}) | ||
}) | ||
} | ||
|
||
function normalizePackageName(name: string): string { | ||
const filepath = name.replace('external ', '').slice(1, -1) | ||
const [scope, packageName] = filepath.split('/') | ||
if (filepath[0] === '@') { | ||
return `${scope}/${packageName}` | ||
} | ||
return scope | ||
} | ||
|
||
function resolveRspackOptions({ | ||
cwd, | ||
isEsm = true, | ||
entryFile, | ||
plugins, | ||
alias, | ||
watch = false, | ||
}: CompilerOptions): RspackOptions { | ||
const targets = ['node >= 18.0.0'] | ||
return { | ||
mode: 'production', | ||
context: cwd, | ||
entry: entryFile, | ||
watch, | ||
target: 'node18.0', | ||
externalsType: isEsm ? 'module' : 'commonjs2', | ||
externals: /^[^./].*/, | ||
resolve: { | ||
alias, | ||
extensions: ['.js', '.ts', '.cjs', '.mjs', '.json5', '.json'], | ||
}, | ||
plugins, | ||
output: { | ||
library: { type: !isEsm ? 'commonjs2' : 'module' }, | ||
filename: 'output.js', | ||
path: '/', | ||
}, | ||
experiments: { outputModule: isEsm }, | ||
optimization: { minimize: !watch }, | ||
module: { | ||
rules: [ | ||
{ | ||
test: /\.json5?$/, | ||
loader: path.join(packageDir, 'json5-loader.cjs'), | ||
type: 'javascript/auto', | ||
}, | ||
{ | ||
test: /\.[cm]?js$/, | ||
use: [ | ||
{ | ||
loader: 'builtin:swc-loader', | ||
options: { | ||
jsc: { parser: { syntax: 'ecmascript' } }, | ||
env: { targets }, | ||
}, | ||
}, | ||
], | ||
}, | ||
{ | ||
test: /\.[cm]?ts$/, | ||
use: [ | ||
{ | ||
loader: 'builtin:swc-loader', | ||
options: { | ||
jsc: { parser: { syntax: 'typescript' } }, | ||
env: { targets }, | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
} | ||
} |
Oops, something went wrong.