UNPKG

neo-builder

Version:

the fastest tiny script packager written in javascript and supporting iife dynamic chaining w/o extra runtime

1,252 lines (1,011 loc) 87.4 kB
//@ts-check const fs = require("fs"); const path = require("path"); const { deepMergeMap, genfileStoreName, findPackagePath, findMainfile, findProjectRoot, fileNameRefine, refineExtension, readDir, isSymbolLink } = require("./utils"); // const { benchmarkFunc, benchStore, commitMark$: $_commitMark } = require("./utils/benchmarks"); const { AbstractImporter } = require("./utils/declarations$"); const { commonjsExportsApply } = require("./utils/exports$"); const { chainingCall, conditionalChain } = require("./utils/monadutils"); const { releaseProcess, cleaningDebugBlocks } = require("./utils/release$"); const { violentShake: forceTreeShake, theShaker } = require("./utils/tree-shaking"); const { version, statHolder } = require("./utils/_versions"); // const { performance } = require('perf_hooks'); // const regex = /^import (((\{([\w, ]+)\})|([\w, ]+)|(\* as \w+)) from )?".\/([\w\-\/]+)"/gm; // const regex = /^import (((\{([\w, ]+)\})|([\w, ]+)|(\* as \w+)) from )?\".\/([\w\-\/]+)\"/gm; // const regex = /^import (((\{([\w, ]+)\})|([\w, ]+)|(\* as \w+)) from )?\"(.\/)?([@\w\-\/]+)\"/gm; // @ + (./) // const regex = /^import (((\{([\w, \$]+)\})|([\w, ]+)|(\* as [\w\$]+)) from )?["'](.?.\/)?([@\w\-\/\.]+)["']/gm; // '" // const regex = /^import (((\{([\w,\s\$]+)\})|([\w, ]+)|(\* as [\w\$]+)) from )?["'](.?.\/)?([@\w\-\/\.]+)["'];?/gm; // '" const namedImportsExpRegex = /^import ((((?<_D>\w+, )?\{([\w,\s\$]+)\})|([\w, ]+)|(\* as [\w\$]+)) from )?["'](.?.\/)?([@\w\-\/\.]+)["'];?/gm; // '" // in the regex possible bug is if the `default` will be placed after named imports (`import {a, b}, d from "a"`)(to fix) // const { encodeLine, decodeLine } = require("./__map"); /** * @typedef {{ * root?: string; * _needMap?: boolean | 1; * extract: { * names?: string[], * default?: string * }, * isEsm?: boolean * }} SealingOptions * * /// UNDER QUESTION: * onTreeShake?: (skiped?: boolean) => void */ /** * @type {{ * sameAsImport: 'as esm import', * doNothing?: 'do nothing' * }} * * inlineTo?: 'inline to script', * applyAndInline?: 'apply and inline', */ const requireOptions = { sameAsImport: 'as esm import', // default for all node_modules // asDynamic: 'as dynamic w await import', // not inside node_modules/ doNothing: 'do nothing' } const fastShaker = {} // /** // * @type {{ // * ModuleNotFound: { // * doNothing: 0, // * useDefaultHandler: 1, // * raiseError: 2 // * } // * }} // */ // export const OnErrorActions = { // ModuleNotFound: { // doNothing: 0, // useDefaultHandler: 1, // raiseError: 2 // } // } /** * @typedef {[number, number, number, number, number?]} VArray * @typedef {import("fs").PathOrFileDescriptor} PathOrFileDescriptor */ let startWrapLinesOffset = 1; let endWrapLinesOffset = 5; var rootOffset = 0; /** * @description expoerted files for uniqie control inside getContent * @type {string[]} */ var exportedFiles = []; let logLinesOption = false; let incrementalOption = false; /** * @type {Importer} */ let importer = null; // exports = { // default: combine, // build: combine, // combine: combine, // integrate, // } /** * @description preapare (remove lazy, prepare options) and build content under rootPath and as per options (applyes importInserts into content) * @param {string} content - source code content; * @param {string} rootPath - path to root of source directory name (required for sourcemaps etc) * @param {BuildOptions & {targetFname?: string}} options - options * @param {Function?} [onSourceMap=null] - onSourceMap * @return {string} code with imported involves */ function combineContent(content, rootPath, options, onSourceMap) { globalOptions = options; globalOptions.advanced?.treeShake && (theShaker.globalOptions = globalOptions); globalOptions.target = options.targetFname; const originContent = content; /// initial global options: rootOffset = 0; sourcemaps.splice(0, sourcemaps.length); Object.keys(modules).forEach(key => delete modules[key]); logLinesOption = options.logStub; incrementalOption = options.advanced ? options.advanced.incremental : false; if (incrementalOption) { // look up startWrapLinesOffset = 3; // start_WrapLinesOffset + 2 endWrapLinesOffset = 8; // end_WrapLinesOffset + 3 } exportedFiles = [] if (options.purgeDebug) { if (options.sourceMaps || options.getSourceMap) { console.warn('\x1B[33m' + 'removeLazy option uncompatible with sourceMap generation now. Therefore it`s passed' + '\x1B[0m'); options.sourceMaps = null; options.getSourceMap = null; } content = cleaningDebugBlocks(content) } content = importInsert(content, rootPath, options); content = mapGenerate({ target: options.targetFname, options, originContent, content, // cachedMap: mapping }); // here plugins if (options.advanced && options.advanced.ts) { // exportedFiles.some(w => w.endsWith('.ts') || w.endsWith('.tsx')) // sourcemaps for ts is not supported now content = options.advanced.ts(content) } console.log(`\n\x1b[34mIn total handled ${statHolder.importsAmount} imports\x1b[0m`); globalOptions.advanced?.debug && console.log(`\x1b[34m- ${statHolder.exports.cjs} cjs exports is found\x1b[0m`); statHolder.imports = 0; statHolder.requires = 0; statHolder.exports.cjs = 0; return content; } /** * * @param {string} entrypoint - file name * @param {string} target - target name * @param {Omit<BuildOptions, 'entryPoint'> & {entryPoint?: string}} options - options * @returns */ function buildFile(entrypoint, target, options) { const timeSure = "File \x1B[32m\"" + target + "\"\x1B[33m built in" console.time(timeSure) const originContent = fs.readFileSync(entrypoint).toString(); const srcFileName = path.resolve(entrypoint); const targetFname = target || path.parse(srcFileName).dir + path.sep + path.parse(srcFileName).name + '.js'; const buildOptions = Object.assign( { entryPoint: path.basename(srcFileName), release: false, targetFname }, options ); try { var legacyFiles = fs.readdirSync ? fs.readdirSync(path.dirname(buildOptions['targetFname'])) : null; } catch (er) { console.warn(`Target dir "${buildOptions['targetFname']}" does not exists. It'll be autocreated.`); fs.mkdirSync(path.dirname(buildOptions['targetFname'])) } // let mapping = null; let content = combineContent(originContent, path.dirname(srcFileName), buildOptions // function onSourceMap() { // // sourcemaps adds to content with targetName // mapping = sourcemaps.map(s => s.debugInfo).reduce((p, n) => p.concat(n)); // mapping.push(null); // \n//# sourceMappingURL=${path.basename(to)}.map` // return mapping; // } ) // content = mapGenerate({ // target: targetFname, // options, // originContent, // content, // cachedMap: mapping // }); if (legacyFiles && statHolder.rebuilds === 0) { legacyFiles.forEach(file => (path.extname(file) == '.js') && fs.rmSync(path.join(path.dirname(targetFname), file))); } fs.writeFileSync(targetFname, content) statHolder.rebuilds++; console.log('\x1B[33m'); console.timeEnd(timeSure) console.log('\x1B[0m'); // console.log(benchStore.toString()) // console.table(benchStore) // console.table(Object.fromEntries(Object.entries(benchStore).filter(([k, v]) => !k.startsWith('bound')))) // console.table(Object.fromEntries(Object.entries(benchStore).reverse())) return content } /** * path manager */ class PathMan { /** * used for static imports inside dynamic imports (TODO check it (on purp perf optimization): why not startsWith condition applied for this in getContext?) * @legacy * @type {string} */ basePath /** * @type {Importer?} */ importer /* * @description keep links (on symlinks) to modules * @TODO use instead of importer.linkedModulePaths */ linkedModules = [] /** * @param {string} dirname * @param { (fileName: PathOrFileDescriptor) => string} pullContent */ constructor(dirname, pullContent) { /** * root directory of source code (not project path. it's different) */ this.dirPath = dirname; /** * */ this.getContent = pullContent || getContent; } } class Importer extends AbstractImporter { /** * @type {PathMan} */ pathMan /** * * @param {PathMan} pathMan */ constructor(pathMan) { super() // this.namedImportsApply = applyNamedImports; this.namedImportsApply = namedImportsApply; /* * module sealing () */ this.moduleStamp = moduleSealing; this.pathMan = pathMan; this.isFastShaking = typeof globalOptions.advanced?.treeShake === 'object' && globalOptions.advanced?.treeShake.method == 'surface'; pathMan.importer = this; } /** * @description call moduleSealing and generate sourcemaps for it * @returns {boolean} * @param {string} fileName * @param {string} fileStoreName, * @param {SealingOptions} args */ attachModule(fileName, fileStoreName, { root, _needMap, extract }) { this.progressFilesStack.push(fileName); let moduleInfo = this.moduleStamp(fileName, { root: root || undefined, _needMap, extract }); this.progressFilesStack.pop(); if (moduleInfo) { // .slice(moduleInfo.wrapperLinesOffset) =>? .slice(moduleInfo.wrapperLinesOffset, -5?) -> inside moduleSealing const linesMap = moduleInfo.lines.map(([moduleInfoLineNumber, isEmpty], /** @type {number} */ i) => { /** номер столбца в сгенерированном файле (#2); индекс исходника в «sources» (#3); номер строки исходника (#4); номер столбца исходника (#5); индекс имени переменной/функции из списка «names»; */ /** * @type {string|unknown} * TODO check type (string or boolean) * */ let lineValue = isEmpty; if (i >= (moduleInfo.lines.length - endWrapLinesOffset) || i < startWrapLinesOffset) { return null; } /** @type {VArray | Array<VArray>} */ let r = _needMap === 1 ? [].map.call(lineValue, (/** @type {any} */ ch, /** @type {any} */ i) => [i, (sourcemaps.length - 1) + 1, moduleInfoLineNumber - startWrapLinesOffset, i]) // i + 1 : [[0, (sourcemaps.length - 1) + 1, moduleInfoLineNumber - startWrapLinesOffset, 1]]; return r; }); sourcemaps.push({ name: fileStoreName.replace('$$', '@').replace(/(\$|__)/g, '/') + '.js', // mappings: linesMap.map(line => line ? encodeLine(line) : '').join(';'), //@ts-ignore (TODO fix type) debugInfo: linesMap }); return true; } return false; } /** * * @param {SealingOptions} options * @param {(name: string) => boolean} inspectUnique * @returns */ generateConverter(options, inspectUnique) { const { root, _needMap, extract } = options; // TODO fix `import pTimeout, { TimeoutError } from 'p-timeout'` return (match, __, $, $$, _defauName, /** @type {string} */ classNames, defauName, moduleName, isrelative, fileName, offset, source) => { if (!options.isEsm) options.isEsm = true; if (_defauName) { defauName = _defauName.match(/[\w_\d\$]+/)[0]; } statHolder.imports += 1; let rawNamedImports = classNames?.split(','); if (classNames && globalOptions.advanced?.treeShake && extract?.names) { //TODO insert before this the first algorithm to remove unused export const name = {} ... (cause of export may not used) /// tree shakes expressions generated from `export * from '...'` (just for named) // - ((removes if import is unused for export and at source code -> remove all import at all)) // - does not tree shake (does not remove) nothing at all if at least one of the imports is used inside code (usefull just for reexport how it have said) const namesRequired = new Set(extract.names); var _imports = rawNamedImports?.map(w => w.trim().split(' as ')); const _exports = _imports.map(names => names.slice().pop()); var requiredExports = _exports.filter(name => { if (namesRequired.has(name)) return true else { // check on using: // const _matched = source.replace(match, '').match(new RegExp(`\\b${name}\\b`), ''); // confuse: Uppy is found in filename import before const _matched = source.slice(offset + match.length).match(new RegExp(`\\b${name}\\b`), ''); if (_matched) { // debugger return true; } else { rawNamedImports = rawNamedImports.filter(named => !named.trimEnd().endsWith(name)) // to content: ; _imports = rawNamedImports?.map(w => w.trim().split(' as ')) // to nested extract return false; } } }); if (!requiredExports.length && globalOptions.advanced?.treeShake) { return `// ==> "${fileName}" has shaken` } } const fileStoreName = this.attachFile(fileName, isrelative, { extract: { names: _imports?.map(names => names.slice()[0]) || rawNamedImports?.map(w => w.trim().split(' ')[0]), default: defauName }, root, _needMap }); /// replace imports to spreads into place: if (defauName && inspectUnique(defauName)) { return `const { default: ${defauName} } = $${fileStoreName.replace('@', '_')}Exports;`; } else if (defauName) { const error = new Error(`Variable '${defauName}' is duplicated by import './${fileName}.js'`); error.name = 'DublicateError'; // throw error; // console.log('\x1b[31m%s\x1b[0m', `${error.name}: ${error.message}`, '\x1b[0m'); console.log('\x1b[31m%s\x1b[0m', `Detected ${error.name} during build process: ${error.message}`, '\x1b[0m'); console.log('Fix the errors and restart the build.'); process.exit(1); } else if (moduleName) { return `const ${moduleName.split(' ').pop()} = $${fileStoreName.replace('@', '_')}Exports;`; } else { // TODO optimize: let entities = rawNamedImports.map(w => { if (~w.indexOf(' as ')) { const importStruct = w.trim().split(' ') // var _impexp = (`${importStruct.pop()}: ${importStruct[0]}`) var _impexp = (`${importStruct[0]}: ${importStruct.pop()}`) return _impexp.trim() } return w; }); for (let entity of entities) { if (~entity.indexOf(':')) { entity = entity.split(': ').pop(); } inspectUnique(entity); } return `const { ${entities.join(', ')} } = $${fileStoreName.replace('@', '_')}Exports`; } }; } /** * @param {string} fileName * @param {string} isrelative * @param {SealingOptions} params */ attachFile(fileName, isrelative, { root, _needMap, extract} ) { const _filename = path.extname(fileName) ? fileName.slice(0, -path.extname(fileName).length) // : fileName.replace(/\.\.\//g, '') : fileName; // .replace(/\.\.\//g, './') // const _root = root && chainingCall( // path.dirname, // (fileName.match(/\.\.\//g)?.length || 0) + +(isrelative.length == 3), // // root.split('/').filter(w => w !== '.').join('/').replace(/\/\.\//g, '/') // root.replace(/\/\.\/?/g, '/') // // root.replace(/\/\.\//g, '/') // ) const fileStoreName = genfileStoreName( // root, fileName isrelative ? nodeModules[fileName] ? undefined : root : undefined, (isrelative || '') + _filename ); const self = this; // if (~fileName.indexOf('debounce')) { // debugger // /** // */ // } /// check module on unique and inject it if does not exists: if (!modules[fileStoreName]) { // const _fileName = (root || '.') + '/' + fileName; moduleSeal(extract); } else if (fileStoreName in theShaker.shakedStore) { const treeShakedModule = theShaker.shakedStore[fileStoreName]; // shaked which are in the current (new) extracts const missedRequiring = treeShakedModule.shaked.filter(w => ~extract?.names.indexOf(w)) if (missedRequiring.length) { // delete modules[fileStoreName]; moduleSeal({ default: extract.default, names: missedRequiring.concat(treeShakedModule.extracted) }); } } // TODO? may be check (possible bug) and optimize this: else if (globalOptions.advanced?.treeShake) { // (this.isFastShaking) ? // in the surface case equate with extract with _extracts const missed = extract?.names?.filter(ex => new Set(fastShaker[fileStoreName]).has(ex)) if (missed?.length) { modules[fileStoreName] = modules[fileStoreName].replace(/exports = \{([\w\d_\$, :]+?)\}/, `exports = { ${missed},$1}`) // globalOptions.verbose && console.log(`\x1B[90m>> \x1B[36m"${_filename}" exports reshaked (${missed})\x1B[0m`) } } else { // debugger } return fileStoreName; function moduleSeal(_extractedNames) { if (isrelative) { const smSuccessAttached = self.attachModule((isrelative || '') + fileName, fileStoreName, { root, _needMap, extract: _extractedNames }); } else { // node modules support if (self.pathMan.getContent == getContent) { nodeModulesPath = nodeModulesPath || findProjectRoot(self.pathMan.dirPath, globalOptions); // or get from cwd if (!fs.existsSync(nodeModulesPath)) { debugger; console.warn('node_modules doesn`t exists. Use $onModuleNotFound method to autoinstall'); } else { const packageName = path.normalize(fileName); let relInsidePathname = self.getMainFile(packageName); // relInsidePathname = self.extractLinkTarget(fileName, relInsidePathname); // nodeModules[fileName] = path.join(packagePath, relInsidePathname); nodeModules[fileName] = relInsidePathname; self.attachModule(fileName, fileStoreName, { // root, // root: '', root: fileName + '/' + path.dirname(relInsidePathname), _needMap, extract: _extractedNames }); } } } } } /** * @description read main/export section from package.json * @param {string} packageName * @returns */ getMainFile(packageName) { // let start = performance.now() let packagePath = path.join(nodeModulesPath, packageName); const packageJson = path.join(packagePath, 'package.json'); // direct import from node_modules (invisaged with-in moduleSealing-&-getContext logic) | import specified in `exports` section /** * @description - always specified to a file! * @type {string|undefined} */ let relInsidePathname = ''; // - but what is the base of the file for the next rel. import from its file? // -- direct import from the module: => get dirname of the file // -- from export: read exports or => get as base of the main file if (fs.existsSync(packageJson)) { relInsidePathname = findMainfile(packageJson); } else if (!path.extname(packageName)) { const packdirsBranch = packageName.split(/[\/\\]/); const rootConfigPath = path.join(nodeModulesPath, packdirsBranch[0], 'package.json') if (fs.existsSync(rootConfigPath)) { // if (~benchmarkFunc(readDir, path.join(nodeModulesPath, packdirsBranch[0])).indexOf('package.json')) { const rootConfig = fs.readFileSync(rootConfigPath).toString() const config = JSON.parse(rootConfig) if (config.exports) { const exportsConfig = config.exports['./' + packageName.split(/[\/\\]/).slice(1).join('/')]; const indexFile = exportsConfig.import || exportsConfig.default || exportsConfig.require || exportsConfig return indexFile.replace('./' + packdirsBranch.slice(1).join('/'), '.') } else { return '' } } else { // debugger } } // $_commitMark(start, 'getMainFile_'); return relInsidePathname; } genChunkName(filename) { // return '$_' + path.basename(filename) + '_' + version + '.js'; const endTrimedFileName = filename.replace(/(\$\{[\w\d_]+\})[\d\w\/-_\.]*/, '$1') return '$_' + path.basename(endTrimedFileName) + '_' + version + '.js'; } /** * @legacy {looking for onSymLink callback inside getContent} * @param {string} fileName * @param {string} relInsidePathname */ extractLinkTarget(fileName, relInsidePathname) { const isSymbolLink = fs.lstatSync(path.join(nodeModulesPath, fileName)).isSymbolicLink(); if (isSymbolLink) { const symbolLink = path.relative(nodeModulesPath, fs.readlinkSync(path.join(nodeModulesPath, fileName))); console.log(symbolLink); // debugger; relInsidePathname = path.join(symbolLink, relInsidePathname); } return relInsidePathname; } joinAllContents(content, options) { const moduleContents = Object.values(modules).filter(Boolean); content = '\n\n//@modules:\n\n\n' + moduleContents.join('\n\n') + `\n\n\n//@${options.entryPoint}: \n` + content; return content; } // /** // * @_param {{ // fileName: string; // fileStoreName: string; // attach_Module: (fileName: string, fileStoreName: string) => boolean; // }} args // * @param {string} fileName // * @param {string} fileStoreName // * @param {(fileName: string, fileStoreName: string) => boolean} attach_Module // */ // attachFile(fileName, fileStoreName, attach_Module) { // // this.currentFile = fileName; // return attach_Module(fileName, fileStoreName); // } } /** * @param {{ * options?: Omit<BuildOptions, "entryPoint"> & { entryPoint?: string; }; * target?: string; originContent?: string; * content?: string; * sourceMaps?: any; * cachedMap?: Array<Array<VArray | null>> * }} options */ function mapGenerate({ options, content, originContent, target, cachedMap }) { let pluginsPerformed = false; if (options.getSourceMap || options.sourceMaps) { /** * @type {string[]} */ const moduleContents = Object.values(modules).filter(Boolean); // let mapping = sourcemaps.reduce((acc, s) => acc + ';' + s.mappings, '').slice(1) + ';' // let accumDebugInfo = sourcemaps.reduce((p, n) => p.debugInfo.concat(n.debugInfo)); /** * @_type {Array<Array<VArray | null>} */ let accumDebugInfo = cachedMap || sourcemaps.map(s => s.debugInfo).reduce((p, n) => p.concat(n)); !cachedMap && accumDebugInfo.push(null); // \n//# sourceMappingURL=${path.basename(to)}.map` if (options.getSourceMap) { const modifiedMap = options.getSourceMap({ //@ts-expect-error mapping: accumDebugInfo, sourcesContent: moduleContents.map(c => c.split('\n').slice(startWrapLinesOffset, -endWrapLinesOffset).join('\n')).concat([originContent]), files: sourcemaps.map(s => s.name) }); // if (modifiedMap) accumDebugInfo = modifiedMap; } if (options.sourceMaps) { // const mapping = accumDebugInfo.map(line => line ? encodeLine(line) + ',' + encodeLine([7, line[1], line[2], 7]) : '').join(';') // const mapping = accumDebugInfo.map(line => line ? encodeLine(line) : '').join(';') // let mapping1 = accumDebugInfo.map(line => line ? line.map(c => encodeLine(c)).join(',') : '').join(';') let rawMapping = accumDebugInfo.map((/** @type {any} */ line) => line ? line : []); if (options.sourceMaps.shift) rawMapping = Array(options.sourceMaps.shift).fill([]).concat(rawMapping) let mapping = options.sourceMaps.encode(rawMapping); const targetFile = (path && target) ? path.basename(target) : '' const mapObject = { version: 3, file: targetFile, sources: sourcemaps.map(s => s.name), // TODO fix sourcemaps for dynamic tests sourcesContent: moduleContents.map(c => c.split('\n').slice(startWrapLinesOffset, -endWrapLinesOffset).join('\n')).concat([originContent]), names: [], mappings: mapping }; /// TODO move to external (to getSourceMap) - DONE if (options.sourceMaps.injectTo) { // let rootMappings = injectMap(options.sourceMaps.injectTo, mapObject); // //_ts-expect-error // mapObject.mappings = options.sourceMaps.encode(handledDataMap.concat(rootMappings)) /// As checked alternative: const rootMaps = options.sourceMaps.injectTo; // TODO decode case like injectMap const { mergedMap, outsideMapInfo } = deepMergeMap({ ...mapObject, files: mapObject.sources, mapping: rawMapping }, { outsideMapInfo: rootMaps, outsideMapping: rootMaps.maps || globalOptions.sourceMaps.decode(rootMaps.mappings) }) outsideMapInfo.mappings = options.sourceMaps.encode(rawMapping = mergedMap); mapObject.sources = outsideMapInfo.sources; mapObject.sourcesContent = outsideMapInfo.sourcesContent; } if (options.plugins) (pluginsPerformed = true) && options.plugins.forEach(plugin => { if (plugin.bundle) { content = plugin.bundle(content, { target, maps: mapObject, rawMap: rawMapping }); } }) if (options.sourceMaps.verbose) console.log(mapObject.sources, mapObject.sourcesContent, rawMapping); if (fs && options.sourceMaps.external === true) { fs.writeFileSync(target + '.map', JSON.stringify(mapObject)); content += `\n//# sourceMappingURL=${targetFile}.map`; } // else if (options.sourceMaps.external === 'monkeyPatch') { // const _content = new String(content); // _content['maps'] = mapObject; // return _content; // } else { const encodedMap = globalThis.document ? btoa(JSON.stringify(mapObject)) // <= for browser : Buffer.from(JSON.stringify(mapObject)).toString('base64'); // <= for node content += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,` + encodedMap; // content += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,` + } } } if (options.plugins && !pluginsPerformed) options.plugins.forEach(plugin => { // if plugins has not performed erlier with sourcemaps: if (plugin.bundle) { content = plugin.bundle(content, { target }); } }) return content; } /** * @typedef {[number, number, number, number, number][][]} RawMapping * @typedef {{ * entryPoint: string; // only for sourcemaps and logging * release?: boolean; // = false (=> remove comments|logs?|minify?? or not) * verbose?: boolean; * purgeDebug?: boolean, * getContent?: (filename: string) => string * onError?: (error: Error) => boolean * logStub?: boolean, // replace standard log to ... * getSourceMap?: ( // conditions like sourceMaps * arg: { * mapping: ([number, number, number, number, number]|[number, number, number, number])[][], * files: string[], * sourcesContent: string[] * }) => Omit<BuildOptions['sourceMaps']['injectTo'], 'maps'> | void * sourceMaps?: { * shift?: number, // = false. Possible true if [release=false] & [treeShaking=false] & [!removeLazy] * encode( * arg: Array<Array<[number] | [number, number, number, number, number?]>> * ): string, * decode?: (arg: string) => [number, number, number, number, number][][], // required with `injectTo` field! * external?: boolean, // | 'monkeyPatch' * charByChar?: boolean, * verbose?: boolean, * injectTo?: { * maps?: [number, number, number, number, number][][], * mappings: string, * sources: string[], // file names * sourcesContent: string[], // source contents according file names * names?: string[] * } * } * advanced?: { * allFilesAre?: 'reqular files' * handleRequireExpression?: typeof requireOptions[keyof typeof requireOptions] * incremental?: boolean, // possible true if [release=false] * treeShake?: boolean | {exclude?: Set<string>, method?: 'surface'|'allover', cjs?: false} // Possible true if [release=true => default>true]. * ts?: Function; * nodeModulesDirname?: string * dynamicImports?:{ * ignore?: string[], * root?: string, * foreignBuilder?: (path: string) => string * } * debug?: boolean * optimizations?: { * ignoreDynamicImports?: true * } * }, * experimental?: { * withConditions?: boolean * } * plugins?: Array<{ * name?: string, * preprocess?: (code: string, options?: { * target: string, * maps?: Omit<BuildOptions['sourceMaps']['injectTo'], 'maps'>, * rawMap?: RawMapping * }) => [string, BuildOptions['sourceMaps']['injectTo']], // preprocc (svelte, vue sfc) * extend?: never & { * filter?: string | RegExp, * callback: (code: string) => {code: string, maps?: BuildOptions['sourceMaps']['injectTo'], rawMap?: RawMapping}, // not Implemented * } // additional middleware (json, css) * bundle?: (code: string, options?: { * target: string, * maps?: Omit<BuildOptions['sourceMaps']['injectTo'], 'maps'>, * rawMap?: RawMapping * }) => string, // postprocessing (jsx, uglify) * }> * }} BuildOptions */ //* onModuleNotFound?: OnErrorActions['ModuleNotFound'][keyof OnErrorActions['ModuleNotFound']] // ?dep /** * @type {BuildOptions & {node_modules_Path?: string, target?: string}} */ let globalOptions = null; /** * absolut path to node_modules * @type {string} */ let nodeModulesPath = null; const nodeModules = {} /** * TODO check * research function (not checked yet) to inject inside map to external map * @param {BuildOptions['sourceMaps']['injectTo']} rootMaps * @param {{version?: number;file?: string;sources?: string[];sourcesContent: any;names?: any[];mappings?: string;source?: any;}} mapObject * @param {BuildOptions['sourceMaps']['decode']} [decode] */ function injectMap(rootMaps, mapObject, decode) { // const rootMaps = options.sourceMaps.injectTo; mapObject.source = mapObject.source.concat(rootMaps.sources); mapObject.sourcesContent = mapObject.sourcesContent.concat(rootMaps.sourcesContent); let rootMapings = rootMaps.maps || (decode || globalOptions.sourceMaps.decode)(rootMaps.mappings); rootMapings = rootMapings.map(line => { if (line && line.length) { line.forEach((ch, i) => { line[i][1] += sourcemaps.length; }); return line; } return []; }); debugger; return rootMapings; } /** * * @param {string} content - content (source code) * @param {string} dirpath - source directory name * @param {BuildOptions} options - options */ function importInsert(content, dirpath, options) { let pathman = new PathMan(dirpath, options.getContent || getContent); const needMap = !!(options.sourceMaps || options.getSourceMap) if (logLinesOption) { content = content.replace(/console.log\(/g, function () { let line = arguments[2].slice(0, arguments[1]).split('\n').length.toString() return 'console.log("' + options.entryPoint + ':' + line + ':", ' }) } const charByChar = options.sourceMaps && options.sourceMaps.charByChar // let regex = /^import \* as (?<module>\w+) from \"\.\/(?<filename>\w+)\"/gm; // content = new Importer(pathman).namedImportsApply(content, undefined, (options.getSourceMap && !options.sourceMaps) ? 1 : needMap); content = (importer = new Importer(pathman)).namedImportsApply( content, {root: undefined, _needMap: (options.sourceMaps && options.sourceMaps.charByChar) ? 1 : needMap, extract: null} ); // const modulesContent = moduleContents.join('\n\n'); // if (globalOptions.advanced?.treeShaking) { // /// FORCE TREE SHAKING // const mergedContent = importer.joinAllContents(content, options); // forceTreeShake(globalOptions, mergedContent, modules); // } content = importer.joinAllContents(content, options); const emptyLineInfo = null if (needMap) { rootOffset += 5 + (sourcemaps.length * 2) + 1; // rootOffset += endWrapLinesOffset + (sourcemaps.length * 2) + startWrapLinesOffset; // rootOffset += 5 + (sourcemaps.length * 2 - 2) + 3; if (sourcemaps[0]) { // sourcemaps[0].mappings = ';;;' + sourcemaps[0].mappings // sourcemaps[0].debugInfo.unshift(emptyLineInfo, emptyLineInfo, emptyLineInfo); sourcemaps[0].debugInfo.unshift(emptyLineInfo, emptyLineInfo, emptyLineInfo, emptyLineInfo); } sourcemaps.forEach(sm => { // sm.mappings = ';;' + sm.mappings // sm.debugInfo.unshift(emptyLineInfo, emptyLineInfo); sm.debugInfo.unshift(emptyLineInfo); }) const linesMap = content.split('\n').slice(rootOffset).map((line, i) => { // /** @type {[number, number, number, number, number?]} */ // let r = [0, sourcemaps.length, i, 0]; /** @type {Array<[number, number, number, number, number?]>} */ let r = charByChar ? [[0, sourcemaps.length, i, 0]] : [].map.call(line, (/** @type {any} */ ch, /** @type {any} */ j) => [j, sourcemaps.length, i, j]); return r; }) // if (!sourcemaps.some(file => file.name === options.entryPoint)) sourcemaps.push({ name: options.entryPoint, // mappings: linesMap.map(line => encodeLine(line)).join(';'), // mappings: linesMap.map(line => line.map(charDebugInfo => encodeLine(charDebugInfo)).join(',')).join(';'), // mappings: ';;;' + linesMap.map(line => encodeLine(line)).join(';'), debugInfo: [emptyLineInfo, emptyLineInfo, emptyLineInfo].concat(linesMap) }) } ///* not recommended, but easy for realization: // const regex = /^import \"\.\/(?<filename>\w+)\"/gm; // content = content.replace(regex, allocPack.bind(pathman)); //*/ // regex = /^import {([\w, ]+)} from \".\/(\w+)\"/gm // content = content.replace(moduleSealing.bind(pathman)); //*/ if (options && options.release) { content = releaseProcess(options, content); // remove multiline comments // content = content.replace(/\n[\n]+/g, () => '\n') // remove unnecessary \n } return content } /** * @type {Record<string, string>} */ const modules = {}; /** * @description JUST FOR DEBUG: */ // const modules = new Proxy({}, { // // deleteProperty(target, prop) { // перехватываем удаление свойства // // //@ts-ignore // // if (~prop.indexOf('debounce')) { // // debugger // // } else { // // delete target[prop]; // // return true; // // } // // } // set(target, prop, value) { // // debugger // target[prop] = value; // return true; // } // }); /** * @type {Array<{ * name: string, * mappings?: never, * debugInfo?: import("sourcemap-codec").SourceMapMappings * }>} * // Array<Array<VArray>> // Array<VArray | Array<VArray>> // Array<VArray> | Array<Array<VArray>> */ const sourcemaps = [] /** * replace imports to object spreads and separate modules * @param {string} content * @param {SealingOptions} importOptions * @this {Importer} * * @example : Supports following forms: ``` import defaultExport from "module_name"; import * as name from "./module-name" import { named } from "./module_name" import { named as alias } from "./module_name" import { named1, named2 } from "./module_name" import { named1, named2 as a } from "./module_name" import "./module_name" ``` Unsupported yet: ``` import defaultExport, * as name from "./module-name"; import defaultExport, { tt } from "./module-name"; /// <= TODO this one ``` */ function namedImportsApply(content, importOptions) { const { root, _needMap } = importOptions; importOptions.isEsm = false; const imports = new Set(); const importApplier = this.generateConverter(importOptions, inspectUnique); const _content = content.replace(namedImportsExpRegex, importApplier); /// dynamic imports apply const ignoreDynamic = globalOptions.advanced?.optimizations?.ignoreDynamicImports; let _content$ = ignoreDynamic ? _content : _content.replace(/(?<!\/\/[^\n]*)(?<!\{)import\(['"`](\.?\.\/)?([\-\w\d\.\$\/@\}\{]+)['"`]\)/g, (/** @this {Importer} */ function (_match, isrelative, filename, src) { if (globalOptions.advanced?.dynamicImports?.foreignBuilder) { const fullName = isrelative ? path.join(root, filename) : path.join(nodeModulesPath = nodeModulesPath || findProjectRoot(this.pathMan.dirPath, globalOptions) + '/', filename) return globalOptions.advanced.dynamicImports?.foreignBuilder(fullName); } // dyncmic variables is appying const match = filename.match(/^([\s\S]+\/)?([\w\d_\-\$]+)?\$\{([\w\d_\$]+)\}([\w\d_\-\$\.]+)?(\/[\s\S]+)?$/); if (match) { const restIndex = match.input.length - match.index - match[0].length; // const [, // firstPathPart, // firstDynamicFilePart, // varname, // lastDynamicFilePart, // lastPathPart // ] = match; const firstPathPart = match[1]; const firstDynamicFilePart = match[2]; const varname = match[3]; const lastDynamicFilePart = match[4]; const lastPathPart = match[5]; if (((match[2] || match[4])?.length > 1) || isrelative) { /// root - possible undefifinded if it is a root file /// firstPathPart - possible undefined if it is like this `./${m}/indexUtil.js` (first part is empty) const currentAbsolutePath = path.join(this.pathMan.dirPath, root || '', firstPathPart || '') let files = fs.readdirSync(isrelative ? currentAbsolutePath : (nodeModulesPath = findProjectRoot(this.pathMan.dirPath, globalOptions) + '/') + match[1] || '').filter( file => file.startsWith(match[2] || '') && file.startsWith(match[4] || '') ) if (lastPathPart) { files = files .map(f => path.join(currentAbsolutePath, f, lastPathPart)) .filter(f => fs.existsSync(f)) .map(file => './' + path.relative(path.join(this.pathMan.dirPath, root || ''), file)) // + match[1] = '' //+ } if (globalOptions.advanced.dynamicImports.ignore) { files = files.filter(n => !~globalOptions.advanced.dynamicImports.ignore.indexOf(n)); } if (files.length) { if (files.length > 10) { console.warn(`Too many files have found for dynamic import matching "${filename}" (inside "${this.currentFile}")`); } // files.map(file => match.input.slice(0, match.input.index) + file + match.input.slice(-restIndex)) // files.map(file => match.input.replace(/\$\{([\w\d_\$]+)\}/, match[3])) files.map(file => (match[1] || '') + file + (match[4] || '')) .forEach(file => { applyDynamicImport.call(importer, isrelative, file, lastPathPart) }) const chunkPath = './' + (globalOptions.advanced?.dynamicImports?.root ?? path.basename(path.dirname(globalOptions.target)) + '/') return `fetch(\`${chunkPath + this.genChunkName(filename)}\`)` + '.then(r => r.text()).then(content => new Function(content)())'; } else { console.warn(`No files matching the pattern "${filename}" could be found for dynamic import during process of "${this.currentFile}"`); } } else { console.warn(`Assumed that filename or packname of dynamic import should also have non-variable part of name`); } } else { if (globalOptions.advanced.dynamicImports.ignore) { if (~globalOptions.advanced.dynamicImports.ignore.indexOf(filename)) { return '' } } return applyDynamicImport.call(importer, isrelative, filename); } }).bind(this)) if (globalOptions?.advanced?.handleRequireExpression === requireOptions.sameAsImport) { // && !importOptions.isEsm // console.log('require import'); // statHolder.exports.cjs++; /// works just for named spread const __content = (_content$ || _content).replace( // /(const|var|let) \{?[ ]*(?<varnames>[\w, :]+)[ ]*\}? = require\(['"](?<filename>[\w\/\.\-]+)['"]\)/g, // TODO make `const|var|let` optional /(const|var|let) ((?<varnames>\{?[\w, ]+\}?) = require\(['"](?<filename>[\w\.-\/]+)['"]\)[,\n\s]*)+(?=;|\n)/g, // TODO make `const|var|let` optional (matchedExpr, key, lastRequire, varnames, filename, $, $$) => { statHolder.requires += 1; if (importOptions.isEsm) { const currentFile = this.currentFile || globalOptions.entryPoint; console.warn('\x1B[33m' + `\n> Warning: require expression used to require "${filename}" within esm module inside file "${currentFile}"` + "\x1B[0m") } // FIXME WHY DOUBLE SEARCH?: matchedExpr = matchedExpr.replace(/(?:(const|var|let) )?(?<varnames>\{?[\w, ]+\}?) = require\(['"](?<filename>[\w\.-\/]+)['"]\)/g, (__, key, varnames, filename) => { // const fileStoreName = genfileStoreName(root, filename = filename.replace(/^\.\//m, '')); const fileStoreName = genfileStoreName(root, filename); if (!modules[fileStoreName]) { const smSuccessAttached = this.attachModule(filename, fileStoreName, importOptions); // if (!smSuccessAttached) { // // doNothing | raise Error | [default].getContent // debugger // this.attachModule(filename, fileStoreName, { root, _needMap }) // return _ // } if (modules[fileStoreName]) { // debugger return `${key || ''} ${varnames} = $${fileStoreName}Exports`; } } const exprStart = __.split('=')[0]; return exprStart + `= $${fileStoreName.replace('@', '_')}Exports` }) return matchedExpr;