Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bundler): add a bundler layer #3638

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
181 changes: 88 additions & 93 deletions lib/middleware/karma.js
Expand Up @@ -59,7 +59,7 @@ function getXUACompatibleUrl (url) {
}

function createKarmaMiddleware (
filesPromise,
currentWebFiles,
serveStaticFile,
serveFile,
injector,
Expand Down Expand Up @@ -132,119 +132,114 @@ function createKarmaMiddleware (
const isRequestingDebugFile = requestUrl === '/debug.html'
const isRequestingClientContextFile = requestUrl === '/client_with_context.html'
if (isRequestingContextFile || isRequestingDebugFile || isRequestingClientContextFile) {
return filesPromise.then((files) => {
let fileServer
let requestedFileUrl
log.debug('custom files', customContextFile, customDebugFile, customClientContextFile)
if (isRequestingContextFile && customContextFile) {
log.debug(`Serving customContextFile ${customContextFile}`)
fileServer = serveFile
requestedFileUrl = customContextFile
} else if (isRequestingDebugFile && customDebugFile) {
log.debug(`Serving customDebugFile ${customDebugFile}`)
fileServer = serveFile
requestedFileUrl = customDebugFile
} else if (isRequestingClientContextFile && customClientContextFile) {
log.debug(`Serving customClientContextFile ${customClientContextFile}`)
fileServer = serveFile
requestedFileUrl = customClientContextFile
} else {
log.debug(`Serving static request ${requestUrl}`)
fileServer = serveStaticFile
requestedFileUrl = requestUrl
}

fileServer(requestedFileUrl, requestedRangeHeader, response, function (data) {
common.setNoCacheHeaders(response)

const scriptTags = []
for (const file of files.included) {
let filePath = file.path
const fileType = file.type || file.detectType()
let fileServer
let requestedFileUrl
log.debug('custom files', customContextFile, customDebugFile, customClientContextFile)
if (isRequestingContextFile && customContextFile) {
log.debug(`Serving customContextFile ${customContextFile}`)
fileServer = serveFile
requestedFileUrl = customContextFile
} else if (isRequestingDebugFile && customDebugFile) {
log.debug(`Serving customDebugFile ${customDebugFile}`)
fileServer = serveFile
requestedFileUrl = customDebugFile
} else if (isRequestingClientContextFile && customClientContextFile) {
log.debug(`Serving customClientContextFile ${customClientContextFile}`)
fileServer = serveFile
requestedFileUrl = customClientContextFile
} else {
log.debug(`Serving static request ${requestUrl}`)
fileServer = serveStaticFile
requestedFileUrl = requestUrl
}

if (!FILE_TYPES.includes(fileType)) {
if (file.type == null) {
log.warn(
'Unable to determine file type from the file extension, defaulting to js.\n' +
` To silence the warning specify a valid type for ${file.originalPath} in the configuration file.\n` +
' See http://karma-runner.github.io/latest/config/files.html'
)
} else {
log.warn(`Invalid file type (${file.type || 'empty string'}), defaulting to js.`)
}
fileServer(requestedFileUrl, requestedRangeHeader, response, function (data) {
common.setNoCacheHeaders(response)
const scriptTags = []
for (const file of currentWebFiles.included) {
let filePath = file.path
const fileType = file.type || file.detectType()

if (!FILE_TYPES.includes(fileType)) {
if (file.type == null) {
log.warn(
'Unable to determine file type from the file extension, defaulting to js.\n' +
` To silence the warning specify a valid type for ${file.originalPath} in the configuration file.\n` +
' See http://karma-runner.github.io/latest/config/files.html'
)
} else {
log.warn(`Invalid file type (${file.type || 'empty string'}), defaulting to js.`)
}
}

if (!file.isUrl) {
filePath = filePathToUrlPath(filePath, basePath, urlRoot, proxyPath)
if (!file.isUrl) {
filePath = filePathToUrlPath(filePath, basePath, urlRoot, proxyPath)

if (requestUrl === '/context.html') {
filePath += '?' + file.sha
}
if (requestUrl === '/context.html') {
filePath += '?' + file.sha
}
}

if (fileType === 'css') {
scriptTags.push(`<link type="text/css" href="${filePath}" rel="stylesheet">`)
} else if (fileType === 'dom') {
scriptTags.push(file.content)
} else if (fileType === 'html') {
scriptTags.push(`<link href="${filePath}" rel="import">`)
if (fileType === 'css') {
scriptTags.push(`<link type="text/css" href="${filePath}" rel="stylesheet">`)
} else if (fileType === 'dom') {
scriptTags.push(file.content)
} else if (fileType === 'html') {
scriptTags.push(`<link href="${filePath}" rel="import">`)
} else {
const scriptType = (SCRIPT_TYPE[fileType] || 'text/javascript')
const crossOriginAttribute = includeCrossOriginAttribute ? 'crossorigin="anonymous"' : ''
if (fileType === 'module') {
scriptTags.push(`<script onerror="throw 'Error loading ${filePath}'" type="${scriptType}" src="${filePath}" ${crossOriginAttribute}></script>`)
} else {
const scriptType = (SCRIPT_TYPE[fileType] || 'text/javascript')
const crossOriginAttribute = includeCrossOriginAttribute ? 'crossorigin="anonymous"' : ''
if (fileType === 'module') {
scriptTags.push(`<script onerror="throw 'Error loading ${filePath}'" type="${scriptType}" src="${filePath}" ${crossOriginAttribute}></script>`)
} else {
scriptTags.push(`<script type="${scriptType}" src="${filePath}" ${crossOriginAttribute}></script>`)
}
scriptTags.push(`<script type="${scriptType}" src="${filePath}" ${crossOriginAttribute}></script>`)
}
}
}

const scriptUrls = []
// For client_with_context, html elements are not added directly through an iframe.
// Instead, scriptTags is stored to window.__karma__.scriptUrls first. Later, the
// client will read window.__karma__.scriptUrls and dynamically add them to the DOM
// using DOMParser.
if (requestUrl === '/client_with_context.html') {
for (const script of scriptTags) {
scriptUrls.push(
// Escape characters with special roles (tags) in HTML. Open angle brackets are parsed as tags
// immediately, even if it is within double quotations in browsers
script.replace(/</g, '\\x3C').replace(/>/g, '\\x3E'))
}
const scriptUrls = []
// For client_with_context, html elements are not added directly through an iframe.
// Instead, scriptTags is stored to window.__karma__.scriptUrls first. Later, the
// client will read window.__karma__.scriptUrls and dynamically add them to the DOM
// using DOMParser.
if (requestUrl === '/client_with_context.html') {
for (const script of scriptTags) {
scriptUrls.push(
// Escape characters with special roles (tags) in HTML. Open angle brackets are parsed as tags
// immediately, even if it is within double quotations in browsers
script.replace(/</g, '\\x3C').replace(/>/g, '\\x3E'))
}
}

const mappings = data.includes('%MAPPINGS%') ? files.served.map((file) => {
const filePath = filePathToUrlPath(file.path, basePath, urlRoot, proxyPath)
.replace(/\\/g, '\\\\') // Windows paths contain backslashes and generate bad IDs if not escaped
.replace(/'/g, '\\\'') // Escape single quotes - double quotes should not be allowed!
const mappings = data.includes('%MAPPINGS%') ? currentWebFiles.served.map((file) => {
const filePath = filePathToUrlPath(file.path, basePath, urlRoot, proxyPath)
.replace(/\\/g, '\\\\') // Windows paths contain backslashes and generate bad IDs if not escaped
.replace(/'/g, '\\\'') // Escape single quotes - double quotes should not be allowed!

return ` '${filePath}': '${file.sha}'`
}) : []
return ` '${filePath}': '${file.sha}'`
}) : []

return data
.replace('%SCRIPTS%', scriptTags.join('\n'))
.replace('%CLIENT_CONFIG%', 'window.__karma__.config = ' + JSON.stringify(client) + ';\n')
.replace('%SCRIPT_URL_ARRAY%', 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n')
.replace('%MAPPINGS%', 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n')
.replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
})
return data
.replace('%SCRIPTS%', scriptTags.join('\n'))
.replace('%CLIENT_CONFIG%', 'window.__karma__.config = ' + JSON.stringify(client) + ';\n')
.replace('%SCRIPT_URL_ARRAY%', 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n')
.replace('%MAPPINGS%', 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n')
.replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
})
} else if (requestUrl === '/context.json') {
return filesPromise.then((files) => {
common.setNoCacheHeaders(response)
response.writeHead(200)
response.end(JSON.stringify({
files: files.included.map((file) => filePathToUrlPath(file.path + '?' + file.sha, basePath, urlRoot, proxyPath))
}))
})
common.setNoCacheHeaders(response)
response.writeHead(200)
response.end(JSON.stringify({
files: currentWebFiles.included.map((file) => filePathToUrlPath(file.path + '?' + file.sha, basePath, urlRoot, proxyPath))
}))
} else {
next()
}

return next()
}
}

createKarmaMiddleware.$inject = [
'filesPromise',
'currentWebFiles',
'serveStaticFile',
'serveFile',
'injector',
Expand Down
52 changes: 25 additions & 27 deletions lib/middleware/source_files.js
Expand Up @@ -18,7 +18,7 @@ function composeUrl (url, basePath, urlRoot) {
}

// Source Files middleware is responsible for serving all the source files under the test.
function createSourceFilesMiddleware (filesPromise, serveFile, basePath, urlRoot) {
function createSourceFilesMiddleware (currentWebFiles, serveFile, basePath, urlRoot) {
return function (request, response, next) {
const requestedFilePath = composeUrl(request.url, basePath, urlRoot)
// When a path contains HTML-encoded characters (e.g %2F used by Jenkins for branches with /)
Expand All @@ -29,39 +29,37 @@ function createSourceFilesMiddleware (filesPromise, serveFile, basePath, urlRoot
log.debug(`Requesting ${request.url}`)
log.debug(`Fetching ${requestedFilePath}`)

return filesPromise.then(function (files) {
// TODO(vojta): change served to be a map rather then an array
const file = findByPath(files.served, requestedFilePath) || findByPath(files.served, requestedFilePathUnescaped)
const rangeHeader = request.headers.range
// TODO(vojta): change served to be a map rather then an array
const file = findByPath(currentWebFiles.served, requestedFilePath) || findByPath(currentWebFiles.served, requestedFilePathUnescaped)
const rangeHeader = request.headers.range

if (file) {
const acceptEncodingHeader = request.headers['accept-encoding']
const matchedEncoding = Object.keys(file.encodings).find(
(encoding) => new RegExp(`(^|.*, ?)${encoding}(,|$)`).test(acceptEncodingHeader)
)
const content = file.encodings[matchedEncoding] || file.content
if (file) {
const acceptEncodingHeader = request.headers['accept-encoding']
const matchedEncoding = Object.keys(file.encodings).find(
(encoding) => new RegExp(`(^|.*, ?)${encoding}(,|$)`).test(acceptEncodingHeader)
)
const content = file.encodings[matchedEncoding] || file.content

serveFile(file.contentPath || file.path, rangeHeader, response, function () {
if (/\?\w+/.test(request.url)) {
common.setHeavyCacheHeaders(response) // files with timestamps - cache one year, rely on timestamps
} else {
common.setNoCacheHeaders(response) // without timestamps - no cache (debug)
}
if (matchedEncoding) {
response.setHeader('Content-Encoding', matchedEncoding)
}
}, content, file.doNotCache)
} else {
next()
}
serveFile(file.contentPath || file.path, rangeHeader, response, function () {
if (/\?\w+/.test(request.url)) {
common.setHeavyCacheHeaders(response) // files with timestamps - cache one year, rely on timestamps
} else {
common.setNoCacheHeaders(response) // without timestamps - no cache (debug)
}
if (matchedEncoding) {
response.setHeader('Content-Encoding', matchedEncoding)
}
}, content, file.doNotCache)
} else {
next()
}

request.resume()
})
request.resume()
}
}

createSourceFilesMiddleware.$inject = [
'filesPromise', 'serveFile', 'config.basePath', 'config.urlRoot'
'currentWebFiles', 'serveFile', 'config.basePath', 'config.urlRoot'
]

exports.create = createSourceFilesMiddleware
8 changes: 6 additions & 2 deletions lib/server.js
Expand Up @@ -21,6 +21,7 @@ const createServeFile = require('./web-server').createServeFile
const createServeStaticFile = require('./web-server').createServeStaticFile
const createFilesPromise = require('./web-server').createFilesPromise
const createWebServer = require('./web-server').createWebServer
const createWebFiles = require('./web-files').createWebFiles
const preprocessor = require('./preprocessor')
const Launcher = require('./launcher').Launcher
const FileList = require('./file-list')
Expand Down Expand Up @@ -90,7 +91,10 @@ class Server extends KarmaEventEmitter {
webServer: ['factory', createWebServer],
serveFile: ['factory', createServeFile],
serveStaticFile: ['factory', createServeStaticFile],
// Obsolete, do not use
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can display a warning when people use this?

filesPromise: ['factory', createFilesPromise],
// The files to be included/served to browser for test
currentWebFiles: ['factory', createWebFiles],
socketServer: ['factory', createSocketIoServer],
executor: ['factory', Executor.factory],
// TODO: Deprecated. Remove in the next major
Expand Down Expand Up @@ -351,8 +355,8 @@ class Server extends KarmaEventEmitter {
}

if (config.autoWatch) {
this.on('file_list_modified', () => {
this.log.debug('List of files has changed, trying to execute')
this.on('web_files_modified', () => {
this.log.debug('List of included/served files has changed, trying to execute')
if (config.restartOnFileChange) {
socketServer.sockets.emit('stop')
}
Expand Down
45 changes: 45 additions & 0 deletions lib/web-files.js
@@ -0,0 +1,45 @@
/**
Container class for the files included in context.html and served upon
request. Two instances fit into the files pipeline:
1. The result from FileList.files, the list of preprocessed input files.
2. The result of bundling #1 stored in currentWebFiles
*/
class WebFiles {
constructor (included, served) {
this.included = [...included]
this.served = [...served]
}
}

let currentWebFiles = new WebFiles([], [])

function noOpBundler (webFiles) {
return new WebFiles(webFiles.included, webFiles.served)
}

let bundler = noOpBundler

function registerBundler (emitter) {
emitter.on('file_list_modified', (modifiedWebFiles) => {
// Any bundler is expected to return a WebFiles object.
// For sourcemap support, //# sourceUrl in the bundler result should
// point back to the input files.
currentWebFiles = Object.assign(currentWebFiles, bundler(modifiedWebFiles))
emitter.emit('web_files_modified', currentWebFiles)
})
}

function createWebFiles (configBundler, instantiatePlugin, emitter) {
if (configBundler) {
bundler = instantiatePlugin('bundler', configBundler)
}
registerBundler(emitter)
return currentWebFiles
}

createWebFiles.$inject = ['config.bundler', 'instantiatePlugin', 'emitter']

module.exports = {
createWebFiles,
WebFiles
}
1 change: 1 addition & 0 deletions lib/web-server.js
Expand Up @@ -36,6 +36,7 @@ function createCustomHandler (customFileHandlers, config) {
createCustomHandler.$inject = ['customFileHandlers', 'config']

function createFilesPromise (emitter, fileList) {
log.info('DEPRECATED filesPromise is not longer used by karma core.')
// Set an empty list of files to avoid race issues with
// file_list_modified not having been emitted yet
let files = fileList.files
Expand Down