diff --git a/packages/jsx-compiler/src/modules/code.js b/packages/jsx-compiler/src/modules/code.js index fb123ad3..7dcc2210 100644 --- a/packages/jsx-compiler/src/modules/code.js +++ b/packages/jsx-compiler/src/modules/code.js @@ -1,5 +1,5 @@ const t = require('@babel/types'); -const { join, relative, dirname, resolve, extname } = require('path'); +const { join, relative, dirname, resolve, extname, sep } = require('path'); const resolveModule = require('resolve'); const { parseExpression } = require('../parser'); const isClassComponent = require('../utils/isClassComponent'); @@ -67,7 +67,7 @@ function getConstructor(type) { */ module.exports = { parse(parsed, code, options) { - const { ast, programPath, defaultExportedPath, exportComponentPath, renderFunctionPath, + const {ast, programPath, defaultExportedPath, exportComponentPath, renderFunctionPath, useCreateStyle, useClassnames, dynamicValue, dynamicRef, dynamicStyle, dynamicEvents, imported, contextList, refs, componentDependentProps, listKeyProps, renderItemFunctions, renderPropsFunctions, renderPropsEmitter, renderPropsListener, eventHandler, eventHandlers = [] } = parsed; const { platform, type, cwd, outputPath, sourcePath, resourcePath, disableCopyNpm, virtualHost } = options; @@ -326,23 +326,33 @@ function renameNpmModules(ast, targetFileDir, outputPath, cwd, resourcePath) { // In tnpm, target will be like following (symbol linked path): // ***/_universal-toast_1.0.0_universal-toast/lib/index.js let packageJSONPath; + let packagePath; try { packageJSONPath = require.resolve(join(npmName, 'package.json'), { paths: searchPaths }); + packagePath = packageJSONPath.replace('package.json', ''); } catch (err) { throw new Error(`You may not have npm installed: "${npmName}"`); } + const rootNodeModulePath = getRootNodeModulePath(rootContext, target); + + // Hard link case if the package is not installed in current package node_modules + const isHardLink = rootNodeModulePath.indexOf(packagePath) === -1; + const moduleBasePath = join(packageJSONPath, '..'); - const realNpmName = relative(nodeModulePath, moduleBasePath); + // Hard link pkg use fake npmName to enable npm folder copy behavior. + const realNpmName = isHardLink ? npmName : relative(rootNodeModulePath, moduleBasePath); const modulePathSuffix = relative(moduleBasePath, target); let ret; + // For disableCopyNpm=false scenario,all paths here are predicted paths,while the actual package copy behaivor is executed in jsx2mp-loader/src.script-loader. if (npmName === value) { ret = relative(targetFileDir, join(outputPath, 'npm', realNpmName, modulePathSuffix)); } else { ret = relative(targetFileDir, join(outputPath, 'npm', value.replace(npmName, realNpmName))); } ret = addRelativePathPrefix(normalizeOutputFilePath(ret)); + // ret => '../npm/_ali/universal-toast/lib/index.js return t.stringLiteral(normalizeFileName(ret)); @@ -661,3 +671,33 @@ function addClearKeyCache(renderFunctionPath) { ) ); } + +function getRootNodeModulePath(root, current) { + const relativePathArray = relative(root, current).split(sep) || []; + + if (relativePathArray.find((item) => item === '..')) { + /** + * Package hoist case exist while `..` is presented in relative path array + */ + const resourcePathArray = current.split('node_modules') || []; + + if (resourcePathArray.length === 1) { + /** + * current file is not in node_modules folder, whiche means that the current file is in a mono package, so we need to use the root node_modules folder + */ + return join(root, 'node_modules'); + } else { + /** + * If relative path array length is greater than 1, it means that the current file is in a nested node_modules folder, so we need to dig into the deepest node_modules folder + */ + return join( + resourcePathArray + .slice(0, resourcePathArray.length - 1) + .join('node_modules'), + 'node_modules' + ); + } + } else { + return join(root, 'node_modules'); + } +} diff --git a/packages/jsx-compiler/src/modules/components.js b/packages/jsx-compiler/src/modules/components.js index ca65f0b4..a852b5bb 100644 --- a/packages/jsx-compiler/src/modules/components.js +++ b/packages/jsx-compiler/src/modules/components.js @@ -377,8 +377,20 @@ function getComponentPath(alias, options) { } else { const { disableCopyNpm } = options; const realNpmFile = resolveModule.sync(alias.from, { basedir: dirname(options.resourcePath), preserveSymlinks: false }); + const pkgName = getNpmName(alias.from); - const realPkgName = getRealNpmPkgName(realNpmFile, pkgName); + + // Package name won't be present in hard link case of realNpmFile + const isHardLink = realNpmFile.indexOf(pkgName) === -1; + // Use fake pkgName as real pkgName for hard link case to generate correct relative path in npm folder + const realPkgName = isHardLink ? pkgName : getRealNpmPkgName(realNpmFile, pkgName); + + let hardLinkPkgPath = ''; + if (isHardLink) { + const hardLinkPkgJSONPath = resolveModule.sync(`${alias.from}/package.json`, { basedir: dirname(options.resourcePath), preserveSymlinks: false }); + hardLinkPkgPath = hardLinkPkgJSONPath.replace(/package\.json$/, ''); + } + const targetFileDir = dirname(join(options.outputPath, relative(options.sourcePath, options.resourcePath))); const npmRelativePath = relative(targetFileDir, join(options.outputPath, 'npm')); @@ -413,7 +425,11 @@ function getComponentPath(alias, options) { const miniappConfigRelativePath = relative(pkg.main, miniappComponentPath); const realMiniappAbsPath = resolve(realNpmFile, miniappConfigRelativePath); - const realMiniappRelativePath = realMiniappAbsPath.slice(realMiniappAbsPath.indexOf(realPkgName) + realPkgName.length); + + // there might be multiple realPkgName in realMiniappAbsPath,so we choose the last one as the relative path for npm folder + const realMiniAppAbsPathArray = realMiniappAbsPath.split(realPkgName) || []; + const realMiniappRelativePath = isHardLink ? relative(hardLinkPkgPath, realMiniappAbsPath) : (realMiniAppAbsPathArray[realMiniAppAbsPathArray.length - 1] || ''); + return normalizeFileName(addRelativePathPrefix(normalizeOutputFilePath(join(npmRelativePath, realPkgName, realMiniappRelativePath)))); } } diff --git a/packages/jsx2mp-loader/src/babel-plugin-rename-import.js b/packages/jsx2mp-loader/src/babel-plugin-rename-import.js index e27548f4..0eed38f6 100644 --- a/packages/jsx2mp-loader/src/babel-plugin-rename-import.js +++ b/packages/jsx2mp-loader/src/babel-plugin-rename-import.js @@ -6,6 +6,8 @@ const { constants: { QUICKAPP }} = require('miniapp-builder-shared'); const { isNpmModule, isWeexModule, isQuickAppModule, isRaxModule, isRaxAppModule, isJsx2mpRuntimeModule, isNodeNativeModule } = require('./utils/judgeModule'); const { addRelativePathPrefix, normalizeOutputFilePath, removeExt } = require('./utils/pathHelper'); const getAliasCorrespondingValue = require('./utils/getAliasCorrespondingValue'); +const getRootNodeModulePath = require('./utils/getRootNodeModulePath'); +const resolveModule = require('resolve'); const RUNTIME = 'jsx2mp-runtime'; @@ -34,8 +36,23 @@ module.exports = function visitor({ types: t }, options) { const target = enhancedResolve.sync(resourcePath, value); - const rootNodeModulePath = join(rootContext, 'node_modules'); - const filePath = relative(dirname(distSourcePath), join(outputPath, 'npm', relative(rootNodeModulePath, target))); + const rootNodeModulePath = getRootNodeModulePath(rootContext, target); + + // Hard link case if the package is not installed in current package node_modules + const isHardLink = target.indexOf(rootNodeModulePath) === -1; + + let hardLinkPkgPath; + if (isHardLink) { + try { + const hardLinkPkgJSONPath = resolveModule.sync(`${value}/package.json`, { basedir: dirname(resourcePath), paths: rootNodeModulePath, preserveSymlinks: false }); + hardLinkPkgPath = hardLinkPkgJSONPath.replace(/package\.json$/, ''); + } catch (e) { + console.error(e); + console.warn(chalk.yellow(`Can not find package.json of ${value} in ${resourcePath}`)); + } + } + + const filePath = relative(dirname(distSourcePath), join(outputPath, 'npm', isHardLink ? join(value, relative(hardLinkPkgPath, target)) : relative(rootNodeModulePath, target))); let modifiedValue = normalizeNpmFileName(addRelativePathPrefix(normalizeOutputFilePath(filePath))); // json file will be transformed to js file if (extname(value) === '.json') { diff --git a/packages/jsx2mp-loader/src/script-loader.js b/packages/jsx2mp-loader/src/script-loader.js index 3fde2066..36bfc71b 100644 --- a/packages/jsx2mp-loader/src/script-loader.js +++ b/packages/jsx2mp-loader/src/script-loader.js @@ -10,6 +10,7 @@ const { isNpmModule, isJSONFile, isTypescriptFile } = require('./utils/judgeModu const isMiniappComponent = require('./utils/isMiniappComponent'); const parse = require('./utils/parseRequest'); const { output, transformCode } = require('./output'); +const getRootNodeModulePath = require('./utils/getRootNodeModulePath'); const ScriptLoader = __filename; @@ -35,12 +36,19 @@ module.exports = function scriptLoader(content) { const isCommonJSON = isJSON && !isAppJSon; const rawContent = isCommonJSON ? content : readFileSync(this.resourcePath, 'utf-8'); + const nodeModulesPathList = getNearestNodeModulesPath(rootContext, this.resourcePath); const currentNodeModulePath = nodeModulesPathList[nodeModulesPathList.length - 1]; - const rootNodeModulePath = join(rootContext, 'node_modules'); + const rootNodeModulePath = getRootNodeModulePath(rootContext, this.resourcePath); + + // Only remove last node_modules to get current package path + const currentPackagePath = currentNodeModulePath.split('node_modules').filter((item) => !!item).join('node_modules'); + + // Hard link case if the current package is not in root package folder + const isHardLink = rootNodeModulePath.indexOf(currentPackagePath) === -1; const isFromNodeModule = cached(function isFromNodeModule(path) { - return path.indexOf(rootNodeModulePath) === 0; + return path.indexOf(rootNodeModulePath) === 0 || isHardLink; }); const isFromConstantDir = cached(isFromTargetDirs(constantDir)); @@ -53,11 +61,24 @@ module.exports = function scriptLoader(content) { const outputFile = (rawContent, isFromNpm = true) => { let distSourcePath; if (isFromNpm) { - const relativeNpmPath = relative(currentNodeModulePath, this.resourcePath); + + const pkgPath = isHardLink ? currentPackagePath : currentNodeModulePath; + + const pkgJsonPath = join(pkgPath, 'package.json'); + + let pkgJSON = ''; + if (existsSync(pkgJsonPath)) { + pkgJSON = readJSONSync(pkgJsonPath); + } + + const relativeNpmPath = relative(pkgPath, this.resourcePath); const splitedNpmPath = relativeNpmPath.split(sep); if (/^_?@/.test(relativeNpmPath)) splitedNpmPath.shift(); // Extra shift for scoped npm. splitedNpmPath.shift(); // Skip npm module package, for cnpm/tnpm will rewrite this. - distSourcePath = normalizeNpmFileName(join(outputPath, 'npm', relative(rootNodeModulePath, this.resourcePath))); + + const relativePathInNpmFolder = isHardLink && pkgJSON.name ? join(pkgJSON.name, relative(pkgPath, this.resourcePath)) : relative(rootNodeModulePath, this.resourcePath); + + distSourcePath = normalizeNpmFileName(join(outputPath, 'npm', relativePathInNpmFolder)); } else { const relativeFilePath = relative( join(rootContext, dirname(entryPath)), @@ -103,7 +124,7 @@ module.exports = function scriptLoader(content) { overwrite: false, filter: filename => { const isJSONFile = extname(filename) === '.json'; - const isNpmDirFile = filename.indexOf('npm') > -1; + const isNpmDirFile = filename.indexOf('/npm/') > -1; // if isThirdMiniappComponent, only exclude the json file of the component itself const filterJSONFile = isThirdMiniappComponent ? isNpmDirFile || !isJSONFile : !isJSONFile; return !/__(mocks|tests?)__/.test(filename) && filterJSONFile; // JSON file will be written later because usingComponents may be modified @@ -117,6 +138,7 @@ module.exports = function scriptLoader(content) { if (platform.type === QUICKAPP) { return; } + if (existsSync(originalComponentConfigPath)) { const componentConfig = readJSONSync(originalComponentConfigPath); if (componentConfig.usingComponents) { @@ -124,9 +146,15 @@ module.exports = function scriptLoader(content) { if (componentConfig.usingComponents.hasOwnProperty(key)) { const componentPath = componentConfig.usingComponents[key]; if (isNpmModule(componentPath)) { + // Build usingComponents relative path for modules in npm folder + const npmFolderPath = distComponentConfigPath.slice(0, distComponentConfigPath.indexOf('npm') + 'npm'.length); + + const predictComponentPathInNpmFolder = join(npmFolderPath, '/', componentPath); + const relativeComponentPath = normalizeNpmFileName(addRelativePathPrefix(relative(dirname(distComponentConfigPath), predictComponentPathInNpmFolder))); + // component from node module const realComponentPath = resolveModule.sync(componentPath, { basedir: this.resourcePath, paths: [this.resourcePath], preserveSymlinks: false }); - const relativeComponentPath = normalizeNpmFileName(addRelativePathPrefix(relative(dirname(sourceNativeMiniappScriptFile), realComponentPath))); + componentConfig.usingComponents[key] = normalizeOutputFilePath(removeExt(relativeComponentPath)); // Native miniapp component js file will loaded by script-loader dependencies.push({ @@ -144,6 +172,7 @@ module.exports = function scriptLoader(content) { } } } + if (!existsSync(distComponentConfigPath)) { ensureFileSync(distComponentConfigPath); writeJSONSync(distComponentConfigPath, componentConfig); @@ -207,7 +236,7 @@ module.exports = function scriptLoader(content) { } const miniappComponentDir = miniappComponentPath.slice(0, miniappComponentPath.lastIndexOf('/')); const source = join(sourcePackagePath, miniappComponentDir); - const target = normalizeNpmFileName(join(outputPath, 'npm', relative(rootNodeModulePath, sourcePackagePath), miniappComponentDir)); + const target = normalizeNpmFileName(join(outputPath, 'npm', isHardLink ? npmName : relative(rootNodeModulePath, sourcePackagePath), miniappComponentDir)); outputDir(source, target, { isThirdMiniappComponent, resourcePath: this.resourcePath @@ -215,17 +244,21 @@ module.exports = function scriptLoader(content) { // Modify referenced component location according to the platform const originalComponentConfigPath = join(sourcePackagePath, miniappComponentPath + '.json'); - const distComponentConfigPath = normalizeNpmFileName(join(outputPath, 'npm', relative(rootNodeModulePath, sourcePackagePath), miniappComponentPath + '.json')); + const distComponentConfigPath = normalizeNpmFileName(join(outputPath, 'npm', isHardLink ? npmName : relative(rootNodeModulePath, sourcePackagePath), miniappComponentPath + '.json')); + checkUsingComponents(dependencies, originalComponentConfigPath, distComponentConfigPath, sourceNativeMiniappScriptFile, npmName); } if (isThirdMiniappComponent) { const source = dirname(this.resourcePath); - const target = dirname(normalizeNpmFileName(join(outputPath, 'npm', relative(rootNodeModulePath, this.resourcePath)))); + + // For hard link case, there won't be pkgname present in resource path, so we need to add it manually + const target = dirname(normalizeNpmFileName(join(outputPath, 'npm', isHardLink ? join(npmName, relative(currentPackagePath, this.resourcePath)) : relative(rootNodeModulePath, this.resourcePath)))); outputDir(source, target); outputFile(rawContent); const originalComponentConfigPath = removeExt(this.resourcePath) + '.json'; - const distComponentConfigPath = normalizeNpmFileName(join(outputPath, 'npm', relative(rootNodeModulePath, removeExt(this.resourcePath) + '.json'))); + const distComponentConfigPath = normalizeNpmFileName(join(outputPath, 'npm', isHardLink ? join(npmName, relative(currentPackagePath, removeExt(this.resourcePath) + '.json')) : relative(rootNodeModulePath, removeExt(this.resourcePath) + '.json') )); + checkUsingComponents(dependencies, originalComponentConfigPath, distComponentConfigPath, this.resourcePath, npmName); } diff --git a/packages/jsx2mp-loader/src/utils/getRootNodeModulePath.js b/packages/jsx2mp-loader/src/utils/getRootNodeModulePath.js new file mode 100644 index 00000000..bcbe26f7 --- /dev/null +++ b/packages/jsx2mp-loader/src/utils/getRootNodeModulePath.js @@ -0,0 +1,33 @@ +const { join, relative, sep } = require('path'); + +function getRootNodeModulePath(root, current) { + const relativePathArray = relative(root, current).split(sep) || []; + + if (relativePathArray.find((item) => item === '..')) { + /** + * Package hoist case exist while `..` is presented in relative path array + */ + const resourcePathArray = current.split('node_modules') || []; + + if (resourcePathArray.length === 1) { + /** + * current file is not in node_modules folder means hard link case, so we need to use the root node_modules folder + */ + return join(root, 'node_modules'); + } else { + /** + * If relative path array length is greater than 1, it means that the current file is in a nested node_modules folder, so we need to dig into the deepest node_modules folder + */ + return join( + resourcePathArray + .slice(0, resourcePathArray.length - 1) + .join('node_modules'), + 'node_modules' + ); + } + } else { + return join(root, 'node_modules'); + } +} + +module.exports = getRootNodeModulePath;