glass-easel-miniprogram-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,9 @@
1
+ # glass-easel-miniprogram-webpack-plugin
2
+
3
+ The webpack plugin for building mini-program code running in glass-easel.
4
+
5
+ Refer to the [glass-easel](https://github.com/wechat-miniprogram/glass-easel) project for further details.
6
+
7
+ ## Usage
8
+
9
+ See the [template](../glass-easel-miniprogram-template/).
package/helpers.js ADDED
@@ -0,0 +1,7 @@
1
+ /* eslint-disable */
2
+
3
+ exports.escapeJsString = (str) => str.replace(/["'\\\r\n`]/g, (c) => {
4
+ if (c === '\r') return '\\r'
5
+ if (c === '\n') return '\\n'
6
+ return `\\${c}`
7
+ })
package/index.js ADDED
@@ -0,0 +1,362 @@
1
+ /* eslint-disable */
2
+
3
+ const fs = require('fs').promises
4
+ const path = require('path')
5
+ const { NormalModule } = require('webpack')
6
+ const { RawSource } = require('webpack-sources')
7
+ const VirtualModulesPlugin = require('webpack-virtual-modules')
8
+ const chokidar = require('chokidar')
9
+ const { TmplGroup } = require('glass-easel-template-compiler')
10
+
11
+ const { escapeJsString } = require('./helpers')
12
+
13
+ const GlassEaselMiniprogramWxssLoader = path.join(__dirname, 'wxss_loader.js')
14
+
15
+ const PLUGIN_NAME = 'GlassEaselMiniprogramWebpackPlugin'
16
+
17
+ class StyleSheetManager {
18
+ constructor() {
19
+ this.map = Object.create(null)
20
+ this.enableStyleScope = Object.create(null)
21
+ this.scopeNameInc = 0
22
+ }
23
+
24
+ add(compPath, srcPath) {
25
+ let scopeNameNum = this.scopeNameInc
26
+ this.scopeNameInc += 1
27
+ let scopeName = ''
28
+ do {
29
+ const n = scopeNameNum % 52
30
+ let c
31
+ if (n >= 26) {
32
+ c = String.fromCharCode(n - 26 + 97)
33
+ } else {
34
+ c = String.fromCharCode(n + 65)
35
+ }
36
+ scopeName += c
37
+ scopeNameNum = Math.floor(scopeNameNum / 52)
38
+ } while (scopeNameNum > 0)
39
+ this.map[compPath] = {
40
+ srcPath,
41
+ scopeName,
42
+ }
43
+ }
44
+
45
+ setStyleIsolation(compPath, styleIsolation, isComponent) {
46
+ const enabled = styleIsolation
47
+ ? styleIsolation !== 'shared' && styleIsolation !== 'page-shared'
48
+ : isComponent
49
+ this.enableStyleScope[compPath] = enabled
50
+ }
51
+
52
+ getScopeName(compPath) {
53
+ if (this.enableStyleScope[compPath]) {
54
+ return this.map[compPath].scopeName
55
+ }
56
+ return undefined
57
+ }
58
+
59
+ toCodeString() {
60
+ const arr = Object.entries(this.map).map(([compPath, { srcPath }]) => {
61
+ const s = `backend.registerStyleSheetContent('${escapeJsString(compPath)}', require('${escapeJsString(srcPath)}'));`
62
+ return s
63
+ })
64
+ return `
65
+ function (backend) { ${arr.join('')} }
66
+ `
67
+ }
68
+ }
69
+
70
+ class GlassEaselMiniprogramWebpackPlugin {
71
+ constructor(options) {
72
+ this.path = options.path || './src'
73
+ this.resourceFilePattern = options.resourceFilePattern || /\.(jpg|jpeg|png|gif|html)$/
74
+ this.defaultEntry = 'pages/index/index'
75
+ this.virtualModules = new VirtualModulesPlugin()
76
+ }
77
+
78
+ apply(compiler) {
79
+ // search paths
80
+ const codeRoot = path.resolve(this.path)
81
+ const params = {
82
+ globalStaticConfig: {},
83
+ compInfoMap: Object.create(null),
84
+ resPathMap: Object.create(null),
85
+ appEntry: null,
86
+ tmplGroup: new TmplGroup(),
87
+ styleSheetManager: new StyleSheetManager(),
88
+ }
89
+
90
+ // determine a path is a component path or not, returning the json content if true
91
+ const isCompPath = async (relPath) => {
92
+ if (!relPath) return null
93
+ let staticConfig = null
94
+ try {
95
+ const json = await fs.readFile(path.join(codeRoot, `${relPath}.json`), { encoding: 'utf8' })
96
+ const parsed = JSON.parse(json)
97
+ if (parsed && (parsed.component === true || typeof parsed.usingComponents === 'object')) {
98
+ staticConfig = parsed
99
+ }
100
+ } catch (e) { /* empty */ }
101
+ return staticConfig
102
+ }
103
+
104
+ // determine a file is located in the code root or not, returning relative path if true
105
+ const normalizePath = (absPath) => {
106
+ const p = path.relative(codeRoot, absPath)
107
+ if (p.split(path.sep, 1)[0] === '..') return null
108
+ return p.split(path.sep).join('/')
109
+ }
110
+
111
+ // search for component files
112
+ let codeRootWatching = false
113
+ const searchCodeRoot = async (enableWatch) => {
114
+ if (codeRootWatching) return
115
+ codeRootWatching = true
116
+ const handleFile = async (relPath) => {
117
+ // for app.json, spread the global field
118
+ if (relPath === 'app.json') {
119
+ try {
120
+ const json = await fs.readFile(path.join(codeRoot, relPath), { encoding: 'utf8' })
121
+ const staticConfig = JSON.parse(json)
122
+ if (staticConfig.usingComponents) {
123
+ params.globalStaticConfig = {
124
+ usingComponents: staticConfig.usingComponents,
125
+ }
126
+ }
127
+ } catch (e) {
128
+ params.globalStaticConfig = {}
129
+ }
130
+ return
131
+ }
132
+
133
+ // for app.ts or app.js, load it first
134
+ if (relPath === 'app.ts' || relPath === 'app.js') {
135
+ params.appEntry = relPath
136
+ }
137
+
138
+ // find component by json files
139
+ const extName = path.extname(relPath)
140
+ if (extName === '.json') {
141
+ const staticConfig = await isCompPath(relPath.slice(0, -extName.length))
142
+ if (staticConfig) {
143
+ const compPath = relPath.slice(0, -5)
144
+ try {
145
+ const tsFileStat = await fs.stat(path.join(codeRoot, `${compPath}.ts`))
146
+ if (tsFileStat.isFile()) {
147
+ params.compInfoMap[compPath] = {
148
+ main: `${compPath}.ts`,
149
+ staticConfig,
150
+ }
151
+ params.styleSheetManager.setStyleIsolation(
152
+ compPath,
153
+ staticConfig.styleIsolation,
154
+ !!staticConfig.component,
155
+ )
156
+ return
157
+ }
158
+ } catch (e) { /* empty */ }
159
+ try {
160
+ const jsFileStat = await fs.stat(path.join(codeRoot, `${compPath}.js`))
161
+ if (jsFileStat.isFile()) {
162
+ params.compInfoMap[compPath] = {
163
+ main: `${compPath}.js`,
164
+ staticConfig,
165
+ }
166
+ return
167
+ }
168
+ } catch (e) { /* empty */ }
169
+ }
170
+ }
171
+
172
+ // add wxml
173
+ if (extName === '.wxml') {
174
+ const src = await fs.readFile(path.join(codeRoot, relPath), { encoding: 'utf8' })
175
+ params.tmplGroup.addTmpl(relPath.slice(0, -extName.length), src)
176
+ // TODO support wxml file remove
177
+ }
178
+
179
+ // add wxss
180
+ if (extName === '.wxss') {
181
+ const srcPath = path.join(codeRoot, relPath)
182
+ params.styleSheetManager.add(relPath.slice(0, -extName.length), srcPath)
183
+ // TODO support wxss file remove
184
+ }
185
+
186
+ // find resource files
187
+ if (this.resourceFilePattern.test(relPath)) {
188
+ params.resPathMap[relPath] = true
189
+ }
190
+ }
191
+
192
+ const removeEntry = (relPath) => {
193
+ delete params.resPathMap[relPath]
194
+ delete params.compInfoMap[relPath]
195
+ }
196
+
197
+ // await readdirp(codeRoot, handleFile)
198
+ await new Promise((resolve, reject) => {
199
+ const promises = []
200
+ const watcher = chokidar.watch(codeRoot, { ignoreInitial: false })
201
+ watcher
202
+ .on('add', (p) => {
203
+ promises.push(handleFile(normalizePath(p)))
204
+ })
205
+ .on('change', (p) => {
206
+ promises.push(handleFile(normalizePath(p)))
207
+ })
208
+ .on('unlink', (p) => {
209
+ removeEntry(normalizePath(p))
210
+ })
211
+ .on('error', (err) => {
212
+ throw new Error(err)
213
+ })
214
+ .on('ready', () => {
215
+ Promise.all(promises)
216
+ .then(() => {
217
+ if (!enableWatch) return watcher.close()
218
+ return null
219
+ })
220
+ .then(resolve)
221
+ .catch(reject)
222
+ })
223
+ })
224
+ }
225
+
226
+ // init component list before run
227
+ compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, async () => {
228
+ await searchCodeRoot(false)
229
+ })
230
+ compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async () => {
231
+ await searchCodeRoot(true)
232
+ })
233
+
234
+ // rewrite component entry paths
235
+ compiler.resolverFactory.hooks.resolver
236
+ .for('normal')
237
+ .tap(PLUGIN_NAME, (resolver) => {
238
+ resolver.hooks.result.tap(PLUGIN_NAME, (data) => {
239
+ const absPath = data.path
240
+ const extName = path.extname(absPath)
241
+ if (extName === '.js' || extName === '.ts') {
242
+ const relPath = normalizePath(absPath)
243
+ if (relPath && params.compInfoMap[relPath.slice(0, -3)]) {
244
+ const redirected = `${absPath.slice(0, -3)}.component`
245
+ if (data.context.issuer !== redirected) {
246
+ data.path = redirected
247
+ }
248
+ }
249
+ }
250
+ return data
251
+ })
252
+ })
253
+
254
+ // add loaders
255
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
256
+ NormalModule.getCompilationHooks(compilation).beforeLoaders
257
+ .tap(PLUGIN_NAME, (loaders, mod) => {
258
+ const absPath = mod.resource
259
+ const extName = path.extname(absPath)
260
+ if (extName === '.ts' || extName === '.js' || extName === '.wxml' || extName === '.wxss') {
261
+ const relPath = path.relative(codeRoot, absPath).split(path.sep).join('/')
262
+ const compPath = relPath.slice(0, -extName.length)
263
+ if (params.compInfoMap[compPath] || compPath === 'app') {
264
+ if (extName === '.wxss') {
265
+ loaders.forEach((x) => {
266
+ if (x.loader === GlassEaselMiniprogramWxssLoader) {
267
+ x.options = {
268
+ classPrefix: params.styleSheetManager.getScopeName(compPath),
269
+ relPath,
270
+ }
271
+ }
272
+ })
273
+ }
274
+ }
275
+ }
276
+ })
277
+ })
278
+
279
+ // collect virtual files
280
+ const virtualModules = this.virtualModules
281
+ virtualModules.apply(compiler)
282
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
283
+ // add virtual component js file
284
+ Object.keys(params.compInfoMap).forEach((compPath) => {
285
+ const compInfo = params.compInfoMap[compPath]
286
+ const json = JSON.stringify(compInfo.staticConfig)
287
+ const scopeName = params.styleSheetManager.getScopeName(compPath)
288
+ const scopeNameStr = scopeName === undefined ? undefined : `'${scopeName}'`
289
+ virtualModules.writeModule(
290
+ path.join(codeRoot, `${compPath}.component`),
291
+ `
292
+ var index = require('${escapeJsString(codeRoot)}/index.js')
293
+ index.codeSpace.addComponentStaticConfig('${escapeJsString(compPath)}', ${json})
294
+ index.codeSpace.addCompiledTemplate('${escapeJsString(compPath)}', {
295
+ groupList: index.genObjectGroups,
296
+ content: index.genObjectGroups['${escapeJsString(compPath)}']
297
+ })
298
+ index.codeSpace.addStyleSheet(
299
+ '${escapeJsString(compPath)}',
300
+ '${escapeJsString(compPath)}',
301
+ ${scopeNameStr},
302
+ )
303
+ index.codeSpace.globalComponentEnv(index.globalObject, '${escapeJsString(compPath)}', () => {
304
+ require('./${escapeJsString(path.basename(compInfo.main))}')
305
+ })
306
+ `,
307
+ )
308
+ })
309
+
310
+ // add virtual index file
311
+ const entryHeader = `
312
+ var adapter = require('glass-easel-miniprogram-adapter')
313
+ var glassEasel = adapter.glassEasel
314
+ var env = new adapter.MiniProgramEnv()
315
+ exports.env = env
316
+ var backend = new glassEasel.domlikeBackend.CurrentWindowBackendContext()
317
+ backend.onEvent((target, type, detail, options) => {
318
+ let cur = target
319
+ while (cur && !cur.__wxElement) cur = cur.parentNode
320
+ if (!cur) return
321
+ glassEasel.triggerEvent(target.__wxElement, type, detail, options)
322
+ })
323
+ var ab = env.associateBackend(backend)
324
+ ;(${params.styleSheetManager.toCodeString()})(ab)
325
+ var codeSpace = env.createCodeSpace('', true)
326
+ codeSpace.addStyleSheet('app', 'app')
327
+ exports.codeSpace = codeSpace
328
+ exports.genObjectGroups = ${params.tmplGroup.getTmplGenObjectGroups()}
329
+ exports.globalObject = (function () {
330
+ if (typeof this !== 'undefined') { return this }
331
+ if (typeof globalThis !== 'undefined') { return globalThis }
332
+ if (typeof self !== 'undefined') { return self }
333
+ if (typeof window !== 'undefined') { return window }
334
+ if (typeof global !== 'undefined') { return global }
335
+ throw new Error('The global object cannot be recognized')
336
+ })()
337
+ `
338
+ const entryFooter = `
339
+ var root = ab.createRoot('glass-easel-root', codeSpace, '${escapeJsString(this.defaultEntry)}')
340
+ var placeholder = document.createElement('span')
341
+ document.body.appendChild(placeholder)
342
+ root.attach(document.body, placeholder)
343
+ `
344
+ const entries = Object.values(params.compInfoMap).map((compInfo) => compInfo.main)
345
+ if (params.appEntry) entries.unshift(params.appEntry)
346
+ virtualModules.writeModule(
347
+ `${codeRoot}/index.js`,
348
+ entryHeader + entries.map((p) => `require('./${escapeJsString(p)}')\n`).join('') + entryFooter,
349
+ )
350
+
351
+ // copy res files
352
+ compilation.hooks.additionalAssets.tapPromise(PLUGIN_NAME, async () => {
353
+ await Promise.all(Object.keys(params.resPathMap).map(async (p) => {
354
+ compilation.assets[p] = new RawSource(await fs.readFile(path.join(codeRoot, p)))
355
+ }))
356
+ })
357
+ })
358
+ }
359
+ }
360
+
361
+ exports.GlassEaselMiniprogramWebpackPlugin = GlassEaselMiniprogramWebpackPlugin
362
+ exports.GlassEaselMiniprogramWxssLoader = GlassEaselMiniprogramWxssLoader
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "glass-easel-miniprogram-webpack-plugin",
3
+ "description": "The webpack plugin of the glass-easel project for MiniProgram file structure",
4
+ "version": "0.1.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/wechat-miniprogram/glass-easel.git"
8
+ },
9
+ "keywords": ["glass-easel"],
10
+ "author": "wechat-miniprogram",
11
+ "license": "MIT",
12
+ "bugs": {
13
+ "url": "https://github.com/wechat-miniprogram/glass-easel/issues"
14
+ },
15
+ "homepage": "https://github.com/wechat-miniprogram/glass-easel",
16
+ "main": "index.js",
17
+ "scripts": {
18
+ "lint": "eslint src/**/*.ts"
19
+ },
20
+ "peerDependencies": {
21
+ "glass-easel": "0.1",
22
+ "glass-easel-miniprogram-adapter": "0.1",
23
+ "webpack": "^5.52.1"
24
+ },
25
+ "dependencies": {
26
+ "chokidar": "^3.5.3",
27
+ "glass-easel-stylesheet-compiler": "0.1",
28
+ "glass-easel-template-compiler": "0.1",
29
+ "source-map": "^0.7.4",
30
+ "webpack-sources": "^3.2.1",
31
+ "webpack-virtual-modules": "^0.4.3"
32
+ }
33
+ }
package/wxss_loader.js ADDED
@@ -0,0 +1,33 @@
1
+ /* eslint-disable */
2
+
3
+ const { SourceMapGenerator, SourceMapConsumer } = require('source-map')
4
+ const { StyleSheetTransformer } = require('glass-easel-stylesheet-compiler')
5
+
6
+ module.exports = function (src, prevMap, meta) {
7
+ const callback = this.async()
8
+ const { classPrefix } = this.query
9
+ const sst = new StyleSheetTransformer(this.resourcePath, src, classPrefix, 750)
10
+ const ss = sst.getContent()
11
+ let map
12
+ if (this.sourceMap) {
13
+ const ssSourceMap = JSON.parse(sst.toSourceMap())
14
+ if (prevMap) {
15
+ const destConsumer = new SourceMapConsumer(ssSourceMap)
16
+ const srcConsumer = new SourceMapConsumer(prevMap)
17
+ Promise.all([destConsumer, srcConsumer]).then(([destConsumer, srcConsumer]) => {
18
+ const gen = SourceMapGenerator.fromSourceMap(destConsumer)
19
+ gen.applySourceMap(srcConsumer, this.resourcePath)
20
+ destConsumer.destroy()
21
+ srcConsumer.destroy()
22
+ map = gen.toJSON()
23
+ callback(null, ss, map, meta)
24
+ return undefined
25
+ }).catch((err) => {
26
+ callback(err)
27
+ })
28
+ } else {
29
+ map = ssSourceMap
30
+ callback(null, ss, map, meta)
31
+ }
32
+ }
33
+ }