├── spec ├── fixtures │ ├── script1.js │ ├── script2.js │ ├── styles │ │ ├── style1.css │ │ ├── style2.css │ │ ├── style4.css │ │ ├── style5.css │ │ ├── style1-with-img1.css │ │ ├── style1-with-img2.css │ │ ├── style2-with-img1.css │ │ ├── style1-with-import1.css │ │ └── style1.scss │ ├── script1-with-require1.js │ ├── images │ │ ├── img1.png │ │ └── img2.png │ └── index.js ├── tsconfig.json ├── support │ └── jasmine.json ├── invalid-options.spec.js ├── disable-option.spec.js ├── sass-loader-interop.spec.js ├── default-options.spec.js ├── shorthand-filename-option.spec.js ├── webpack-dev-server-interop.spec.js ├── css-with-import.spec.js ├── extract-text-plugin-interop.spec.js ├── file-loader-interop.spec.js ├── common-requests-between-entries.spec.js ├── css-entry-plugin.spec.js ├── helpers │ └── common.js └── html-plugin-interop.spec.js ├── packages └── @types │ ├── webpack-sources │ ├── lib │ │ └── Source.d.ts │ └── package.json │ ├── tapable │ ├── package.json │ └── index.d.ts │ ├── webpack │ ├── package.json │ ├── lib │ │ ├── Entrypoint.d.ts │ │ ├── Chunk.d.ts │ │ ├── ModuleReason.d.ts │ │ ├── dependencies │ │ │ ├── ModuleDependency.d.ts │ │ │ ├── SingleEntryDependency.d.ts │ │ │ └── MultiEntryDependency.d.ts │ │ ├── common-types.d.ts │ │ ├── webpack.d.ts │ │ ├── Module.d.ts │ │ ├── MultiModule.d.ts │ │ ├── Dependency.d.ts │ │ ├── NormalModuleFactory.d.ts │ │ ├── Compiler.d.ts │ │ └── Compilation.d.ts │ └── index.d.ts │ ├── enhanced-resolve │ ├── package.json │ └── lib │ │ └── Resolver.d.ts │ ├── html-webpack-plugin │ ├── package.json │ └── index.d.ts │ └── extract-text-webpack-plugin │ ├── package.json │ └── index.d.ts ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .travis.yml ├── .gitignore ├── src ├── index.ts ├── tsconfig.json ├── interop │ ├── html-webpack-plugin │ │ └── index.ts │ ├── tapable │ │ └── index.ts │ └── webpack │ │ └── index.ts ├── CssEntryPluginError.ts ├── models.ts ├── HtmlWebpackPluginCssEntryFix.ts ├── CssEntryPlugin.ts ├── options.ts └── CssEntryCompilation.ts ├── .nycrc ├── .babelrc ├── .editorconfig ├── LICENSE ├── tsconfig.json ├── package.json ├── tslint.json ├── gulpfile.js └── README.md /spec/fixtures/script1.js: -------------------------------------------------------------------------------- 1 | console.log("script1"); 2 | -------------------------------------------------------------------------------- /spec/fixtures/script2.js: -------------------------------------------------------------------------------- 1 | console.log("script2"); 2 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style1.css: -------------------------------------------------------------------------------- 1 | .style1-class { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style2.css: -------------------------------------------------------------------------------- 1 | .style2-class { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style4.css: -------------------------------------------------------------------------------- 1 | .style4-class { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style5.css: -------------------------------------------------------------------------------- 1 | .style5-class { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /packages/@types/webpack-sources/lib/Source.d.ts: -------------------------------------------------------------------------------- 1 | export default class Source { 2 | } 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | node_js: 5 | - "node" 6 | script: npm test 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /tmp 4 | /lib 5 | /dist 6 | /coverage 7 | /.nyc_output 8 | *.log 9 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style1-with-img1.css: -------------------------------------------------------------------------------- 1 | .style1-img1-class { 2 | background: url("../images/img1.png"); 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style1-with-img2.css: -------------------------------------------------------------------------------- 1 | .style1-img2-class { 2 | background: url("../images/img2.png"); 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style2-with-img1.css: -------------------------------------------------------------------------------- 1 | .style2-img1-class { 2 | background: url("../images/img1.png"); 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/script1-with-require1.js: -------------------------------------------------------------------------------- 1 | let script1 = require("./script2"); 2 | 3 | console.log("script1-with-require1"); 4 | -------------------------------------------------------------------------------- /packages/@types/tapable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tapable-lib", 3 | "version": "1.0.0", 4 | "types": "index.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@types/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-lib", 3 | "version": "1.0.0", 4 | "types": "index.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/images/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomachristian/css-entry-webpack-plugin/HEAD/spec/fixtures/images/img1.png -------------------------------------------------------------------------------- /spec/fixtures/images/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomachristian/css-entry-webpack-plugin/HEAD/spec/fixtures/images/img2.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import CssEntryPlugin from "./CssEntryPlugin"; 2 | 3 | export default CssEntryPlugin; 4 | 5 | // TODO: Export types 6 | -------------------------------------------------------------------------------- /packages/@types/webpack-sources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-sources-lib", 3 | "version": "1.0.0", 4 | "types": "index.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@types/enhanced-resolve/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enhanced-resolve-lib", 3 | "version": "1.0.0", 4 | "types": "index.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@types/html-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-webpack-plugin-lib", 3 | "version": "1.0.0", 4 | "types": "index.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@types/extract-text-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extract-text-webpack-plugin-lib", 3 | "version": "1.0.0", 4 | "types": "index.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/Entrypoint.d.ts: -------------------------------------------------------------------------------- 1 | import Chunk from "./Chunk"; 2 | 3 | export default class Entrypoint { 4 | name: string; 5 | chunks: Chunk[]; 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style1-with-import1.css: -------------------------------------------------------------------------------- 1 | @import "style1.css"; 2 | 3 | .style1-import1-class { 4 | color: red; 5 | } 6 | 7 | .hello { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "html", 4 | "text", 5 | "lcov" 6 | ], 7 | "include": [ 8 | "**/lib/**/*.js", "**/src/**/*.ts" 9 | ], 10 | "cache": false 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | "bluebird-global" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/Chunk.d.ts: -------------------------------------------------------------------------------- 1 | import Module from "./Module"; 2 | 3 | export default class Chunk { 4 | name: string; 5 | files: string[]; 6 | modules: Module[]; 7 | entryModule?: Module; 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "targets": { "node": true } }] 4 | ], 5 | "plugins": [ 6 | "add-module-exports", 7 | "transform-promise-to-bluebird" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "types": [ 5 | "jasmine", 6 | "node", 7 | "bluebird-global" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/ModuleReason.d.ts: -------------------------------------------------------------------------------- 1 | import Module from "./Module"; 2 | 3 | export default class ModuleReason { 4 | module: Module; 5 | dependency: any; 6 | 7 | constructor(module: Module, dependency: any); 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*.[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/dependencies/ModuleDependency.d.ts: -------------------------------------------------------------------------------- 1 | import Dependency from "../Dependency"; 2 | 3 | export default class ModuleDependency extends Dependency { 4 | request: string; 5 | userRequest: string; 6 | 7 | constructor(request: string); 8 | } 9 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/common-types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Entry { 2 | [name: string]: string | string[]; 3 | } 4 | 5 | export type Loader = any; 6 | 7 | export interface Configuration { 8 | context?: string; 9 | entry?: string | string[] | Entry; 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/styles/style1.scss: -------------------------------------------------------------------------------- 1 | .style1-class { 2 | color: red; 3 | 4 | .style1-sub-class { 5 | color: green; 6 | } 7 | } 8 | 9 | 10 | .error { 11 | color: red; 12 | } 13 | 14 | .error2 { 15 | @extend .error; 16 | font-size: 10px; 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | max_line_length = 233 7 | 8 | [package.json] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.{js, ts, json, .babelrc}] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 4 17 | trim_trailing_whitespace = true 18 | -------------------------------------------------------------------------------- /src/interop/html-webpack-plugin/index.ts: -------------------------------------------------------------------------------- 1 | import { WithHtmlWebpackPlugin, HtmlData, HtmlAssetTag } from "html-webpack-plugin"; 2 | import { Compilation } from "../webpack"; 3 | 4 | export { HtmlData, HtmlAssetTag }; 5 | 6 | export function getHtmlWebpackPluginCompilation(compilation: Compilation): WithHtmlWebpackPlugin { 7 | return compilation as any; 8 | } 9 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/webpack.d.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "tapable"; 2 | import { Configuration, Entry, Loader } from "./common-types"; 3 | import Compiler, { CompilerPlugin } from "./Compiler"; 4 | import { CompilationPlugin } from "./Compilation"; 5 | 6 | export { Configuration, Entry, Loader, Plugin, CompilerPlugin, CompilationPlugin }; 7 | export { Compiler }; 8 | -------------------------------------------------------------------------------- /packages/@types/webpack/index.d.ts: -------------------------------------------------------------------------------- 1 | import webpack from "./lib/webpack"; 2 | 3 | export * from "./lib/webpack"; 4 | export default webpack; 5 | 6 | // Forward webpack-sources 7 | import Source from "webpack-sources/lib/Source"; 8 | 9 | export { Source }; 10 | 11 | // Forward enhanced-resolve 12 | import Resolver from "enhanced-resolve/lib/Resolver"; 13 | 14 | export { Resolver }; 15 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/dependencies/SingleEntryDependency.d.ts: -------------------------------------------------------------------------------- 1 | import ModuleDependency from "./ModuleDependency"; 2 | 3 | export default class SingleEntryDependency extends ModuleDependency { 4 | type: "single entry"; // ? 5 | 6 | /** 7 | * Can be added to SingleEntryDependency. 8 | */ 9 | loc?: string; 10 | 11 | constructor(request: string); 12 | } 13 | -------------------------------------------------------------------------------- /src/CssEntryPluginError.ts: -------------------------------------------------------------------------------- 1 | export default class CssEntryPluginError extends Error { 2 | constructor(message: string) { 3 | super("CssEntryPlugin: " + message); 4 | 5 | if (Error.hasOwnProperty("captureStackTrace")) { 6 | Error.captureStackTrace(this, this.constructor); 7 | } 8 | 9 | this.name = "CssEntryPluginError"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/Module.d.ts: -------------------------------------------------------------------------------- 1 | import ModuleReason from "./ModuleReason"; 2 | 3 | export default class Module /*extends DepedenciesBlock*/ { 4 | context: string | null; 5 | reasons: ModuleReason[]; 6 | 7 | /* identifier = null */ 8 | /* readableIdentifier = null */ 9 | /* build = null */ 10 | /* source = null */ 11 | /* size = null */ 12 | /* nameForCondition = null */ 13 | } 14 | -------------------------------------------------------------------------------- /spec/invalid-options.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin with invalid options", () => { 2 | describe("configured with unsupported options", () => { 3 | it("fails with invalid options error", () => { 4 | expect(() => { 5 | new CssEntryPlugin(10); 6 | }).toThrowError(CssEntryPluginError, /should be of type string, function or object/); 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/dependencies/MultiEntryDependency.d.ts: -------------------------------------------------------------------------------- 1 | import Dependency from "../Dependency"; 2 | import SingleEntryDependency from "./SingleEntryDependency"; 3 | 4 | export default class MultiEntryDependency extends Dependency { 5 | type: "multi entry"; // ? 6 | name: string; 7 | dependencies: SingleEntryDependency[]; 8 | 9 | constructor(dependencies: SingleEntryDependency[], name: string); 10 | } 11 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/MultiModule.d.ts: -------------------------------------------------------------------------------- 1 | import Module from "./Module"; 2 | import Dependency from "./Dependency"; 3 | import SingleEntryDependency from "./dependencies/SingleEntryDependency"; 4 | 5 | export default class MultiModule extends Module { 6 | context: string; 7 | dependencies: SingleEntryDependency[]; 8 | name: string; 9 | built: boolean; 10 | cacheable: boolean; 11 | 12 | constructor(context: string, dependencies: any/*ModuleDependency*/[], name: string); 13 | 14 | identifier(): string; 15 | readableIdentifier(): string; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/Dependency.d.ts: -------------------------------------------------------------------------------- 1 | export interface Reference { 2 | 3 | } 4 | 5 | export default class Dependency { 6 | isEqualResource(other: Dependency): boolean; 7 | getReference(): Reference; 8 | getExports(): null; 9 | getWarnings(): null; 10 | getErrors(): null; 11 | updateHash(hash: string): void; 12 | disconnect(): void; 13 | //compare(a: UnknownType<"location">, b: UnknownType<"location">): UnknownType<"some return type">; 14 | //static compare(a: UnknownType<"location">, b: UnknownType<"location">): UnknownType<"some return type">; 15 | } 16 | -------------------------------------------------------------------------------- /packages/@types/enhanced-resolve/lib/Resolver.d.ts: -------------------------------------------------------------------------------- 1 | export default class Resolver { 2 | resolve(context: ResolveContext, path: string, request: string, callback: LoggingCallbackWrapper): any; 3 | } 4 | 5 | export interface LoggingCallbackTools { 6 | log?(msg: string): void; 7 | stack?: string[] | undefined; 8 | missing?: string[] | { 9 | push: (item: string) => void; 10 | }; 11 | } 12 | 13 | export interface LoggingCallbackWrapper extends LoggingCallbackTools { 14 | (err?: Error | null, ...args: any[]): any; 15 | } 16 | 17 | export interface ResolveContext { 18 | issuer?: string; 19 | } 20 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/NormalModuleFactory.d.ts: -------------------------------------------------------------------------------- 1 | import Tapable, { AsyncWaterfallCallback } from "tapable"; 2 | import { Loader } from "./common-types"; 3 | import Dependency from "./Dependency"; 4 | 5 | export default class NormalModuleFactory extends Tapable { 6 | plugin(name: "after-resolve", 7 | handler: (this: NormalModuleFactory, 8 | data: AfterResolveData, callback: AfterResolveCallback) => void): void; 9 | } 10 | 11 | export interface AfterResolveData { 12 | request: string; 13 | rawRequest: string; 14 | resource: string; 15 | loaders: Loader[]; 16 | dependencies: Dependency[]; 17 | } 18 | 19 | export type AfterResolveCallback = AsyncWaterfallCallback; 20 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | import { Compilation } from "src/interop/webpack"; 2 | import CssEntryCompilation from "src/CssEntryCompilation"; 3 | 4 | export interface EntryInfo { 5 | isMulti: boolean; 6 | name: string; 7 | } 8 | 9 | export const isCssEntry = Symbol("isCssEntry"); 10 | 11 | export interface TaggedMultiModule { 12 | // NOTE: Should work after https://github.com/Microsoft/TypeScript/issues/5579 13 | // [isCssEntry]: boolean; 14 | } 15 | 16 | export interface WithCssEntryPlugin { 17 | applyPlugins(name: "css-entry-compilation", compilation: CssEntryCompilation): void; 18 | } 19 | 20 | export function getCssEntryPluginCompilation(compilation: Compilation): WithCssEntryPlugin { 21 | return compilation as any; 22 | } 23 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/Compiler.d.ts: -------------------------------------------------------------------------------- 1 | import Tapable from "tapable"; 2 | import Compilation, { CompilationParams } from "./Compilation"; 3 | import NormalModuleFactory from "./NormalModuleFactory"; 4 | import { Entry, Configuration } from "./common-types"; 5 | 6 | export default class Compiler extends Tapable { 7 | options: Configuration; 8 | context: string; 9 | 10 | apply(...plugins: (((this: this, compiler: this) => void) | CompilerPlugin)[]): void; 11 | 12 | plugin(name: "entry-option", 13 | handler: (this: Compiler, context: string, entry: Entry) => void): void; 14 | 15 | plugin(name: "normal-module-factory", 16 | handler: (this: Compiler, normalModuleFactory: NormalModuleFactory) => void): void; 17 | 18 | plugin(name: "compilation" | "this-compilation", 19 | handler: (this: Compiler, 20 | compilation: Compilation, params: CompilationParams) => void): void; 21 | } 22 | 23 | export interface CompilerPlugin { 24 | apply(compiler: Compiler): void; 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "gulp", 4 | "isShellCommand": true, 5 | "_runner": "terminal", 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "args": [], 10 | "isBuildCommand": true, 11 | "isBackground": false, 12 | "problemMatcher": [ 13 | "$tsc", 14 | "$gulp-tsc" 15 | ] 16 | }, 17 | { 18 | "taskName": "fast-build", 19 | "args": ["--sourcemaps"], 20 | "isBuildCommand": true, 21 | "isBackground": false, 22 | "problemMatcher": [ 23 | "$tsc", 24 | "$gulp-tsc" 25 | ] 26 | }, 27 | { 28 | "taskName": "test", 29 | "args": [], 30 | "isTestCommand": true 31 | }, 32 | { 33 | "taskName": "watch", 34 | "args": [], 35 | "isBackground": true 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Cristian Toma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/interop/tapable/index.ts: -------------------------------------------------------------------------------- 1 | import Tapable, 2 | { AsyncWaterfallCallback, AsyncWaterfallHandler, 3 | AsyncCallback, AsyncHandler } from "tapable"; 4 | 5 | export default Tapable; 6 | 7 | /** 8 | * Transforms a promise returning function to an AsyncWaterfallHandler function. 9 | * @param handler The function that returns the promise. 10 | * @returns The AsyncWaterfallHandler function. 11 | */ 12 | export function toAsyncWaterfallHandler( 13 | handler: (value: T) => Promise | T): AsyncWaterfallHandler { 14 | return (value, callback) => Promise.resolve() 15 | .then(() => handler(value)) 16 | .then(newValue => callback(null, newValue), 17 | err => callback(err, value)); 18 | } 19 | 20 | /** 21 | * Transforms a promise returning function to an AsyncHandler function. 22 | * @param handler The function that returns the promise. 23 | * @returns The AsyncHandler function. 24 | */ 25 | export function toAsyncHandler( 26 | handler: () => Promise): AsyncHandler { 27 | return callback => Promise.resolve() 28 | .then(() => handler()) 29 | .then(() => callback(), 30 | err => callback(err)); 31 | } 32 | -------------------------------------------------------------------------------- /packages/@types/tapable/index.d.ts: -------------------------------------------------------------------------------- 1 | export default class Tapable { 2 | _plugins: { 3 | [propName: string]: Handler[] 4 | }; 5 | 6 | /** 7 | * invoke all plugins with this attached. 8 | * This method is just to "apply" plugins' definition, so that the real event listeners can be registered into 9 | * registry. Mostly the `apply` method of a plugin is the main place to place extension logic. 10 | */ 11 | //apply(...plugins: (((this: this) => any) | Plugin)[]): void; 12 | } 13 | 14 | /*export interface TapableHook< 15 | TName extends string, 16 | THandler extends Function> { 17 | //applyPlugins(name: TName): void; 18 | 19 | plugin(name: TName, handler: THandler): void; 20 | }*/ 21 | 22 | export interface Handler { 23 | (...args: any[]): void; 24 | } 25 | 26 | export interface Plugin { 27 | apply(...args: any[]): void; 28 | } 29 | 30 | export type AsyncWaterfallHandler = (value: T, callback: AsyncWaterfallCallback) => void; 31 | export type AsyncWaterfallCallback = (err: Error | null | undefined, nextValue: T) => void; 32 | 33 | export type AsyncHandler = (callback: AsyncCallback) => void; 34 | export type AsyncCallback = (err?: Error) => void; 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug (testbed)", 8 | "cwd": "${workspaceRoot}", 9 | "program": "${workspaceRoot}/webpack.config.js", 10 | // Required for source maps to work 11 | "outFiles": ["${workspaceRoot}/lib/**/*"], 12 | "sourceMaps": true, 13 | "env": { 14 | "RUN_WEBPACK": "true" 15 | }, 16 | "preLaunchTask": "fast-build" 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Debug (tests)", 22 | "cwd": "${workspaceRoot}", 23 | "program": "${workspaceRoot}/node_modules/gulp/bin/gulp.js", 24 | "args": [ 25 | "fast-test" 26 | ], 27 | // Required for source maps to work 28 | "outFiles": ["${workspaceRoot}/lib/**/*"], 29 | "sourceMaps": true, 30 | "env": { 31 | "RUN_WEBPACK": "true" 32 | }, 33 | "preLaunchTask": "fast-build" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/@types/html-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | import { AsyncWaterfallCallback } from "tapable"; 2 | import Compilation from "webpack/lib/Compilation"; 3 | import Chunk from "webpack/lib/Chunk"; 4 | 5 | export default class HtmlWebpackPlugin { 6 | } 7 | 8 | export interface WithHtmlWebpackPlugin { 9 | plugin(name: "html-webpack-plugin-alter-asset-tags", 10 | handler: (this: Compilation, 11 | htmlData: HtmlData, callback: AlterAssetTagsCallback) => void): void; 12 | 13 | plugin(name: "html-webpack-plugin-alter-chunks", 14 | handler: (this: Compilation, 15 | chunks: Chunk[], context: { plugin: HtmlWebpackPlugin }) => Chunk[]): void; 16 | } 17 | 18 | export type AlterAssetTagsCallback = AsyncWaterfallCallback; 19 | 20 | export interface HtmlData { 21 | head: HtmlAssetTag[]; 22 | body: HtmlAssetTag[]; 23 | plugin: HtmlWebpackPlugin; 24 | chunks: any[]; 25 | outputName: string; 26 | } 27 | 28 | export interface HtmlAssetTag { 29 | tagName: string; 30 | closeTag?: boolean; 31 | selfClosingTag?: boolean; 32 | attributes: HtmlAssetTagAttributes; 33 | } 34 | 35 | export interface HtmlAssetTagAttributes { 36 | [attributeName: string]: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/HtmlWebpackPluginCssEntryFix.ts: -------------------------------------------------------------------------------- 1 | import { toAsyncWaterfallHandler } from "./interop/tapable"; 2 | import { CompilationPlugin, Compilation } from "./interop/webpack"; 3 | import { HtmlData, HtmlAssetTag, 4 | getHtmlWebpackPluginCompilation } from "./interop/html-webpack-plugin"; 5 | 6 | export default class HtmlWebpackPluginCssEntryFix implements CompilationPlugin { 7 | apply(compilation: Compilation): void { 8 | // Support for HtmlWebpackPlugin interop 9 | getHtmlWebpackPluginCompilation(compilation).plugin( 10 | "html-webpack-plugin-alter-asset-tags", 11 | toAsyncWaterfallHandler(htmlData => 12 | this.onHtmlWebpackPluginAlterAssetTags(htmlData))); 13 | } 14 | 15 | /** 16 | * Called by the HtmlWebpackPlugin for other plugins to change the assetTag definitions. 17 | * @param htmlPluginData The html plugin data that contains the asset tags. 18 | * @param callback The callback to call when ready. 19 | * @see https://github.com/jantimon/html-webpack-plugin/blob/master/index.js 20 | * @see https://github.com/jantimon/html-webpack-plugin#events 21 | */ 22 | // TODO: Create fix PR to https://github.com/jantimon/html-webpack-plugin/blob/master/index.js 23 | public async onHtmlWebpackPluginAlterAssetTags(htmlData: HtmlData): Promise { 24 | const filterFn = (tag: HtmlAssetTag) => 25 | !(tag.tagName === "script" && tag.attributes.src.match(/\.css$/)); 26 | 27 | htmlData.head = htmlData.head.filter(filterFn); 28 | htmlData.body = htmlData.body.filter(filterFn); 29 | 30 | return htmlData; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/@types/extract-text-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Loader } from "webpack"; 2 | import Compiler, { CompilerPlugin } from "webpack/lib/Compiler"; 3 | 4 | export default class ExtractTextPlugin implements CompilerPlugin { 5 | id: number; 6 | options: Options; 7 | 8 | /** Create a plugin instance defining the extraction target file(s) for the files loaded by `extract` */ 9 | constructor(options: string | Options); 10 | 11 | /** 12 | * Creates an extracting loader from an existing loader. 13 | * Use the resulting loader in `module.rules`/`module.loaders`. 14 | */ 15 | extract: (loader: Loader | Loader[] | ExtractOptions) => Loader[]; 16 | 17 | apply(compiler: Compiler): void; 18 | } 19 | 20 | export interface Options { 21 | /** the filename of the result file. May contain `[name]`, `[id]` and `[contenthash]` */ 22 | filename: string | ((getPath: ((template: string) => string)) => string); 23 | /** extract from all additional chunks too (by default it extracts only from the initial chunk(s)) */ 24 | allChunks?: boolean; 25 | /** disables the plugin */ 26 | disable?: boolean; 27 | /** Unique ident for this plugin instance. (For advanced usage only, by default automatically generated) */ 28 | id?: string; 29 | } 30 | 31 | export interface ExtractOptions { 32 | /** the loader(s) that should be used for converting the resource to a css exporting module */ 33 | use: Loader | Loader[]; 34 | /** the loader(s) that should be used when the css is not extracted (i.e. in an additional chunk when `allChunks: false`) */ 35 | fallback?: Loader | Loader[]; 36 | /** override the `publicPath` setting for this loader */ 37 | publicPath?: string; 38 | } 39 | -------------------------------------------------------------------------------- /spec/fixtures/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | script1: { 3 | path: "./script1.js", 4 | content: [`console.log("script1")`] 5 | }, 6 | script2: { 7 | path: "./script2.js", 8 | content: [`console.log("script2")`] 9 | }, 10 | 11 | image1: { 12 | path: "./images/img1.png" 13 | }, 14 | 15 | style1WithImg1: { 16 | path: "./styles/style1-with-img1.css", 17 | content: ["style1-img1-class"], 18 | img1: { 19 | file: "img1.png" 20 | } 21 | }, 22 | style2WithImg1: { 23 | path: "./styles/style2-with-img1.css", 24 | content: ["style2-img1-class"], 25 | img1: { 26 | file: "img1.png" 27 | } 28 | }, 29 | style1WithImg2: { 30 | path: "./styles/style1-with-img2.css", 31 | content: ["style1-img2-class"], 32 | img2: { 33 | file: "img2.png" 34 | } 35 | }, 36 | 37 | style1WithImport1: { 38 | path: "./styles/style1-with-import1.css", 39 | content: ["style1-import1-class"], 40 | import1: { 41 | file: "style1.css" 42 | } 43 | }, 44 | 45 | scss: { 46 | style1: { 47 | path: "./styles/style1.scss", 48 | content: ["style1-class", ".style1-class .style1-sub-class"] 49 | } 50 | }, 51 | 52 | style1: { 53 | path: "./styles/style1.css", 54 | content: ["style1-class"] 55 | }, 56 | style2: { 57 | path: "./styles/style2.css", 58 | content: ["style2-class"] 59 | }, 60 | style4: { 61 | path: "./styles/style4.css", 62 | content: ["style4-class"] 63 | }, 64 | style5: { 65 | path: "./styles/style5.css", 66 | content: ["style5-class"] 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Output 4 | "outDir": "./lib", 5 | "target": "es6", 6 | "module": "es6", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "removeComments": false, 10 | "alwaysStrict": true, 11 | "newLine": "LF", 12 | 13 | // Module resolution, libs and types 14 | "moduleResolution": "node", 15 | "lib": [ 16 | "es5", 17 | "es2015.core", 18 | "es2015.collection", 19 | "es2015.iterable", 20 | "es2015.symbol", 21 | "es2015.symbol.wellknown" 22 | ], 23 | "baseUrl": ".", 24 | "paths": { 25 | "*": [ 26 | "packages/@types/*", 27 | "*" 28 | ] 29 | }, 30 | "typeRoots": [ 31 | "./node_modules/@types", 32 | "./packages/@types" 33 | ], 34 | /*"types": [ 35 | "node" 36 | ],*/ 37 | 38 | // Type System 39 | // Remove @internal annotated symbols from .d.ts files 40 | "stripInternal": true, 41 | "strictNullChecks": true, 42 | "allowUnreachableCode": false, 43 | "allowUnusedLabels": false, 44 | "noImplicitAny": false, //TODO: fix this 45 | "noImplicitReturns": true, 46 | "noImplicitUseStrict": false, 47 | "noFallthroughCasesInSwitch": true, 48 | "noImplicitThis": true, 49 | "allowSyntheticDefaultImports": true, 50 | "importHelpers": true, 51 | 52 | // Compiler 53 | "diagnostics": true, 54 | "pretty": true, 55 | "listFiles": true, 56 | "listEmittedFiles": true 57 | }, 58 | "compileOnSave": false, 59 | "include": [ 60 | "./src" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /packages/@types/webpack/lib/Compilation.d.ts: -------------------------------------------------------------------------------- 1 | import Tapable, { AsyncCallback } from "tapable"; 2 | import Chunk from "./Chunk"; 3 | import Module from "./Module"; 4 | import NormalModuleFactory from "./NormalModuleFactory"; 5 | import Entrypoint from "./Entrypoint"; 6 | import Source from "webpack-sources/lib/Source"; 7 | import Resolver from "enhanced-resolve/lib/Resolver"; 8 | 9 | export default class Compilation extends Tapable { 10 | chunks: Chunk[]; 11 | assets: Assets; 12 | entries: Module[]; 13 | resolvers: Resolvers; 14 | entrypoints: Entrypoints; 15 | 16 | errors: (Error | string)[]; 17 | warnings: (Error | string)[]; 18 | 19 | apply(...plugins: (((this: this, compilation: this) => void) | CompilationPlugin)[]): void; 20 | 21 | plugin(name: "after-seal", 22 | handler: (this: Compilation, callback: AfterSealCallback) => void): void; 23 | 24 | plugin(name: "optimize-tree", 25 | handler: (this: Compilation, 26 | chunks: Chunk[], modules: Module[], 27 | callback: OptimizeTreeCallback) => void): void; 28 | 29 | plugin(name: "additional-assets", 30 | handler: (this: Compilation, callback: AdditionalAssetsCallback) => void): void; 31 | } 32 | 33 | export interface CompilationPlugin { 34 | apply(compilation: Compilation): void; 35 | } 36 | 37 | export interface CompilationParams { 38 | normalModuleFactory: NormalModuleFactory; 39 | } 40 | 41 | export interface Assets { 42 | [assetName: string]: Source; 43 | } 44 | 45 | export type AfterSealCallback = AsyncCallback; 46 | export type OptimizeTreeCallback = AsyncCallback; 47 | export type AdditionalAssetsCallback = AsyncCallback; 48 | 49 | export interface Resolvers { 50 | normal: Resolver; 51 | context: Resolver; 52 | loader: Resolver; 53 | } 54 | 55 | export interface Entrypoints { 56 | [entryName: string]: Entrypoint; 57 | } 58 | -------------------------------------------------------------------------------- /src/interop/webpack/index.ts: -------------------------------------------------------------------------------- 1 | import { Source, Resolver, Loader } from "webpack"; 2 | import Compiler, { CompilerPlugin } from "webpack/lib/Compiler"; 3 | import Compilation, { CompilationPlugin, AfterSealCallback, Assets } from "webpack/lib/Compilation"; 4 | import Module from "webpack/lib/Module"; 5 | import MultiModule from "webpack/lib/MultiModule"; 6 | import Chunk from "webpack/lib/Chunk"; 7 | import SingleEntryDependency from "webpack/lib/dependencies/SingleEntryDependency"; 8 | import MultiEntryDependency from "webpack/lib/dependencies/MultiEntryDependency"; 9 | import { AfterResolveData } from "webpack/lib/NormalModuleFactory"; 10 | 11 | export { 12 | Resolver, Source, Loader, 13 | Compiler, CompilerPlugin, 14 | Compilation, CompilationPlugin, AfterSealCallback, Assets, 15 | Module, MultiModule, 16 | Chunk, 17 | SingleEntryDependency, MultiEntryDependency, 18 | AfterResolveData }; 19 | 20 | export const multiEntryDependencyLocSeparator = ":"; 21 | 22 | export const webpackDevServerResourceTests = [ 23 | /webpack-dev-server(\/|\\)client/, 24 | /webpack\/hot/ 25 | ]; 26 | 27 | export function loaderToIdent(data) { 28 | if (!data.options) 29 | return data.loader; 30 | if (typeof data.options === "string") 31 | return data.loader + "?" + data.options; 32 | if (typeof data.options !== "object") 33 | throw new Error("loader options must be string or object"); 34 | if (data.ident) 35 | return data.loader + "??" + data.ident; 36 | return data.loader + "?" + JSON.stringify(data.options); 37 | } 38 | 39 | export function loadersToRequestIdent(loaders, resource) { 40 | return loaders.map(loaderToIdent).concat([resource]).join("!"); 41 | } 42 | 43 | export function isWebpackDevServerResource(resource: string): boolean { 44 | return webpackDevServerResourceTests 45 | .some(resourceTest => resourceTest.test(resource)); 46 | } 47 | 48 | export function excludeWebpackDevServerResources(resources: string[]): string[] { 49 | return resources.filter(resource => !isWebpackDevServerResource(resource)); 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-entry-webpack-plugin", 3 | "version": "1.0.0-beta.4", 4 | "author": "Cristian Toma", 5 | "description": "A Webpack plugin that allows CSS files as the entry.", 6 | "main": "lib/index.js", 7 | "files": [ 8 | "lib/" 9 | ], 10 | "scripts": { 11 | "start": "gulp watch", 12 | "clean": "gulp clean", 13 | "build": "gulp build", 14 | "test": "gulp test", 15 | "test:cover": "gulp test --cover" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/tomachristian/css-entry-webpack-plugin.git" 20 | }, 21 | "keywords": [ 22 | "css", 23 | "style", 24 | "sass", 25 | "scss", 26 | "less", 27 | "stylus", 28 | "styl", 29 | "entry", 30 | "webpack", 31 | "plugin", 32 | "bundle" 33 | ], 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/tomachristian/css-entry-webpack-plugin/issues" 37 | }, 38 | "homepage": "https://github.com/tomachristian/css-entry-webpack-plugin#readme", 39 | "devDependencies": { 40 | "@types/bluebird-global": "^3.5.0", 41 | "@types/jasmine": "^2.5.46", 42 | "@types/lodash": "^4.14.55", 43 | "@types/node": "^7.0.8", 44 | "babel": "^6.23.0", 45 | "babel-plugin-add-module-exports": "^0.2.1", 46 | "babel-plugin-transform-promise-to-bluebird": "^1.1.1", 47 | "babel-preset-env": "^1.2.2", 48 | "css-loader": "^0.27.3", 49 | "file-loader": "^0.10.1", 50 | "gulp": "github:gulpjs/gulp#4.0", 51 | "gulp-babel": "^6.1.2", 52 | "gulp-clean": "^0.3.2", 53 | "gulp-if": "^2.0.2", 54 | "gulp-jasmine": "^2.4.2", 55 | "gulp-shell": "^0.6.3", 56 | "gulp-sourcemaps": "^2.4.1", 57 | "gulp-typescript": "^3.1.6", 58 | "gulp-util": "^3.0.8", 59 | "html-webpack-plugin": "^2.28.0", 60 | "jasmine": "^2.5.3", 61 | "jasmine-spec-reporter": "^3.2.0", 62 | "jasmine-supertest": "^1.0.0", 63 | "lodash": "^4.17.4", 64 | "merge2": "^1.0.3", 65 | "node-sass": "^4.5.0", 66 | "nyc": "^10.1.2", 67 | "rimraf": "^2.6.1", 68 | "sass-loader": "^6.0.3", 69 | "supertest": "^3.0.0", 70 | "tslint": "^4.5.1", 71 | "typescript": "^2.2.1", 72 | "webpack": "^2.2.1", 73 | "webpack-dev-server": "^2.4.2", 74 | "webpack-merge": "^4.0.0" 75 | }, 76 | "dependencies": { 77 | "bluebird": "^3.5.0", 78 | "extract-text-webpack-plugin": "^2.1.0", 79 | "tslib": "^1.6.0", 80 | "webpack": "^2.2.1" 81 | }, 82 | "engines": { 83 | "node": ">=4.3.0 <5.0.0 || >=5.10" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /spec/disable-option.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin disabled", () => { 2 | beforeEach(done => { 3 | this.webpack = webpackTestFixture(jasmine) 4 | .withCssEntryPlugin({ 5 | disable: true 6 | }) 7 | .cleanOutput(done); 8 | }); 9 | 10 | describe("configured with a shorthand single entry (.css)", () => { 11 | beforeEach(done => { 12 | this.webpack 13 | .config({ 14 | entry: fixtures.style1.path 15 | }) 16 | .run(done); 17 | }); 18 | beforeEach(() => expect(this.webpack).toSucceed()); 19 | 20 | it("generates a single js bundle", () => { 21 | expect(this.webpack).toOutput({ 22 | fileCount: 1, 23 | file: "main.bundle.js", 24 | withContent: fixtures.style1.content 25 | }); 26 | }); 27 | }); 28 | 29 | describe("configured with a shorthand single entry (.js)", () => { 30 | beforeEach(done => { 31 | this.webpack 32 | .config({ 33 | entry: fixtures.script1.path 34 | }) 35 | .run(done); 36 | }); 37 | beforeEach(() => expect(this.webpack).toSucceed()); 38 | 39 | it("generates a single js bundle", () => { 40 | expect(this.webpack).toOutput({ 41 | fileCount: 1, 42 | file: "main.bundle.js", 43 | withContent: fixtures.script1.content 44 | }); 45 | }); 46 | }); 47 | 48 | describe("configured with multi entry (1: .js, 2: .css)", () => { 49 | beforeEach(done => { 50 | this.webpack 51 | .config({ 52 | entry: { 53 | "style": fixtures.style1.path, 54 | "script": fixtures.script1.path 55 | } 56 | }) 57 | .run(done); 58 | }); 59 | beforeEach(() => expect(this.webpack).toSucceed()); 60 | 61 | it("generates two js bundles", () => { 62 | expect(this.webpack).toOutput({ 63 | fileCount: 2 64 | }); 65 | 66 | expect(this.webpack).toOutput({ 67 | file: "style.bundle.js", 68 | withContent: fixtures.style1.content 69 | }); 70 | 71 | expect(this.webpack).toOutput({ 72 | file: "script.bundle.js", 73 | withContent: fixtures.script1.content 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsRules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-trailing-whitespace": true, 15 | "no-unsafe-finally": true, 16 | "no-unused-variable": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "double" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "always" 29 | ], 30 | "triple-equals": [ 31 | true, 32 | "allow-null-check" 33 | ], 34 | "variable-name": [ 35 | true, 36 | "ban-keywords" 37 | ], 38 | "whitespace": [ 39 | true, 40 | "check-branch", 41 | "check-decl", 42 | "check-operator", 43 | "check-separator", 44 | "check-type" 45 | ] 46 | }, 47 | "rules": { 48 | "class-name": true, 49 | "comment-format": [ 50 | true, 51 | "check-space" 52 | ], 53 | "indent": [ 54 | true, 55 | "spaces" 56 | ], 57 | "no-eval": true, 58 | "no-internal-module": true, 59 | "no-trailing-whitespace": true, 60 | "no-unsafe-finally": true, 61 | "no-var-keyword": true, 62 | "one-line": [ 63 | true, 64 | "check-open-brace", 65 | "check-whitespace" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "double" 70 | ], 71 | "semicolon": [ 72 | true, 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "variable-name": [ 90 | true, 91 | "ban-keywords" 92 | ], 93 | "whitespace": [ 94 | true, 95 | "check-branch", 96 | "check-decl", 97 | "check-operator", 98 | "check-separator", 99 | "check-type" 100 | ] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/CssEntryPlugin.ts: -------------------------------------------------------------------------------- 1 | import { toAsyncWaterfallHandler, toAsyncHandler } from "./interop/tapable"; 2 | import { CompilerPlugin, Compiler, AfterResolveData } from "./interop/webpack"; 3 | import ExtractTextPlugin from "extract-text-webpack-plugin"; 4 | import { Options, NormalizedOptions, normalizeOptions } from "./options"; 5 | import { getCssEntryPluginCompilation } from "./models"; 6 | import CssEntryPluginError from "./CssEntryPluginError"; 7 | import CssEntryCompilation from "./CssEntryCompilation"; 8 | import HtmlWebpackPluginCssEntryFix from "./HtmlWebpackPluginCssEntryFix"; 9 | 10 | export default class CssEntryPlugin implements CompilerPlugin { 11 | public readonly options: NormalizedOptions; 12 | 13 | /** 14 | * Creates a new instance of the CssEntryPlugin. 15 | * @param options The configuration options (required). 16 | */ 17 | public constructor(options?: Options) { 18 | this.options = normalizeOptions(options); 19 | } 20 | 21 | /** 22 | * Called once by the compiler when installing the plugin. 23 | * @param compiler The compiler instance. 24 | */ 25 | apply(compiler: Compiler): void { 26 | // We will use a single ExtractTextPlugin to extract the css entries 27 | let extractTextPlugin = new ExtractTextPlugin({ 28 | disable: this.options.disable, 29 | filename: this.options.output.filename 30 | }); 31 | 32 | compiler.apply(extractTextPlugin); 33 | 34 | // Using 'this-compilation' (do not hook into child compilations) 35 | compiler.plugin("this-compilation", (compilation, params) => { 36 | extractTextPlugin.options.disable = this.options.disable; 37 | if (this.options.disable === true) return; 38 | 39 | // Creating a CssEntryCompilation scoped to the new Compilation instance 40 | let cssEntryCompilation = new CssEntryCompilation( 41 | this.options, compiler, compilation, extractTextPlugin); 42 | 43 | getCssEntryPluginCompilation(compilation) 44 | .applyPlugins("css-entry-compilation", cssEntryCompilation); 45 | 46 | params.normalModuleFactory.plugin( 47 | "after-resolve", toAsyncWaterfallHandler(data => 48 | cssEntryCompilation.onNormalModuleFactoryAfterResolve(data))); 49 | 50 | compilation.plugin( 51 | "after-seal", toAsyncHandler(() => 52 | cssEntryCompilation.onCompilationAfterSeal())); 53 | 54 | compilation.apply(new HtmlWebpackPluginCssEntryFix()); 55 | }); 56 | } 57 | 58 | /** 59 | * Enables the plugin. 60 | */ 61 | enable(): void { 62 | this.options.disable = false; 63 | } 64 | 65 | /** 66 | * Disables the plugin. 67 | */ 68 | disable(): void { 69 | this.options.disable = true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /spec/sass-loader-interop.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin and SassLoader for scss files", () => { 2 | beforeEach(done => { 3 | this.webpack = webpackTestFixture(jasmine) 4 | .withCssEntryPlugin() 5 | .config({ 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.scss$/, 10 | use: [ 11 | { 12 | loader: "css-loader", 13 | options: { sourceMap: true } 14 | }, 15 | { 16 | loader: "sass-loader", 17 | options: { sourceMap: true } 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }) 24 | .cleanOutput(done); 25 | }); 26 | 27 | describe("configured with a shorthand single entry", () => { 28 | beforeEach(() => { 29 | this.webpack 30 | .config({ 31 | entry: fixtures.scss.style1.path 32 | }); 33 | }); 34 | 35 | describe("with loader default options", () => { 36 | beforeEach(done => this.webpack.run(done)); 37 | beforeEach(() => expect(this.webpack).toSucceed()); 38 | 39 | it("generates a single css bundle with the compiled scss", () => { 40 | expect(this.webpack).toOutput({ 41 | content: fixtures.scss.style1.content 42 | }); 43 | }); 44 | 45 | it("generates the css bundle only", () => { 46 | expect(this.webpack).toOutput({ 47 | fileCount: 1 48 | }); 49 | }); 50 | }); 51 | 52 | describe("with loader source maps", () => { 53 | beforeEach(done => { 54 | this.webpack 55 | .config({ 56 | devtool: "source-map" 57 | }) 58 | .run(done); 59 | }); 60 | beforeEach(() => expect(this.webpack).toSucceed()); 61 | 62 | it("generates a single css bundle with the compiled scss", () => { 63 | expect(this.webpack).toOutput({ 64 | content: fixtures.scss.style1.content 65 | }); 66 | }); 67 | 68 | it("generates a single css map for the bundle", () => { 69 | expect(this.webpack).toOutput({ 70 | file: "main.bundle.css.map", 71 | withContent: [ 72 | "styles/style1.scss", 73 | "@extend" 74 | ] 75 | }); 76 | }); 77 | 78 | it("generates the css bundle only", () => { 79 | expect(this.webpack).toOutput({ 80 | fileCount: 2 81 | }); 82 | }); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const merge = require("merge2"); 4 | 5 | const gulp = require("gulp"); 6 | const gulpif = require("gulp-if"); 7 | const util = require("gulp-util"); 8 | const clean = require("gulp-clean"); 9 | const ts = require("gulp-typescript"); 10 | const babel = require("gulp-babel"); 11 | const sourcemaps = require("gulp-sourcemaps"); 12 | const jasmine = require("gulp-jasmine"); 13 | const shell = require('gulp-shell') 14 | const SpecReporter = require('jasmine-spec-reporter').SpecReporter; 15 | // TODO: jslint 16 | const outDir = path.join(__dirname, "lib"), 17 | tmpDir = path.join(__dirname, "tmp"), 18 | specsDir = path.join(__dirname, "spec"), 19 | srcDir = path.join(__dirname, "src"); 20 | 21 | let tsProject, tsReporter; 22 | 23 | const buildConfig = { 24 | // Disables the clean process 25 | dirty: !!util.env.dirty, 26 | // Disabled new builds 27 | fast: !!util.env.fast, 28 | sourcemaps: !!util.env.sourcemaps, 29 | cover: !!util.env.cover 30 | } 31 | 32 | // Cover requires sourcemaps 33 | if (buildConfig.cover) buildConfig.sourcemaps = true; 34 | 35 | if (buildConfig.dirty) { 36 | console.log("> Dirty build"); 37 | } 38 | if (buildConfig.fast) { 39 | console.log("> Fast build"); 40 | } 41 | if (buildConfig.sourcemaps) { 42 | console.log("> Sourcemaps enabled"); 43 | } 44 | if (buildConfig.cover) { 45 | console.log("> Code coverage enabled"); 46 | } 47 | 48 | gulp.task("clean:lib", () => { 49 | return gulp.src(outDir, { read: false, allowEmpty: true }) 50 | .pipe(gulpif(!buildConfig.dirty, clean())); 51 | }); 52 | 53 | gulp.task("clean:tmp", () => { 54 | return gulp.src(tmpDir, { read: false, allowEmpty: true }) 55 | .pipe(gulpif(!buildConfig.dirty, clean())); 56 | }); 57 | 58 | gulp.task("clean", gulp.parallel("clean:lib", "clean:tmp")); 59 | 60 | gulp.task("build", gulp.series("clean", () => { 61 | if (buildConfig.fast) { 62 | return Promise.resolve(); 63 | } 64 | 65 | if (!tsProject) { 66 | tsProject = ts.createProject( 67 | "tsconfig.json", 68 | require(path.join(srcDir, "tsconfig.json")).compilerOptions); 69 | tsReporter = ts.reporter.fullReporter(); 70 | } 71 | 72 | const build = tsProject.src() 73 | .pipe(gulpif(buildConfig.sourcemaps, sourcemaps.init())) 74 | .pipe(tsProject(tsReporter)); 75 | 76 | return merge([ 77 | build.js 78 | .pipe(babel()) 79 | .pipe(gulpif(buildConfig.sourcemaps, sourcemaps.write("./", { 80 | mapSources: (sourcePath, file) => { 81 | return path.resolve(outDir, sourcePath); 82 | } 83 | }))) 84 | .pipe(gulp.dest(outDir)), 85 | 86 | build.dts 87 | .pipe(gulp.dest(outDir)) 88 | ]); 89 | })); 90 | 91 | gulp.task("watch", gulp.series("build", function watch(_) { 92 | buildConfig.dirty = true; 93 | buildConfig.fast = false; 94 | gulp.watch(path.join(srcDir, "**/*"), { mode: "poll" }, gulp.series("build")); 95 | })); 96 | 97 | gulp.task("test:cover", shell.task("nyc -c false gulp test --dirty --fast")); 98 | gulp.task("test", gulp.series("build", buildConfig.cover ? gulp.series("test:cover") : function test() { 99 | const config = require(path.join(specsDir, "support/jasmine.json")); 100 | const reporter = new SpecReporter({ 101 | spec: { 102 | displayPending: true, 103 | displayDuration: true, 104 | displaySuccessful: true, 105 | displayFailed: true, 106 | displayErrorMessages: true, 107 | displayStacktrace: true 108 | } 109 | }); 110 | // TODO: Load glob from spec/support/jasmine.json (config) 111 | return gulp.src(path.join(specsDir, "**/*.spec.js")) 112 | .pipe(jasmine({ 113 | config: config, 114 | reporter: reporter 115 | })); 116 | })); 117 | 118 | gulp.task("dev", gulp.series("watch")); 119 | gulp.task("default", gulp.series("build")); 120 | -------------------------------------------------------------------------------- /spec/default-options.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin", () => { 2 | beforeEach(done => { 3 | this.webpack = webpackTestFixture(jasmine) 4 | .cleanOutput(done); 5 | }); 6 | 7 | describe("without options", () => { 8 | beforeEach(() => { 9 | this.webpack 10 | .withCssEntryPlugin(null, true); 11 | }); 12 | 13 | describe("configured with a shorthand single entry (.css)", () => { 14 | beforeEach(done => { 15 | this.webpack 16 | .config({ 17 | entry: fixtures.style1.path 18 | }) 19 | .run(done); 20 | }); 21 | 22 | it("generates a single css bundle with the default filename", () => { 23 | expect(this.webpack).toSucceed(); 24 | 25 | expect(this.webpack).toOutput({ 26 | fileCount: 1, 27 | file: "main.css", 28 | withContent: fixtures.style1.content 29 | }); 30 | }); 31 | }); 32 | 33 | describe("configured with multi entry (1: .js, 2: .css)", () => { 34 | beforeEach(done => { 35 | this.webpack 36 | .config({ 37 | entry: { 38 | "style": fixtures.style1.path, 39 | "script": fixtures.script1.path 40 | } 41 | }) 42 | .run(done); 43 | }); 44 | 45 | it("generates one js bundle and one css bundle with the default filename", () => { 46 | expect(this.webpack).toSucceed(); 47 | 48 | expect(this.webpack).toOutput({ 49 | fileCount: 2 50 | }); 51 | 52 | expect(this.webpack).toOutput({ 53 | file: "style.css", 54 | withContent: fixtures.style1.content 55 | }); 56 | 57 | expect(this.webpack).toOutput({ 58 | file: "script.bundle.js", 59 | withContent: fixtures.script1.content 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | describe("configured with empty object options", () => { 66 | beforeEach(done => { 67 | this.webpack = webpackTestFixture(jasmine) 68 | .withCssEntryPlugin({}, true) 69 | .cleanOutput(done); 70 | }); 71 | 72 | describe("configured with a shorthand single entry (.css)", () => { 73 | beforeEach(done => { 74 | this.webpack 75 | .config({ 76 | entry: fixtures.style1.path 77 | }) 78 | .run(done); 79 | }); 80 | 81 | it("generates a single css bundle with the default filename", () => { 82 | expect(this.webpack).toSucceed(); 83 | 84 | expect(this.webpack).toOutput({ 85 | fileCount: 1, 86 | file: "main.css", 87 | withContent: fixtures.style1.content 88 | }); 89 | }); 90 | }); 91 | 92 | describe("configured with multi entry (1: .js, 2: .css)", () => { 93 | beforeEach(done => { 94 | this.webpack 95 | .config({ 96 | entry: { 97 | "style": fixtures.style1.path, 98 | "script": fixtures.script1.path 99 | } 100 | }) 101 | .run(done); 102 | }); 103 | 104 | it("generates one js bundle and one css bundle with the default filename", () => { 105 | expect(this.webpack).toSucceed(); 106 | 107 | expect(this.webpack).toOutput({ 108 | fileCount: 2 109 | }); 110 | 111 | expect(this.webpack).toOutput({ 112 | file: "style.css", 113 | withContent: fixtures.style1.content 114 | }); 115 | 116 | expect(this.webpack).toOutput({ 117 | file: "script.bundle.js", 118 | withContent: fixtures.script1.content 119 | }); 120 | }); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /spec/shorthand-filename-option.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin with shorthand output filename option", () => { 2 | beforeEach(done => { 3 | this.webpack = webpackTestFixture(jasmine) 4 | .cleanOutput(done); 5 | }); 6 | 7 | describe("configured with a path template", () => { 8 | beforeEach(done => { 9 | this.webpack = webpackTestFixture(jasmine) 10 | .withCssEntryPlugin("[name].spec.bundle.css", true) 11 | .cleanOutput(done); 12 | }); 13 | 14 | describe("configured with a shorthand single entry (.css)", () => { 15 | beforeEach(done => { 16 | this.webpack 17 | .config({ 18 | entry: fixtures.style1.path 19 | }) 20 | .run(done); 21 | }); 22 | 23 | it("generates a single css bundle with the configured filename", () => { 24 | expect(this.webpack).toSucceed(); 25 | 26 | expect(this.webpack).toOutput({ 27 | fileCount: 1, 28 | file: "main.spec.bundle.css", 29 | withContent: fixtures.style1.content 30 | }); 31 | }); 32 | }); 33 | 34 | describe("configured with multi entry (1: .js, 2: .css)", () => { 35 | beforeEach(done => { 36 | this.webpack 37 | .config({ 38 | entry: { 39 | "style": fixtures.style1.path, 40 | "script": fixtures.script1.path 41 | } 42 | }) 43 | .run(done); 44 | }); 45 | 46 | it("generates one js bundle and one css bundle with the configured filename", () => { 47 | expect(this.webpack).toSucceed(); 48 | 49 | expect(this.webpack).toOutput({ 50 | fileCount: 2 51 | }); 52 | 53 | expect(this.webpack).toOutput({ 54 | file: "style.spec.bundle.css", 55 | withContent: fixtures.style1.content 56 | }); 57 | 58 | expect(this.webpack).toOutput({ 59 | file: "script.bundle.js", 60 | withContent: fixtures.script1.content 61 | }); 62 | }); 63 | }); 64 | }); 65 | 66 | describe("configured with a function", () => { 67 | beforeEach(done => { 68 | this.webpack = webpackTestFixture(jasmine) 69 | .withCssEntryPlugin(getPath => "prefix-" + getPath("[name].spec.bundle.css"), true) 70 | .cleanOutput(done); 71 | }); 72 | 73 | describe("configured with a shorthand single entry (.css)", () => { 74 | beforeEach(done => { 75 | this.webpack 76 | .config({ 77 | entry: fixtures.style1.path 78 | }) 79 | .run(done); 80 | }); 81 | 82 | it("generates a single css bundle with the configured filename", () => { 83 | expect(this.webpack).toSucceed(); 84 | 85 | expect(this.webpack).toOutput({ 86 | fileCount: 1, 87 | file: "prefix-main.spec.bundle.css", 88 | withContent: fixtures.style1.content 89 | }); 90 | }); 91 | }); 92 | 93 | describe("configured with multi entry (1: .js, 2: .css)", () => { 94 | beforeEach(done => { 95 | this.webpack 96 | .config({ 97 | entry: { 98 | "style": fixtures.style1.path, 99 | "script": fixtures.script1.path 100 | } 101 | }) 102 | .run(done); 103 | }); 104 | 105 | it("generates one js bundle and one css bundle with the configured filename", () => { 106 | expect(this.webpack).toSucceed(); 107 | 108 | expect(this.webpack).toOutput({ 109 | fileCount: 2 110 | }); 111 | 112 | expect(this.webpack).toOutput({ 113 | file: "prefix-style.spec.bundle.css", 114 | withContent: fixtures.style1.content 115 | }); 116 | 117 | expect(this.webpack).toOutput({ 118 | file: "script.bundle.js", 119 | withContent: fixtures.script1.content 120 | }); 121 | }); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /spec/webpack-dev-server-interop.spec.js: -------------------------------------------------------------------------------- 1 | const Server = require("webpack-dev-server/lib/Server"); 2 | const finishTestcase = require("jasmine-supertest"); 3 | 4 | describe("Running CssEntryPlugin with webpack-dev-server", function () { 5 | beforeEach(function (done) { 6 | this.webpack = webpackTestFixture(jasmine) 7 | .withCssEntryPlugin() 8 | .cleanOutput(done); 9 | }); 10 | 11 | afterEach(function (done) { 12 | this.webpack.close(done); 13 | }); 14 | 15 | const testCases = [ 16 | { 17 | name: "configured with two single entries (1: .js, 2: .css) " + 18 | "generates one css bundle and one js bundle", 19 | config: { 20 | entry: { 21 | "test1": fixtures.script1.path, 22 | "test2": fixtures.style1.path 23 | } 24 | }, 25 | output: { 26 | files: [ 27 | { 28 | file: "test1.bundle.js", 29 | content: fixtures.script1.content 30 | }, 31 | { 32 | file: "test2.bundle.css", 33 | content: fixtures.style1.content 34 | }, 35 | "!test2.bundle.js" 36 | ] 37 | } 38 | }, 39 | { 40 | name: "configured with two single entries (1: .css, 2: .css) " + 41 | "generates two css bundles", 42 | config: { 43 | entry: { 44 | "test1": fixtures.style1.path, 45 | "test2": fixtures.style2.path 46 | } 47 | }, 48 | output: { 49 | files: [ 50 | { 51 | file: "test1.bundle.css", 52 | content: fixtures.style1.content 53 | }, 54 | { 55 | file: "test2.bundle.css", 56 | content: fixtures.style2.content 57 | }, 58 | "!test1.bundle.js", 59 | "!test2.bundle.js" 60 | ] 61 | } 62 | }, 63 | { 64 | name: "configured with two single entries with same file (1: 1.css, 2: 1.css) " + 65 | "generates two css bundles with the same css", 66 | config: { 67 | entry: { 68 | "test1": fixtures.style1.path, 69 | "test2": fixtures.style1.path 70 | } 71 | }, 72 | output: { 73 | files: [ 74 | { 75 | file: "test1.bundle.css", 76 | content: fixtures.style1.content 77 | }, 78 | { 79 | file: "test2.bundle.css", 80 | content: fixtures.style1.content 81 | }, 82 | "!test1.bundle.js", 83 | "!test2.bundle.js" 84 | ] 85 | } 86 | } 87 | ]; 88 | 89 | testCases.forEach(testCase => { 90 | it(testCase.name, function (done) { 91 | this.webpack.config(testCase.config) 92 | .serve(() => { 93 | const requests = testCase.output.files.map(file => { 94 | if (typeof file === "string") { 95 | if (file.startsWith("!")) { 96 | file = { 97 | exists: false, 98 | file: file.substr(1) 99 | }; 100 | } 101 | else { 102 | file = { 103 | file 104 | }; 105 | } 106 | } 107 | 108 | return this.webpack.client 109 | .get("/" + file.file) 110 | .expect(file.exists === false ? 404 : 200) 111 | .then(res => { 112 | if (file.content) { 113 | file.content.forEach(text => expect(res.text).toContain(text)); 114 | } 115 | return res; 116 | }); 117 | }); 118 | 119 | Promise.all(requests) 120 | .then(() => done(), 121 | err => done.fail(err)); 122 | }); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /spec/css-with-import.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin for css files with imports", () => { 2 | beforeEach(done => { 3 | this.webpack = webpackTestFixture(jasmine) 4 | .withCssEntryPlugin() 5 | .cleanOutput(done); 6 | }); 7 | 8 | describe("configured with a shorthand single entry", () => { 9 | beforeEach(done => { 10 | this.webpack 11 | .config({ 12 | entry: fixtures.style1WithImport1.path 13 | }) 14 | .run(done); 15 | }); 16 | beforeEach(() => expect(this.webpack).toSucceed()); 17 | 18 | it("generates a single css bundle with the imported css", () => { 19 | expect(this.webpack).toOutput({ 20 | content: [ 21 | ...fixtures.style1WithImport1.content, 22 | ...fixtures.style1.content 23 | ] 24 | }); 25 | }); 26 | 27 | it("generates the css bundle only", () => { 28 | expect(this.webpack).toOutput({ 29 | fileCount: 1 30 | }); 31 | }); 32 | }); 33 | 34 | describe("configured with two entries, both with the same file", () => { 35 | beforeEach(done => { 36 | this.webpack 37 | .config({ 38 | entry: { 39 | "test1": fixtures.style1WithImport1.path, 40 | "test2": fixtures.style1WithImport1.path 41 | } 42 | }) 43 | .run(done); 44 | }); 45 | beforeEach(() => expect(this.webpack).toSucceed()); 46 | 47 | it("generates two css bundles, both with the imported css", () => { 48 | expect(this.webpack).toOutput({ 49 | entry: "test1", 50 | withContent: [ 51 | ...fixtures.style1WithImport1.content, 52 | ...fixtures.style1.content 53 | ] 54 | }); 55 | 56 | expect(this.webpack).toOutput({ 57 | entry: "test2", 58 | withContent: [ 59 | ...fixtures.style1WithImport1.content, 60 | ...fixtures.style1.content 61 | ] 62 | }); 63 | }); 64 | 65 | it("generates two css bundles only", () => { 66 | expect(this.webpack).toOutput({ 67 | fileCount: 2 68 | }); 69 | }); 70 | }); 71 | 72 | describe("configured with two entries, both with the same file and one of them with the imported file in the entry", () => { 73 | beforeEach(done => { 74 | this.webpack 75 | .config({ 76 | entry: { 77 | "test1": fixtures.style1WithImport1.path, 78 | "test2": [fixtures.style1WithImport1.path, fixtures.style1.path] 79 | } 80 | }) 81 | .run(done); 82 | }); 83 | beforeEach(() => expect(this.webpack).toSucceed()); 84 | 85 | it("generates two css bundles, both with the imported css", () => { 86 | expect(this.webpack).toOutput({ 87 | entry: "test1", 88 | withContent: [ 89 | ...fixtures.style1WithImport1.content, 90 | ...fixtures.style1.content 91 | ] 92 | }); 93 | 94 | expect(this.webpack).toOutput({ 95 | entry: "test2", 96 | withContent: [ 97 | ...fixtures.style1WithImport1.content, 98 | ...fixtures.style1.content 99 | ] 100 | }); 101 | }); 102 | 103 | it("does not add the content of the additional file twice", () => { 104 | expect(this.webpack).toOutput({ 105 | entry: "test2", 106 | withContent: [ 107 | ...fixtures.style1WithImport1.content, 108 | ...fixtures.style1.content 109 | ], 110 | onlyOnce: true 111 | }); 112 | }); 113 | 114 | it("generates two css bundles only", () => { 115 | expect(this.webpack).toOutput({ 116 | fileCount: 2 117 | }); 118 | }); 119 | }); 120 | 121 | describe("configured with two entries, one with a file that has an import of the file in the other entry", () => { 122 | beforeEach(done => { 123 | this.webpack 124 | .config({ 125 | entry: { 126 | "test1": fixtures.style1WithImport1.path, 127 | "test2": [fixtures.style1.path] 128 | } 129 | }) 130 | .run(done); 131 | }); 132 | beforeEach(() => expect(this.webpack).toSucceed()); 133 | 134 | it("generates two css bundles, both with the imported css", () => { 135 | expect(this.webpack).toOutput({ 136 | entry: "test1", 137 | withContent: [ 138 | ...fixtures.style1WithImport1.content, 139 | ...fixtures.style1.content 140 | ] 141 | }); 142 | 143 | expect(this.webpack).toOutput({ 144 | entry: "test2", 145 | withContent: fixtures.style1.content 146 | }); 147 | }); 148 | 149 | it("generates two css bundles only", () => { 150 | expect(this.webpack).toOutput({ 151 | fileCount: 2 152 | }); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /spec/extract-text-plugin-interop.spec.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 2 | 3 | describe("Running CssEntryPlugin and ExtractTextPlugin", () => { 4 | beforeEach(done => { 5 | this.webpack = webpackTestFixture(jasmine) 6 | .cleanOutput(done); 7 | }); 8 | 9 | describe("registered after CssEntryPlugin", () => { 10 | beforeEach(() => { 11 | this.webpack 12 | .withCssEntryPlugin() 13 | .config({ 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.css$/, 18 | issuer: /import\d\.css$/, 19 | use: ExtractTextPlugin.extract({ 20 | use: "css-loader" 21 | }) 22 | } 23 | ] 24 | }, 25 | 26 | plugins: [ 27 | new ExtractTextPlugin("other-styles.css") 28 | ] 29 | }); 30 | }); 31 | 32 | describe("configured with a shorthand single entry, that references an extracted css", () => { 33 | beforeEach(done => { 34 | this.webpack 35 | .config({ 36 | entry: fixtures.style1WithImport1.path 37 | }) 38 | .run(done); 39 | }); 40 | beforeEach(() => expect(this.webpack).toSucceed()); 41 | 42 | it("generates a single css bundle", () => { 43 | expect(this.webpack).toOutput({ 44 | content: fixtures.style1WithImport1.content 45 | }); 46 | }); 47 | 48 | it("generates the css bundle only", () => { 49 | expect(this.webpack).toOutput({ 50 | fileCount: 1 51 | }); 52 | }); 53 | }); 54 | }); 55 | 56 | describe("registered before CssEntryPlugin", () => { 57 | beforeEach(() => { 58 | this.webpack 59 | .config({ 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.css$/, 64 | issuer: /import\d\.css$/, 65 | use: ExtractTextPlugin.extract({ 66 | use: "css-loader" 67 | }) 68 | } 69 | ] 70 | }, 71 | 72 | plugins: [ 73 | new ExtractTextPlugin("other-styles.css") 74 | ] 75 | }) 76 | .withCssEntryPlugin(); 77 | }); 78 | 79 | describe("configured with a shorthand single entry, that references an extracted css", () => { 80 | beforeEach(done => { 81 | this.webpack 82 | .config({ 83 | entry: fixtures.style1WithImport1.path 84 | }) 85 | .run(done); 86 | }); 87 | beforeEach(() => expect(this.webpack).toSucceed()); 88 | 89 | it("generates a single css bundle", () => { 90 | expect(this.webpack).toOutput({ 91 | content: fixtures.style1WithImport1.content 92 | }); 93 | }); 94 | 95 | it("generates the css bundle only", () => { 96 | expect(this.webpack).toOutput({ 97 | fileCount: 1 98 | }); 99 | }); 100 | }); 101 | }); 102 | 103 | describe("registered after and before CssEntryPlugin", () => { 104 | beforeEach(() => { 105 | let extractTextPlugin1 = new ExtractTextPlugin("other-styles1.css"); 106 | 107 | this.webpack 108 | .config({ 109 | plugins: [ 110 | extractTextPlugin1 111 | ] 112 | }) 113 | .withCssEntryPlugin() 114 | .config({ 115 | module: { 116 | rules: [ 117 | { 118 | test: /\.css$/, 119 | issuer: /import\d\.css$/, 120 | use: ExtractTextPlugin.extract({ 121 | use: "css-loader" 122 | }) 123 | } 124 | ] 125 | }, 126 | 127 | plugins: [ 128 | new ExtractTextPlugin("other-styles.css") 129 | ] 130 | }); 131 | }); 132 | 133 | describe("configured with a shorthand single entry, that references an extracted css", () => { 134 | beforeEach(done => { 135 | this.webpack 136 | .config({ 137 | entry: fixtures.style1WithImport1.path 138 | }) 139 | .run(done); 140 | }); 141 | beforeEach(() => expect(this.webpack).toSucceed()); 142 | 143 | it("generates a single css bundle", () => { 144 | expect(this.webpack).toOutput({ 145 | content: fixtures.style1WithImport1.content 146 | }); 147 | }); 148 | 149 | it("generates the css bundle only", () => { 150 | expect(this.webpack).toOutput({ 151 | fileCount: 1 152 | }); 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { EntryInfo } from "./models"; 2 | import CssEntryPluginError from "./CssEntryPluginError"; 3 | 4 | // Follow the standard https://webpack.js.org/configuration/output/#output-filename 5 | const defaultOutputFilename = "[name].css", 6 | defaultExtensions = [".css", ".scss", ".less", ".styl"]; 7 | 8 | export function normalizeOptions(options?: Options): NormalizedOptions { 9 | // Sanitize 10 | if (!options) { 11 | options = {}; 12 | } 13 | else if (typeof options === "string" || 14 | typeof options === "function") { 15 | options = { 16 | output: { 17 | filename: options 18 | } 19 | }; 20 | } 21 | 22 | if (typeof options !== "object") { 23 | throw new CssEntryPluginError("'options' should be of type string, function or object"); 24 | } 25 | 26 | return { 27 | disable: !!options.disable, 28 | 29 | output: normalizeOutputOptions(options.output), 30 | 31 | includeCssEntry: makeIncludeCssEntry(options), 32 | isCssResource: makeIsCssResource(options) 33 | }; 34 | } 35 | 36 | function normalizeOutputOptions(options?: OutputOptions): NormalizedOutputOptions { 37 | // Sanitize 38 | if (!options || !options.filename) { 39 | return { 40 | filename: defaultOutputFilename 41 | }; 42 | } 43 | 44 | if (typeof options !== "object") { 45 | throw new CssEntryPluginError("'output' option should be of type object"); 46 | } 47 | 48 | if (typeof options.filename !== "string" && 49 | typeof options.filename !== "function") { 50 | throw new CssEntryPluginError( 51 | "'output.filename' option should be of type string or function"); 52 | } 53 | 54 | return { 55 | filename: options.filename 56 | }; 57 | } 58 | 59 | function makeIncludeCssEntry(options: OptionsObject): IncludeCssEntryFunction { 60 | if (options.entries && options.ignoreEntries) { 61 | throw new CssEntryPluginError("Both 'entries' and 'excludeEntries' specified"); 62 | } 63 | 64 | if (options.entries) { 65 | return entryConditionToMatcher(options.entries); 66 | } 67 | 68 | if (options.ignoreEntries) { 69 | return entryConditionToMatcher(options.ignoreEntries, true); 70 | } 71 | 72 | return () => true; 73 | } 74 | 75 | function makeIsCssResource(options: OptionsObject): IsCssResourceFunction { 76 | if (!options.extensions && !options.test) { 77 | options.extensions = defaultExtensions; 78 | } 79 | 80 | if (options.extensions && options.test) { 81 | throw new CssEntryPluginError("Both 'extensions' and 'test' specified"); 82 | } 83 | 84 | if (options.extensions) { 85 | if (!Array.isArray(options.extensions) && 86 | typeof options.extensions !== "string") { 87 | throw new CssEntryPluginError( 88 | "Option 'extensions' should be an array of strings or a string"); 89 | } 90 | 91 | let extensions = Array.isArray(options.extensions) 92 | ? [...options.extensions] 93 | : [options.extensions]; 94 | 95 | return (resource: string, entry: any) => { 96 | for (let ext of extensions) { 97 | if (resource.endsWith(ext)) return true; 98 | } 99 | 100 | return false; 101 | }; 102 | } 103 | 104 | if (options.test) { 105 | if (typeof options.test !== "function" && 106 | !(options.test instanceof RegExp)) { 107 | throw new CssEntryPluginError( 108 | "Option 'test' should be a function or a regular expression"); 109 | } 110 | 111 | if (options.test instanceof RegExp) { 112 | let regexp = options.test; 113 | options.test = (resource, entry) => regexp.test(resource); 114 | } 115 | 116 | return options.test; 117 | } 118 | 119 | return () => true; 120 | } 121 | 122 | function entryConditionToMatcher( 123 | condition: EntryCondition, negate: boolean = false): IncludeCssEntryFunction { 124 | let fn = (entry: EntryInfo) => true; 125 | 126 | if (typeof condition === "string") { 127 | fn = entry => entry.name === condition; 128 | } 129 | else if (Array.isArray(condition)) { 130 | fn = entry => condition.indexOf(entry.name) !== -1; 131 | } 132 | else if (condition instanceof RegExp) { 133 | fn = entry => condition.test(entry.name); 134 | } 135 | else if (typeof condition === "function") { 136 | fn = condition; 137 | } 138 | 139 | return negate 140 | ? _ => !fn(_) 141 | : fn; 142 | } 143 | 144 | export type EntryCondition = RegExp | string | string[] | ((entry: EntryInfo) => boolean); 145 | export type EntryResourceCondition = RegExp | ((resource: string, entry: EntryInfo) => boolean); 146 | 147 | export interface OptionsObject { 148 | disable?: boolean; 149 | 150 | /** 151 | * Output options. 152 | */ 153 | output?: OutputOptions; 154 | 155 | /** 156 | * The condition for the entries to include. 157 | */ 158 | entries?: EntryCondition; 159 | 160 | /** 161 | * The condition for the entries to ignore. 162 | */ 163 | ignoreEntries?: EntryCondition; 164 | 165 | /** 166 | * Which file extensions will be valid in a css entry. 167 | */ 168 | extensions?: string | string[]; 169 | 170 | /** 171 | * A condition to match valid files for css entries. 172 | */ 173 | test?: EntryResourceCondition; 174 | } 175 | 176 | export interface OutputOptions { 177 | /** 178 | * This option determines the name of each output bundle. 179 | * The bundle is written to the directory specified 180 | * by the output.path option (specified in the Webpack configuration). 181 | */ 182 | filename?: FilenameOption; 183 | } 184 | 185 | export type FilenameTemplate = string; 186 | export type GetPathFunction = (template: FilenameTemplate) => string; 187 | export type FilenameDynamicOption = (getPath: GetPathFunction) => string; 188 | export type FilenameOption = FilenameTemplate | FilenameDynamicOption; 189 | 190 | export type Options = FilenameOption | OptionsObject; 191 | 192 | export interface NormalizedOutputOptions { 193 | filename: FilenameOption; 194 | } 195 | 196 | export interface NormalizedOptions { 197 | disable: boolean; 198 | 199 | output: NormalizedOutputOptions; 200 | 201 | includeCssEntry: IncludeCssEntryFunction; 202 | isCssResource: IsCssResourceFunction; 203 | } 204 | 205 | export type IncludeCssEntryFunction = (entry: EntryInfo) => boolean; 206 | export type IsCssResourceFunction = (resource: string, entry: EntryInfo) => boolean; 207 | -------------------------------------------------------------------------------- /spec/file-loader-interop.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin and FileLoader", () => { 2 | beforeEach(done => { 3 | this.webpack = webpackTestFixture(jasmine) 4 | .withCssEntryPlugin() 5 | .config({ 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.png$/, 10 | use: { 11 | loader: "file-loader", 12 | options: { 13 | name: "[name].[ext]" 14 | } 15 | } 16 | } 17 | ] 18 | } 19 | }) 20 | .cleanOutput(done); 21 | }); 22 | 23 | describe("configured with a shorthand single entry, that references a file", () => { 24 | beforeEach(done => { 25 | this.webpack 26 | .config({ 27 | entry: fixtures.style1WithImg1.path 28 | }) 29 | .run(done); 30 | }); 31 | beforeEach(() => expect(this.webpack).toSucceed()); 32 | 33 | it("generates a single css bundle with the referenced file path changed", () => { 34 | expect(this.webpack).toOutput({ 35 | content: [ 36 | ...fixtures.style1WithImg1.content, 37 | `url(${fixtures.style1WithImg1.img1.file})` 38 | ] 39 | }); 40 | }); 41 | 42 | it("generates the referenced file", () => { 43 | expect(this.webpack).toOutput({ 44 | file: fixtures.style1WithImg1.img1.file 45 | }); 46 | }); 47 | 48 | it("generates the css bundle and referenced file only", () => { 49 | expect(this.webpack).toOutput({ 50 | fileCount: 2 51 | }); 52 | }); 53 | }); 54 | 55 | describe("configured with a multi-main entry of two files, that both reference a file each", () => { 56 | beforeEach(done => { 57 | this.webpack 58 | .config({ 59 | entry: [ 60 | fixtures.style1WithImg1.path, 61 | fixtures.style1WithImg2.path 62 | ] 63 | }) 64 | .run(done); 65 | }); 66 | beforeEach(() => expect(this.webpack).toSucceed()); 67 | 68 | it("generates a single css bundle with the referenced file paths changed", () => { 69 | expect(this.webpack).toOutput({ 70 | content: [ 71 | ...fixtures.style1WithImg1.content, 72 | ...fixtures.style1WithImg2.content, 73 | `url(${fixtures.style1WithImg1.img1.file})`, 74 | `url(${fixtures.style1WithImg2.img2.file})` 75 | ] 76 | }); 77 | }); 78 | 79 | it("generates the referenced files", () => { 80 | expect(this.webpack).toOutput({ 81 | file: fixtures.style1WithImg1.img1.file 82 | }); 83 | 84 | expect(this.webpack).toOutput({ 85 | file: fixtures.style1WithImg2.img2.file 86 | }); 87 | }); 88 | 89 | it("generates the css bundle and both referenced files only", () => { 90 | expect(this.webpack).toOutput({ 91 | fileCount: 3 92 | }); 93 | }); 94 | }); 95 | 96 | describe("configured with two entries of one file each, that both reference a file each", () => { 97 | beforeEach(done => { 98 | this.webpack 99 | .config({ 100 | entry: { 101 | "test1": fixtures.style1WithImg1.path, 102 | "test2": fixtures.style1WithImg2.path 103 | } 104 | }) 105 | .run(done); 106 | }); 107 | beforeEach(() => expect(this.webpack).toSucceed()); 108 | 109 | it("generates two css bundles with the referenced file path changed", () => { 110 | expect(this.webpack).toOutput({ 111 | entry: "test1", 112 | withContent: [ 113 | ...fixtures.style1WithImg1.content, 114 | `url(${fixtures.style1WithImg1.img1.file})` 115 | ] 116 | }); 117 | 118 | expect(this.webpack).toOutput({ 119 | entry: "test2", 120 | withContent: [ 121 | ...fixtures.style1WithImg2.content, 122 | `url(${fixtures.style1WithImg2.img2.file})` 123 | ] 124 | }); 125 | }); 126 | 127 | it("generates the referenced files", () => { 128 | expect(this.webpack).toOutput({ 129 | file: fixtures.style1WithImg1.img1.file 130 | }); 131 | 132 | expect(this.webpack).toOutput({ 133 | file: fixtures.style1WithImg2.img2.file 134 | }); 135 | }); 136 | 137 | it("generates both css bundles and both referenced files only", () => { 138 | expect(this.webpack).toOutput({ 139 | fileCount: 4 140 | }); 141 | }); 142 | }); 143 | 144 | describe("configured with two entries of one file each, that both reference the same file", () => { 145 | beforeEach(done => { 146 | this.webpack 147 | .config({ 148 | entry: { 149 | "test1": fixtures.style1WithImg1.path, 150 | "test2": fixtures.style2WithImg1.path 151 | } 152 | }) 153 | .run(done); 154 | }); 155 | beforeEach(() => expect(this.webpack).toSucceed()); 156 | 157 | it("generates two css bundles with the referenced file path changed", () => { 158 | expect(this.webpack).toOutput({ 159 | entry: "test1", 160 | withContent: [ 161 | ...fixtures.style1WithImg1.content, 162 | `url(${fixtures.style1WithImg1.img1.file})` 163 | ] 164 | }); 165 | 166 | expect(this.webpack).toOutput({ 167 | entry: "test2", 168 | withContent: [ 169 | ...fixtures.style2WithImg1.content, 170 | `url(${fixtures.style2WithImg1.img1.file})` 171 | ] 172 | }); 173 | }); 174 | 175 | it("generates the referenced file", () => { 176 | expect(this.webpack).toOutput({ 177 | file: fixtures.style1WithImg1.img1.file 178 | }); 179 | }); 180 | 181 | it("generates both css bundles and the single referenced files only", () => { 182 | expect(this.webpack).toOutput({ 183 | fileCount: 3 184 | }); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _With the advent of native CSS modules support in Webpack (https://github.com/webpack/webpack/pull/6448) and due to lack of time in maintaining this package, the project has been discontinued. If anyone wants to continue work on this, PRs are welcome._ 2 | 3 |
4 | 5 | 7 | 8 |

CSS Entry Plugin

9 |
10 | 11 | [![Build Status][plugin-travis-shield]][plugin-travis-url] 12 | [![License][plugin-license-shield]][plugin-npm-url] 13 | 14 | A [Webpack][webpack-url] plugin that simplifies creation of CSS-only bundles. 15 | 16 | Installation 17 | ------------ 18 | [![NPM Version][plugin-npm-version-shield]][plugin-npm-url] 19 | [![Dependency Status][plugin-npm-dependencies-shield]][plugin-npm-dependencies-url] 20 | 21 | Install the plugin using [npm][npm-url]: 22 | ```shell 23 | $ npm install css-entry-webpack-plugin --save-dev 24 | ``` 25 | 26 | [![npm](https://nodei.co/npm/css-entry-webpack-plugin.png?downloads=true&downloadRank=true&stars=true)][plugin-npm-url] 27 | 28 | Basic Usage 29 | ----------- 30 | The plugin will identify the entries that contain only CSS resources and will generate CSS bundles for them. 31 | 32 | webpack.config.js 33 | ```js 34 | const CssEntryPlugin = require("css-entry-webpack-plugin"); 35 | 36 | module.exports = { 37 | entry: { 38 | "styles": ["src/style1.css", "src/style2.css"], 39 | "main": "src/index.js" 40 | }, 41 | output: { 42 | path: "dist", 43 | filename: "[name].bundle.js" 44 | }, 45 | module: { 46 | rules: [ 47 | // This is required 48 | { 49 | test: /\.css$/, 50 | use: "css-loader" 51 | } 52 | ] 53 | }, 54 | plugins: [ 55 | new CssEntryPlugin({ 56 | output: { 57 | filename: "[name].bundle.css" 58 | } 59 | }) 60 | ] 61 | }; 62 | ``` 63 | 64 | will output two files 65 | 66 | `main.bundle.js` and `styles.bundle.css` 67 | 68 | API 69 | --- 70 | 71 | ```js 72 | new CssEntryPlugin(options: String | Object) 73 | ``` 74 | 75 | #### `options` 76 | Type: `String | Function | Object`
77 | Optional 78 | 79 | Specifies the options for the `CssEntryPlugin`. 80 | 81 | The shorthand version allows you to specify the `output.filename` directly as a `String` or a `Function`, this will be equivalent to passing an object with `output.filename`. See [`output.filename`](#outputfilename) for details on the possible values. 82 | 83 | ```js 84 | new CssEntryPlugin(/* option: String | Function */) 85 | // is equivalent to 86 | new CssEntryPlugin({ 87 | output: { 88 | filename: /* option */ 89 | } 90 | }) 91 | ``` 92 | 93 | When specified as an `Object`, the following options are available: 94 | 95 | ##### `output` 96 | Type: `Object`
97 | Optional 98 | 99 | Specifies a set of options instructing the plugin on how and where to output your CSS bundles. It works in a similar fashion to [Webpack's `output` option](https://webpack.js.org/configuration/output/#output-filename). 100 | 101 | ```js 102 | new CssEntryPlugin({ 103 | output: { /* output options */ } 104 | }) 105 | ``` 106 | 107 | ##### `output.filename` 108 | Type: `String | Function`
109 | Default: `[name].css`
110 | Optional 111 | 112 | This option determines the name of each CSS output bundle. The bundle is written to the directory specified by the [Webpack `output.path` option](https://webpack.js.org/configuration/output/#output-path). It works in a similar fashion to [Webpack's `output.filename` option](https://webpack.js.org/configuration/output/#output-filename) and [`ExtractTextPlugin`'s `filename` option](https://github.com/webpack-contrib/extract-text-webpack-plugin#options). 113 | 114 | For a single [`entry`](https://webpack.js.org/configuration/entry-context#entry) point, this can be a static name. 115 | 116 | ```js 117 | filename: "bundle.css" 118 | ``` 119 | 120 | However, when creating multiple bundles via more than one entry point, you should use a [template string](https://github.com/webpack/webpack/blob/master/lib/TemplatedPathPlugin.js) with one of the following substitutions to give each bundle a unique name. 121 | 122 | Using the entry name: 123 | 124 | ```js 125 | filename: "[name].bundle.css" 126 | ``` 127 | 128 | Using the internal chunk id: 129 | 130 | ```js 131 | filename: "[id].bundle.css" 132 | ``` 133 | 134 | The following substitutions are available in template strings: 135 | 136 | |Substitution|Description| 137 | |:----------:|:----------| 138 | |`[name]`|The module name or name of the chunk| 139 | |`[id]`|The number of the chunk or module identifier| 140 | |`[contenthash]`|The hash of the content of the extracted file| 141 | 142 | Any combination of these substitutions is allowed (eg. `"[name].[id].css"`). 143 | 144 | The option can also be specified as a `Function` which should return the `filename` as a string without substitutions. 145 | 146 | ```js 147 | filename: function (getPath /* (template: string) => string */) { 148 | return "prefix-" + getPath("[name].[id].css"); 149 | } 150 | ``` 151 | 152 | The `Function` has the signature `(getPath: ((template: string) => string)) => string` where `getPath` is a function passed as the first argument, that can be used to perform the substitutions on a given template string to retrieve the original path. 153 | 154 | Note this option is called `filename` but you are still allowed to use or return something like `"css/[name]/bundle.css"` to create a folder structure. 155 | 156 | Note this option only affects CSS output files for entries matched by this plugin (CSS entries). 157 | 158 | ##### `entries` 159 | Type: `String | String[] | RegExp | Function`
160 | Optional and mutually exclusive with [`ignoreEntries`](#ignoreentries) 161 | 162 | Specifies the entry or entries to consider as possible CSS entries. Other entries will be ignored. 163 | 164 | ##### `ignoreEntries` 165 | Type: `String | String[] | RegExp | Function`
166 | Optional and mutually exclusive with [`entries`](#entries) 167 | 168 | Specifies the entry or entries to ignore. Other entries will be considered as possible CSS entries. 169 | 170 | ##### `extensions` 171 | Type: `String | String[]`
172 | Default: `[".css", ".scss", ".less", ".styl"]`
173 | Optional and mutually exclusive with [`test`](#test) 174 | 175 | Specifies which file extensions are valid for files/resources inside considered CSS entries. 176 | 177 | ##### `test` 178 | Type: `RegExp | Function`
179 | Optional and mutually exclusive with [`extensions`](#extensions) 180 | 181 | Specifies which files/resources are valid for considered CSS entries. 182 | 183 | ##### `disable` 184 | Type: `Boolean`
185 | Default: `false`
186 | Optional 187 | 188 | Disables the plugin. 189 | 190 | [webpack-url]: https://webpack.js.org/ 191 | [npm-url]: https://www.npmjs.com/ 192 | 193 | [plugin-npm-url]: https://npmjs.com/package/css-entry-webpack-plugin 194 | [plugin-npm-dependencies-url]: https://david-dm.org/tomachristian/css-entry-webpack-plugin 195 | [plugin-travis-url]: https://travis-ci.org/tomachristian/css-entry-webpack-plugin 196 | 197 | [plugin-license-shield]: https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square 198 | [plugin-npm-version-shield]: https://img.shields.io/npm/v/css-entry-webpack-plugin.svg?style=flat-square 199 | [plugin-npm-dependencies-shield]: https://david-dm.org/tomachristian/css-entry-webpack-plugin.svg?style=flat-square 200 | [plugin-travis-shield]: https://img.shields.io/travis/tomachristian/css-entry-webpack-plugin/develop.svg?style=flat-square 201 | -------------------------------------------------------------------------------- /spec/common-requests-between-entries.spec.js: -------------------------------------------------------------------------------- 1 | describe("Running CssEntryPlugin for entries with common requests", () => { 2 | beforeEach(done => { 3 | this.webpack = webpackTestFixture(jasmine) 4 | .withCssEntryPlugin() 5 | .cleanOutput(done); 6 | }); 7 | 8 | describe("configured with two single entries, both with the same file", () => { 9 | beforeEach(done => { 10 | this.webpack 11 | .config({ 12 | entry: { 13 | "test1": fixtures.style1.path, 14 | "test2": fixtures.style1.path 15 | } 16 | }) 17 | .run(done); 18 | }); 19 | beforeEach(() => expect(this.webpack).toSucceed()); 20 | 21 | it("generates two css bundles, both with the same css", () => { 22 | expect(this.webpack).toOutput({ 23 | entry: "test1", 24 | withContent: fixtures.style1.content, 25 | onlyOnce: true 26 | }); 27 | 28 | expect(this.webpack).toOutput({ 29 | entry: "test2", 30 | withContent: fixtures.style1.content, 31 | onlyOnce: true 32 | }); 33 | }); 34 | 35 | it("generates two css bundles only", () => { 36 | expect(this.webpack).toOutput({ 37 | fileCount: 2 38 | }); 39 | }); 40 | }); 41 | 42 | describe("configured with one single entry and a multi entry, both with the same file only", () => { 43 | beforeEach(done => { 44 | this.webpack 45 | .config({ 46 | entry: { 47 | "test1": fixtures.style1.path, 48 | "test2": [fixtures.style1.path] 49 | } 50 | }) 51 | .run(done); 52 | }); 53 | beforeEach(() => expect(this.webpack).toSucceed()); 54 | 55 | it("generates two css bundles, both with the same css", () => { 56 | expect(this.webpack).toOutput({ 57 | entry: "test1", 58 | withContent: fixtures.style1.content, 59 | onlyOnce: true 60 | }); 61 | 62 | expect(this.webpack).toOutput({ 63 | entry: "test2", 64 | withContent: fixtures.style1.content, 65 | onlyOnce: true 66 | }); 67 | }); 68 | 69 | it("generates two css bundles only", () => { 70 | expect(this.webpack).toOutput({ 71 | fileCount: 2 72 | }); 73 | }); 74 | }); 75 | 76 | describe("configured with two multi entries, both with the same file only", () => { 77 | beforeEach(done => { 78 | this.webpack 79 | .config({ 80 | entry: { 81 | "test1": [fixtures.style1.path], 82 | "test2": [fixtures.style1.path] 83 | } 84 | }) 85 | .run(done); 86 | }); 87 | beforeEach(() => expect(this.webpack).toSucceed()); 88 | 89 | it("generates two css bundles, both with the same css", () => { 90 | expect(this.webpack).toOutput({ 91 | entry: "test1", 92 | withContent: fixtures.style1.content, 93 | onlyOnce: true 94 | }); 95 | 96 | expect(this.webpack).toOutput({ 97 | entry: "test2", 98 | withContent: fixtures.style1.content, 99 | onlyOnce: true 100 | }); 101 | }); 102 | 103 | it("generates two css bundles only", () => { 104 | expect(this.webpack).toOutput({ 105 | fileCount: 2 106 | }); 107 | }); 108 | }); 109 | 110 | describe("configured with two multi entries, both with the same two files only", () => { 111 | beforeEach(done => { 112 | this.webpack 113 | .config({ 114 | entry: { 115 | "test1": [fixtures.style1.path, fixtures.style2.path], 116 | "test2": [fixtures.style1.path, fixtures.style2.path] 117 | } 118 | }) 119 | .run(done); 120 | }); 121 | beforeEach(() => expect(this.webpack).toSucceed()); 122 | 123 | it("generates two css bundles, both with the same css", () => { 124 | expect(this.webpack).toOutput({ 125 | entry: "test1", 126 | withContent: [ 127 | ...fixtures.style1.content, 128 | ...fixtures.style2.content 129 | ], 130 | onlyOnce: true 131 | }); 132 | 133 | expect(this.webpack).toOutput({ 134 | entry: "test2", 135 | withContent: [ 136 | ...fixtures.style1.content, 137 | ...fixtures.style2.content 138 | ], 139 | onlyOnce: true 140 | }); 141 | }); 142 | 143 | it("generates two css bundles only", () => { 144 | expect(this.webpack).toOutput({ 145 | fileCount: 2 146 | }); 147 | }); 148 | }); 149 | 150 | describe("configured with one single entry and a multi entry with the same file and an extra non-css file", () => { 151 | beforeEach(done => { 152 | this.webpack 153 | .config({ 154 | entry: { 155 | "test1": fixtures.style1.path, 156 | "test2": [fixtures.style1.path, fixtures.script1.path] 157 | } 158 | }) 159 | .run(done); 160 | }); 161 | beforeEach(() => expect(this.webpack).toSucceed()); 162 | 163 | it("generates one css bundle and one js bundle, both with the same css", () => { 164 | expect(this.webpack).toOutput({ 165 | entry: "test1", 166 | withContent: fixtures.style1.content, 167 | onlyOnce: true 168 | }); 169 | 170 | expect(this.webpack).toOutput({ 171 | file: "test2.bundle.js", 172 | withContent: [ 173 | ...fixtures.script1.content, 174 | ...fixtures.style1.content 175 | ], 176 | onlyOnce: true 177 | }); 178 | }); 179 | 180 | it("generates one css bundle and one js bundle only", () => { 181 | expect(this.webpack).toOutput({ 182 | fileCount: 2 183 | }); 184 | }); 185 | }); 186 | 187 | describe("configured with two multi entries with the same file and one with an extra non-css file", () => { 188 | beforeEach(done => { 189 | this.webpack 190 | .config({ 191 | entry: { 192 | "test1": [fixtures.style1.path], 193 | "test2": [fixtures.style1.path, fixtures.script1.path] 194 | } 195 | }) 196 | .run(done); 197 | }); 198 | beforeEach(() => expect(this.webpack).toSucceed()); 199 | 200 | it("generates one css bundle and one js bundle, both with the same css", () => { 201 | expect(this.webpack).toOutput({ 202 | entry: "test1", 203 | withContent: fixtures.style1.content, 204 | onlyOnce: true 205 | }); 206 | 207 | expect(this.webpack).toOutput({ 208 | file: "test2.bundle.js", 209 | withContent: [ 210 | ...fixtures.script1.content, 211 | ...fixtures.style1.content 212 | ], 213 | onlyOnce: true 214 | }); 215 | }); 216 | 217 | it("generates one css bundle and one js bundle only", () => { 218 | expect(this.webpack).toOutput({ 219 | fileCount: 2 220 | }); 221 | }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /src/CssEntryCompilation.ts: -------------------------------------------------------------------------------- 1 | import Tapable from "./interop/tapable"; 2 | import { Compiler, Compilation, 3 | Resolver, Loader, 4 | SingleEntryDependency, MultiModule, Chunk, Assets, 5 | AfterResolveData, 6 | multiEntryDependencyLocSeparator, loadersToRequestIdent, 7 | excludeWebpackDevServerResources, isWebpackDevServerResource 8 | } from "./interop/webpack"; 9 | import ExtractTextPlugin from "extract-text-webpack-plugin"; 10 | import { NormalizedOptions } from "./options"; 11 | import { EntryInfo, TaggedMultiModule, isCssEntry } from "./models"; 12 | import CssEntryPluginError from "./CssEntryPluginError"; 13 | 14 | // TODO: Add spec that it works with dynamic entry (function) 15 | // TODO: Test when two entries share a common file and one entry is excluded (when both are multi) 16 | // TODO: Spec for how many times a condition from options is called 17 | /** @internal */ 18 | export default class CssEntryCompilation extends Tapable { 19 | private breakingChangeErrorReported = false; 20 | 21 | public cssEntries: Set = new Set(); 22 | public nonCssEntries: Set = new Set(); 23 | 24 | constructor( 25 | private options: NormalizedOptions, 26 | private compiler: Compiler, 27 | private compilation: Compilation, 28 | private extractTextPlugin: ExtractTextPlugin) { 29 | super(); 30 | } 31 | 32 | /** 33 | * Called after the NormalModuleFactory has resolved a request. 34 | * @param data The data for the resolved request. 35 | */ 36 | public async onNormalModuleFactoryAfterResolve( 37 | data: AfterResolveData): Promise { 38 | if (!data.dependencies) { 39 | this.reportBreakingChange("Could not get 'dependencies' from AfterResolveData"); 40 | return data; 41 | } 42 | 43 | if (!this.isEntryRequestResolve(data)) { 44 | return data; 45 | } 46 | 47 | let depedency = data.dependencies[0] as SingleEntryDependency; 48 | let entry = this.extractEntryInfo(depedency); 49 | 50 | if (!entry) return data; 51 | 52 | return await this.onEntryRequestAfterResolve(entry, data); 53 | } 54 | 55 | private isEntryRequestResolve(data: AfterResolveData): boolean { 56 | // Only one dependency is added for an entry module 57 | // See webpack/lib/Compilation.js: _addModuleChain declaration 58 | // RISK: Webpack can change and pass multiple SingleEntryDependency instances 59 | return data.dependencies.length === 1 && 60 | data.dependencies[0] instanceof SingleEntryDependency; 61 | } 62 | 63 | /** 64 | * Extracts entry information from a given SingleEntryDependency. 65 | * @param dependency The SingleEntryDependency to extract the entry information from. 66 | * @returns The entry information or null if none could be extracted. 67 | */ 68 | private extractEntryInfo(dependency: SingleEntryDependency): EntryInfo | null { 69 | if (typeof dependency.loc !== "string") { 70 | this.reportBreakingChange("Could not get 'loc' from SingleEntryDependency"); 71 | return null; 72 | } 73 | 74 | let sep = dependency.loc.lastIndexOf(multiEntryDependencyLocSeparator); 75 | let name = sep === -1 76 | ? dependency.loc 77 | : dependency.loc.substr(0, sep); 78 | 79 | if (!name) { 80 | this.addWarning("Entry with no name found"); 81 | return null; 82 | } 83 | 84 | return { 85 | isMulti: sep !== -1, 86 | name: name 87 | }; 88 | } 89 | 90 | /** 91 | * Called after the NormalModuleFactory has resolved the request from an entry. 92 | * @param entry The entry information. 93 | * @param data The data for the resolved request. 94 | * @returns A promise that finishes after the plugin logic has finished. 95 | */ 96 | private async onEntryRequestAfterResolve( 97 | entry: EntryInfo, data: AfterResolveData): Promise { 98 | // TODO: do isCssResource() 99 | let isCssEntry = await this.isCssEntry(entry, data); 100 | 101 | if (!isCssEntry) { 102 | this.nonCssEntries.add(entry.name); 103 | return data; 104 | } 105 | 106 | this.cssEntries.add(entry.name); 107 | 108 | if (isWebpackDevServerResource(data.resource)) { 109 | return data; 110 | } 111 | 112 | return this.extractCss(data); 113 | } 114 | 115 | /** 116 | * Checks if the css from the request of the entry should be extracted. 117 | * @param entryInfo The entry information. 118 | * @param data The data for the resolved request. 119 | * @returns A promise that finishes after the check logic has finished or a boolean. 120 | */ 121 | private async isCssEntry(entry: EntryInfo, data: AfterResolveData): Promise { 122 | // Skip if already marked as a non-CSS entry 123 | if (this.nonCssEntries.has(entry.name)) return false; 124 | 125 | // Check configuration options 126 | if (!this.cssEntries.has(entry.name) && 127 | !this.options.includeCssEntry(entry)) return false; 128 | 129 | // Single entry 130 | if (!entry.isMulti && 131 | this.options.isCssResource(data.resource, entry)) { 132 | // Valid single entry with valid css resource 133 | return true; 134 | } 135 | 136 | // Multi entry 137 | if (entry.isMulti) { 138 | if (!this.cssEntries.has(entry.name)) { 139 | // This is the first time validating this entry 140 | return this.isMultiCssEntry(entry); 141 | } 142 | 143 | // Already validated this, it is a valid css entry 144 | return true; 145 | } 146 | 147 | return false; 148 | } 149 | 150 | private async isMultiCssEntry(entryInfo: EntryInfo): Promise { 151 | // We do a check to see if all resources in the entry are valid css resources 152 | let multiModule = this.findMultiModule(entryInfo); 153 | if (!multiModule) { 154 | this.reportBreakingChange("Could not find associated MultiModule of entry"); 155 | return false; 156 | } 157 | 158 | let resources = await this.resolveResources(multiModule); 159 | resources = excludeWebpackDevServerResources(resources); 160 | 161 | let hasOnlyCssResources = resources.every(resource => 162 | this.options.isCssResource(resource, entryInfo)); 163 | 164 | let taggedModule = multiModule as TaggedMultiModule; 165 | taggedModule[isCssEntry] = hasOnlyCssResources; 166 | 167 | return hasOnlyCssResources; 168 | } 169 | 170 | private resolveResources(module: MultiModule): Promise { 171 | const resolver = this.compilation.resolvers.normal, 172 | context = this.compiler.context; 173 | 174 | return Promise.all(module.dependencies 175 | .map(depedency => this.resolveResource(resolver, context, depedency))); 176 | } 177 | 178 | private resolveResource(resolver: Resolver, context: string, 179 | dependecy: SingleEntryDependency): Promise { 180 | return new Promise((resolve, reject) => { 181 | resolver.resolve({}, context, dependecy.request, (err, data: string) => { 182 | if (err) { 183 | reject(err); 184 | } 185 | else { 186 | resolve(data); 187 | } 188 | }); 189 | }); 190 | } 191 | 192 | private findMultiModule(entryInfo: EntryInfo): MultiModule | null { 193 | if (!entryInfo.isMulti) return null; 194 | 195 | for (let module of this.compilation.entries) { 196 | if (module instanceof MultiModule && 197 | module.name === entryInfo.name) { 198 | return module; 199 | } 200 | } 201 | 202 | return null; 203 | } 204 | 205 | private extractCss(data: AfterResolveData): AfterResolveData { 206 | const originalLoaders = data.loaders; 207 | data.loaders = this.extractTextPlugin.extract({ 208 | use: originalLoaders 209 | }); 210 | 211 | // Recalculate the 'request', this is required 212 | data.request = loadersToRequestIdent(data.loaders, data.resource); 213 | 214 | return data; 215 | } 216 | 217 | /** 218 | * Called after the sealing of the compilation. 219 | * @param callback The callback to call when ready. 220 | * @see https://github.com/webpack/docs/wiki/plugins#seal 221 | */ 222 | public async onCompilationAfterSeal(): Promise { 223 | this.fixMissingCssEntries(); 224 | this.alterCssChunks(this.compilation.assets, this.compilation.chunks); 225 | } 226 | 227 | private fixMissingCssEntries(): void { 228 | let allEntryNames = Object.keys(this.compilation.entrypoints); 229 | 230 | for (let name of allEntryNames) { 231 | if (this.cssEntries.has(name)) continue; 232 | 233 | let entrypoint = this.compilation.entrypoints[name]; 234 | let hasOnlyCssModules = entrypoint.chunks.every(chunk => { 235 | let taggedModule = chunk.entryModule as TaggedMultiModule; 236 | return taggedModule && taggedModule[isCssEntry] === true; 237 | }); 238 | 239 | if (hasOnlyCssModules) { 240 | this.cssEntries.add(entrypoint.name); 241 | } 242 | } 243 | } 244 | 245 | private alterCssChunks(assets: Assets, chunks: Chunk[]): void { 246 | for (let chunk of chunks) { 247 | if (!this.isCssChunk(chunk)) continue; 248 | 249 | this.alterCssChunk(assets, chunk); 250 | } 251 | } 252 | 253 | /** 254 | * Checks if the chunk is a CSS chunk. 255 | * @param chunk The chunk instance or chunk name. 256 | * @returns True if the chunk is a CSS chunk, or false otherwise. 257 | */ 258 | private isCssChunk(chunk: string | Chunk): boolean { 259 | let chunkName = chunk instanceof Chunk 260 | ? chunk.name 261 | : chunk; 262 | 263 | return this.cssEntries.has(chunkName); 264 | } 265 | 266 | private alterCssChunk(assets: Assets, chunk: Chunk): void { 267 | let cssFiles: string[] = []; 268 | 269 | for (let file of chunk.files) { 270 | if (file.match(/\.js(\.map)?$/)) { 271 | // Remove JS file from assets and chunk 272 | delete assets[file]; 273 | continue; 274 | } 275 | 276 | // Keep CSS file 277 | cssFiles.push(file); 278 | } 279 | 280 | chunk.files = cssFiles; 281 | } 282 | 283 | private reportBreakingChange(message: string): void { 284 | if (!this.breakingChangeErrorReported) { 285 | this.addWarning(message + " (possible breaking change in Webpack)"); 286 | this.breakingChangeErrorReported = true; 287 | } 288 | } 289 | 290 | private addWarning(err: Error | string): Error { 291 | if (typeof err === "string") { 292 | err = new CssEntryPluginError(err); 293 | } 294 | 295 | this.compilation.warnings.push(err); 296 | return err; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /spec/css-entry-plugin.spec.js: -------------------------------------------------------------------------------- 1 | describe("CssEntryPlugin", () => { 2 | beforeEach(cleanOutput); 3 | 4 | it("generates a single output file for a single string entry point", done => { 5 | testWebpackWithCssEntryPlugin({ 6 | entry: fixtures.style1.path 7 | }, (err, stats) => { 8 | expectNoErrorsAndNoWarnings(err, stats); 9 | expectOutputFileExists(stats); 10 | expectOutputFileToContain(stats, fixtures.style1.content); 11 | done(); 12 | }); 13 | }); 14 | 15 | it("generates a single output file for a single object entry point", done => { 16 | testWebpackWithCssEntryPlugin({ 17 | entry: { 18 | "test": fixtures.style1.path 19 | } 20 | }, (err, stats) => { 21 | expectNoErrorsAndNoWarnings(err, stats); 22 | expectOutputFileExists(stats, "test"); 23 | expectOutputFileToContain(stats, "test", fixtures.style1.content); 24 | done(); 25 | }); 26 | }); 27 | 28 | it("generates a single output file for a single array entry point (one file path string)", done => { 29 | testWebpackWithCssEntryPlugin({ 30 | entry: [fixtures.style1.path] 31 | }, (err, stats) => { 32 | expectNoErrorsAndNoWarnings(err, stats); 33 | expectOutputFileExists(stats); 34 | expectOutputFileToContain(stats, fixtures.style1.content); 35 | done(); 36 | }); 37 | }); 38 | 39 | it("generates a single output file for a single array entry point (two file path strings)", done => { 40 | testWebpackWithCssEntryPlugin({ 41 | entry: [ 42 | fixtures.style1.path, 43 | fixtures.style2.path 44 | ] 45 | }, (err, stats) => { 46 | expectNoErrorsAndNoWarnings(err, stats); 47 | expectOutputFileExists(stats); 48 | expectOutputFileToContain(stats, [ 49 | ...fixtures.style1.content, 50 | ...fixtures.style2.content 51 | ]); 52 | done(); 53 | }); 54 | }); 55 | 56 | it("generates a single output file for a single array entry point (three file path strings)", done => { 57 | testWebpackWithCssEntryPlugin({ 58 | entry: [ 59 | fixtures.style1.path, 60 | fixtures.style2.path, 61 | fixtures.style4.path 62 | ] 63 | }, (err, stats) => { 64 | expectNoErrorsAndNoWarnings(err, stats); 65 | expectOutputFileExists(stats); 66 | expectOutputFileToContain(stats, [ 67 | ...fixtures.style1.content, 68 | ...fixtures.style2.content, 69 | ...fixtures.style4.content 70 | ]); 71 | done(); 72 | }); 73 | }); 74 | 75 | it("generates two output files for two string entry points", done => { 76 | testWebpackWithCssEntryPlugin({ 77 | entry: { 78 | "test1": fixtures.style1.path, 79 | "test2": fixtures.style2.path 80 | } 81 | }, (err, stats) => { 82 | expectNoErrorsAndNoWarnings(err, stats); 83 | expectOutputFileExists(stats, ["test1", "test2"]); 84 | expectOutputFileToContain(stats, "test1", fixtures.style1.content); 85 | expectOutputFileToContain(stats, "test2", fixtures.style2.content); 86 | done(); 87 | }); 88 | }); 89 | 90 | it("generates two output files for one string entry point and one array entry point", done => { 91 | testWebpackWithCssEntryPlugin({ 92 | entry: { 93 | "test1": fixtures.style1.path, 94 | "test2": [fixtures.style2.path, fixtures.style4.path] 95 | } 96 | }, (err, stats) => { 97 | expectNoErrorsAndNoWarnings(err, stats); 98 | expectOutputFileExists(stats, ["test1", "test2"]); 99 | expectOutputFileToContain(stats, "test1", fixtures.style1.content); 100 | expectOutputFileToContain(stats, "test2", [ 101 | ...fixtures.style2.content, 102 | ...fixtures.style4.content 103 | ]); 104 | done(); 105 | }); 106 | }); 107 | 108 | it("generates two output files for two array entry points", done => { 109 | testWebpackWithCssEntryPlugin({ 110 | entry: { 111 | "test1": [fixtures.style1.path, fixtures.style5.path], 112 | "test2": [fixtures.style2.path, fixtures.style4.path] 113 | } 114 | }, (err, stats) => { 115 | expectNoErrorsAndNoWarnings(err, stats); 116 | expectOutputFileExists(stats, ["test1", "test2"]); 117 | expectOutputFileToContain(stats, "test1", [ 118 | ...fixtures.style1.content, 119 | ...fixtures.style5.content 120 | ]); 121 | expectOutputFileToContain(stats, "test2", [ 122 | ...fixtures.style2.content, 123 | ...fixtures.style4.content 124 | ]); 125 | done(); 126 | }); 127 | }); 128 | 129 | describe("when css entry chunks share modules", () => { 130 | describe("and one entry point is a single string path", () => { 131 | it("generates two output files that both have a common module", done => { 132 | testWebpackWithCssEntryPlugin({ 133 | entry: { 134 | "test1": [fixtures.style1.path, fixtures.style5.path], 135 | "test2": fixtures.style1.path 136 | } 137 | }, (err, stats) => { 138 | expectNoErrorsAndNoWarnings(err, stats); 139 | expectOutputFileExists(stats, ["test1", "test2"]); 140 | expectOutputFileToContain(stats, "test1", [ 141 | ...fixtures.style1.content, 142 | ...fixtures.style5.content 143 | ]); 144 | expectOutputFileToContain(stats, "test2", fixtures.style1.content); 145 | done(); 146 | }); 147 | }); 148 | }); 149 | 150 | it("generates two output files that both have a common module", done => { 151 | testWebpackWithCssEntryPlugin({ 152 | entry: { 153 | "test1": [fixtures.style1.path, fixtures.style5.path], 154 | "test2": [fixtures.style1.path, fixtures.style4.path] 155 | } 156 | }, (err, stats) => { 157 | expectNoErrorsAndNoWarnings(err, stats); 158 | expectOutputFileExists(stats, ["test1", "test2"]); 159 | expectOutputFileToContain(stats, "test1", [ 160 | ...fixtures.style1.content, 161 | ...fixtures.style5.content 162 | ]); 163 | expectOutputFileToContain(stats, "test2", [ 164 | ...fixtures.style1.content, 165 | ...fixtures.style4.content 166 | ]); 167 | done(); 168 | }); 169 | }); 170 | 171 | it("generates three output files of which all have a common module", done => { 172 | testWebpackWithCssEntryPlugin({ 173 | entry: { 174 | "test1": [fixtures.style5.path, fixtures.style1.path], 175 | "test2": [fixtures.style1.path, fixtures.style4.path], 176 | "test3": [fixtures.style2.path, fixtures.style1.path] 177 | } 178 | }, (err, stats) => { 179 | expectNoErrorsAndNoWarnings(err, stats); 180 | expectOutputFileExists(stats, ["test1", "test2", "test3"]); 181 | expectOutputFileToContain(stats, "test1", [ 182 | ...fixtures.style1.content, 183 | ...fixtures.style5.content 184 | ]); 185 | expectOutputFileToContain(stats, "test2", [ 186 | ...fixtures.style1.content, 187 | ...fixtures.style4.content 188 | ]); 189 | expectOutputFileToContain(stats, "test3", [ 190 | ...fixtures.style1.content, 191 | ...fixtures.style2.content 192 | ]); 193 | done(); 194 | }); 195 | }); 196 | 197 | it("generates four output files of which all have a common module", done => { 198 | testWebpackWithCssEntryPlugin({ 199 | entry: { 200 | "test1": [fixtures.style5.path, fixtures.style1.path], 201 | "test2": [fixtures.style1.path, fixtures.style4.path], 202 | "test3": [fixtures.style2.path, fixtures.style1.path], 203 | "test4": fixtures.style1.path 204 | } 205 | }, (err, stats) => { 206 | expectNoErrorsAndNoWarnings(err, stats); 207 | expectOutputFileExists(stats, ["test1", "test2", "test3", "test4"]); 208 | expectOutputFileToContain(stats, "test1", [ 209 | ...fixtures.style1.content, 210 | ...fixtures.style5.content 211 | ]); 212 | expectOutputFileToContain(stats, "test2", [ 213 | ...fixtures.style1.content, 214 | ...fixtures.style4.content 215 | ]); 216 | expectOutputFileToContain(stats, "test3", [ 217 | ...fixtures.style1.content, 218 | ...fixtures.style2.content 219 | ]); 220 | expectOutputFileToContain(stats, "test4", fixtures.style1.content); 221 | done(); 222 | }); 223 | }); 224 | 225 | it("generates four output files of which pairs of two have a common module", done => { 226 | testWebpackWithCssEntryPlugin({ 227 | entry: { 228 | "test1": [fixtures.style1.path, fixtures.style2.path], 229 | "test2": [fixtures.style2.path, fixtures.style4.path], 230 | "test3": [fixtures.style4.path, fixtures.style5.path], 231 | "test4": [fixtures.style5.path, fixtures.style1.path] 232 | } 233 | }, (err, stats) => { 234 | expectNoErrorsAndNoWarnings(err, stats); 235 | expectOutputFileExists(stats, ["test1", "test2", "test3", "test4"]); 236 | expectOutputFileToContain(stats, "test1", [ 237 | ...fixtures.style1.content, 238 | ...fixtures.style2.content 239 | ]); 240 | expectOutputFileToContain(stats, "test2", [ 241 | ...fixtures.style2.content, 242 | ...fixtures.style4.content 243 | ]); 244 | expectOutputFileToContain(stats, "test3", [ 245 | ...fixtures.style4.content, 246 | ...fixtures.style5.content 247 | ]); 248 | expectOutputFileToContain(stats, "test4", [ 249 | ...fixtures.style5.content, 250 | ...fixtures.style1.content 251 | ]); 252 | done(); 253 | }); 254 | }); 255 | }); 256 | 257 | // TODO: Different files import same file 258 | // TODO: Same file in multiple entries, imports a file 259 | }); 260 | -------------------------------------------------------------------------------- /spec/helpers/common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const rimraf = require("rimraf"); 5 | 6 | const CssEntryPlugin = require("../../lib"); 7 | const CssEntryPluginError = require("../../lib/CssEntryPluginError"); 8 | const webpack = require("webpack"); 9 | const webpackMerge = require("webpack-merge"); 10 | const Server = require("webpack-dev-server/lib/Server"); 11 | const request = require("supertest"); 12 | 13 | const FIXTURES_DIR = path.join(__dirname, "../fixtures"); 14 | const OUTPUT_DIR = path.join(__dirname, "../../tmp"); 15 | 16 | const fixtures = require(FIXTURES_DIR); 17 | 18 | jasmine.getEnv().defaultTimeoutInterval = 30000; 19 | 20 | class WebpackTestFixture { 21 | constructor(inputDir, outputDir) { 22 | this.cleanOutput = this.cleanOutput.bind(this); 23 | this.run = this.run.bind(this); 24 | 25 | this.inputDir = inputDir; 26 | this.outputDir = outputDir; 27 | 28 | this.webpackConfig = { 29 | context: this.inputDir, 30 | 31 | output: { 32 | path: this.outputDir, 33 | filename: "[name].bundle.js" 34 | }, 35 | 36 | resolve: { 37 | extensions: [".js", ".css", ".scss"] 38 | }, 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.css$/, 44 | use: "css-loader" 45 | } 46 | ] 47 | } 48 | }; 49 | 50 | this.result = null; 51 | 52 | this.compiler = null; 53 | this.server = null; 54 | this.client = null; 55 | } 56 | 57 | cleanOutput(done) { 58 | rimraf(OUTPUT_DIR, done); 59 | return this; 60 | } 61 | 62 | withoutRules() { 63 | if (!this.webpackConfig.module) return this; 64 | 65 | this.webpackConfig.module.rules = []; 66 | return this; 67 | } 68 | 69 | withCssEntryPlugin(cssEntryPluginConfig, asIs) { 70 | if (asIs !== true) { 71 | const defaultCssEntryPluginConfig = { 72 | output: { 73 | filename: "[name].bundle.css" // TODO: Should be [name].css, like the default name 74 | } 75 | }; 76 | 77 | cssEntryPluginConfig = webpackMerge(defaultCssEntryPluginConfig, cssEntryPluginConfig); 78 | } 79 | 80 | return this.config({ 81 | plugins: [ 82 | new CssEntryPlugin(cssEntryPluginConfig) 83 | ] 84 | }); 85 | } 86 | 87 | config(additionalConfig) { 88 | this.webpackConfig = webpackMerge(this.webpackConfig, additionalConfig); 89 | return this; 90 | } 91 | 92 | run(done) { 93 | let run = new Promise((resolve, reject) => { 94 | try { 95 | webpack(this.webpackConfig, (err, stats) => { 96 | this.result = { err, stats }; 97 | resolve(this); 98 | }); 99 | } 100 | catch (err) { 101 | console.error(err); 102 | this.result = { 103 | err, 104 | stats: null 105 | }; 106 | resolve(this); 107 | } 108 | }); 109 | 110 | if (!done) { 111 | return run; 112 | } 113 | 114 | run.then(done, err => console.error(err)); 115 | return this; 116 | } 117 | 118 | serve(done) { 119 | const host = "localhost", 120 | port = 8080; 121 | 122 | let run = new Promise((resolve, reject) => { 123 | try { 124 | let devServerOptions = this.webpackConfig.devServer || {}; 125 | 126 | devServerOptions.host = host; 127 | devServerOptions.port = port; 128 | 129 | if (devServerOptions.quiet === undefined) { 130 | devServerOptions.quiet = true; 131 | } 132 | 133 | if (devServerOptions.inline === undefined) { 134 | devServerOptions.inline = true; 135 | } 136 | 137 | Server.addDevServerEntrypoints(this.webpackConfig, devServerOptions); 138 | this.compiler = webpack(this.webpackConfig); 139 | this.server = new Server(this.compiler, devServerOptions); 140 | this.client = request(this.server.app); 141 | 142 | this.server.listen(port, host, err => { 143 | this.result = { 144 | err, 145 | stats: null 146 | }; 147 | resolve(this); 148 | }); 149 | } 150 | catch (err) { 151 | console.error(err); 152 | this.result = { 153 | err, 154 | stats: null 155 | }; 156 | resolve(this); 157 | } 158 | }); 159 | 160 | if (!done) return run; 161 | 162 | run.then(done, err => console.error(err)); 163 | return this; 164 | } 165 | 166 | close(done) { 167 | let close = new Promise((resolve, reject) => { 168 | if (!this.server) { 169 | resolve(this); 170 | return; 171 | } 172 | 173 | this.server.close(() => { 174 | this.server = null; 175 | resolve(this); 176 | }); 177 | }); 178 | 179 | if (!done) return close; 180 | 181 | close.then(done, err => console.error(err)); 182 | return this; 183 | } 184 | } 185 | 186 | function webpackTestFixture(jasmine) { 187 | jasmine.addMatchers(customMatchers); 188 | 189 | return new WebpackTestFixture(FIXTURES_DIR, OUTPUT_DIR); 190 | } 191 | 192 | RegExp.escape = function (s) { 193 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 194 | }; 195 | 196 | const customMatchers = { 197 | toOutput() { 198 | return { 199 | compare(actual, expected) { 200 | if (expected.content && !expected.file) { 201 | expected.file = "main.bundle.css"; 202 | expected.withContent = expected.content; 203 | } 204 | else if (expected.entry && !expected.file) { 205 | expected.file = expected.entry + ".bundle.css"; 206 | } 207 | 208 | if (expected.fileCount) { 209 | let dir = fs.readdirSync(OUTPUT_DIR); 210 | 211 | if (dir.length !== expected.fileCount) { 212 | return { 213 | pass: false, 214 | message: `Expected webpack to output only ${expected.fileCount} files, ` + 215 | `but found ${dir.length} files.` 216 | } 217 | } 218 | } 219 | 220 | if (expected.file) { 221 | let filePath = path.join(OUTPUT_DIR, expected.file); 222 | let fileExists = fs.existsSync(filePath); 223 | 224 | if (!fileExists) { 225 | return { 226 | pass: false, 227 | message: `Expected webpack to output file '${expected.file}', ` + 228 | `but the output file was not found.` // TODO: List all the output files 229 | } 230 | } 231 | 232 | let fileContent = (expected.withContent || expected.withoutContent) 233 | ? fs.readFileSync(filePath) 234 | : null; 235 | 236 | if (expected.withContent) { 237 | let expectedContent = expected.withContent; 238 | 239 | for (let expectedPart of expectedContent) { 240 | let occurences = (fileContent.toString() 241 | .match(new RegExp(RegExp.escape(expectedPart), "g")) || []) 242 | .length; 243 | 244 | if ((expected.onlyOnce === true && occurences !== 1) || 245 | (expected.onlyOnce !== true && occurences === 0)) { 246 | return { 247 | pass: false, 248 | message: `Expected output file '${expected.file}' ` + 249 | `to contain:\n` + expectedPart + ". But it contains:\n" + fileContent 250 | } 251 | } 252 | } 253 | } 254 | 255 | if (expected.withoutContent) { 256 | let unexpectedContent = expected.withoutContent; 257 | 258 | for (let unexpectedPart of unexpectedContent) { 259 | if (fileContent.indexOf(unexpectedPart) !== -1) { 260 | return { 261 | pass: false, 262 | message: `Expected output file '${expected.file}' ` + 263 | `to not contain:\n` + unexpectedPart 264 | } 265 | } 266 | } 267 | } 268 | } 269 | 270 | return { 271 | pass: true 272 | }; 273 | } 274 | }; 275 | }, 276 | 277 | toSucceed() { 278 | return { 279 | compare(actual) { 280 | if (!actual || !actual.result) { 281 | return { 282 | pass: false, 283 | message: "Expected webpack to succeed, but it did not return a result." 284 | } 285 | } 286 | 287 | let { err, stats } = actual.result; 288 | 289 | if (err) { 290 | return { 291 | pass: false, 292 | message: "Expected webpack to succeed, but it failed with:\n" + 293 | err.toString() 294 | }; 295 | } 296 | 297 | let compilationErrors = (stats.compilation.errors || []).join('\n'); 298 | if (compilationErrors !== '') { 299 | return { 300 | pass: false, 301 | message: "Expected webpack to succeed, but it failed with:\n" + 302 | compilationErrors 303 | }; 304 | } 305 | 306 | let compilationWarnings = (stats.compilation.warnings || []).join('\n'); 307 | if (compilationErrors !== '') { 308 | return { 309 | pass: false, 310 | message: "Expected webpack to succeed, but it returned some warnings:\n" + 311 | compilationErrors 312 | }; 313 | } 314 | 315 | return { 316 | pass: true 317 | }; 318 | } 319 | }; 320 | } 321 | }; 322 | 323 | ////////////////////////// 324 | 325 | function cleanOutput(done) { 326 | rimraf(OUTPUT_DIR, done); 327 | } 328 | 329 | function testWebpack(webpackConfig, cb) { 330 | const defaultWebpackConfig = { 331 | context: FIXTURES_DIR, 332 | 333 | output: { 334 | path: OUTPUT_DIR, 335 | filename: "[name].bundle.js" 336 | }, 337 | 338 | resolve: { 339 | extensions: [".js", ".css", ".scss"] 340 | }, 341 | 342 | module: { 343 | rules: [{ 344 | test: /\.css$/, 345 | use: "css-loader" 346 | }] 347 | } 348 | }; 349 | const config = webpackMerge(defaultWebpackConfig, webpackConfig); 350 | 351 | return webpack(config, cb); 352 | } 353 | 354 | function testWebpackWithCssEntryPlugin(webpackConfig, cssEntryPluginConfig, cb) { 355 | const defaultCssEntryPluginConfig = { 356 | output: { 357 | filename: "[name].bundle.css" 358 | } 359 | }; 360 | 361 | if (!cb && typeof cssEntryPluginConfig === "function") { 362 | cb = cssEntryPluginConfig; 363 | cssEntryPluginConfig = defaultCssEntryPluginConfig; 364 | } 365 | 366 | const config = webpackMerge({ 367 | plugins: [ 368 | new CssEntryPlugin(cssEntryPluginConfig) 369 | ] 370 | }, webpackConfig); 371 | 372 | return testWebpack(config, cb); 373 | } 374 | 375 | function expectOutputFileExists(stats, outputFiles) { 376 | outputFiles = outputFiles || "main"; 377 | 378 | if (!Array.isArray(outputFiles)) { 379 | outputFiles = [outputFiles]; 380 | } 381 | 382 | for (let outputFile of outputFiles) { 383 | if (!outputFile.endsWith(".css")) { 384 | outputFile = outputFile + ".bundle.css"; 385 | } 386 | 387 | let outputFileExists = fs.existsSync(path.join(OUTPUT_DIR, outputFile)); 388 | expect(outputFileExists).toBe(true); 389 | } 390 | } 391 | 392 | function expectNoErrorsAndNoWarnings(err, stats) { 393 | expect(err).toBeFalsy(); 394 | 395 | let compilationErrors = (stats.compilation.errors || []).join('\n'); 396 | expect(compilationErrors).toBe(''); 397 | 398 | let compilationWarnings = (stats.compilation.warnings || []).join('\n'); 399 | expect(compilationWarnings).toBe(''); 400 | } 401 | 402 | function expectOutputFileToContain(stats, outputFile, strings) { 403 | if (Array.isArray(outputFile)) { 404 | strings = outputFile; 405 | outputFile = "main"; 406 | } 407 | 408 | if (!outputFile.endsWith(".css")) { 409 | outputFile = outputFile + ".bundle.css"; 410 | } 411 | 412 | let content = fs.readFileSync(path.join(OUTPUT_DIR, outputFile)); 413 | for (let str of strings) { 414 | expect(content).toContain(str); 415 | } 416 | } 417 | 418 | function expectHtmlOutputFileContent() { 419 | let htmlContent = fs.readFileSync(path.join(OUTPUT_DIR, "index.html")); 420 | return expect(htmlContent); 421 | } 422 | 423 | //// 424 | 425 | global.CssEntryPlugin = CssEntryPlugin; 426 | global.CssEntryPluginError = CssEntryPluginError; 427 | 428 | global.fixtures = fixtures; 429 | global.webpackTestFixture = webpackTestFixture; 430 | 431 | global.cleanOutput = cleanOutput; 432 | global.testWebpack = testWebpack; 433 | global.testWebpackWithCssEntryPlugin = testWebpackWithCssEntryPlugin; 434 | global.expectOutputFileExists = expectOutputFileExists; 435 | global.expectNoErrorsAndNoWarnings = expectNoErrorsAndNoWarnings; 436 | global.expectOutputFileToContain = expectOutputFileToContain; 437 | global.expectHtmlOutputFileContent = expectHtmlOutputFileContent; 438 | 439 | /*module.exports = { 440 | FIXTURES_DIR, 441 | OUTPUT_DIR, 442 | 443 | WebpackTestFixture, 444 | webpackTestFixture, 445 | fixtures, 446 | 447 | cleanOutput, 448 | testWebpack, 449 | testWebpackWithCssEntryPlugin, 450 | expectOutputFileExists, 451 | expectNoErrorsAndNoWarnings, 452 | expectOutputFileToContain, 453 | 454 | expectHtmlOutputFileContent 455 | };*/ 456 | -------------------------------------------------------------------------------- /spec/html-plugin-interop.spec.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | 3 | describe("Running CssEntryPlugin and HtmlWebpackPlugin", () => { 4 | beforeEach(done => { 5 | this.webpack = webpackTestFixture(jasmine) 6 | .withCssEntryPlugin() 7 | .cleanOutput(done); 8 | }); 9 | 10 | describe("with default options", () => { 11 | beforeEach(() => { 12 | this.webpack 13 | .config({ 14 | plugins: [ 15 | new HtmlWebpackPlugin() 16 | ] 17 | }); 18 | }); 19 | 20 | describe("configured with a shorthand single entry", () => { 21 | beforeEach(done => { 22 | this.webpack 23 | .config({ 24 | entry: fixtures.style1.path 25 | }) 26 | .run(done); 27 | }); 28 | beforeEach(() => expect(this.webpack).toSucceed()); 29 | 30 | it("generates a single css bundle", () => { 31 | expect(this.webpack).toOutput({ 32 | content: fixtures.style1.content 33 | }); 34 | }); 35 | 36 | it("generates a single html file with the link for the css bundle", () => { 37 | expect(this.webpack).toOutput({ 38 | file: "index.html", 39 | withContent: [ 40 | `` 41 | ], 42 | onlyOnce: true, 43 | 44 | withoutContent: [ 45 | `src="main.bundle.js"`, 46 | ` { 52 | expect(this.webpack).toOutput({ 53 | fileCount: 2 54 | }); 55 | }); 56 | }); 57 | 58 | describe("configured with two entries, both with one file each", () => { 59 | beforeEach(done => { 60 | this.webpack 61 | .config({ 62 | entry: { 63 | "test1": fixtures.style1.path, 64 | "test2": fixtures.style2.path 65 | } 66 | }) 67 | .run(done); 68 | }); 69 | beforeEach(() => expect(this.webpack).toSucceed()); 70 | 71 | it("generates two css bundles", () => { 72 | expect(this.webpack).toOutput({ 73 | entry: "test1", 74 | withContent: fixtures.style1.content 75 | }); 76 | 77 | expect(this.webpack).toOutput({ 78 | entry: "test2", 79 | withContent: fixtures.style2.content 80 | }); 81 | }); 82 | 83 | it("generates a single html file with the links for the css bundles", () => { 84 | expect(this.webpack).toOutput({ 85 | file: "index.html", 86 | withContent: [ 87 | ``, 88 | `` 89 | ], 90 | onlyOnce: true, 91 | 92 | withoutContent: [ 93 | ` { 99 | expect(this.webpack).toOutput({ 100 | fileCount: 3 101 | }); 102 | }); 103 | }); 104 | 105 | describe("with separate css and js entry points", () => { 106 | beforeEach(done => { 107 | this.webpack 108 | .config({ 109 | entry: { 110 | "test1": fixtures.style1.path, 111 | "test2": fixtures.script1.path 112 | } 113 | }) 114 | .run(done); 115 | }); 116 | beforeEach(() => expect(this.webpack).toSucceed()); 117 | 118 | it("generates one css bundle", () => { 119 | expect(this.webpack).toOutput({ 120 | entry: "test1", 121 | withContent: fixtures.style1.content 122 | }); 123 | }); 124 | 125 | it("generates one js bundle", () => { 126 | expect(this.webpack).toOutput({ 127 | file: "test2.bundle.js", 128 | withContent: fixtures.script1.content 129 | }); 130 | }); 131 | 132 | it("generates a single html file with the link for the css bundle and the script for the js bundle", () => { 133 | expect(this.webpack).toOutput({ 134 | file: "index.html", 135 | withContent: [ 136 | ``, 137 | `src="test2.bundle.js">` 138 | ], 139 | onlyOnce: true, 140 | 141 | withoutContent: [ 142 | `href="test2.bundle.js"`, 143 | `src="test1.bundle.css"` 144 | ] 145 | }); 146 | }); 147 | 148 | it("generates one css bundle, a js bundle and html only", () => { 149 | expect(this.webpack).toOutput({ 150 | fileCount: 3 151 | }); 152 | }); 153 | }); 154 | }); 155 | 156 | describe("with explicit chunks excluded", () => { 157 | beforeEach(() => { 158 | this.webpack 159 | .config({ 160 | plugins: [ 161 | new HtmlWebpackPlugin({ 162 | excludeChunks: ["test1"] 163 | }) 164 | ] 165 | }); 166 | }); 167 | 168 | describe("configured with a single entry", () => { 169 | beforeEach(done => { 170 | this.webpack 171 | .config({ 172 | entry: { 173 | "test1": fixtures.style1.path 174 | } 175 | }) 176 | .run(done); 177 | }); 178 | beforeEach(() => expect(this.webpack).toSucceed()); 179 | 180 | it("generates a single css bundle", () => { 181 | expect(this.webpack).toOutput({ 182 | entry: "test1", 183 | withContent: fixtures.style1.content 184 | }); 185 | }); 186 | 187 | it("generates a single html file without the link for the css bundle", () => { 188 | expect(this.webpack).toOutput({ 189 | file: "index.html", 190 | withoutContent: [ 191 | ` { 198 | expect(this.webpack).toOutput({ 199 | fileCount: 2 200 | }); 201 | }); 202 | }); 203 | 204 | describe("configured with two entries and one is excluded", () => { 205 | beforeEach(done => { 206 | this.webpack 207 | .config({ 208 | entry: { 209 | "test1": fixtures.style1.path, 210 | "test2": fixtures.style2.path 211 | } 212 | }) 213 | .run(done); 214 | }); 215 | beforeEach(() => expect(this.webpack).toSucceed()); 216 | 217 | it("generates two css bundles", () => { 218 | expect(this.webpack).toOutput({ 219 | entry: "test1", 220 | withContent: fixtures.style1.content 221 | }); 222 | 223 | expect(this.webpack).toOutput({ 224 | entry: "test2", 225 | withContent: fixtures.style2.content 226 | }); 227 | }); 228 | 229 | it("generates a single html file without the link for the excluded css bundle", () => { 230 | expect(this.webpack).toOutput({ 231 | file: "index.html", 232 | withContent: [ 233 | `` 234 | ], 235 | onlyOnce: true, 236 | 237 | withoutContent: [ 238 | `` 240 | ] 241 | }); 242 | }); 243 | 244 | it("generates the two css bundles and html only", () => { 245 | expect(this.webpack).toOutput({ 246 | fileCount: 3 247 | }); 248 | }); 249 | }); 250 | 251 | describe("with separate css and js entry points", () => { 252 | describe("with css excluded", () => { 253 | beforeEach(done => { 254 | this.webpack 255 | .config({ 256 | entry: { 257 | "test1": fixtures.style1.path, 258 | "test2": fixtures.script1.path 259 | } 260 | }) 261 | .run(done); 262 | }); 263 | beforeEach(() => expect(this.webpack).toSucceed()); 264 | 265 | it("generates one css bundle", () => { 266 | expect(this.webpack).toOutput({ 267 | entry: "test1", 268 | withContent: fixtures.style1.content 269 | }); 270 | }); 271 | 272 | it("generates one js bundle", () => { 273 | expect(this.webpack).toOutput({ 274 | file: "test2.bundle.js", 275 | withContent: fixtures.script1.content 276 | }); 277 | }); 278 | 279 | it("generates a single html file with only the script for the js bundle", () => { 280 | expect(this.webpack).toOutput({ 281 | file: "index.html", 282 | withContent: [ 283 | `src="test2.bundle.js">` 284 | ], 285 | onlyOnce: true, 286 | 287 | withoutContent: [ 288 | ` { 296 | expect(this.webpack).toOutput({ 297 | fileCount: 3 298 | }); 299 | }); 300 | }); 301 | 302 | describe("with js excluded", () => { 303 | beforeEach(done => { 304 | this.webpack 305 | .config({ 306 | entry: { 307 | "test1": fixtures.script1.path, 308 | "test2": fixtures.style1.path 309 | } 310 | }) 311 | .run(done); 312 | }); 313 | beforeEach(() => expect(this.webpack).toSucceed()); 314 | 315 | it("generates one css bundle", () => { 316 | expect(this.webpack).toOutput({ 317 | entry: "test2", 318 | withContent: fixtures.style1.content 319 | }); 320 | }); 321 | 322 | it("generates one js bundle", () => { 323 | expect(this.webpack).toOutput({ 324 | file: "test1.bundle.js", 325 | withContent: fixtures.script1.content 326 | }); 327 | }); 328 | 329 | it("generates a single html file with only the link for the css bundle", () => { 330 | expect(this.webpack).toOutput({ 331 | file: "index.html", 332 | withContent: [ 333 | `` 334 | ], 335 | onlyOnce: true, 336 | 337 | withoutContent: [ 338 | `href="test1.bundle.js"`, 339 | ` { 345 | expect(this.webpack).toOutput({ 346 | fileCount: 3 347 | }); 348 | }); 349 | }); 350 | }); 351 | }); 352 | 353 | describe("with explicit chunks included", () => { 354 | beforeEach(() => { 355 | this.webpack 356 | .config({ 357 | plugins: [ 358 | new HtmlWebpackPlugin({ 359 | chunks: ["test1"] 360 | }) 361 | ] 362 | }); 363 | }); 364 | 365 | describe("configured with two entries and one is included", () => { 366 | beforeEach(done => { 367 | this.webpack 368 | .config({ 369 | entry: { 370 | "test1": fixtures.style1.path, 371 | "test2": fixtures.style2.path 372 | } 373 | }) 374 | .run(done); 375 | }); 376 | beforeEach(() => expect(this.webpack).toSucceed()); 377 | 378 | it("generates two css bundles", () => { 379 | expect(this.webpack).toOutput({ 380 | entry: "test1", 381 | withContent: fixtures.style1.content 382 | }); 383 | 384 | expect(this.webpack).toOutput({ 385 | entry: "test2", 386 | withContent: fixtures.style2.content 387 | }); 388 | }); 389 | 390 | it("generates a single html file only with the link for the included css bundle", () => { 391 | expect(this.webpack).toOutput({ 392 | file: "index.html", 393 | withContent: [ 394 | `` 395 | ], 396 | onlyOnce: true, 397 | 398 | withoutContent: [ 399 | `` 401 | ] 402 | }); 403 | }); 404 | 405 | it("generates the two css bundles and html only", () => { 406 | expect(this.webpack).toOutput({ 407 | fileCount: 3 408 | }); 409 | }); 410 | }); 411 | }); 412 | }); 413 | --------------------------------------------------------------------------------