diff --git a/src/core/create_compilers/extract_css.ts b/src/core/create_compilers/extract_css.ts index 663abd9a5..e885154c9 100644 --- a/src/core/create_compilers/extract_css.ts +++ b/src/core/create_compilers/extract_css.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import hash from 'string-hash'; import * as codec from 'sourcemap-codec'; import { PageComponent, Dirs } from '../../interfaces'; -import { CompileResult } from './interfaces'; +import { CompileResult, Chunk } from './interfaces'; import { posixify } from '../../utils' const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,'; @@ -46,6 +46,65 @@ type SourceMap = { mappings: string; }; +function get_css_from_modules(modules: string[], css_map: Map, dirs: Dirs) { + const parts: string[] = []; + const mappings: number[][][] = []; + + const combined_map: SourceMap = { + version: 3, + file: null, + sources: [], + sourcesContent: [], + names: [], + mappings: null + }; + + modules.forEach(module => { + if (!/\.css$/.test(module)) return; + + const css = css_map.get(module); + + const { code, map } = extract_sourcemap(css, module); + + parts.push(code); + + if (map) { + const lines = codec.decode(map.mappings); + + if (combined_map.sources.length > 0 || combined_map.names.length > 0) { + lines.forEach(line => { + line.forEach(segment => { + // adjust source index + segment[1] += combined_map.sources.length; + + // adjust name index + if (segment[4]) segment[4] += combined_map.names.length; + }); + }); + } + + combined_map.sources.push(...map.sources); + combined_map.sourcesContent.push(...map.sourcesContent); + combined_map.names.push(...map.names); + + mappings.push(...lines); + } + }); + + if (parts.length > 0) { + combined_map.mappings = codec.encode(mappings); + + combined_map.sources = combined_map.sources.map(source => path.relative(`${dirs.dest}/client`, source)); + + return { + code: parts.join('\n'), + map: combined_map + }; + } + + return null; +} + export default function extract_css(client_result: CompileResult, components: PageComponent[], dirs: Dirs) { const result: { main: string | null; @@ -57,151 +116,94 @@ export default function extract_css(client_result: CompileResult, components: Pa if (!client_result.css_files) return; // Rollup-only for now - const unaccounted_for = new Set(); + let asset_dir = `${dirs.dest}/client`; + if (process.env.SAPPER_LEGACY_BUILD) asset_dir += '/legacy'; - const css_map = new Map(); - client_result.css_files.forEach(css => { - unaccounted_for.add(css.id); - css_map.set(css.id, css.code); - }); + const unclaimed = new Set(client_result.css_files.map(x => x.id)); - const chunk_map = new Map(); + const lookup = new Map(); client_result.chunks.forEach(chunk => { - chunk_map.set(chunk.file, chunk); + lookup.set(chunk.file, chunk); }); - const chunks_with_css = new Set(); - - // figure out which chunks belong to which components... - const component_owners = new Map(); - client_result.chunks.forEach(chunk => { - chunk.modules.forEach(module => { - const component = posixify(path.relative(dirs.routes, module)); - component_owners.set(component, chunk); - }); + const css_map = new Map(); + client_result.css_files.forEach(css_module => { + css_map.set(css_module.id, css_module.code); }); - const chunks_depended_upon_by_component = new Map(); - - // ...so we can figure out which chunks don't belong - components.forEach(component => { - const chunk = component_owners.get(component.file); - if (!chunk) { - // this should never happen! - throw new Error(`Could not find chunk that owns ${component.file}`); - } - - const chunks = new Set([chunk]); - chunks.forEach(chunk => { - chunk.imports.forEach((file: string) => { - const chunk = chunk_map.get(file); - if (chunk) chunks.add(chunk); - }); - }); - - chunks.forEach(chunk => { - chunk.modules.forEach((module: string) => { - unaccounted_for.delete(module); - }); - }); - - chunks_depended_upon_by_component.set( - component, - chunks - ); - }); + const chunks_with_css = new Set(); - function get_css_from_modules(modules: string[]) { - const parts: string[] = []; - const mappings: number[][][] = []; - - const combined_map: SourceMap = { - version: 3, - file: null, - sources: [], - sourcesContent: [], - names: [], - mappings: null - }; + // concatenate and emit CSS + client_result.chunks.forEach(chunk => { + const css_modules = chunk.modules.filter(m => css_map.has(m)); + if (!css_modules.length) return; - modules.forEach(module => { - if (!/\.css$/.test(module)) return; + const css = get_css_from_modules(css_modules, css_map, dirs); - const css = css_map.get(module); + const { code, map } = css; - const { code, map } = extract_sourcemap(css, module); + const output_file_name = chunk.file.replace(/\.js$/, '.css'); - parts.push(code); + map.file = output_file_name; + map.sources = map.sources.map(source => path.relative(`${asset_dir}`, source)); - if (map) { - const lines = codec.decode(map.mappings); + fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=./${output_file_name}.map */`); + fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' ')); - if (combined_map.sources.length > 0 || combined_map.names.length > 0) { - lines.forEach(line => { - line.forEach(segment => { - // adjust source index - segment[1] += combined_map.sources.length; + chunks_with_css.add(chunk); + }); - // adjust name index - if (segment[4]) segment[4] += combined_map.names.length; - }); - }); - } + const entry = path.resolve(dirs.src, 'client.js'); + const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1); - combined_map.sources.push(...map.sources); - combined_map.sourcesContent.push(...map.sourcesContent); - combined_map.names.push(...map.names); + const entry_chunk_dependencies: Set = new Set([entry_chunk]); + const entry_css_modules: string[] = []; - mappings.push(...lines); - } + // recursively find the chunks this component depends on + entry_chunk_dependencies.forEach(chunk => { + chunk.imports.forEach(file => { + entry_chunk_dependencies.add(lookup.get(file)); }); - if (parts.length > 0) { - combined_map.mappings = codec.encode(mappings); - - combined_map.sources = combined_map.sources.map(source => path.relative(`${dirs.dest}/client`, source)); - - return { - code: parts.join('\n'), - map: combined_map - }; + if (chunks_with_css.has(chunk)) { + chunk.modules.forEach(file => { + unclaimed.delete(file); + if (css_map.has(file)) { + entry_css_modules.push(file); + } + }); } + }); - return null; - } - - let asset_dir = `${dirs.dest}/client`; - if (process.env.SAPPER_LEGACY_BUILD) asset_dir += '/legacy'; - - const replacements = new Map(); - - chunks_depended_upon_by_component.forEach((chunks, component) => { - const chunks_with_css = Array.from(chunks).filter(chunk => { - const css = get_css_from_modules(chunk.modules); + // figure out which (css-having) chunks each component depends on + components.forEach(component => { + const resolved = path.resolve(dirs.routes, component.file); + const chunk: Chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(resolved) !== -1); - if (css) { - const { code, map } = css; + if (!chunk) { + // this should never happen! + throw new Error(`Could not find chunk that owns ${component.file}`); + } - const output_file_name = chunk.file.replace(/\.js$/, '.css'); + const chunk_dependencies: Set = new Set([chunk]); + const css_dependencies: string[] = []; - map.file = output_file_name; - map.sources = map.sources.map(source => path.relative(`${asset_dir}`, source)); + // recursively find the chunks this component depends on + chunk_dependencies.forEach(chunk => { + chunk.imports.forEach(file => { + chunk_dependencies.add(lookup.get(file)); + }); - fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=./${output_file_name}.map */`); - fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' ')); + if (chunks_with_css.has(chunk)) { + css_dependencies.push(chunk.file.replace(/\.js$/, '.css')); - return true; + chunk.modules.forEach(file => { + unclaimed.delete(file); + }); } }); - const files = chunks_with_css.map(chunk => chunk.file.replace(/\.js$/, '.css')); - - replacements.set( - component.file, - files - ); - - result.chunks[component.file] = files; + result.chunks[component.file] = css_dependencies; }); fs.readdirSync(asset_dir).forEach(file => { @@ -210,13 +212,17 @@ export default function extract_css(client_result: CompileResult, components: Pa const source = fs.readFileSync(`${asset_dir}/${file}`, 'utf-8'); const replaced = source.replace(/["']__SAPPER_CSS_PLACEHOLDER:(.+?)__["']/g, (m, route) => { - return JSON.stringify(replacements.get(route)); + return JSON.stringify(result.chunks[route]); }); fs.writeFileSync(`${asset_dir}/${file}`, replaced); }); - const leftover = get_css_from_modules(Array.from(unaccounted_for)); + unclaimed.forEach(file => { + entry_css_modules.push(css_map.get(file)); + }); + + const leftover = get_css_from_modules(entry_css_modules, css_map, dirs); if (leftover) { const { code, map } = leftover;