import * as jsdom from 'jsdom'; import * as assert from 'assert'; import * as glob from 'tiny-glob/sync.js'; import * as fs from 'fs'; import * as path from 'path'; import * as colors from 'kleur'; // for coverage purposes, we need to test source files, // but for sanity purposes, we need to test dist files export function loadSvelte(test) { process.env.TEST = test ? 'true' : ''; const resolved = require.resolve('../compiler.js'); delete require.cache[resolved]; return require(resolved); } export const svelte = loadSvelte(); export function exists(path) { try { fs.statSync(path); return true; } catch (err) { return false; } } export function tryToLoadJson(file) { try { return JSON.parse(fs.readFileSync(file)); } catch (err) { if (err.code !== 'ENOENT') throw err; return null; } } export function tryToReadFile(file) { try { return fs.readFileSync(file, 'utf-8'); } catch (err) { if (err.code !== 'ENOENT') throw err; return null; } } export const virtualConsole = new jsdom.VirtualConsole(); const { window } = new jsdom.JSDOM('
', {virtualConsole}); global.document = window.document; global.getComputedStyle = window.getComputedStyle; global.navigator = {userAgent: 'fake'}; export function env() { window._svelteTransitionManager = null; window.document.body.innerHTML = '
'; return window; } function cleanChildren(node) { let previous = null; // sort attributes const attributes = Array.from(node.attributes).sort((a, b) => { return a.name < b.name ? -1 : 1; }); attributes.forEach(attr => { node.removeAttribute(attr.name); }); attributes.forEach(attr => { node.setAttribute(attr.name, attr.value); }); // recurse [...node.childNodes].forEach(child => { if (child.nodeType === 3) { // text if ( node.namespaceURI === 'http://www.w3.org/2000/svg' && node.tagName !== 'text' && node.tagName !== 'tspan' ) { node.removeChild(child); } child.data = child.data.replace(/\s{2,}/g, '\n'); if (previous && previous.nodeType === 3) { previous.data += child.data; previous.data = previous.data.replace(/\s{2,}/g, '\n'); node.removeChild(child); child = previous; } } else { cleanChildren(child); } previous = child; }); // collapse whitespace if (node.firstChild && node.firstChild.nodeType === 3) { node.firstChild.data = node.firstChild.data.replace(/^\s+/, ''); if (!node.firstChild.data) node.removeChild(node.firstChild); } if (node.lastChild && node.lastChild.nodeType === 3) { node.lastChild.data = node.lastChild.data.replace(/\s+$/, ''); if (!node.lastChild.data) node.removeChild(node.lastChild); } } export function normalizeHtml(window, html) { try { const node = window.document.createElement('div'); node.innerHTML = html .replace(//g, '') .replace(/>[\s\r\n]+<') .trim(); cleanChildren(node, ''); return node.innerHTML.replace(/<\/?noscript\/?>/g, ''); } catch (err) { throw new Error(`Failed to normalize HTML:\n${html}`); } } export function setupHtmlEqual() { const window = env(); assert.htmlEqual = (actual, expected, message) => { assert.deepEqual( normalizeHtml(window, actual), normalizeHtml(window, expected), message ); }; } export function loadConfig(file) { try { const resolved = require.resolve(file); delete require.cache[resolved]; const config = require(resolved); return config.default || config; } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { return {}; } throw err; } } export function addLineNumbers(code) { return code .split('\n') .map((line, i) => { i = String(i + 1); while (i.length < 3) i = ` ${i}`; return ( colors.gray(` ${i}: `) + line.replace(/^\t+/, match => match.split('\t').join(' ')) ); }) .join('\n'); } function capitalise(str) { return str[0].toUpperCase() + str.slice(1); } export function showOutput(cwd, options = {}, compile = svelte.compile) { glob('**/*.html', { cwd }).forEach(file => { if (file[0] === '_') return; const name = path.basename(file) .slice(0, -path.extname(file).length) .replace(/^\d/, '_$&') .replace(/[^a-zA-Z0-9_$]/g, ''); const { js } = compile( fs.readFileSync(`${cwd}/${file}`, 'utf-8'), Object.assign(options, { filename: file, name: capitalise(name) }) ); console.log( // eslint-disable-line no-console `\n>> ${colors.cyan().bold(file)}\n${addLineNumbers(js.code)}\n<< ${colors.cyan().bold(file)}` ); }); } const start = /\n(\t+)/; export function deindent(strings, ...values) { const indentation = start.exec(strings[0])[1]; const pattern = new RegExp(`^${indentation}`, 'gm'); let result = strings[0].replace(start, '').replace(pattern, ''); let trailingIndentation = getTrailingIndentation(result); for (let i = 1; i < strings.length; i += 1) { let expression = values[i - 1]; const string = strings[i].replace(pattern, ''); if (Array.isArray(expression)) { expression = expression.length ? expression.join('\n') : null; } if (expression || expression === '') { const value = String(expression).replace( /\n/g, `\n${trailingIndentation}` ); result += value + string; } else { let c = result.length; while (/\s/.test(result[c - 1])) c -= 1; result = result.slice(0, c) + string; } trailingIndentation = getTrailingIndentation(result); } return result.trim().replace(/\t+$/gm, ''); } function getTrailingIndentation(str) { let i = str.length; while (str[i - 1] === ' ' || str[i - 1] === '\t') i -= 1; return str.slice(i, str.length); } export function spaces(i) { let result = ''; while (i--) result += ' '; return result; }