@lynx-js/template-webpack-plugin-canary 0.6.3-canary-20250302-1abf8f01
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/CHANGELOG.md +124 -0
- package/LICENSE +202 -0
- package/README.md +4 -0
- package/lib/LynxAsyncChunksRuntimeModule.d.ts +4 -0
- package/lib/LynxAsyncChunksRuntimeModule.js +30 -0
- package/lib/LynxEncodePlugin.d.ts +105 -0
- package/lib/LynxEncodePlugin.js +192 -0
- package/lib/LynxTemplatePlugin.d.ts +329 -0
- package/lib/LynxTemplatePlugin.js +486 -0
- package/lib/css/ast.d.ts +3 -0
- package/lib/css/ast.js +11 -0
- package/lib/css/cssChunksToMap.d.ts +6 -0
- package/lib/css/cssChunksToMap.js +27 -0
- package/lib/css/debundle.d.ts +1 -0
- package/lib/css/debundle.js +86 -0
- package/lib/css/encode.d.ts +17 -0
- package/lib/css/encode.js +36 -0
- package/lib/css/index.d.ts +3 -0
- package/lib/css/index.js +6 -0
- package/lib/css/plugins/index.d.ts +1 -0
- package/lib/css/plugins/index.js +5 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +13 -0
- package/package.json +51 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// Copyright 2024 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
var _a;
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { AsyncSeriesBailHook, AsyncSeriesWaterfallHook, SyncWaterfallHook, } from '@rspack/lite-tapable';
|
|
7
|
+
import groupBy from 'object.groupby';
|
|
8
|
+
import { RuntimeGlobals } from '@lynx-js/webpack-runtime-globals';
|
|
9
|
+
import { cssChunksToMap } from './css/cssChunksToMap.js';
|
|
10
|
+
import { createLynxAsyncChunksRuntimeModule } from './LynxAsyncChunksRuntimeModule.js';
|
|
11
|
+
const LynxTemplatePluginHooksMap = new WeakMap();
|
|
12
|
+
/**
|
|
13
|
+
* Add hooks to the webpack compilation object to allow foreign plugins to
|
|
14
|
+
* extend the LynxTemplatePlugin
|
|
15
|
+
*/
|
|
16
|
+
function createLynxTemplatePluginHooks() {
|
|
17
|
+
return {
|
|
18
|
+
asyncChunkName: new SyncWaterfallHook(['pluginArgs']),
|
|
19
|
+
beforeEncode: new AsyncSeriesWaterfallHook(['pluginArgs']),
|
|
20
|
+
encode: new AsyncSeriesBailHook(['pluginArgs']),
|
|
21
|
+
beforeEmit: new AsyncSeriesWaterfallHook(['pluginArgs']),
|
|
22
|
+
afterEmit: new AsyncSeriesWaterfallHook(['pluginArgs']),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* LynxTemplatePlugin
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export class LynxTemplatePlugin {
|
|
31
|
+
options;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns all public hooks of the Lynx template webpack plugin for the given compilation
|
|
37
|
+
*/
|
|
38
|
+
static getLynxTemplatePluginHooks(compilation) {
|
|
39
|
+
let hooks = LynxTemplatePluginHooksMap.get(compilation);
|
|
40
|
+
// Setup the hooks only once
|
|
41
|
+
if (hooks === undefined) {
|
|
42
|
+
LynxTemplatePluginHooksMap.set(compilation, hooks = createLynxTemplatePluginHooks());
|
|
43
|
+
}
|
|
44
|
+
return hooks;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `defaultOptions` is the default options that the {@link LynxTemplatePlugin} uses.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* `defaultOptions` can be used to change part of the option and keep others as the default value.
|
|
51
|
+
*
|
|
52
|
+
* ```js
|
|
53
|
+
* // webpack.config.js
|
|
54
|
+
* import { LynxTemplatePlugin } from '@lynx-js/template-webpack-plugin'
|
|
55
|
+
* export default {
|
|
56
|
+
* plugins: [
|
|
57
|
+
* new LynxTemplatePlugin({
|
|
58
|
+
* ...LynxTemplatePlugin.defaultOptions,
|
|
59
|
+
* enableRemoveCSSScope: true,
|
|
60
|
+
* }),
|
|
61
|
+
* ],
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
static defaultOptions = Object
|
|
68
|
+
.freeze({
|
|
69
|
+
filename: '[name].bundle',
|
|
70
|
+
lazyBundleFilename: 'async/[name].[fullhash].bundle',
|
|
71
|
+
intermediate: '.rspeedy',
|
|
72
|
+
chunks: 'all',
|
|
73
|
+
excludeChunks: [],
|
|
74
|
+
// lynx-specific
|
|
75
|
+
customCSSInheritanceList: undefined,
|
|
76
|
+
debugInfoOutside: true,
|
|
77
|
+
enableICU: false,
|
|
78
|
+
enableA11y: true,
|
|
79
|
+
enableAccessibilityElement: false,
|
|
80
|
+
enableCSSInheritance: false,
|
|
81
|
+
enableCSSInvalidation: false,
|
|
82
|
+
enableCSSSelector: true,
|
|
83
|
+
enableNewGesture: false,
|
|
84
|
+
enableParallelElement: true,
|
|
85
|
+
defaultDisplayLinear: true,
|
|
86
|
+
enableRemoveCSSScope: false,
|
|
87
|
+
pipelineSchedulerConfig: 0x00010000,
|
|
88
|
+
targetSdkVersion: '3.2',
|
|
89
|
+
removeDescendantSelectorScope: false,
|
|
90
|
+
dsl: 'react_nodiff',
|
|
91
|
+
encodeBinary: 'napi',
|
|
92
|
+
experimental_isLazyBundle: false,
|
|
93
|
+
cssPlugins: [],
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Convert the css chunks to css map.
|
|
97
|
+
*
|
|
98
|
+
* @param cssChunks - The CSS chunks content.
|
|
99
|
+
* @param options - The encode options.
|
|
100
|
+
* @returns The CSS map and css source.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```
|
|
104
|
+
* (console.log(await convertCSSChunksToMap(
|
|
105
|
+
* '.red { color: red; }',
|
|
106
|
+
* {
|
|
107
|
+
* targetSdkVersion: '3.2',
|
|
108
|
+
* enableCSSSelector: true,
|
|
109
|
+
* },
|
|
110
|
+
* )));
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
static convertCSSChunksToMap(cssChunks, plugins) {
|
|
114
|
+
return cssChunksToMap(cssChunks, plugins);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The entry point of a webpack plugin.
|
|
118
|
+
* @param compiler - the webpack compiler
|
|
119
|
+
*/
|
|
120
|
+
apply(compiler) {
|
|
121
|
+
new LynxTemplatePluginImpl(compiler, Object.assign({}, LynxTemplatePlugin.defaultOptions, this.options));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
class LynxTemplatePluginImpl {
|
|
125
|
+
name = 'LynxTemplatePlugin';
|
|
126
|
+
static #taskQueue = null;
|
|
127
|
+
hash;
|
|
128
|
+
constructor(compiler, options) {
|
|
129
|
+
this.#options = options;
|
|
130
|
+
const { createHash } = compiler.webpack.util;
|
|
131
|
+
this.hash = createHash(compiler.options.output.hashFunction ?? 'xxhash64');
|
|
132
|
+
compiler.hooks.initialize.tap(this.name, () => {
|
|
133
|
+
// entryName to fileName conversion function
|
|
134
|
+
const userOptionFilename = this.#options.filename;
|
|
135
|
+
const filenameFunction = typeof userOptionFilename === 'function'
|
|
136
|
+
? userOptionFilename
|
|
137
|
+
// Replace '[name]' with entry name
|
|
138
|
+
: (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
|
|
139
|
+
/** output filenames for the given entry names */
|
|
140
|
+
const entryNames = Object.keys(compiler.options.entry);
|
|
141
|
+
const outputFileNames = new Set((entryNames.length > 0 ? entryNames : ['main']).map((name) => filenameFunction(name)));
|
|
142
|
+
outputFileNames.forEach((outputFileName) => {
|
|
143
|
+
// convert absolute filename into relative so that webpack can
|
|
144
|
+
// generate it at correct location
|
|
145
|
+
let filename = outputFileName;
|
|
146
|
+
if (path.resolve(filename) === path.normalize(filename)) {
|
|
147
|
+
filename = path.relative(
|
|
148
|
+
/** Once initialized the path is always a string */
|
|
149
|
+
compiler.options.output.path, filename);
|
|
150
|
+
}
|
|
151
|
+
compiler.hooks.thisCompilation.tap(this.name, (compilation) => {
|
|
152
|
+
compilation.hooks.processAssets.tapPromise({
|
|
153
|
+
name: this.name,
|
|
154
|
+
stage:
|
|
155
|
+
/**
|
|
156
|
+
* Generate the html after minification and dev tooling is done
|
|
157
|
+
* and source-map is generated
|
|
158
|
+
*/
|
|
159
|
+
compiler.webpack.Compilation
|
|
160
|
+
.PROCESS_ASSETS_STAGE_REPORT,
|
|
161
|
+
}, () => {
|
|
162
|
+
return this.#generateTemplate(compiler, compilation, filename);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
compiler.hooks.thisCompilation.tap(this.name, compilation => {
|
|
168
|
+
const onceForChunkSet = new WeakSet();
|
|
169
|
+
const LynxAsyncChunksRuntimeModule = createLynxAsyncChunksRuntimeModule(compiler.webpack);
|
|
170
|
+
const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation);
|
|
171
|
+
compilation.hooks.runtimeRequirementInTree.for(RuntimeGlobals.lynxAsyncChunkIds).tap(this.name, (chunk, set) => {
|
|
172
|
+
if (onceForChunkSet.has(chunk)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
onceForChunkSet.add(chunk);
|
|
176
|
+
// TODO: only add `getFullHash` when using fullhash
|
|
177
|
+
set.add(compiler.webpack.RuntimeGlobals.getFullHash);
|
|
178
|
+
compilation.addRuntimeModule(chunk, new LynxAsyncChunksRuntimeModule((chunkName) => {
|
|
179
|
+
const filename = hooks.asyncChunkName.call(chunkName);
|
|
180
|
+
return this.#getAsyncFilenameTemplate(filename);
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
compilation.hooks.processAssets.tapPromise(this.name, async () => {
|
|
184
|
+
await this.#generateAsyncTemplate(compilation);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
// There are multiple `LynxTemplatePlugin`s registered in one compiler.
|
|
188
|
+
// We only want to tap `hooks.done` on one of them.
|
|
189
|
+
if (_a.#taskQueue === null) {
|
|
190
|
+
_a.#taskQueue = [];
|
|
191
|
+
compiler.hooks.done.tap(this.name, () => {
|
|
192
|
+
const queue = _a.#taskQueue;
|
|
193
|
+
_a.#taskQueue = [];
|
|
194
|
+
if (queue) {
|
|
195
|
+
Promise.all(queue.map(fn => fn()))
|
|
196
|
+
.catch((error) => {
|
|
197
|
+
compiler.getInfrastructureLogger('LynxTemplatePlugin').error(error);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async #generateTemplate(_compiler, compilation, filenameTemplate) {
|
|
204
|
+
// Get all entry point names for this template file
|
|
205
|
+
const entryNames = Array.from(compilation.entrypoints.keys());
|
|
206
|
+
const filteredEntryNames = this.#filterEntryChunks(entryNames, this.#options.chunks, this.#options.excludeChunks);
|
|
207
|
+
const assetsInfoByGroups = this.#getAssetsInformationByGroups(compilation, filteredEntryNames);
|
|
208
|
+
await this.#encodeByAssetsInformation(compilation, assetsInfoByGroups, filteredEntryNames, filenameTemplate, this.#options.intermediate,
|
|
209
|
+
/** isAsync */ this.#options.experimental_isLazyBundle);
|
|
210
|
+
}
|
|
211
|
+
static #asyncChunkGroups = new WeakMap();
|
|
212
|
+
#getAsyncChunkGroups(compilation) {
|
|
213
|
+
let asyncChunkGroups = _a.#asyncChunkGroups.get(compilation);
|
|
214
|
+
if (asyncChunkGroups) {
|
|
215
|
+
return asyncChunkGroups;
|
|
216
|
+
}
|
|
217
|
+
asyncChunkGroups = groupBy(compilation.chunkGroups
|
|
218
|
+
.filter(cg => !cg.isInitial()), (cg) => cg.origins
|
|
219
|
+
.sort((a, b) => a.request.localeCompare(b.request))
|
|
220
|
+
.map(({ request }) => request)
|
|
221
|
+
.join('|'));
|
|
222
|
+
_a.#asyncChunkGroups.set(compilation, asyncChunkGroups);
|
|
223
|
+
return asyncChunkGroups;
|
|
224
|
+
}
|
|
225
|
+
#getAsyncFilenameTemplate(filename) {
|
|
226
|
+
return this.#options.lazyBundleFilename.replace(/\[name\]/, filename);
|
|
227
|
+
}
|
|
228
|
+
static #encodedTemplate = new WeakMap();
|
|
229
|
+
async #generateAsyncTemplate(compilation) {
|
|
230
|
+
const asyncChunkGroups = this.#getAsyncChunkGroups(compilation);
|
|
231
|
+
const intermediateRoot = path.dirname(this.#options.intermediate);
|
|
232
|
+
const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation);
|
|
233
|
+
// We cache the encoded template so that it will not be encoded twice
|
|
234
|
+
if (!_a.#encodedTemplate.has(compilation)) {
|
|
235
|
+
_a.#encodedTemplate.set(compilation, new Set());
|
|
236
|
+
}
|
|
237
|
+
const encodedTemplate = _a.#encodedTemplate.get(compilation);
|
|
238
|
+
await Promise.all(Object.entries(asyncChunkGroups).map(([entryName, chunkGroups]) => {
|
|
239
|
+
const chunkNames =
|
|
240
|
+
// We use the chunk name(provided by `webpackChunkName`) as filename
|
|
241
|
+
chunkGroups
|
|
242
|
+
.map(cg => hooks.asyncChunkName.call(cg.name))
|
|
243
|
+
.filter(chunkName => chunkName !== undefined);
|
|
244
|
+
const filename = Array.from(new Set(chunkNames)).join('_');
|
|
245
|
+
// If no filename is found, avoid generating async template
|
|
246
|
+
if (!filename) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const filenameTemplate = this.#getAsyncFilenameTemplate(filename);
|
|
250
|
+
// Ignore the encoded templates
|
|
251
|
+
if (encodedTemplate.has(filenameTemplate)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
encodedTemplate.add(filenameTemplate);
|
|
255
|
+
const asyncAssetsInfoByGroups = this.#getAssetsInformationByFilenames(compilation, chunkGroups.flatMap(cg => cg.getFiles()));
|
|
256
|
+
return this.#encodeByAssetsInformation(compilation, asyncAssetsInfoByGroups, [entryName], filenameTemplate, path.join(intermediateRoot, 'async', filename),
|
|
257
|
+
/** isAsync */ true);
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
async #encodeByAssetsInformation(compilation, assetsInfoByGroups, entryNames, filenameTemplate, intermediate, isAsync) {
|
|
261
|
+
const compiler = compilation.compiler;
|
|
262
|
+
const { customCSSInheritanceList, debugInfoOutside, defaultDisplayLinear, enableICU, enableA11y, enableAccessibilityElement, enableCSSInheritance, enableCSSInvalidation, enableCSSSelector, enableNewGesture, enableParallelElement, enableRemoveCSSScope, pipelineSchedulerConfig, removeDescendantSelectorScope, targetSdkVersion, dsl, cssPlugins, } = this.#options;
|
|
263
|
+
const isDev = process.env['NODE_ENV'] === 'development'
|
|
264
|
+
|| compiler.options.mode === 'development';
|
|
265
|
+
const css = cssChunksToMap(assetsInfoByGroups.css
|
|
266
|
+
.map(chunk => compilation.getAsset(chunk.name))
|
|
267
|
+
.filter((v) => !!v)
|
|
268
|
+
.map(asset => asset.source.source().toString()), cssPlugins);
|
|
269
|
+
const encodeRawData = {
|
|
270
|
+
compilerOptions: {
|
|
271
|
+
enableFiberArch: true,
|
|
272
|
+
useLepusNG: true,
|
|
273
|
+
enableReuseContext: true,
|
|
274
|
+
bundleModuleMode: 'ReturnByFunction',
|
|
275
|
+
templateDebugUrl: '',
|
|
276
|
+
debugInfoOutside,
|
|
277
|
+
defaultDisplayLinear,
|
|
278
|
+
enableCSSInvalidation,
|
|
279
|
+
enableCSSSelector,
|
|
280
|
+
enableLepusDebug: isDev,
|
|
281
|
+
enableParallelElement,
|
|
282
|
+
enableRemoveCSSScope,
|
|
283
|
+
targetSdkVersion,
|
|
284
|
+
},
|
|
285
|
+
sourceContent: {
|
|
286
|
+
dsl,
|
|
287
|
+
appType: isAsync ? 'DynamicComponent' : 'card',
|
|
288
|
+
config: {
|
|
289
|
+
lepusStrict: true,
|
|
290
|
+
useNewSwiper: true,
|
|
291
|
+
enableICU,
|
|
292
|
+
enableNewIntersectionObserver: true,
|
|
293
|
+
enableNativeList: true,
|
|
294
|
+
enableA11y,
|
|
295
|
+
enableAccessibilityElement,
|
|
296
|
+
customCSSInheritanceList,
|
|
297
|
+
enableCSSInheritance,
|
|
298
|
+
enableNewGesture,
|
|
299
|
+
pipelineSchedulerConfig,
|
|
300
|
+
removeDescendantSelectorScope,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
css: {
|
|
304
|
+
...css,
|
|
305
|
+
chunks: assetsInfoByGroups.css,
|
|
306
|
+
},
|
|
307
|
+
lepusCode: {
|
|
308
|
+
// TODO: support multiple lepus chunks
|
|
309
|
+
root: assetsInfoByGroups.lepus[0],
|
|
310
|
+
chunks: [],
|
|
311
|
+
},
|
|
312
|
+
manifest: Object.fromEntries(assetsInfoByGroups.js.map(asset => {
|
|
313
|
+
return [asset.name, asset.source.source().toString()];
|
|
314
|
+
})),
|
|
315
|
+
customSections: {},
|
|
316
|
+
};
|
|
317
|
+
const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation);
|
|
318
|
+
const { encodeData } = await hooks.beforeEncode.promise({
|
|
319
|
+
encodeData: encodeRawData,
|
|
320
|
+
filenameTemplate,
|
|
321
|
+
entryNames,
|
|
322
|
+
});
|
|
323
|
+
const { lepusCode } = encodeData;
|
|
324
|
+
const resolvedEncodeOptions = {
|
|
325
|
+
...encodeData,
|
|
326
|
+
css: {
|
|
327
|
+
...css,
|
|
328
|
+
chunks: undefined,
|
|
329
|
+
contentMap: undefined,
|
|
330
|
+
},
|
|
331
|
+
lepusCode: {
|
|
332
|
+
// TODO: support multiple lepus chunks
|
|
333
|
+
root: lepusCode.root?.source.source().toString(),
|
|
334
|
+
lepusChunk: Object.fromEntries(lepusCode.chunks.map(asset => {
|
|
335
|
+
return [asset.name, asset.source.source().toString()];
|
|
336
|
+
})),
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
const debugInfoPath = path.posix.format({
|
|
340
|
+
dir: intermediate,
|
|
341
|
+
base: 'debug-info.json',
|
|
342
|
+
});
|
|
343
|
+
// TODO: Support publicPath function
|
|
344
|
+
if (typeof compiler.options.output.publicPath === 'string'
|
|
345
|
+
&& compiler.options.output.publicPath !== 'auto'
|
|
346
|
+
&& compiler.options.output.publicPath !== '/') {
|
|
347
|
+
resolvedEncodeOptions.compilerOptions['templateDebugUrl'] = new URL(debugInfoPath, compiler.options.output.publicPath).toString();
|
|
348
|
+
}
|
|
349
|
+
const { RawSource } = compiler.webpack.sources;
|
|
350
|
+
if (isDebug() || isDev) {
|
|
351
|
+
compilation.emitAsset(path.format({
|
|
352
|
+
dir: intermediate,
|
|
353
|
+
base: 'tasm.json',
|
|
354
|
+
}), new RawSource(JSON.stringify(resolvedEncodeOptions, null, 2)));
|
|
355
|
+
Object.entries(resolvedEncodeOptions.lepusCode.lepusChunk).forEach(([name, content]) => {
|
|
356
|
+
compilation.emitAsset(path.format({
|
|
357
|
+
dir: intermediate,
|
|
358
|
+
name,
|
|
359
|
+
ext: '.js',
|
|
360
|
+
}), new RawSource(content));
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const { buffer, debugInfo } = await hooks.encode.promise({
|
|
365
|
+
encodeOptions: resolvedEncodeOptions,
|
|
366
|
+
intermediate,
|
|
367
|
+
});
|
|
368
|
+
const filename = compilation.getPath(filenameTemplate, {
|
|
369
|
+
// @ts-expect-error we have a mock chunk here to make contenthash works in webpack
|
|
370
|
+
chunk: {},
|
|
371
|
+
contentHash: this.hash.update(buffer).digest('hex').toString(),
|
|
372
|
+
});
|
|
373
|
+
const { template } = await hooks.beforeEmit.promise({
|
|
374
|
+
finalEncodeOptions: resolvedEncodeOptions,
|
|
375
|
+
debugInfo,
|
|
376
|
+
template: buffer,
|
|
377
|
+
outputName: filename,
|
|
378
|
+
lepus: encodeData.lepusCode.chunks,
|
|
379
|
+
});
|
|
380
|
+
compilation.emitAsset(filename, new RawSource(template, false));
|
|
381
|
+
if (isDebug() || isDev) {
|
|
382
|
+
compilation.emitAsset(debugInfoPath, new RawSource(debugInfo));
|
|
383
|
+
}
|
|
384
|
+
await hooks.afterEmit.promise({ outputName: filename });
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (error && typeof error === 'object' && 'error_msg' in error) {
|
|
388
|
+
compilation.errors.push(
|
|
389
|
+
// TODO: use more human-readable error message(i.e.: using sourcemap to get source code)
|
|
390
|
+
// or give webpack/rspack with location of bundle
|
|
391
|
+
new compiler.webpack.WebpackError(error.error_msg));
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
compilation.errors.push(error);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Return all chunks from the compilation result which match the exclude and include filters
|
|
400
|
+
*/
|
|
401
|
+
#filterEntryChunks(chunks, includedChunks, excludedChunks) {
|
|
402
|
+
return chunks.filter((chunkName) => {
|
|
403
|
+
// Skip if the chunks should be filtered and the given chunk was not added explicity
|
|
404
|
+
if (Array.isArray(includedChunks) && !includedChunks.includes(chunkName)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
// Skip if the chunks should be filtered and the given chunk was excluded explicity
|
|
408
|
+
if (excludedChunks.includes(chunkName)) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
// Add otherwise
|
|
412
|
+
return true;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The getAssetsInformationByGroups extracts the asset information of a webpack compilation for all given entry names.
|
|
417
|
+
*/
|
|
418
|
+
#getAssetsInformationByGroups(compilation, entryNames) {
|
|
419
|
+
const filenames = entryNames.flatMap(entryName => {
|
|
420
|
+
/** entryPointUnfilteredFiles - also includes hot module update files */
|
|
421
|
+
const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName)
|
|
422
|
+
.getFiles();
|
|
423
|
+
return entryPointUnfilteredFiles.filter((chunkFile) => {
|
|
424
|
+
const asset = compilation.getAsset(chunkFile);
|
|
425
|
+
// Prevent hot-module files from being included:
|
|
426
|
+
const assetMetaInformation = asset?.info ?? {};
|
|
427
|
+
return !(assetMetaInformation.hotModuleReplacement
|
|
428
|
+
?? assetMetaInformation.development);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
return this.#getAssetsInformationByFilenames(compilation, filenames);
|
|
432
|
+
}
|
|
433
|
+
#getAssetsInformationByFilenames(compilation, filenames) {
|
|
434
|
+
const assets = {
|
|
435
|
+
// Will contain all js and mjs files
|
|
436
|
+
js: [],
|
|
437
|
+
// Will contain all css files
|
|
438
|
+
css: [],
|
|
439
|
+
// Will contain all lepus files
|
|
440
|
+
lepus: [],
|
|
441
|
+
};
|
|
442
|
+
// Extract paths to .js, .lepus and .css files from the current compilation
|
|
443
|
+
const entryPointPublicPathMap = {};
|
|
444
|
+
const extensionRegexp = /\.(css|js)(?:\?|$)/;
|
|
445
|
+
filenames.forEach((filename) => {
|
|
446
|
+
const extMatch = extensionRegexp.exec(filename);
|
|
447
|
+
// Skip if the public path is not a .css, .mjs or .js file
|
|
448
|
+
if (!extMatch) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
// Skip if this file is already known
|
|
452
|
+
// (e.g. because of common chunk optimizations)
|
|
453
|
+
if (entryPointPublicPathMap[filename]) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const asset = compilation.getAsset(filename);
|
|
457
|
+
if (asset.info['lynx:main-thread']) {
|
|
458
|
+
assets.lepus.push(asset);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
entryPointPublicPathMap[filename] = true;
|
|
462
|
+
// ext will contain .js or .css, because .mjs recognizes as .js
|
|
463
|
+
const ext = (extMatch[1] === 'mjs' ? 'js' : extMatch[1]);
|
|
464
|
+
assets[ext].push(asset);
|
|
465
|
+
});
|
|
466
|
+
return assets;
|
|
467
|
+
}
|
|
468
|
+
#options;
|
|
469
|
+
}
|
|
470
|
+
_a = LynxTemplatePluginImpl;
|
|
471
|
+
export function isDebug() {
|
|
472
|
+
if (!process.env['DEBUG']) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
const values = process.env['DEBUG'].toLocaleLowerCase().split(',');
|
|
476
|
+
return [
|
|
477
|
+
'rspeedy',
|
|
478
|
+
'*',
|
|
479
|
+
'rspeedy:*',
|
|
480
|
+
'rspeedy:template',
|
|
481
|
+
].some((key) => values.includes(key));
|
|
482
|
+
}
|
|
483
|
+
export function isRsdoctor() {
|
|
484
|
+
return process.env['RSDOCTOR'] === 'true';
|
|
485
|
+
}
|
|
486
|
+
//# sourceMappingURL=LynxTemplatePlugin.js.map
|
package/lib/css/ast.d.ts
ADDED
package/lib/css/ast.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright 2024 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
import * as CSS from '@lynx-js/css-serializer';
|
|
5
|
+
export function cssToAst(content, plugins) {
|
|
6
|
+
const parsedCSS = CSS.parse(content, {
|
|
7
|
+
plugins,
|
|
8
|
+
});
|
|
9
|
+
return [parsedCSS.root, parsedCSS.errors];
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=ast.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cssToAst } from './ast.js';
|
|
2
|
+
import { debundleCSS } from './debundle.js';
|
|
3
|
+
export function cssChunksToMap(cssChunks, plugins) {
|
|
4
|
+
const cssMap = cssChunks
|
|
5
|
+
.reduce((cssMap, css) => {
|
|
6
|
+
debundleCSS(css, cssMap);
|
|
7
|
+
return cssMap;
|
|
8
|
+
}, new Map());
|
|
9
|
+
return {
|
|
10
|
+
cssMap: Object.fromEntries(Array.from(cssMap.entries()).map(([cssId, content]) => {
|
|
11
|
+
const [root] = cssToAst(content.join('\n'), plugins);
|
|
12
|
+
root.forEach(rule => {
|
|
13
|
+
if (rule.type === 'ImportRule') {
|
|
14
|
+
// For example: '/981029' -> '981029'
|
|
15
|
+
rule.href = rule.href.replace('/', '');
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return [cssId, root];
|
|
19
|
+
})),
|
|
20
|
+
cssSource: Object.fromEntries(Array.from(cssMap.keys()).map(cssId => [
|
|
21
|
+
cssId,
|
|
22
|
+
`/cssId/${cssId}.css`,
|
|
23
|
+
])),
|
|
24
|
+
contentMap: cssMap,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=cssChunksToMap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function debundleCSS(code: string, css: Map<number, string[]>): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright 2024 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
import * as cssTree from 'css-tree';
|
|
5
|
+
// `COMMON_CSS` is the global styles that applies to all the elements.
|
|
6
|
+
// It should always has `cssId: 0`.
|
|
7
|
+
const COMMON_CSS = '/common.css';
|
|
8
|
+
const COMMON_CSS_ID = 0;
|
|
9
|
+
export function debundleCSS(code, css) {
|
|
10
|
+
const ast = cssTree.parse(code);
|
|
11
|
+
const fileKeyToCSSContent = new Map();
|
|
12
|
+
const cssIdToFileKeys = new Map();
|
|
13
|
+
const fileKeyToCSSId = new Map([[COMMON_CSS, COMMON_CSS_ID]]);
|
|
14
|
+
cssTree.walk(ast, {
|
|
15
|
+
visit: 'Atrule',
|
|
16
|
+
enter(node, item, list) {
|
|
17
|
+
if (node.type === 'Atrule' && node.prelude
|
|
18
|
+
&& node.prelude.type === 'AtrulePrelude' // Minify/Format will change `cssId` to `cssid`.
|
|
19
|
+
&& node.name.toLowerCase() === 'cssId'.toLowerCase()) {
|
|
20
|
+
// @cssId "842372" "foo.css" {}
|
|
21
|
+
const [cssIdNode, fileKeyNode] = node.prelude.children.toArray()
|
|
22
|
+
.filter(({ type }) => type !== 'WhiteSpace');
|
|
23
|
+
if (cssIdNode?.type === 'String'
|
|
24
|
+
&& fileKeyNode?.type === 'String'
|
|
25
|
+
&& node.block) {
|
|
26
|
+
const cssId = Number(cssIdNode.value);
|
|
27
|
+
if (Number.isNaN(cssId)) {
|
|
28
|
+
throw new Error(`Invalid cssId: @cssId "${cssIdNode.value}" "${fileKeyNode.value}"`);
|
|
29
|
+
}
|
|
30
|
+
const fileKey = fileKeyNode.value;
|
|
31
|
+
let fileKeys = cssIdToFileKeys.get(cssId);
|
|
32
|
+
if (typeof fileKeys === 'undefined') {
|
|
33
|
+
// Every file should import COMMON_CSS(cssId: 0).
|
|
34
|
+
// Otherwise, the global styles cannot be resolved by elements.
|
|
35
|
+
fileKeys = new Set([COMMON_CSS]);
|
|
36
|
+
cssIdToFileKeys.set(cssId, fileKeys);
|
|
37
|
+
}
|
|
38
|
+
fileKeys.add(fileKey);
|
|
39
|
+
if (!fileKeyToCSSContent.has(fileKey)) {
|
|
40
|
+
fileKeyToCSSContent.set(fileKey, cssTree.generate({
|
|
41
|
+
type: 'StyleSheet',
|
|
42
|
+
children: node.block.children,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
list.remove(item);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
// If there are Rules left in the AST(e.g.: some rules that are not in `@file {}`),
|
|
51
|
+
// we treat them as global styles. Global styles should be added to COMMON_CSS(cssId: 0).
|
|
52
|
+
const commonCss = cssTree.generate(ast);
|
|
53
|
+
if (commonCss) {
|
|
54
|
+
emplaceCSSStyleSheet(css, COMMON_CSS_ID, commonCss);
|
|
55
|
+
}
|
|
56
|
+
// TODO: resolve conflict with scoped cssId
|
|
57
|
+
// E.g.: we may generate a cssId hash that is equal to the index of source files.
|
|
58
|
+
//
|
|
59
|
+
// For each CSS source file, we create a CSSStyleSheet.
|
|
60
|
+
// The scoped CSSStyleSheet will use `@import` to reference all the imported CSSStyleSheets.
|
|
61
|
+
//
|
|
62
|
+
// Note that the `Map.prototype.keys()` returns an iterator in insertion order.
|
|
63
|
+
// This will make sure that the stylesheets are created in the same order of CSS.
|
|
64
|
+
Array.from(fileKeyToCSSContent.keys()).forEach((fileKey, index) => {
|
|
65
|
+
// Starts from 1
|
|
66
|
+
// 0 is the common CSS
|
|
67
|
+
index = index + 1;
|
|
68
|
+
fileKeyToCSSId.set(fileKey, index);
|
|
69
|
+
emplaceCSSStyleSheet(css, index, fileKeyToCSSContent.get(fileKey));
|
|
70
|
+
});
|
|
71
|
+
// TODO: remove /cssId/0.css if not exists in the cssMap
|
|
72
|
+
// For each scoped CSSStyleSheet, we should import the real CSSStyleSheet.
|
|
73
|
+
// So that the styles can be resolved with the scoped cssId.
|
|
74
|
+
cssIdToFileKeys.forEach((fileKeys, cssId) => {
|
|
75
|
+
emplaceCSSStyleSheet(css, cssId, Array.from(fileKeys).map(fileKey => `@import "${fileKeyToCSSId.get(fileKey)}";`).join('\n'));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function emplaceCSSStyleSheet(map, key, value) {
|
|
79
|
+
if (map.has(key)) {
|
|
80
|
+
map.get(key).push(value);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
map.set(key, [value]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=debundle.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CSS } from '../index.js';
|
|
2
|
+
export declare function encodeCSS(cssChunks: string[], { enableCSSSelector, enableRemoveCSSScope, targetSdkVersion, }: {
|
|
3
|
+
/**
|
|
4
|
+
* {@inheritdoc @lynx-js/react-rsbuild-plugin#PluginReactLynxOptions.enableCSSSelector}
|
|
5
|
+
*/
|
|
6
|
+
enableCSSSelector: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* {@inheritdoc @lynx-js/react-rsbuild-plugin#PluginReactLynxOptions.enableRemoveCSSScope}
|
|
9
|
+
*/
|
|
10
|
+
enableRemoveCSSScope: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* {@inheritdoc @lynx-js/react-rsbuild-plugin#PluginReactLynxOptions.enableRemoveCSSScope}
|
|
13
|
+
*/
|
|
14
|
+
targetSdkVersion: string;
|
|
15
|
+
}, plugins?: CSS.Plugin[], encode?: (options: any) => Promise<{
|
|
16
|
+
buffer: Buffer;
|
|
17
|
+
}>): Promise<Buffer>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright 2024 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
import { removeFunctionWhiteSpace } from '@lynx-js/css-serializer/dist/plugins/removeFunctionWhiteSpace.js';
|
|
5
|
+
import { cssChunksToMap } from './cssChunksToMap.js';
|
|
6
|
+
export async function encodeCSS(cssChunks, { enableCSSSelector, enableRemoveCSSScope, targetSdkVersion, }, plugins = [removeFunctionWhiteSpace()],
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
8
|
+
encode = (options) => {
|
|
9
|
+
const buffer = Buffer.from(JSON.stringify(options));
|
|
10
|
+
return Promise.resolve({
|
|
11
|
+
buffer,
|
|
12
|
+
});
|
|
13
|
+
}) {
|
|
14
|
+
const css = cssChunksToMap(cssChunks, plugins);
|
|
15
|
+
const encodeOptions = {
|
|
16
|
+
compilerOptions: {
|
|
17
|
+
// Do not remove this, it will crash :)
|
|
18
|
+
enableFiberArch: true,
|
|
19
|
+
useLepusNG: true,
|
|
20
|
+
bundleModuleMode: 'ReturnByFunction',
|
|
21
|
+
enableCSSSelector,
|
|
22
|
+
targetSdkVersion,
|
|
23
|
+
},
|
|
24
|
+
sourceContent: {
|
|
25
|
+
appType: 'card',
|
|
26
|
+
config: {
|
|
27
|
+
lepusStrict: true,
|
|
28
|
+
enableRemoveCSSScope,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
css,
|
|
32
|
+
};
|
|
33
|
+
const { buffer } = await encode(encodeOptions);
|
|
34
|
+
return buffer;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=encode.js.map
|