@tarojs/rn-runner 3.5.7-alpha.9 → 3.5.8

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,114 @@
1
+ import { readFile } from 'fs'
2
+ import { createServer } from 'http'
3
+ import * as mime from 'mime-types'
4
+ import { extname, join } from 'path'
5
+ import * as qr from 'qrcode-terminal'
6
+ import { URL } from 'url'
7
+
8
+ import { getOpenHost, isWin, PLAYGROUNDINFO } from '../utils'
9
+
10
+ interface PreviewOption {
11
+ out: string
12
+ platform: string
13
+ assetsDest?: string
14
+ }
15
+
16
+ const drawableFileTypes = new Set<string>([
17
+ 'gif',
18
+ 'jpeg',
19
+ 'jpg',
20
+ 'png',
21
+ 'webp',
22
+ 'xml'
23
+ ])
24
+
25
+ function getAndroidAssetSuffix (scale: number): string {
26
+ switch (scale) {
27
+ case 0.75:
28
+ return 'ldpi'
29
+ case 1:
30
+ return 'mdpi'
31
+ case 1.5:
32
+ return 'hdpi'
33
+ case 2:
34
+ return 'xhdpi'
35
+ case 3:
36
+ return 'xxhdpi'
37
+ case 4:
38
+ return 'xxxhdpi'
39
+ default:
40
+ return ''
41
+ }
42
+ }
43
+
44
+ function getAndroidResourceFolderName (pathname:string): string {
45
+ const ext = extname(pathname).replace(/^./, '').toLowerCase()
46
+ if (!drawableFileTypes.has(ext)) {
47
+ return 'raw'
48
+ }
49
+ const suffix = getAndroidAssetSuffix(1) // TODO: auto scale
50
+ const androidFolder = `drawable-${suffix}`
51
+ return androidFolder
52
+ }
53
+
54
+ function getAndroidResourceIdentifier (pathname:string): string {
55
+ if (pathname[0] === '/') {
56
+ pathname = pathname.substr(1)
57
+ }
58
+ const ext = extname(pathname).toLowerCase()
59
+ const extReg = new RegExp(ext + '$')
60
+ return pathname
61
+ .replace(extReg, '')
62
+ .toLowerCase()
63
+ .replace(/\//g, '_')
64
+ .replace(/([^a-z0-9_])/g, '')
65
+ .replace(/^assets_/, '') + ext
66
+ }
67
+
68
+ export default (opt: PreviewOption):void => {
69
+ const port = process.env.PORT || 8079
70
+ const host = `http://${getOpenHost()}:${port}`
71
+
72
+ createServer(function (request, response) {
73
+ const url = new URL(request.url || '', host)
74
+
75
+ console.log(`${request.method} ${request.url}`)
76
+
77
+ if (url.pathname === '/inspector/device') {
78
+ response.writeHead(404)
79
+ response.end('404', 'utf-8')
80
+ return
81
+ }
82
+
83
+ let filePath
84
+ const contentType = mime.lookup(url.pathname)
85
+
86
+ if (url.pathname === '/index.js') {
87
+ filePath = opt.out
88
+ } else if (opt.platform === 'ios') {
89
+ filePath = join(opt.assetsDest || '', url.pathname)
90
+ } else if (opt.platform === 'android') {
91
+ filePath = join(opt.assetsDest || '', getAndroidResourceFolderName(url.pathname), getAndroidResourceIdentifier(url.pathname))
92
+ }
93
+
94
+ readFile(filePath, function (error, content) {
95
+ if (error) {
96
+ if (error.code === 'ENOENT') {
97
+ response.writeHead(404)
98
+ response.end('404', 'utf-8')
99
+ } else {
100
+ response.writeHead(500)
101
+ response.end('500', 'utf-8')
102
+ }
103
+ } else {
104
+ response.writeHead(200, { 'Content-Type': contentType })
105
+ response.end(content, 'utf-8')
106
+ }
107
+ })
108
+ }).listen(port)
109
+
110
+ const url = `${host}/index.js`
111
+ console.log(PLAYGROUNDINFO)
112
+ console.log(`print qrcode of ${opt.platform} bundle '${url}':`)
113
+ qr.generate(url, { small: !isWin })
114
+ }
@@ -0,0 +1,96 @@
1
+ import { emptyModulePath } from '@tarojs/rn-supporter'
2
+ import * as MetroTerminalReporter from 'metro/src/lib/TerminalReporter'
3
+ import { Terminal } from 'metro-core'
4
+
5
+ export class TerminalReporter {
6
+ _reporter: any
7
+ _conditionalFileStore: any
8
+ metroServerInstance: any
9
+ _initialized: boolean
10
+ _entry: string
11
+ _sourceRoot: string
12
+
13
+ constructor (entry: string, sourceRoot: string, conditionalFileStore: any, metroServerInstance?: any) {
14
+ this._reporter = new MetroTerminalReporter(new Terminal(process.stdout))
15
+ this._conditionalFileStore = conditionalFileStore
16
+ this.metroServerInstance = metroServerInstance
17
+ this._initialized = false
18
+ this._entry = entry
19
+ this._sourceRoot = sourceRoot
20
+ }
21
+
22
+ async update (args) {
23
+ // 当依赖图加载之后,检测app和页面配置文件的变化
24
+ switch (args.type) {
25
+ case 'initialize_started':
26
+ this._reporter.terminal.log(`
27
+ ##### ## ##### #### ##### ###### ## #### ##### # # ## ##### # # # ######
28
+ # # # # # # # # # # # # # # # ## # # # # # # # #
29
+ # # # # # # # # # ##### # # # # # # # # # # # # # #####
30
+ # ###### ##### # # ##### # ###### # # # # # ###### # # # # #
31
+ # # # # # # # # # # # # # # # # ## # # # # # # #
32
+ # # # # # #### # # ###### # # #### # # # # # # # ## ######
33
+ `)
34
+ break
35
+ case 'bundle_build_started':
36
+ args.bundleDetails.entryFile = './index'
37
+ this._reporter.update(args)
38
+ break
39
+ case 'bundle_build_done': {
40
+ this._reporter.update(args)
41
+ const realEntryPath = require.resolve(emptyModulePath)
42
+ if (this._initialized) {
43
+ // 恢复入口页面的缓存
44
+ this._reporter.ignoreEntryFileCache = false
45
+ return
46
+ }
47
+ this._initialized = true
48
+ if (!this.metroServerInstance) {
49
+ return
50
+ }
51
+ const incrementalBundler = this.metroServerInstance.getBundler()
52
+ const deltaBundler = incrementalBundler.getDeltaBundler()
53
+ const bundler = incrementalBundler.getBundler()
54
+ const findEntryGraphId = keys => {
55
+ for (const k of keys) {
56
+ if (JSON.parse(k).entryFile === realEntryPath) {
57
+ return k
58
+ }
59
+ }
60
+ return null
61
+ }
62
+ // 获取入口文件的graph
63
+ const entryGraphId = findEntryGraphId(incrementalBundler._revisionsByGraphId.keys())
64
+ const entryGraphVersion = await incrementalBundler.getRevisionByGraphId(entryGraphId)
65
+
66
+ // 监听DeltaCalculator的change事件,把入口文件也加入到_modifiedFiles集合中
67
+ bundler.getDependencyGraph().then(dependencyGraph => {
68
+ dependencyGraph.getWatcher().on('change', ({ eventsQueue }) => {
69
+ const changedFiles = eventsQueue.map(item => item.filePath)
70
+ // 如果配置文件修改之后,把入口文件添加到修改列表中
71
+ const deltaCalculator = deltaBundler._deltaCalculators.get(entryGraphVersion.graph)
72
+ const isConfigurationModified = keys => {
73
+ for (const k of keys) {
74
+ if (k.includes('.config') && k.includes(this._sourceRoot)) {
75
+ return true
76
+ }
77
+ }
78
+ return false
79
+ }
80
+ if (isConfigurationModified(changedFiles)) {
81
+ // 忽略入口文件的转译结果缓存
82
+ this._conditionalFileStore.ignoreEntryFileCache = true
83
+ deltaCalculator._modifiedFiles.add(realEntryPath)
84
+ this._reporter.terminal.flush()
85
+ console.log('\nConfiguration(s) are changed.')
86
+ }
87
+ })
88
+ })
89
+ }
90
+ break
91
+ default:
92
+ this._reporter.update(args)
93
+ break
94
+ }
95
+ }
96
+ }
package/src/index.ts CHANGED
@@ -1,44 +1,104 @@
1
- import { previewDev, previewProd } from '@tarojs/rn-supporter'
2
- import { spawn } from 'child_process'
3
- import { constants, copyFile } from 'fs'
1
+ import saveAssets from '@react-native-community/cli-plugin-metro/build/commands/bundle/saveAssets'
2
+ import { createDevServerMiddleware } from '@react-native-community/cli-server-api'
3
+ import { PLATFORMS } from '@tarojs/helper'
4
4
  import * as fse from 'fs-extra'
5
- import { dirname, join } from 'path'
5
+ import * as Metro from 'metro'
6
+ import { getResolveDependencyFn } from 'metro/src/lib/transformHelpers'
7
+ import * as Server from 'metro/src/Server'
8
+ import * as outputBundle from 'metro/src/shared/output/bundle'
9
+ import * as path from 'path'
10
+ import * as qr from 'qrcode-terminal'
11
+ import * as readline from 'readline'
12
+ import * as url from 'url'
6
13
 
14
+ import getMetroConfig from './config'
7
15
  import buildComponent from './config/build-component'
16
+ import { getRNConfigEntry } from './config/config-holder'
17
+ import preview from './config/preview'
18
+ import { TerminalReporter } from './config/terminal-reporter'
19
+ import { getOpenHost, isWin, PLAYGROUNDINFO } from './utils'
8
20
 
9
- // 确认根目录下 metro.config.js index.js 是否存在
10
- const files = ['metro.config.js', 'index.js']
11
- function confirmFiles () {
12
- files.forEach(file => {
13
- const filePath = join(process.cwd(), file)
14
- copyFile(join(__dirname, '..', 'templates', file), filePath, constants.COPYFILE_EXCL, err => {
15
- if (err) {
16
- if (err.code !== 'EEXIST') {
17
- // 不重复生成配置文件
18
- console.log(err)
19
- }
20
- } else {
21
- console.log(`${file} created`)
21
+ function concatOutputFileName (config: any): string {
22
+ // 优先级:--bundle-output > config.output > config.outputRoot
23
+ let output = path.join(config.outputRoot, 'index.bundle')
24
+ if (config.output) {
25
+ const outputType = typeof config.output
26
+ if (outputType === 'string') {
27
+ output = config.output
28
+ } else if (outputType === 'object') {
29
+ output = config.output[config.deviceType]
30
+ if (!output) {
31
+ console.error(`lack value for 'rn.output' configuration with platform '${config.deviceType}': ${JSON.stringify(config.output)}`)
22
32
  }
23
- })
24
- })
33
+ } else {
34
+ console.error(`invalid value for 'rn.output' configuration: ${JSON.stringify(config.output)}`)
35
+ }
36
+ }
37
+ if (config.bundleOutput) {
38
+ output = config.bundleOutput
39
+ }
40
+ const res = path.isAbsolute(output) ? output : path.join('.', output)
41
+ fse.ensureDirSync(path.dirname(res))
42
+ return res
25
43
  }
26
44
 
27
- const isWin = /^win/.test(process.platform)
28
- const npxCmd = isWin ? 'npx.cmd' : 'npx'
45
+ function concatOutputAssetsDest (config: any): string | undefined {
46
+ // 优先级:--assets-dest > config.output > config.outputRoot
47
+ let assetDest
48
+ if (!config?.deviceType || !config?.output) {
49
+ assetDest = config.outputRoot
50
+ } else {
51
+ assetDest = config.deviceType === 'ios' ? config.output.iosAssetsDest : config.output.androidAssetsDest
52
+ }
53
+ if (config.assetsDest) {
54
+ assetDest = config.assetsDest
55
+ }
56
+ if (!assetDest) return undefined
57
+ const res = path.isAbsolute(assetDest) ? assetDest : path.join('.', assetDest)
58
+ fse.ensureDirSync(path.dirname(res))
59
+ return res
60
+ }
29
61
 
30
- export default async function build (_appPath: string, config: any): Promise<any> {
31
- process.env.TARO_ENV = 'rn'
62
+ function getOutputSourceMapOption (config: any): Record<string, any> {
63
+ if (!config?.deviceType) {
64
+ return {}
65
+ }
32
66
  const isIos = config.deviceType === 'ios'
33
- const cliParams:string[] = []
34
- config.output = config.output || {}
35
- // cli & config 参数透传
67
+ const sourceMapUrl = config.sourceMapUrl || (isIos ? config?.output?.iosSourceMapUrl : config?.output?.androidSourceMapUrl)
68
+ const sourcemapOutput = config.sourcemapOutput || (isIos ? config?.output?.iosSourcemapOutput : config?.output?.androidSourcemapOutput)
69
+ const sourcemapSourcesRoot = config.sourcemapSourcesRoot || (isIos ? config?.output?.iosSourcemapSourcesRoot : config?.output?.androidSourcemapSourcesRoot)
70
+ sourcemapOutput && fse.ensureDirSync(path.dirname(sourcemapOutput))
71
+ return {
72
+ sourceMapUrl,
73
+ sourcemapOutput,
74
+ sourcemapSourcesRoot
75
+ }
76
+ }
77
+
78
+ // TODO: 返回值
79
+ // HttpServer | {code: string, map: string}
80
+ // IBuildConfig
81
+ export default async function build (_appPath: string, config: any): Promise<any> {
82
+ process.env.TARO_ENV = PLATFORMS.RN
83
+ // TODO:新增环境变量是否可以在metro构建过程中可以访问到?
84
+ const entry = getRNConfigEntry()
85
+ config.entry = entry
86
+ const metroConfig = await getMetroConfig(config)
87
+ const sourceRoot = config.sourceRoot || 'src'
88
+
89
+ const commonOptions = {
90
+ platform: config.deviceType,
91
+ minify: process.env.NODE_ENV === 'production' || !config.isWatch,
92
+ dev: config.isWatch
93
+ }
36
94
  if (config.resetCache) {
37
- cliParams.push('--reset-cache')
95
+ metroConfig.resetCache = config.resetCache
38
96
  }
39
97
  if (config.publicPath) {
40
- cliParams.push('--public-path', config.publicPath)
98
+ metroConfig.transformer.publicPath = config.publicPath
41
99
  }
100
+ metroConfig.reporter = new TerminalReporter(entry, sourceRoot, metroConfig.cacheStores[0])
101
+
42
102
  const onFinish = function (error?) {
43
103
  if (typeof config.onBuildFinish === 'function') {
44
104
  config.onBuildFinish({
@@ -48,79 +108,145 @@ export default async function build (_appPath: string, config: any): Promise<any
48
108
  }
49
109
  if (error instanceof Error) throw error
50
110
  }
111
+
51
112
  if (config.isBuildNativeComp) {
52
113
  return buildComponent(
53
114
  _appPath,
54
115
  config
55
116
  )
56
- }
57
- confirmFiles()
58
- if (config.isWatch) {
117
+ } else if (config.isWatch) {
118
+ if (!metroConfig.server || (metroConfig.server.useGlobalHotkey === undefined)) {
119
+ if (!metroConfig.server) {
120
+ metroConfig.server = {}
121
+ }
122
+ metroConfig.server.useGlobalHotkey = true
123
+ }
59
124
  if (config.port) {
60
- cliParams.push('--port', config.port)
125
+ metroConfig.server.port = config.port
61
126
  }
62
- try {
63
- spawn(npxCmd, ['react-native', 'start'].concat(cliParams), {
64
- stdio: 'inherit'
127
+
128
+ const {
129
+ middleware,
130
+ messageSocketEndpoint,
131
+ websocketEndpoints
132
+ } = createDevServerMiddleware({
133
+ port: metroConfig.server.port,
134
+ watchFolders: metroConfig.watchFolders
135
+ })
136
+ metroConfig.server.enhanceMiddleware = (metroMiddleware, metroServer) => {
137
+ metroConfig.reporter.metroServerInstance = metroServer
138
+
139
+ // bundle路由只识别/index.bundle
140
+ return middleware.use((req, res, next) => {
141
+ // eslint-disable-next-line node/no-deprecated-api
142
+ const urlObj = url.parse(req.url)
143
+ if (/\/[^]+.bundle/.test(urlObj.pathname || '') && (urlObj.pathname || '').toLowerCase() !== '/index.bundle') {
144
+ res.writeHead(400)
145
+ res.end('Please access /index.bundle for entry bundling.')
146
+ } else if (/^\/debugger-ui\//.test(urlObj.pathname || '')) {
147
+ next()
148
+ } else {
149
+ metroMiddleware(req, res, next)
150
+ }
65
151
  })
66
- if(config.qr) {
67
- previewDev({
68
- port: parseInt(config.port) || 8081,
69
- })
152
+ }
153
+
154
+ // 支持host
155
+ return Metro.runServer(metroConfig, {
156
+ ...commonOptions,
157
+ hmrEnabled: true,
158
+ websocketEndpoints
159
+ }).then(server => {
160
+ console.log(`React-Native Dev server is running on port: ${metroConfig.server.port}`)
161
+ console.log('\n\nTo reload the app press "r"\nTo open developer menu press "d"\n')
162
+
163
+ readline.emitKeypressEvents(process.stdin)
164
+ process.stdin.setRawMode && process.stdin.setRawMode(true)
165
+ process.stdin.on('keypress', (_key, data) => {
166
+ const { ctrl, name } = data
167
+ if (name === 'r') {
168
+ messageSocketEndpoint.broadcast('reload')
169
+ console.log('Reloading app...')
170
+ } else if (name === 'd') {
171
+ messageSocketEndpoint.broadcast('devMenu')
172
+ console.log('Opening developer menu...')
173
+ } else if (ctrl && (name === 'c')) {
174
+ process.exit()
175
+ }
176
+ })
177
+
178
+ if (config.qr) {
179
+ const host = getOpenHost()
180
+ if (host) {
181
+ const url = `taro://${host}:${metroConfig.server.port}`
182
+ console.log(PLAYGROUNDINFO)
183
+ console.log(`print qrcode of '${url}':`)
184
+ qr.generate(url, { small: !isWin })
185
+ } else {
186
+ console.log('print qrcode error: host not found.')
187
+ }
70
188
  }
71
189
  onFinish(null)
72
- } catch(e) {
190
+ return server
191
+ }).catch(e => {
73
192
  onFinish(e)
74
- }
193
+ })
75
194
  } else {
76
- const defaultOutputDir = join(process.cwd(), config.outputRoot || 'dist')
77
- const defaultBundleOutput = join(defaultOutputDir, 'index.bundle')
78
- const bundleOutput = (config.bundleOutput ? config.bundleOutput : (isIos ? config.output.ios : config.output.android)) || defaultBundleOutput
79
- fse.ensureDirSync(dirname(bundleOutput))
80
- cliParams.push('--bundle-output', bundleOutput)
81
-
82
- const sourcemapOutput = config.sourcemapOutput ? config.sourcemapOutput : (isIos ? config.output.iosSourcemapOutput : config.output.androidSourcemapOutput)
83
- if (sourcemapOutput) {
84
- cliParams.push('--sourcemap-output', sourcemapOutput)
195
+ const options = {
196
+ ...commonOptions,
197
+ entry: './index',
198
+ out: concatOutputFileName(config)
85
199
  }
86
- const sourceMapUrl = config.sourceMapUrl ? config.sourceMapUrl : (isIos ? config.output.iosSourceMapUrl : config.output.androidSourceMapUrl)
87
- if (sourceMapUrl) {
88
- cliParams.push('--sourcemap-use-absolute-path', sourceMapUrl)
200
+ const savedBuildFunc = outputBundle.build
201
+ outputBundle.build = async (packagerClient, requestOptions) => {
202
+ const resolutionFn = await getResolveDependencyFn(packagerClient.getBundler().getBundler(), requestOptions.platform)
203
+ // try for test case build_noWatch
204
+ try {
205
+ requestOptions.entryFile = resolutionFn(metroConfig.projectRoot, requestOptions.entryFile)
206
+ } catch (e) {} // eslint-disable-line no-empty
207
+ return savedBuildFunc(packagerClient, requestOptions)
89
208
  }
90
209
 
91
- const sourcemapSourcesRoot = config.sourcemapSourcesRoot ? config.sourcemapSourcesRoot : (isIos ? config.output.iosSourcemapSourcesRoot : config.output.androidSourcemapSourcesRoot)
92
- if (sourcemapSourcesRoot) {
93
- cliParams.push('--sourcemap-sources-root', sourcemapSourcesRoot)
94
- }
210
+ const server = new Server(metroConfig)
95
211
 
96
- const assetsDest = (config.assetsDest ? config.assetsDest : (isIos ? config.output.iosAssetsDest : config.output.androidAssetsDest)) || defaultOutputDir
97
- cliParams.push('--assets-dest', assetsDest)
212
+ const sourceMapOption = getOutputSourceMapOption(config)
98
213
 
99
214
  try {
100
- spawn(npxCmd, [
101
- 'react-native',
102
- 'bundle',
103
- '--platform',
104
- config.deviceType,
105
- '--dev',
106
- 'false',
107
- '--entry-file',
108
- 'index.js'
109
- ].concat(cliParams), {
110
- stdio: 'inherit'
215
+ const requestOptions = {
216
+ ...commonOptions,
217
+ ...sourceMapOption,
218
+ entryFile: options.entry,
219
+ inlineSourceMap: false,
220
+ createModuleIdFactory: metroConfig.serializer.createModuleIdFactory
221
+ }
222
+ const bundle = await outputBundle.build(server, requestOptions)
223
+ const outputOptions = {
224
+ ...commonOptions,
225
+ ...sourceMapOption,
226
+ bundleOutput: options.out
227
+ }
228
+ await outputBundle.save(bundle, outputOptions, console.log)
229
+
230
+ // Save the assets of the bundle
231
+ const outputAssets = await server.getAssets({
232
+ ...Server.DEFAULT_BUNDLE_OPTIONS,
233
+ ...requestOptions
111
234
  })
112
- if(config.qr) {
113
- process.on('beforeExit', () => {
114
- previewProd({
115
- out: bundleOutput,
116
- platform: config.deviceType,
117
- assetsDest: assetsDest,
235
+ const assetsDest = concatOutputAssetsDest(config)
236
+ return await saveAssets(outputAssets, options.platform, assetsDest).then(() => {
237
+ if (config.qr) {
238
+ preview({
239
+ out: options.out,
240
+ assetsDest,
241
+ platform: options.platform
118
242
  })
119
- })
120
- }
121
- onFinish(null)
122
- } catch(e) {
243
+ }
244
+ onFinish(null)
245
+ })
246
+ } catch (e) {
123
247
  onFinish(e)
248
+ } finally {
249
+ server.end()
124
250
  }
125
251
  }
126
252
  }
package/src/utils.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { networkInterfaces } from 'os'
2
+
3
+ export function getOpenHost () {
4
+ let result
5
+ const interfaces = networkInterfaces()
6
+ for (const devName in interfaces) {
7
+ const isEnd = interfaces[devName]?.some(item => {
8
+ // 取IPv4, 不为127.0.0.1的内网ip
9
+ if (['IPv4', 4, '4'].includes(item.family) && item.address !== '127.0.0.1' && !item.internal) {
10
+ result = item.address
11
+ return true
12
+ }
13
+ return false
14
+ })
15
+ // 若获取到ip, 结束遍历
16
+ if (isEnd) {
17
+ break
18
+ }
19
+ }
20
+ return result
21
+ }
22
+
23
+ export const PLAYGROUNDREPO = 'https://github.com/wuba/taro-playground'
24
+
25
+ export const PLAYGROUNDINFO = `use [Taro Playground App](${PLAYGROUNDREPO}) to scan`
26
+
27
+ export const isWin = /^win/.test(process.platform)
@@ -1 +0,0 @@
1
- import '@tarojs/rn-supporter/entry-file.js'
@@ -1,8 +0,0 @@
1
- const { mergeConfig } = require('metro-config')
2
- const { getMetroConfig } = require('@tarojs/rn-supporter')
3
-
4
- module.exports = mergeConfig({
5
- // custom your metro config here
6
- // https://facebook.github.io/metro/docs/configuration
7
- resolver: {}
8
- }, getMetroConfig())