Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

include css depended upon by entry point, even if also depended on by a lazily-loaded component #471

Merged
merged 2 commits into from
Oct 16, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 125 additions & 119 deletions src/core/create_compilers/extract_css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,';
Expand Down Expand Up @@ -46,6 +46,65 @@ type SourceMap = {
mappings: string;
};

function get_css_from_modules(modules: string[], css_map: Map<string, string>, 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;
Expand All @@ -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<Chunk> = 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<Chunk> = 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 => {
Expand All @@ -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;

Expand Down