Skip to content

Commit

Permalink
feat: add support for build
Browse files Browse the repository at this point in the history
  • Loading branch information
pengzhanbo committed Aug 5, 2024
1 parent 8d55448 commit 2bf1786
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 223 deletions.
169 changes: 169 additions & 0 deletions src/core/build.ts
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 }
}
154 changes: 154 additions & 0 deletions src/core/createRspackCompiler.ts
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 },
},
},
],
},
],
},
}
}
Loading

0 comments on commit 2bf1786

Please sign in to comment.