Procházet zdrojové kódy

Rebuilt SnippetAdapter. Aligned to the changes made in Umberto.

Kamil Piechaczek před 6 roky
rodič
revize
b824eca30f
3 změnil soubory, kde provedl 228 přidání a 59 odebrání
  1. 1 0
      package.json
  2. 212 58
      scripts/docs/snippetadapter.js
  3. 15 1
      yarn.lock

+ 1 - 0
package.json

@@ -85,6 +85,7 @@
     "mini-css-extract-plugin": "^0.4.0",
     "minimatch": "^3.0.4",
     "postcss-loader": "^3.0.0",
+    "progress-bar-webpack-plugin": "^1.12.1",
     "raw-loader": "^1.0.0",
     "style-loader": "^0.23.0",
     "svgo": "^1.1.0",

+ 212 - 58
scripts/docs/snippetadapter.js

@@ -12,78 +12,178 @@ const { bundler, styles } = require( '@ckeditor/ckeditor5-dev-utils' );
 const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' );
 const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
 const UglifyJsWebpackPlugin = require( 'uglifyjs-webpack-plugin' );
+const ProgressBarPlugin = require( 'progress-bar-webpack-plugin' );
 
-const webpackProcesses = new Map();
+const DEFAULT_LANGUAGE = 'en';
 
-module.exports = function snippetAdapter( data ) {
-	if ( !data.snippetSource.js ) {
-		throw new Error( `Missing snippet source for "${ data.snippetPath }".` );
+/**
+ * @param {Set.<Snippet>} snippets
+ * @param {Object} options
+ * @param {Object.<String, Function>} umbertoHelpers
+ * @returns {Promise}
+ */
+module.exports = function snippetAdapter( snippets, options, umbertoHelpers ) {
+	const { getSnippetPlaceholder, getSnippetSourcePaths } = umbertoHelpers;
+
+	const snippetsDependencies = new Map();
+
+	// For each snippet, load its config. If the snippet has defined dependencies, load those as well.
+	for ( const snippetData of snippets ) {
+		if ( !snippetData.snippetSources.js ) {
+			throw new Error( `Missing snippet source for "${ snippetData.snippetName }".` );
+		}
+
+		snippetData.snippetConfig = readSnippetConfig( snippetData.snippetSources.js );
+		snippetData.snippetConfig.language = snippetData.snippetConfig.language || DEFAULT_LANGUAGE;
+
+		if ( snippetData.snippetConfig.dependencies ) {
+			for ( const dependencyName of snippetData.snippetConfig.dependencies ) {
+				// Do not load the same dependency more than once.
+				if ( snippetsDependencies.has( dependencyName ) ) {
+					continue;
+				}
+
+				// Find a root path where to look for the snippet's sources. We just want to pass them through Webpack.
+				const snippetBasePathRegExp = new RegExp( snippetData.snippetName.replace( /\//g, '\\/' ) + '.*$' );
+				const snippetBasePath = snippetData.snippetSources.js.replace( snippetBasePathRegExp, '' );
+
+				const dependencySnippet = {
+					snippetSources: getSnippetSourcePaths( snippetBasePath, dependencyName ),
+					snippetName: dependencyName,
+					outputPath: snippetData.outputPath,
+					destinationPath: snippetData.destinationPath,
+					isDependency: true
+				};
+
+				if ( !dependencySnippet.snippetSources.js ) {
+					throw new Error( `Missing snippet source for "${ dependencySnippet.snippetName }".` );
+				}
+
+				dependencySnippet.snippetConfig = readSnippetConfig( dependencySnippet.snippetSources.js );
+				dependencySnippet.snippetConfig.language = dependencySnippet.snippetConfig.language || DEFAULT_LANGUAGE;
+
+				snippetsDependencies.set( dependencyName, dependencySnippet );
+			}
+		}
 	}
 
-	const snippetConfig = readSnippetConfig( data.snippetSource.js );
-	const outputPath = path.join( data.outputPath, data.snippetPath );
+	for ( const snippetData of snippetsDependencies.values() ) {
+		snippets.add( snippetData );
+	}
 
-	const webpackConfig = getWebpackConfig( {
-		entry: data.snippetSource.js,
-		outputPath,
-		language: snippetConfig.language,
-		production: data.options.production,
-		definitions: data.options.definitions || {}
-	} );
+	const groupedSnippetsByLanguage = {};
+
+	// Group snippets by language. There is no way to build different languages in a single Webpack process.
+	// Webpack must be called as many times as different languages are being used in snippets.
+	for ( const snippetData of snippets ) {
+		if ( !groupedSnippetsByLanguage[ snippetData.snippetConfig.language ] ) {
+			groupedSnippetsByLanguage[ snippetData.snippetConfig.language ] = new Set();
+		}
+
+		groupedSnippetsByLanguage[ snippetData.snippetConfig.language ].add( snippetData );
+	}
+
+	// For each language prepare own Webpack configuration.
+	const webpackConfigs = Object.keys( groupedSnippetsByLanguage )
+		.map( language => {
+			return getWebpackConfig( groupedSnippetsByLanguage[ language ], {
+				language,
+				production: options.production,
+				definitions: options.definitions || {}
+			} );
+		} );
+
+	let promise = Promise.resolve();
 
-	let promise;
+	if ( !webpackConfigs.length ) {
+		return promise;
+	}
 
-	// See #530.
-	if ( webpackProcesses.has( outputPath ) ) {
-		promise = webpackProcesses.get( outputPath );
-	} else {
-		promise = runWebpack( webpackConfig );
-		webpackProcesses.set( outputPath, promise );
+	for ( const config of webpackConfigs ) {
+		promise = promise.then( () => runWebpack( config ) );
 	}
 
 	return promise
 		.then( () => {
-			const wasCSSGenerated = fs.existsSync( path.join( outputPath, 'snippet.css' ) );
-			const cssFiles = [
-				path.join( data.basePath, 'assets', 'snippet-styles.css' )
-			];
-
-			// CSS may not be generated by Webpack if a snippet's JS file didn't import any SCSS files.
-			if ( wasCSSGenerated ) {
-				cssFiles.unshift( path.join( data.relativeOutputPath, data.snippetPath, 'snippet.css' ) );
+			// Group snippets by destination path in order to attach required HTML code and assets (CSS and JS).
+			const groupedSnippetsByDestinationPath = {};
+
+			for ( const snippetData of snippets ) {
+				if ( !groupedSnippetsByDestinationPath[ snippetData.destinationPath ] ) {
+					groupedSnippetsByDestinationPath[ snippetData.destinationPath ] = new Set();
+				}
+
+				groupedSnippetsByDestinationPath[ snippetData.destinationPath ].add( snippetData );
 			}
 
-			// If the snippet is a dependency of a parent snippet, append JS and CSS to HTML and save to disk.
-			if ( data.isDependency ) {
-				let htmlFile = fs.readFileSync( data.snippetSource.html ).toString();
+			for ( const destinationPath of Object.keys( groupedSnippetsByDestinationPath ) ) {
+				const snippetsOnPage = groupedSnippetsByDestinationPath[ destinationPath ];
+
+				const cssFiles = [];
+				const jsFiles = [];
+
+				let content = fs.readFileSync( destinationPath ).toString();
+
+				for ( const snippetData of snippetsOnPage ) {
+					// CSS may not be generated by Webpack if a snippet's JS file didn't import any CSS files.
+					const wasCSSGenerated = fs.existsSync( path.join( snippetData.outputPath, snippetData.snippetName, 'snippet.css' ) );
+
+					// If the snippet is a dependency, append JS and CSS to HTML save to disk and continue.
+					if ( snippetData.isDependency ) {
+						let htmlFile = fs.readFileSync( snippetData.snippetSources.html ).toString();
 
-				if ( wasCSSGenerated ) {
-					htmlFile += '<link rel="stylesheet" href="snippet.css" type="text/css">';
+						if ( wasCSSGenerated ) {
+							htmlFile += '<link rel="stylesheet" href="snippet.css" type="text/css">';
+						}
+
+						htmlFile += '<script src="snippet.js"></script>';
+
+						fs.writeFileSync( path.join( snippetData.outputPath, snippetData.snippetName, 'snippet.html' ), htmlFile );
+
+						continue;
+					}
+
+					let snippetHTML;
+
+					if ( fs.existsSync( snippetData.snippetSources.html ) ) {
+						snippetHTML = fs.readFileSync( snippetData.snippetSources.html ).toString();
+						snippetHTML = snippetHTML.replace( /%BASE_PATH%/g, snippetData.basePath );
+						snippetHTML = `<div class="live-snippet">${ snippetHTML }</div>`;
+					} else {
+						snippetHTML = '';
+					}
+
+					content = content.replace( getSnippetPlaceholder( snippetData.snippetName ), snippetHTML );
+
+					jsFiles.push( path.join( snippetData.basePath, 'assets', 'snippet.js' ) );
+					jsFiles.push( path.join( snippetData.relativeOutputPath, snippetData.snippetName, 'snippet.js' ) );
+
+					cssFiles.push( path.join( snippetData.basePath, 'assets', 'snippet-styles.css' ) );
+
+					if ( wasCSSGenerated ) {
+						cssFiles.unshift( path.join( snippetData.relativeOutputPath, snippetData.snippetName, 'snippet.css' ) );
+					}
 				}
 
-				htmlFile += '<script src="snippet.js"></script>';
+				const cssImportsHTML = [ ...new Set( cssFiles ) ]
+					.map( importPath => `    <link rel="stylesheet" href="${ importPath }" type="text/css">` )
+					.join( '\n' )
+					.replace( /^\s+/, '' );
 
-				fs.writeFileSync( path.join( outputPath, 'snippet.html' ), htmlFile );
-			}
+				const jsImportsHTML = [ ...new Set( jsFiles ) ]
+					.map( importPath => `    <script src="${ importPath }"></script>` )
+					.join( '\n' )
+					.replace( /^\s+/, '' );
 
-			return {
-				html: fs.readFileSync( data.snippetSource.html ),
-				assets: {
-					js: [
-						// Load snippet helpers first.
-						path.join( data.basePath, 'assets', 'snippet.js' ),
-
-						// Then load the actual snippet code.
-						path.join( data.relativeOutputPath, data.snippetPath, 'snippet.js' )
-					],
-					css: cssFiles
-				},
-				dependencies: snippetConfig.dependencies
-			};
+				content = content.replace( '<!--UMBERTO: SNIPPET: CSS-->', cssImportsHTML );
+				content = content.replace( '<!--UMBERTO: SNIPPET: JS-->', jsImportsHTML );
+
+				fs.writeFileSync( destinationPath, content );
+			}
 		} );
 };
 
-function getWebpackConfig( config ) {
+function getWebpackConfig( snippets, config ) {
 	// Stringify all definitions values. The `DefinePlugin` injects definition values as they are so we need to stringify them,
 	// so they will become real strings in the generated code. See https://webpack.js.org/plugins/define-plugin/ for more information.
 	const definitions = {};
@@ -92,16 +192,15 @@ function getWebpackConfig( config ) {
 		definitions[ definitionKey ] = JSON.stringify( config.definitions[ definitionKey ] );
 	}
 
-	return {
+	const webpackConfig = {
 		mode: config.production ? 'production' : 'development',
 
 		devtool: 'source-map',
 
-		entry: config.entry,
+		entry: {},
 
 		output: {
-			path: config.outputPath,
-			filename: 'snippet.js'
+			filename: '[name]/snippet.js'
 		},
 
 		optimization: {
@@ -119,15 +218,18 @@ function getWebpackConfig( config ) {
 		},
 
 		plugins: [
-			new MiniCssExtractPlugin( { filename: 'snippet.css' } ),
+			new MiniCssExtractPlugin( { filename: '[name]/snippet.css' } ),
 			new CKEditorWebpackPlugin( {
-				language: config.language || 'en'
+				language: config.language
 			} ),
 			new webpack.BannerPlugin( {
 				banner: bundler.getLicenseBanner(),
 				raw: true
 			} ),
-			new webpack.DefinePlugin( definitions )
+			new webpack.DefinePlugin( definitions ),
+			new ProgressBarPlugin( {
+				format: `Building snippets for language "${ config.language }": :percent (:msg)`,
+			} )
 		],
 
 		// Configure the paths so building CKEditor 5 snippets work even if the script
@@ -165,6 +267,20 @@ function getWebpackConfig( config ) {
 			]
 		}
 	};
+
+	for ( const snippetData of snippets ) {
+		if ( !webpackConfig.output.path ) {
+			webpackConfig.output.path = snippetData.outputPath;
+		}
+
+		if ( webpackConfig.entry[ snippetData.snippetName ] ) {
+			continue;
+		}
+
+		webpackConfig.entry[ snippetData.snippetName ] = snippetData.snippetSources.js;
+	}
+
+	return webpackConfig;
 }
 
 function runWebpack( webpackConfig ) {
@@ -199,3 +315,41 @@ function readSnippetConfig( snippetSourcePath ) {
 
 	return JSON.parse( configSourceMatch[ 1 ] );
 }
+
+/**
+ * @typedef {Object} Snippet
+ *
+ * @property {SnippetSource} snippetSources Sources of the snippet.
+ *
+ * @property {String} snippetName Name of the snippet. Defined directly after `@snippet` tag.
+ *
+ * @property {String} outputPath An absolute path where to write file produced by the `snippetAdapter`.
+ *
+ * @property {String} destinationPath An absolute path to the file where the snippet is being used.
+ *
+ * @property {SnippetConfiguration} snippetConfig={} Additional configuration of the snippet. It's being read from the snippet's source.
+ *
+ * @property {String} [basePath] Relative path from the processed file to the root of the documentation.
+ *
+ * @property {String} [relativeOutputPath] The same like `basePath` but for the output path (where processed file will be saved).
+ *
+ * @property {Boolean} [isDependency] Whether parsed snippet is a dependency of other snippet.
+ */
+
+/**
+ * @typedef {Object} SnippetSource
+ *
+ * @property {<String>} html An absolute path to the HTML sample.
+ *
+ * @property {<String>} css An absolute path to the CSS sample.
+ *
+ * @property {<String>} js An absolute path to the JS sample.
+ */
+
+/**
+ * @typedef {Object} SnippetConfiguration
+ *
+ * @property {<String>} [language] A language that will be used for building the editor.
+ *
+ * @property {Array.<String>} [dependencies] Names of samples that are required to working.
+ */

+ 15 - 1
yarn.lock

@@ -7533,7 +7533,7 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.0.4, object.assign@^4.1.0:
+object.assign@^4.0.1, object.assign@^4.0.4, object.assign@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
   integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
@@ -8406,6 +8406,20 @@ process@^0.11.10:
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
 
+progress-bar-webpack-plugin@^1.12.1:
+  version "1.12.1"
+  resolved "https://registry.yarnpkg.com/progress-bar-webpack-plugin/-/progress-bar-webpack-plugin-1.12.1.tgz#bbf3b1137a4ba2474eeb111377d6c1a580c57dd1"
+  integrity sha512-tVbPB5xBbqNwdH3mwcxzjL1r1Vrm/xGu93OsqVSAbCaXGoKFvfWIh0gpMDpn2kYsPVRSAIK0pBkP9Vfs+JJibQ==
+  dependencies:
+    chalk "^1.1.1"
+    object.assign "^4.0.1"
+    progress "^1.1.8"
+
+progress@^1.1.8:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+  integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
+
 progress@^2.0.0:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"