mmir-webpack
Version:
Integration for MMIR library in webpack builds
238 lines (205 loc) • 10.9 kB
text/typescript
import { isAbsolute, join, basename, extname } from 'path';
import { readdirSync, existsSync } from 'fs';
import { AppConfig , BuildAppConfig, WebpackAppConfig } from 'mmir-tooling';
import { isWebpackConfig } from 'mmir-tooling/tools/type-utils';
import { warn } from 'mmir-tooling/utils/log-utils';
import fileUtils from 'mmir-tooling/utils/filepath-utils';
import resourcesConfig from '../webpack-resources-config';
function isExistingDirectory(absPath: string): boolean {
return existsSync(absPath) && fileUtils.isDirectory(absPath);
}
/**
* get entry for (webpack) alias entry
*
* NOTE webpack aliases may have suffix $ which indicates that the alias must match exactly (i.e. is not part of the ID itself!)
*
*
* @param aliasId the ID to look up in the alias map
* @param aliasMap the webpack alias map
* @return the alias entry (i.e. file/directory path) or undefined, if there is no entry
*
* @see https://webpack.js.org/configuration/resolve/#resolvealias
*/
export function getAliasEntry(aliasId: string, aliasMap: {[id: string]: string}): string | undefined {
// NOTE webpack aliases may have suffix $ which indicates that the alias must match exactly (i.e. is not part of the ID itself!)
return aliasMap[aliasId + '$'] || aliasMap[aliasId];
}
/**
* check if the alias entry is an exact-match entry in the alias mapping
*
* NOTE if there is no entry in the `aliasMap`, this function always returns `false`
*
* @param aliasId the ID
* @param aliasMap the webpack alias map
* @return `true` if the ID is an extact-match lookup ID (for webpack resolve alias mapping)
*
* @see https://webpack.js.org/configuration/resolve/#resolvealias
*/
export function isExactAliasEntry(aliasId: string, aliasMap: {[id: string]: string}): boolean {
if (aliasId && aliasMap[aliasId])
return aliasId[aliasId.length-1] === '$';
return false;
}
/**
* convert an ID to an exact-match alias entry (for webpack resolve aliases)
*
* @param moduleId the ID
* @return the alias ID for exact-matching
*/
export function toExactAliasId(moduleId: string): string {
// add suffix $ to indicated to webpack that this alias entry must match extactly (i.e. must not be expanded)
if(!moduleId){
warn('resolve-utils: trying to convert empty or undefined module ID to alias entry '+JSON.stringify(moduleId));
return moduleId;
}
if(moduleId[moduleId.length-1] === '$'){
return moduleId;
}
return moduleId + '$';
}
/**
* HELPER set an (webpack) alias entry, either as an "exact-matching" entry, or as an in-exact entry (i.e. can be expanded by webpack)
*
* NOTE: if `alias` already contains an entry (either as exact match or in-exact match), then the existing entry is overwritten
*
* @param aliasId the module ID (should not end with '$')
* @param aliasEntry the entry for the to be written to the alias map
* @param aliasMap the alias mapping
* @param isPreferExact if `true`, the entry is tried as an exact match, if `false` as an in-exact match
* (but if an entry for the ID already exists, either as exact or as in-exact match, the existing entry is overwritten)
*/
export function setAliasEntry(aliasId: string, aliasEntry: string, aliasMap: {[id: string]: string}, isPreferExact: boolean): void {
let id: string = toExactAliasId(aliasId);
if(isPreferExact){
if(aliasMap[aliasId]){
// if prefer exact, but there is already an in-exact entry: do override the in-exact one
id = aliasId;
}
} else {
if(!aliasMap[id]){
// if prefer in-exact: only use in-exact one, if there is no entry yet for an exact one (i.e. if there is alredy an exact entry, do overwrite that with the new entry)
id = aliasId;
}
}
aliasMap[id] = aliasEntry;
}
/**
* add/overwrite module alias (i.e. mapping module ID to file path)
*
* @param {{paths?: {[moduleId: string]: string}}} mmirAppConfig the app-specific configuration: applies module-path-specifications from mmirAppConfig.paths
* @param {[{[moduleId: string]: string}]} alias the (default) mapping of module IDs to (absolute) paths
* @returns a list of alias entries that should be in-exact (i.e. can be expanded by webpack alias resolver)
*/
export function addAliasFrom(mmirAppConfig: AppConfig | BuildAppConfig | WebpackAppConfig, alias: {[moduleId: string]: string}): undefined | {aliasId: string, absolutePath: string}[] {
if(isWebpackConfig(mmirAppConfig)){
// log('adding/overwriting paths with app paths: ', mmirAppConfig.paths);
const appRoot = mmirAppConfig.rootPath || process.cwd();
const hasPackages = Array.isArray(mmirAppConfig.packages);
const packageIds = hasPackages? new Set<string>(mmirAppConfig.packages.map(item => item.name)) : null;
const inexactEntries: {aliasId: string, absolutePath: string}[] = [];
let p: string, isAbs: boolean, absPath: string;
for (let n in mmirAppConfig.paths) {
p = mmirAppConfig.paths[n];
isAbs = isAbsolute(p);
absPath = isAbs? p : join(appRoot, p);
// do not add entries for package-definitions:
// the resolved files of the package-defition will be added later (see below)
if((!hasPackages || !packageIds.has(n)) && !isExistingDirectory(absPath)){
// NOTE for package-entries (i.e. directories) and entries with absolute paths (most likely user defined entries),
// do not create "extact match entry", i.e. those may get expanded by webpack when resolving
// ... but do this only, if there is not already an exact-match entry: if there is one, do replace the exact-match entry
setAliasEntry(n, absPath, alias, false);
} else {
inexactEntries.push({aliasId: n, absolutePath: absPath});
}
}
if(hasPackages){
expandPackageResources(appRoot, alias, mmirAppConfig.packages);
}
return inexactEntries;
}
//DISABLED redirection must be handled by NormalModuleReplacementPlugin, because loadFile is not directly require'ed, but vie package (sub) path 'mmirf/util'
// //add "proxy" for mmirf/util/loadFile, so that inlined resouces get returned directly:
// alias['mmirf/util/loadFile$'] = path.resolve('./runtime/webpack-loadFile.js');
// var origLoadFile = path.resolve(alias['mmirf/util'], 'loadFile.js');
// alias['mmirf/util/loadFile__raw$'] = origLoadFile;
}
/**
* expand require.js package definitions & add them to the alias dictionary
*
* NOTE: this is not needed by webpacks module resolver (i.e. to find modules within the package definition), but
* it is need for preserving the module IDs within the package, i.e. for use of `mmir.require(<module within the package>)`
*
* TODO extract this function to mmir-tooling?
*
* @param {string} rootDir the mmir root dir (usually node_modules/mmir-lib/lib/ )
* @param {{[id: string]: string}} alias dictionary for module ID/alias -> module-path (INOUT parameter)
* @param {Array<{name: string, location: string}>} resourcesConfigPackages list of require.js package definitions
*/
export function expandPackageResources(rootDir: string, alias: {[id: string]: string}, resourcesConfigPackages: {name: string, location: string}[]): void {
var getFiles = function(dir: string, relDir: string, baseId?: string): void {
var files = readdirSync(dir);
files.forEach(function(f){
var absPath = join(dir, f);
var relPath = relDir? join(relDir, f) : f;
if(fileUtils.isDirectory(absPath)){
getFiles(dir, relPath);
} else {
//create id for file/module (without file-extension):
var fid = fileUtils.normalizePath(join(baseId, basename(relPath, extname(relPath))));
setAliasEntry(fid, absPath, alias, true);
// console.log(' added package module ', fid, ' -> ', absPath);
}
});
};
// list: Array<{name: string, location: string}>
resourcesConfigPackages.forEach(function(p: &{location: string, name: string}){
getFiles(join(rootDir, p.location), '', p.name);
});
}
export function createResolveAlias(mmirAppConfig: WebpackAppConfig, mmirLibRootDir: string, webpackRootDir: string): {[id: string]: string} {
var paths = resourcesConfig.paths;
const hasPackages = Array.isArray(resourcesConfig.packages);
const packageIds = hasPackages? new Set<string>(resourcesConfig.packages.map(item => item.name)) : null;
const inexactEntries: {aliasId: string, absolutePath: string}[] = [];
var alias = {}, p: string;
for (var n in paths) {
p = paths[n];
if(/^build-tool\//.test(n)){
setAliasEntry(n, isAbsolute(p)? p : join(webpackRootDir, p), alias, true);
} else {
const isAbs = isAbsolute(p);
const absPath = isAbs? p : join(mmirLibRootDir, p);
// do not add entries for package-definitions:
// the resolved files of the package-defition will be added later (see below)
if(!hasPackages || !packageIds.has(n)){
// NOTE for package-entries (i.e. directories) and entries with absolute paths (most likely user defined entries),
// do not create "extact match entry", i.e. those may get expanded by webpack when resolving
// ... but do this only, if there is not already an exact-match entry: if there is one, do replace the exact-match entry
setAliasEntry(n, absPath, alias, !isExistingDirectory(absPath));
} else if (hasPackages){
inexactEntries.push({aliasId: n, absolutePath: absPath});
}
}
}
if(hasPackages){
expandPackageResources(mmirLibRootDir, alias, resourcesConfig.packages);
}
const additionalInexactEntries = addAliasFrom(mmirAppConfig, alias);
if(additionalInexactEntries){
for(const pe of additionalInexactEntries){
inexactEntries.push(pe);
}
}
if(mmirAppConfig.disableLogging){
setAliasEntry('mmirf/logger', getAliasEntry('mmirf/loggerDisabled', alias), alias, true);
}
// do add inexact alias entries (e.g. for packages) lastly
// (i.e. entries that may get expanded by webpack)
if(inexactEntries.length > 0){
for(const pe of inexactEntries){
setAliasEntry(pe.aliasId, pe.absolutePath, alias, false);
}
}
return alias;
}