@elementor/generate-wordpress-asset-file-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.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { webpack } from 'webpack';
8
+ import { GenerateWordPressAssetFileWebpackPlugin } from '../index';
9
+ import { vol } from 'memfs';
10
+
11
+ jest.mock( 'fs', () => jest.requireActual( 'memfs' ) );
12
+
13
+ describe( '@elementor/generate-wordpress-asset-file-webpack-plugin', () => {
14
+ beforeEach( () => {
15
+ const fileContent = `
16
+ import elementor from '@elementor/editor';
17
+ import wp from '@wordpress/element';
18
+ import other from '@other/package';
19
+
20
+ elementor();
21
+ wp();
22
+ other();
23
+ `;
24
+
25
+ fs.writeFileSync( path.resolve( '/app.js' ), fileContent );
26
+ fs.writeFileSync( path.resolve( '/extension.js' ), fileContent );
27
+ fs.writeFileSync( path.resolve( '/util.js' ), fileContent );
28
+ } );
29
+
30
+ afterEach( () => {
31
+ vol.reset();
32
+ } );
33
+
34
+ it( 'should generate assets files', ( done ) => {
35
+ // Arrange.
36
+ const compiler = webpack( {
37
+ mode: 'development',
38
+ entry: {
39
+ app: path.resolve( '/app.js' ),
40
+ extension: path.resolve( '/extension.js' ),
41
+ util: path.resolve( '/util.js' ),
42
+ },
43
+ output: {
44
+ filename: '[name].js',
45
+ path: path.resolve( '/dist' ),
46
+ },
47
+ externals: {
48
+ // Required so webpack won't try to resolve those packages, which don't exist.
49
+ '@elementor/editor': 'editor',
50
+ '@wordpress/element': 'wp',
51
+ '@other/package': 'other',
52
+ },
53
+ plugins: [
54
+ new GenerateWordPressAssetFileWebpackPlugin( {
55
+ handlePrefix: 'elementor-test-',
56
+ apps: [ 'app' ],
57
+ extensions: [ 'extension' ],
58
+ handlesMap: {
59
+ exact: {
60
+ '@elementor/editor': 'elementor-editor',
61
+ },
62
+ startsWith: {
63
+ '@wordpress/': 'wp-',
64
+ },
65
+ },
66
+ i18n: {
67
+ domain: 'elementor-test',
68
+ replaceRequestedFile: true,
69
+ },
70
+ } ),
71
+ ],
72
+ } );
73
+
74
+ // Expect.
75
+ expect.assertions( 7 );
76
+
77
+ // Act.
78
+ compiler.run( ( err, stats ) => {
79
+ // Assert.
80
+ expect( err ).toBe( null );
81
+ expect( stats?.hasErrors() ).toBe( false );
82
+ expect( stats?.hasWarnings() ).toBe( false );
83
+
84
+ const files = [
85
+ 'app.asset.php',
86
+ 'extension.asset.php',
87
+ 'util.asset.php',
88
+ 'loader.php',
89
+ ];
90
+
91
+ files.forEach( ( fileName ) => {
92
+ const fileContent = fs.readFileSync( path.resolve( `/dist/${ fileName }` ), { encoding: 'utf8' } );
93
+
94
+ expect( fileContent ).toMatchSnapshot( fileName );
95
+ } );
96
+
97
+ done();
98
+ } );
99
+ } );
100
+ } );
package/src/index.ts ADDED
@@ -0,0 +1,263 @@
1
+ // Inspired by "Dependency Extraction Webpack Plugin" by @wordpress team.
2
+ // Link: https://github.com/WordPress/gutenberg/tree/trunk/packages/dependency-extraction-webpack-plugin
3
+ import { sources, Compilation, Compiler, Chunk } from 'webpack';
4
+
5
+ type HandlesMap = {
6
+ exact: Record<string, string>;
7
+ startsWith: Record<string, string>;
8
+ }
9
+
10
+ type Options = {
11
+ handlePrefix: string;
12
+ handlesMap?: Partial<HandlesMap>
13
+ apps?: string[];
14
+ extensions?: string[];
15
+ i18n?: {
16
+ domain: string;
17
+ replaceRequestedFile?: boolean;
18
+ }
19
+ }
20
+
21
+ type NormalizedOptions = {
22
+ handlePrefix: string;
23
+ handlesMap: HandlesMap;
24
+ apps: string[];
25
+ extensions: string[];
26
+ i18n: {
27
+ domain: string | null;
28
+ replaceRequestedFile: boolean;
29
+ }
30
+ }
31
+
32
+ type Module = {
33
+ userRequest?: string;
34
+ modules?: Module[];
35
+ }
36
+
37
+ const baseHandlesMap: HandlesMap = {
38
+ exact: {
39
+ react: 'react',
40
+ 'react-dom': 'react-dom',
41
+ },
42
+ startsWith: {
43
+ '@elementor/': 'elementor-packages-',
44
+ '@wordpress/': 'wp-',
45
+ },
46
+ };
47
+
48
+ export class GenerateWordPressAssetFileWebpackPlugin {
49
+ options: NormalizedOptions;
50
+
51
+ constructor( options: Options ) {
52
+ this.options = this.normalizeOptions( options );
53
+ }
54
+
55
+ apply( compiler: Compiler ) {
56
+ compiler.hooks.thisCompilation.tap( this.constructor.name, ( compilation ) => {
57
+ let handlesAssetsMap: Record<string, string>;
58
+
59
+ compilation.hooks.processAssets.tap( { name: this.constructor.name }, () => {
60
+ handlesAssetsMap = [ ...compilation.entrypoints ].reduce<Record<string, string>>( ( map, [ entryName, entrypoint ] ) => {
61
+ const chunk = entrypoint.chunks.find( ( { name } ) => name === entryName );
62
+
63
+ if ( ! chunk ) {
64
+ return map;
65
+ }
66
+
67
+ const chunkJSFile = this.getFileFromChunk( chunk );
68
+
69
+ if ( ! chunkJSFile ) {
70
+ return map;
71
+ }
72
+
73
+ const deps = this.getDepsFromChunk( compilation, chunk );
74
+
75
+ const assetFilename = this.generateAssetsFileName(
76
+ compilation.getPath( '[file]', { filename: chunkJSFile } )
77
+ );
78
+
79
+ const handle = this.generateHandleName( entryName );
80
+
81
+ const content = this.createAssetsFileContent( {
82
+ deps,
83
+ entryName,
84
+ i18n: this.options.i18n,
85
+ } );
86
+
87
+ // Add source and file into compilation for webpack to output.
88
+ compilation.assets[ assetFilename ] = new sources.RawSource( content );
89
+
90
+ chunk.files.add( assetFilename );
91
+
92
+ map[ handle ] = assetFilename;
93
+
94
+ return map;
95
+ }, {} );
96
+ } );
97
+
98
+ compilation.hooks.afterProcessAssets.tap( { name: this.constructor.name }, () => {
99
+ const loaderFileContent = this.getLoaderFileContent( handlesAssetsMap );
100
+
101
+ compilation.assets[ 'loader.php' ] = new sources.RawSource( loaderFileContent );
102
+ } );
103
+ } );
104
+ }
105
+
106
+ getDepsFromChunk( compilation: Compilation, chunk: Chunk ) {
107
+ const depsSet = new Set<string>();
108
+
109
+ compilation.chunkGraph.getChunkModules( chunk ).forEach( ( module ) => {
110
+ // There are some issues with types in webpack, so we need to cast it.
111
+ const theModule = module as Module;
112
+
113
+ [ ...( theModule.modules || [] ), theModule ].forEach( ( subModule ) => {
114
+ if ( subModule.userRequest && this.isExternalDep( subModule.userRequest ) ) {
115
+ depsSet.add( subModule.userRequest );
116
+ }
117
+ } );
118
+ } );
119
+
120
+ return depsSet;
121
+ }
122
+
123
+ createAssetsFileContent( {
124
+ deps,
125
+ i18n,
126
+ entryName,
127
+ }: {
128
+ deps: Set<string>;
129
+ i18n: NormalizedOptions[ 'i18n' ];
130
+ entryName: string;
131
+ } ) {
132
+ const handleName = this.generateHandleName( entryName );
133
+ const type = this.getEntryType( entryName );
134
+
135
+ const depsAsString = [ ...deps ]
136
+ .map( ( dep ) => this.getHandleFromDep( dep ) )
137
+ .filter( ( dep ) => dep !== handleName )
138
+ .sort()
139
+ .map( ( dep ) => `'${ dep }',` )
140
+ .join( '\n\t\t' );
141
+
142
+ const i18nContent = i18n.domain ? `[
143
+ 'domain' => '${ i18n.domain }',
144
+ 'replace_requested_file' => ${ ( i18n.replaceRequestedFile ?? false ).toString() },
145
+ ]` : '[]';
146
+
147
+ const content =
148
+ `<?php
149
+ if ( ! defined( 'ABSPATH' ) ) {
150
+ exit;
151
+ }
152
+ /**
153
+ * This file is generated by Webpack, do not edit it directly.
154
+ */
155
+ return [
156
+ 'handle' => '${ handleName }',
157
+ 'src' => plugins_url( '/', __FILE__ ) . '${ entryName }{{MIN_SUFFIX}}.js',
158
+ 'i18n' => ${ i18nContent },
159
+ 'type' => '${ type }',
160
+ 'deps' => [
161
+ ${ depsAsString }
162
+ ],
163
+ ];
164
+ `;
165
+
166
+ return content;
167
+ }
168
+
169
+ getLoaderFileContent( entriesData: Record<string, string> ) {
170
+ const entriesContent = Object.entries( entriesData ).map( ( [ handle, assetFileName ] ) => {
171
+ return `
172
+ $data['${ handle }'] = require __DIR__ . '/${ assetFileName }';`;
173
+ } );
174
+
175
+ return `<?php
176
+ if ( ! defined( 'ABSPATH' ) ) {
177
+ exit;
178
+ }
179
+ /**
180
+ * This file is generated by Webpack, do not edit it directly.
181
+ */
182
+ add_filter( 'elementor/editor-v2/packages/config', function( $data ) {
183
+ ${ entriesContent.join( '\n' ) }
184
+ return $data;
185
+ } );
186
+ `;
187
+ }
188
+
189
+ getEntryType( entryName: string ) {
190
+ if ( this.options.extensions.includes( entryName ) ) {
191
+ return 'extension';
192
+ }
193
+
194
+ if ( this.options.apps.includes( entryName ) ) {
195
+ return 'app';
196
+ }
197
+
198
+ return 'util';
199
+ }
200
+
201
+ getFileFromChunk( chunk: Chunk ) {
202
+ return [ ...chunk.files ].find( ( f ) => /\.js$/i.test( f ) );
203
+ }
204
+
205
+ isExternalDep( request: string ) {
206
+ const { startsWith, exact } = this.options.handlesMap;
207
+
208
+ return request && (
209
+ Object.keys( exact ).includes( request ) ||
210
+ Object.keys( startsWith ).some( ( dep ) => request.startsWith( dep ) )
211
+ );
212
+ }
213
+
214
+ getHandleFromDep( dep: string ) {
215
+ const { startsWith, exact } = this.options.handlesMap;
216
+
217
+ if ( Object.keys( exact ).includes( dep ) ) {
218
+ return exact[ dep ];
219
+ }
220
+
221
+ for ( const [ key, value ] of Object.entries( startsWith ) ) {
222
+ if ( dep.startsWith( key ) ) {
223
+ return dep.replace( key, value );
224
+ }
225
+ }
226
+
227
+ return dep;
228
+ }
229
+
230
+ generateHandleName( name: string ) {
231
+ if ( this.options.handlePrefix ) {
232
+ return `${ this.options.handlePrefix }${ name }`;
233
+ }
234
+
235
+ return name;
236
+ }
237
+
238
+ generateAssetsFileName( filename: string ) {
239
+ return filename.replace( /(\.min)?\.js$/i, '.asset.php' );
240
+ }
241
+
242
+ normalizeOptions( options: Options ): NormalizedOptions {
243
+ return {
244
+ ...options,
245
+ handlesMap: {
246
+ exact: {
247
+ ...baseHandlesMap.exact,
248
+ ...( options?.handlesMap?.exact || {} ),
249
+ },
250
+ startsWith: {
251
+ ...baseHandlesMap.startsWith,
252
+ ...( options?.handlesMap?.startsWith || {} ),
253
+ },
254
+ },
255
+ apps: options?.apps || [],
256
+ extensions: options?.extensions || [],
257
+ i18n: {
258
+ domain: options?.i18n?.domain || null,
259
+ replaceRequestedFile: options?.i18n?.replaceRequestedFile ?? false,
260
+ },
261
+ };
262
+ }
263
+ }