UNPKG

glov-build-concat

Version:
278 lines (267 loc) 9.11 kB
// TODO: If a proc function returns an error, we don't handle that well between // runs, probably messes up our internal out_array and other caching? const assert = require('assert'); const { asyncSeries, asyncEach } = require('glov-async'); const sourcemap = require('glov-build-sourcemap'); function nopProc(job, file, next) { next(null, file); } function sourcemapProc(job, file, next) { sourcemap.init(job, file, function (err, map, ignored, stripped) { if (err || !map) { return void next(err || `No sourcemap found for ${file.relative}`); } next(null, { contents: file.contents, code: stripped, map, }); }); } function outputSourcemap(job, opts, out_arr) { let { output } = opts; // Concatenate lines and sourcemaps let lines = []; let final_map = { mappings: [], sources: [], sourcesContent: [], }; let name_to_idx = Object.create(null); let names = []; for (let ii = 0; ii < out_arr.length; ++ii) { let file = out_arr[ii]; if (typeof file === 'string') { // preamble / postamble lines = lines.concat(file.split('\n')); continue; } let code = file.code; let new_lines = code.toString().split('\n'); let map = file.map; map = sourcemap.decode(map); // combine assert(final_map.mappings.length <= lines.length); while (final_map.mappings.length < lines.length) { final_map.mappings.push([]); // [[]] instead? } assert(map.sources); assert(map.sourcesContent); assert.equal(map.sources.length, map.sourcesContent.length); let start_source_idx = final_map.sources.length; final_map.sources = final_map.sources.concat(map.sources); final_map.sourcesContent = final_map.sourcesContent.concat(map.sourcesContent); for (let line_num = 0; line_num < map.mappings.length; ++line_num) { let line_map = map.mappings[line_num]; let out_line_map = []; for (let jj = 0; jj < line_map.length; ++jj) { let map_elem = line_map[jj]; if (map_elem.length <= 1) { // just output char offset, meaningless? pass it through out_line_map.push(map_elem); } else if (map_elem.length === 4 || map_elem.length === 5) { let elem = [ // mostly pass-through map_elem[0], map_elem[1] + start_source_idx, // source file index map_elem[2], map_elem[3], ]; if (map_elem.length === 5) { let name = map.names[map_elem[4]]; assert(name); let name_idx = name_to_idx[name]; if (name_idx === undefined) { name_idx = names.length; name_to_idx[name] = name_idx; names.push(name); } elem.push(name_idx); } out_line_map.push(elem); } else { assert(false); } } final_map.mappings.push(out_line_map); } lines = lines.concat(new_lines); } if (names.length) { final_map.names = names; } // Rename any duplicated sources, this avoids a bug on Chrome v102+ let seen_sources = {}; for (let ii = 0; ii < final_map.sources.length; ++ii) { let fn = final_map.sources[ii]; if (seen_sources[fn]) { seen_sources[fn]++; final_map.sources[ii] = `${fn}(${seen_sources[fn]})`; } else { seen_sources[fn] = 1; } } let sourcemap_params = { relative: output, contents: Buffer.from(lines.join('\n')), map: sourcemap.encode(output, final_map), }; if (typeof opts.sourcemap === 'object') { for (let key in opts.sourcemap) { sourcemap_params[key] = opts.sourcemap[key]; } } sourcemap.out(job, sourcemap_params); } module.exports = function concat(opts) { let { preamble, postamble, proc, output, key, comparator } = opts; proc = proc || nopProc; preamble = preamble || ''; postamble = postamble || ''; key = key || 'relative'; assert.equal(typeof output, 'string'); assert.equal(typeof proc, 'function'); assert.equal(typeof preamble, 'string'); assert.equal(typeof postamble, 'string'); assert.equal(typeof key, 'string'); const IDX = Symbol('idx'); function cmp(a, b) { if (a[key] !== b[key]) { return a[key] < b[key] ? -1 : 1; } if (a.priority !== undefined && b.priority !== undefined) { return a.priority > b.priority ? -1 : 1; } return 0; } comparator = comparator || cmp; let do_sourcemaps = Boolean(opts.sourcemap); if (do_sourcemaps) { assert(!opts.proc, 'sourcemap option not currently compatible with proc option'); // Could chain, if needed proc = sourcemapProc; } function concatFunc(job, done) { let updated_files = job.getFilesUpdated(); let user_data = job.getUserData(); user_data.file_map = user_data.file_map || {}; let list_change = 0; let updates = 0; asyncSeries([ function (next) { asyncEach(updated_files, function (f, next) { if (!f.contents) { if (user_data.file_map[f.relative]) { ++list_change; delete user_data.file_map[f.relative]; } return next(); } if (!job.isFileBase(f)) { return next(); } proc(job, f, function (err, outfile) { if (err) { return void next(err); } if (!outfile) { // skip it, remove output if it was previously output if (user_data.file_map[f.relative]) { ++list_change; delete user_data.file_map[f.relative]; } return next(); } assert(outfile); assert(outfile.contents); if (!outfile.relative) { outfile.relative = f.relative; } // Caller shouldn't be explicitly storing a different relative path assert.equal(outfile.relative, f.relative); if (Buffer.isBuffer(outfile.contents)) { let contents = outfile.contents.toString(); outfile = { ...outfile, contents, }; } assert.equal(typeof outfile.contents, 'string'); let existing = user_data.file_map[f.relative]; if (!existing) { ++list_change; user_data.file_map[f.relative] = outfile; } else { // Name not allowed to change, assume canonized from f.relative assert.equal(existing[key], outfile[key]); ++updates; user_data.file_map[f.relative] = outfile; if (existing[IDX] !== undefined) { outfile[IDX] = existing[IDX]; user_data.out_arr[existing[IDX]] = do_sourcemaps ? outfile : outfile.contents; } } next(); }); }, next); }, function (next) { job.log(`concat: ${list_change + updates} updates`); if (list_change || !user_data.out_arr) { // Rebuild array to be concatenated user_data.file_list = Object.values(user_data.file_map).sort(comparator); user_data.out_arr = []; if (preamble) { user_data.out_arr.push(preamble); } let prev_elem_key = {}; for (let ii = 0; ii < user_data.file_list.length; ++ii) { let elem = user_data.file_list[ii]; if (prev_elem_key === elem[key]) { // same key, we must be lower priority, skip } else { elem[IDX] = user_data.out_arr.length; user_data.out_arr.push(do_sourcemaps ? elem : elem.contents); prev_elem_key = elem[key]; } } if (postamble) { user_data.out_arr.push(postamble); } } if (key !== 'relative') { if (list_change || user_data.had_dup) { // Check for dups let files = user_data.file_list; user_data.had_dup = false; for (let ii = 0; ii < files.length - 1; ++ii) { let file = files[ii]; let next_file = files[ii + 1]; if (file[key] === next_file[key] && file.priority === next_file.priority) { job.error(`Two elements with the same key ${key}="${file[key]}"${file.priority !== undefined ? ` and same priority=${file.priority}` : ''}: ` + `${file.relative} and ${next_file.relative}`); user_data.had_dup = true; } } } } if (opts.sourcemap) { outputSourcemap(job, opts, user_data.out_arr); } else { job.out({ relative: output, contents: Buffer.from(user_data.out_arr.join('\n')), }); } next(); }, ], done); } return { type: 'all', func: concatFunc, version: [ outputSourcemap, opts, ...(opts.version || []), ], }; };