@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.
@@ -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
@@ -0,0 +1,3 @@
1
+ import * as CSS from '@lynx-js/css-serializer';
2
+ import type { Plugin } from '@lynx-js/css-serializer';
3
+ export declare function cssToAst(content: string, plugins: Plugin[]): [CSS.LynxStyleNode[], CSS.ParserError[]];
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,6 @@
1
+ import type * as CSS from '@lynx-js/css-serializer';
2
+ export declare function cssChunksToMap(cssChunks: string[], plugins: CSS.Plugin[]): {
3
+ cssMap: Record<string, CSS.LynxStyleNode[]>;
4
+ cssSource: Record<string, string>;
5
+ contentMap: Map<number, string[]>;
6
+ };
@@ -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