diff --git a/packages/jsx2mp-loader/.prettierrc.js b/packages/jsx2mp-loader/.prettierrc.js new file mode 100644 index 00000000..23c8ce5a --- /dev/null +++ b/packages/jsx2mp-loader/.prettierrc.js @@ -0,0 +1,3 @@ +const { getPrettierConfig } = require('@iceworks/spec'); + +module.exports = getPrettierConfig('rax'); diff --git a/packages/jsx2mp-loader/package.json b/packages/jsx2mp-loader/package.json index 4c8d5138..0004f911 100644 --- a/packages/jsx2mp-loader/package.json +++ b/packages/jsx2mp-loader/package.json @@ -20,8 +20,10 @@ "babel-plugin-danger-remove-unused-import": "^2.0.0", "babel-plugin-minify-dead-code-elimination-while-loop-fixed": "^0.3.0", "babel-plugin-transform-define": "^1.3.1", + "buffer-json": "^2.0.0", "chalk": "^2.4.2", "convert-source-map": "^1.6.0", + "crypto": "^1.0.1", "csso": "^3.5.1", "enhanced-resolve": "^4.1.1", "fs-extra": "^8.1.0", diff --git a/packages/jsx2mp-loader/src/app-loader.js b/packages/jsx2mp-loader/src/app-loader.js index 22c1a8ce..8492d5f7 100644 --- a/packages/jsx2mp-loader/src/app-loader.js +++ b/packages/jsx2mp-loader/src/app-loader.js @@ -87,6 +87,7 @@ module.exports = async function appLoader(content) { type: 'app', platform, rootDir, + resourcePath: this.resourcePath }; output(outputContent, rawContent, outputOption); diff --git a/packages/jsx2mp-loader/src/component-loader.js b/packages/jsx2mp-loader/src/component-loader.js index 0e2503f0..4b46c569 100644 --- a/packages/jsx2mp-loader/src/component-loader.js +++ b/packages/jsx2mp-loader/src/component-loader.js @@ -1,15 +1,18 @@ const { existsSync, mkdirpSync } = require('fs-extra'); -const { relative, join, dirname, resolve } = require('path'); +const { relative, join, dirname, resolve, extname } = require('path'); const { getOptions } = require('loader-utils'); +const { constants: { QUICKAPP }} = require('miniapp-builder-shared'); const chalk = require('chalk'); const cached = require('./cached'); const { removeExt, isFromTargetDirs, doubleBackslash, normalizeOutputFilePath, addRelativePathPrefix, getHighestPriorityPackage } = require('./utils/pathHelper'); const eliminateDeadCode = require('./utils/dce'); const { isTypescriptFile } = require('./utils/judgeModule'); +const { minify } = require('./utils/minifyCode'); const parse = require('./utils/parseRequest'); const processCSS = require('./styleProcessor'); -const { output } = require('./output'); +const { output, writeFileWithDirCheck } = require('./output'); +const { getCache, getCacheDirName } = require('./utils/useCache'); const ScriptLoader = require.resolve('./script-loader'); const MINIAPP_PLUGIN_COMPONENTS_REG = /^plugin\:\/\//; @@ -22,10 +25,11 @@ module.exports = async function componentLoader(content) { } const loaderOptions = getOptions(this); - const { rootDir, platform, entryPath, outputPath, constantDir, mode, disableCopyNpm, turnOffSourceMap, aliasEntries, injectAppCssComponent, virtualHost } = loaderOptions; + const { rootDir, platform, entryPath, outputPath, constantDir, mode, disableCopyNpm, turnOffSourceMap, aliasEntries, injectAppCssComponent, virtualHost, cache } = loaderOptions; const resourcePath = this.resourcePath; const rootContext = this.rootContext; const absoluteConstantDir = constantDir.map(dir => join(rootContext, dir)); + const isQuickApp = platform.type === QUICKAPP; const sourcePath = join(rootContext, dirname(entryPath)); @@ -33,100 +37,13 @@ module.exports = async function componentLoader(content) { const distFileWithoutExt = removeExt(join(outputPath, relativeSourcePath), platform.type); const isFromConstantDir = cached(isFromTargetDirs(absoluteConstantDir)); + const outputPathCode = distFileWithoutExt + '.js'; + const outputPathJson = distFileWithoutExt + '.json'; + const outputPathCss = distFileWithoutExt + platform.extension.css; + const outputPathTemplate = distFileWithoutExt + platform.extension.xml; - const JSXCompilerPath = getHighestPriorityPackage('jsx-compiler', this.rootContext); - const compiler = require(JSXCompilerPath); - - const compilerOptions = Object.assign({}, compiler.baseOptions, { - resourcePath: this.resourcePath, - outputPath, - sourcePath, - type: 'component', - platform, - sourceFileName: this.resourcePath, - disableCopyNpm, - turnOffSourceMap, - aliasEntries, - virtualHost - }); - - let transformed; - try { - const rawContentAfterDCE = eliminateDeadCode(content); - transformed = compiler(rawContentAfterDCE, compilerOptions); - } catch (e) { - console.log(chalk.red(`\n[${platform.name}] Error occured when handling Component ${this.resourcePath}`)); - if (process.env.DEBUG === 'true') { - throw new Error(e); - } else { - const errMsg = e.node ? `${e.message}\nat ${this.resourcePath}` : `Unknown compile error! please check your code at ${this.resourcePath}`; - throw new Error(errMsg); - } - } - - const { style, assets } = await processCSS(transformed.cssFiles, sourcePath); - transformed.style = style; - transformed.assets = assets; - - const config = Object.assign({}, transformed.config); - if (Array.isArray(transformed.dependencies)) { - transformed.dependencies.forEach(dep => { - this.addDependency(dep); - }); - } - if (config.usingComponents) { - const usingComponents = {}; - Object.keys(config.usingComponents).forEach(key => { - const value = config.usingComponents[key]; - - if (/^c-/.test(key)) { - const result = MINIAPP_PLUGIN_COMPONENTS_REG.test(value) ? value : removeExt(addRelativePathPrefix(relative(dirname(this.resourcePath), value))); - usingComponents[key] = normalizeOutputFilePath(result); - } else { - usingComponents[key] = normalizeOutputFilePath(value); - } - }); - config.usingComponents = usingComponents; - } - - const distFileDir = dirname(distFileWithoutExt); - if (!existsSync(distFileDir)) mkdirpSync(distFileDir); - - // Only works when developing miniapp plugin, to declare the use of __app_css component - if (injectAppCssComponent) { - const appCssComponentPath = resolve(outputPath, '__app_css', 'index'); - const relativeAppCssComponentPath = addRelativePathPrefix(relative(distFileDir, appCssComponentPath)); - config.usingComponents = { - '__app_css': relativeAppCssComponentPath, - ...config.usingComponents - }; - } - - const outputContent = { - code: transformed.code, - map: transformed.map, - css: transformed.style || '', - json: config, - template: transformed.template, - assets: transformed.assets, - importComponents: transformed.importComponents, - iconfontMap: transformed.iconfontMap, - }; - const outputOption = { - outputPath: { - code: distFileWithoutExt + '.js', - json: distFileWithoutExt + '.json', - css: distFileWithoutExt + platform.extension.css, - template: distFileWithoutExt + platform.extension.xml, - assets: outputPath - }, - mode, - platform, - isTypescriptFile: isTypescriptFile(this.resourcePath), - rootDir, - }; - - output(outputContent, content, outputOption); + const cacheDirectory = getCacheDirName({ config: cache, mode }); + const cacheContent = getCache({ filePath: this.resourcePath, cacheDirectory: join(rootDir, cacheDirectory) }); function isCustomComponent(name, usingComponents = {}) { const matchingPath = join(dirname(resourcePath), name); @@ -142,41 +59,216 @@ module.exports = async function componentLoader(content) { return false; } - const dependencies = []; - Object.keys(transformed.imported).forEach(name => { - if (isCustomComponent(name, transformed.usingComponents)) { - const componentPath = resolve(dirname(resourcePath), name); - dependencies.push({ - name: isFromConstantDir(componentPath) ? name : `${name}?role=component`, // Native miniapp component js file will loaded by script-loader - options: loaderOptions + // cache and cacheContent exsit + if (cache && cacheContent) { + if (cacheContent.code) { + writeFileWithDirCheck( outputPathCode, cacheContent.code, { rootDir }); + } + + if (cacheContent.json) { + writeFileWithDirCheck( outputPathJson, cacheContent.json, { rootDir, type: 'json' }); + } + + if (cacheContent.template) { + writeFileWithDirCheck( outputPathTemplate, cacheContent.template, { rootDir }); + } + + if (cacheContent.css) { + writeFileWithDirCheck( outputPathCss, cacheContent.css, { rootDir }); + } + + const assets = cacheContent.assets; + if (assets && Object.keys(assets).length) { + Object.keys(assets).forEach((asset) => { + const ext = extname(asset); + let content = assets[asset]; + if (content) { + if (isQuickApp) { + content = content.replace(/rpx/g, 'px'); + } + if (mode === 'build') { + content = minify(content, ext); + } + const assetsOutputPath = join(outputPath, asset); + writeFileWithDirCheck(assetsOutputPath, content, { rootDir }); + } }); - } else { - const importedArray = transformed.imported[name]; - let entirePush = false; - importedArray.forEach(importedContent => { - // Component library - if (importedContent.isFromComponentLibrary) { + } + + const dependencies = []; + if (cacheContent.imported) { + const imported = cacheContent.imported; + Object.keys(imported).forEach(name => { + if (isCustomComponent(name, cacheContent.usingComponents)) { + const componentPath = resolve(dirname(resourcePath), name); dependencies.push({ - name, - loader: ScriptLoader, - options: Object.assign({}, loaderOptions, { - importedComponent: importedContent.local - }) + name: isFromConstantDir(componentPath) ? name : `${name}?role=component`, // Native miniapp component js file will loaded by script-loader + options: loaderOptions }); } else { - if (!entirePush) { - dependencies.push({ name }); - entirePush = true; - } + const importedArray = imported[name]; + let entirePush = false; + importedArray.forEach(importedContent => { + // Component library + if (importedContent.isFromComponentLibrary) { + dependencies.push({ + name, + loader: ScriptLoader, + options: Object.assign({}, loaderOptions, { + importedComponent: importedContent.local + }) + }); + } else { + if (!entirePush) { + dependencies.push({ name }); + entirePush = true; + } + } + }); } }); } - }); + return [ + `/* Generated by JSX2MP ComponentLoader, sourceFile: ${this.resourcePath}. */`, + generateDependencies(dependencies), + ].join('\n'); + } else { + const JSXCompilerPath = getHighestPriorityPackage('jsx-compiler', this.rootContext); + const compiler = require(JSXCompilerPath); - return [ - `/* Generated by JSX2MP ComponentLoader, sourceFile: ${this.resourcePath}. */`, - generateDependencies(dependencies), - ].join('\n'); + const compilerOptions = Object.assign({}, compiler.baseOptions, { + resourcePath: this.resourcePath, + outputPath, + sourcePath, + type: 'component', + platform, + sourceFileName: this.resourcePath, + disableCopyNpm, + turnOffSourceMap, + aliasEntries, + virtualHost + }); + + let transformed; + try { + const rawContentAfterDCE = eliminateDeadCode(content); + transformed = compiler(rawContentAfterDCE, compilerOptions); + } catch (e) { + console.log(chalk.red(`\n[${platform.name}] Error occured when handling Component ${this.resourcePath}`)); + if (process.env.DEBUG === 'true') { + throw new Error(e); + } else { + const errMsg = e.node ? `${e.message}\nat ${this.resourcePath}` : `Unknown compile error! please check your code at ${this.resourcePath}`; + throw new Error(errMsg); + } + } + + const { style, assets } = await processCSS(transformed.cssFiles, sourcePath); + transformed.style = style; + transformed.assets = assets; + + const config = Object.assign({}, transformed.config); + if (Array.isArray(transformed.dependencies)) { + transformed.dependencies.forEach(dep => { + this.addDependency(dep); + }); + } + if (config.usingComponents) { + const usingComponents = {}; + Object.keys(config.usingComponents).forEach(key => { + const value = config.usingComponents[key]; + + if (/^c-/.test(key)) { + const result = MINIAPP_PLUGIN_COMPONENTS_REG.test(value) ? value : removeExt(addRelativePathPrefix(relative(dirname(this.resourcePath), value))); + usingComponents[key] = normalizeOutputFilePath(result); + } else { + usingComponents[key] = normalizeOutputFilePath(value); + } + }); + config.usingComponents = usingComponents; + } + + const distFileDir = dirname(distFileWithoutExt); + if (!existsSync(distFileDir)) mkdirpSync(distFileDir); + + // Only works when developing miniapp plugin, to declare the use of __app_css component + if (injectAppCssComponent) { + const appCssComponentPath = resolve(outputPath, '__app_css', 'index'); + const relativeAppCssComponentPath = addRelativePathPrefix(relative(distFileDir, appCssComponentPath)); + config.usingComponents = { + '__app_css': relativeAppCssComponentPath, + ...config.usingComponents + }; + } + + const outputContent = { + code: transformed.code, + map: transformed.map, + css: transformed.style || '', + json: config, + template: transformed.template, + assets: transformed.assets, + importComponents: transformed.importComponents, + iconfontMap: transformed.iconfontMap, + imported: transformed.imported, + usingComponents: transformed.usingComponents + }; + + const outputOption = { + outputPath: { + code: outputPathCode, + json: outputPathJson, + css: outputPathCss, + template: outputPathTemplate, + assets: outputPath + }, + cache, + mode, + platform, + isTypescriptFile: isTypescriptFile(this.resourcePath), + rootDir, + resourcePath: this.resourcePath + }; + + + output(outputContent, content, outputOption); + + const dependencies = []; + Object.keys(transformed.imported).forEach(name => { + if (isCustomComponent(name, transformed.usingComponents)) { + const componentPath = resolve(dirname(resourcePath), name); + dependencies.push({ + name: isFromConstantDir(componentPath) ? name : `${name}?role=component`, // Native miniapp component js file will loaded by script-loader + options: loaderOptions + }); + } else { + const importedArray = transformed.imported[name]; + let entirePush = false; + importedArray.forEach(importedContent => { + // Component library + if (importedContent.isFromComponentLibrary) { + dependencies.push({ + name, + loader: ScriptLoader, + options: Object.assign({}, loaderOptions, { + importedComponent: importedContent.local + }) + }); + } else { + if (!entirePush) { + dependencies.push({ name }); + entirePush = true; + } + } + }); + } + }); + + return [ + `/* Generated by JSX2MP ComponentLoader, sourceFile: ${this.resourcePath}. */`, + generateDependencies(dependencies), + ].join('\n'); + } }; function generateDependencies(dependencies) { diff --git a/packages/jsx2mp-loader/src/output.js b/packages/jsx2mp-loader/src/output.js index c7fb7468..db4b7c20 100644 --- a/packages/jsx2mp-loader/src/output.js +++ b/packages/jsx2mp-loader/src/output.js @@ -4,6 +4,7 @@ const { transformSync } = require('@babel/core'); const { constants: { QUICKAPP }} = require('miniapp-builder-shared'); const { minify, minifyJS, minifyCSS, minifyXML } = require('./utils/minifyCode'); const addSourceMap = require('./utils/addSourceMap'); +const { saveCache, getCacheDirName } = require('./utils/useCache'); function transformCode(rawContent, mode, externalPlugins = [], externalPreset = []) { const presets = [].concat(externalPreset); @@ -54,9 +55,10 @@ function transformCode(rawContent, mode, externalPlugins = [], externalPreset = * @param {object} options */ function output(content, raw, options) { - const { mode, outputPath, externalPlugins = [], isTypescriptFile, platform, type, rootDir } = options; - let { code, config, json, css, map, template, assets, importComponents = [], iconfontMap } = content; + const { mode, outputPath, externalPlugins = [], isTypescriptFile, platform, type, rootDir, cache, resourcePath } = options; + let { code, config, json, css, map, template, assets, imported, usingComponents, importComponents = [], iconfontMap } = content; const isQuickApp = platform.type === QUICKAPP; + const collection = {}; if (isTypescriptFile) { externalPlugins.unshift(require('@babel/plugin-transform-typescript')); @@ -119,10 +121,12 @@ function output(content, raw, options) { } } writeFileWithDirCheck(outputPath.code, code, { rootDir }); + collection.code = code; } if (json) { writeFileWithDirCheck(outputPath.json, json, { rootDir, type: 'json' }); + collection.json = json; } if (template) { if (isQuickApp) { @@ -147,6 +151,7 @@ function output(content, raw, options) { } } writeFileWithDirCheck(outputPath.template, template, { rootDir }); + collection.template = template; } if (css) { if (isQuickApp) { @@ -165,13 +170,16 @@ function output(content, raw, options) { css = css.replace(/rpx/g, 'px'); } writeFileWithDirCheck(outputPath.css, css, { rootDir }); + collection.css = css; } if (config) { writeFileWithDirCheck(outputPath.config, config, { rootDir }); + collection.config = config; } // Write extra assets if (assets) { + collection.assets = assets; Object.keys(assets).forEach((asset) => { const ext = extname(asset); let content = assets[asset]; @@ -185,6 +193,20 @@ function output(content, raw, options) { writeFileWithDirCheck(assetsOutputPath, content, { rootDir }); }); } + + if (imported) { + collection.imported = imported; + } + + if (usingComponents) { + collection.usingComponents = usingComponents; + } + + // save cache + if (cache) { + const cacheDirectory = getCacheDirName({ config: cache, mode}); + saveCache(collection, { filePath: resourcePath, cacheDirectory: join(rootDir, cacheDirectory) }); + } } /** @@ -246,5 +268,6 @@ function writeFileWithDirCheck(filePath, content, { type = 'file', rootDir }) { module.exports = { output, - transformCode + transformCode, + writeFileWithDirCheck }; diff --git a/packages/jsx2mp-loader/src/page-loader.js b/packages/jsx2mp-loader/src/page-loader.js index a1b05e04..ca648503 100644 --- a/packages/jsx2mp-loader/src/page-loader.js +++ b/packages/jsx2mp-loader/src/page-loader.js @@ -1,16 +1,20 @@ -const { readFileSync, existsSync, mkdirpSync, readJSONSync } = require('fs-extra'); +const { existsSync, mkdirpSync, readJSONSync, extname } = require('fs-extra'); const { relative, join, dirname, resolve } = require('path'); +const { constants: { QUICKAPP }} = require('miniapp-builder-shared'); const { getOptions } = require('loader-utils'); const chalk = require('chalk'); +const { minify } = require('./utils/minifyCode'); const cached = require('./cached'); const { removeExt, isFromTargetDirs, doubleBackslash, normalizeOutputFilePath, addRelativePathPrefix, getHighestPriorityPackage } = require('./utils/pathHelper'); const eliminateDeadCode = require('./utils/dce'); const processCSS = require('./styleProcessor'); -const { output } = require('./output'); const { isTypescriptFile } = require('./utils/judgeModule'); const parse = require('./utils/parseRequest'); +const { output, writeFileWithDirCheck } = require('./output'); +const { getCache, getCacheDirName } = require('./utils/useCache'); const ScriptLoader = require.resolve('./script-loader'); + const MINIAPP_PLUGIN_COMPONENTS_REG = /^plugin\:\/\//; module.exports = async function pageLoader(content) { @@ -21,9 +25,10 @@ module.exports = async function pageLoader(content) { } const loaderOptions = getOptions(this); - const { rootDir, platform, entryPath, mode, disableCopyNpm, constantDir, turnOffSourceMap, outputPath, aliasEntries, injectAppCssComponent } = loaderOptions; + const { rootDir, platform, entryPath, mode, disableCopyNpm, constantDir, turnOffSourceMap, outputPath, aliasEntries, injectAppCssComponent, cache } = loaderOptions; const resourcePath = this.resourcePath; const rootContext = this.rootContext; + const isQuickApp = platform.type === QUICKAPP; const absoluteConstantDir = constantDir.map(dir => join(rootContext, dir)); const sourcePath = join(rootContext, dirname(entryPath)); @@ -31,154 +36,247 @@ module.exports = async function pageLoader(content) { const targetFilePath = join(outputPath, relativeSourcePath); const isFromConstantDir = cached(isFromTargetDirs(absoluteConstantDir)); - - const JSXCompilerPath = getHighestPriorityPackage('jsx-compiler', this.rootContext); - const compiler = require(JSXCompilerPath); - - const compilerOptions = Object.assign({}, compiler.baseOptions, { - resourcePath: this.resourcePath, - outputPath, - sourcePath, - type: 'page', - platform, - sourceFileName: this.resourcePath, - disableCopyNpm, - turnOffSourceMap, - aliasEntries - }); - const rawContentAfterDCE = eliminateDeadCode(content); - - let transformed; - try { - transformed = compiler(rawContentAfterDCE, compilerOptions); - } catch (e) { - console.log(chalk.red(`\n[${platform.name}] Error occured when handling Page ${this.resourcePath}`)); - if (process.env.DEBUG === 'true') { - throw new Error(e); - } else { - const errMsg = e.node ? `${e.message}\nat ${this.resourcePath}` : `Unknown compile error! please check your code at ${this.resourcePath}`; - throw new Error(errMsg); - } - } - - const { style, assets } = await processCSS(transformed.cssFiles, sourcePath); - transformed.style = style; - transformed.assets = assets; - - const pageDistDir = dirname(targetFilePath); - if (!existsSync(pageDistDir)) mkdirpSync(pageDistDir); - const distFileWithoutExt = removeExt(join(outputPath, relativeSourcePath), platform.type); + const outputPathCode = distFileWithoutExt + '.js'; const pageConfigPath = distFileWithoutExt + '.json'; - let config = { - ...transformed.config - }; - if (existsSync(pageConfigPath)) { - const pageConfig = readJSONSync(pageConfigPath); - delete pageConfig.usingComponents; - Object.assign(config, pageConfig); - } - if (Array.isArray(transformed.dependencies)) { - transformed.dependencies.forEach(dep => { - this.addDependency(dep); - }); - } - - if (config.usingComponents) { - const usingComponents = {}; - Object.keys(config.usingComponents).forEach(key => { - const value = config.usingComponents[key]; - if (/^c-/.test(key)) { - const result = MINIAPP_PLUGIN_COMPONENTS_REG.test(value) ? value : removeExt(addRelativePathPrefix(relative(dirname(this.resourcePath), value))); - usingComponents[key] = normalizeOutputFilePath(result); - } else { - usingComponents[key] = normalizeOutputFilePath(value); - } - }); - config.usingComponents = usingComponents; - } + const outputPathJson = pageConfigPath; + const outputPathCss = distFileWithoutExt + platform.extension.css; + const outputPathTemplate = distFileWithoutExt + platform.extension.xml; - // Only works when developing miniapp plugin, to declare the use of __app_css component - if (injectAppCssComponent) { - const appCssComponentPath = resolve(outputPath, '__app_css', 'index'); - const relativeAppCssComponentPath = addRelativePathPrefix(relative(pageDistDir, appCssComponentPath)); - config.usingComponents = { - '__app_css': relativeAppCssComponentPath, - ...config.usingComponents - }; - } - - const outputContent = { - code: transformed.code, - map: transformed.map, - css: transformed.style || '', - json: config, - template: transformed.template, - assets: transformed.assets, - importComponents: transformed.importComponents, - iconfontMap: transformed.iconfontMap, - }; - const outputOption = { - outputPath: { - code: distFileWithoutExt + '.js', - json: pageConfigPath, - css: distFileWithoutExt + platform.extension.css, - template: distFileWithoutExt + platform.extension.xml, - assets: outputPath - }, - mode, - platform, - isTypescriptFile: isTypescriptFile(this.resourcePath), - rootDir, - }; - - output(outputContent, content, outputOption); + const cacheDirectory = getCacheDirName({ config: cache, mode }); + const cacheContent = getCache({ filePath: this.resourcePath, cacheDirectory: join(rootDir, cacheDirectory) }); function isCustomComponent(name, usingComponents = {}) { const matchingPath = join(dirname(resourcePath), name); for (let key in usingComponents) { - if (usingComponents.hasOwnProperty(key) - && usingComponents[key].indexOf(matchingPath) === 0) { + if ( + usingComponents.hasOwnProperty(key) + && usingComponents[key] + && usingComponents[key].indexOf(matchingPath) === 0 + ) { return true; } } return false; } - const dependencies = []; - Object.keys(transformed.imported).forEach(name => { - if (isCustomComponent(name, transformed.usingComponents)) { - const componentPath = resolve(dirname(resourcePath), name); - dependencies.push({ - name: isFromConstantDir(componentPath) ? name : `${name}?role=component`, // Native miniapp component js file will be loaded by script-loader - options: loaderOptions + // cache and cacheContent exsit + if (cache && cacheContent) { + if (cacheContent.code) { + writeFileWithDirCheck( outputPathCode, cacheContent.code, { rootDir }); + } + + if (cacheContent.json) { + writeFileWithDirCheck( outputPathJson, cacheContent.json, { rootDir, type: 'json' }); + } + + if (cacheContent.template) { + writeFileWithDirCheck( outputPathTemplate, cacheContent.template, { rootDir }); + } + + if (cacheContent.css) { + writeFileWithDirCheck( outputPathCss, cacheContent.css, { rootDir }); + } + + const assets = cacheContent.assets; + if (assets && Object.keys(assets).length) { + Object.keys(assets).forEach((asset) => { + const ext = extname(asset); + let content = assets[asset]; + if (content) { + if (isQuickApp) { + content = content.replace(/rpx/g, 'px'); + } + if (mode === 'build') { + content = minify(content, ext); + } + const assetsOutputPath = join(outputPath, asset); + console.log('assetsOutputPath', assetsOutputPath, asset); + writeFileWithDirCheck(assetsOutputPath, content, { rootDir }); + } }); - } else { - const importedArray = transformed.imported[name]; - let entirePush = false; - importedArray.forEach(importedContent => { - // Component library - if (importedContent.isFromComponentLibrary) { + } + + const dependencies = []; + if (cacheContent.imported) { + const imported = cacheContent.imported; + Object.keys(imported).forEach(name => { + if (isCustomComponent(name, cacheContent.usingComponents)) { + const componentPath = resolve(dirname(resourcePath), name); dependencies.push({ - name, - loader: ScriptLoader, - options: Object.assign({}, loaderOptions, { - importedComponent: importedContent.local - }) + name: isFromConstantDir(componentPath) ? name : `${name}?role=component`, // Native miniapp component js file will loaded by script-loader + options: loaderOptions }); } else { - if (!entirePush) { - dependencies.push({ name }); - entirePush = true; - } + const importedArray = imported[name]; + let entirePush = false; + importedArray.forEach(importedContent => { + // Component library + if (importedContent.isFromComponentLibrary) { + dependencies.push({ + name, + loader: ScriptLoader, + options: Object.assign({}, loaderOptions, { + importedComponent: importedContent.local + }) + }); + } else { + if (!entirePush) { + dependencies.push({ name }); + entirePush = true; + } + } + }); } }); } - }); - return [ - `/* Generated by JSX2MP PageLoader, sourceFile: ${this.resourcePath}. */`, - generateDependencies(dependencies), - ].join('\n'); + + + return [ + `/* Generated by JSX2MP ComponentLoader, sourceFile: ${this.resourcePath}. */`, + generateDependencies(dependencies), + ].join('\n'); + } else { + const JSXCompilerPath = getHighestPriorityPackage('jsx-compiler', this.rootContext); + const compiler = require(JSXCompilerPath); + + const compilerOptions = Object.assign({}, compiler.baseOptions, { + resourcePath: this.resourcePath, + outputPath, + sourcePath, + type: 'page', + platform, + sourceFileName: this.resourcePath, + disableCopyNpm, + turnOffSourceMap, + aliasEntries + }); + const rawContentAfterDCE = eliminateDeadCode(content); + + let transformed; + try { + transformed = compiler(rawContentAfterDCE, compilerOptions); + } catch (e) { + console.log(chalk.red(`\n[${platform.name}] Error occured when handling Page ${this.resourcePath}`)); + if (process.env.DEBUG === 'true') { + throw new Error(e); + } else { + const errMsg = e.node ? `${e.message}\nat ${this.resourcePath}` : `Unknown compile error! please check your code at ${this.resourcePath}`; + throw new Error(errMsg); + } + } + + const { style, assets } = await processCSS(transformed.cssFiles, sourcePath); + transformed.style = style; + transformed.assets = assets; + + const pageDistDir = dirname(targetFilePath); + if (!existsSync(pageDistDir)) mkdirpSync(pageDistDir); + + let config = { + ...transformed.config + }; + if (existsSync(pageConfigPath)) { + const pageConfig = readJSONSync(pageConfigPath); + delete pageConfig.usingComponents; + Object.assign(config, pageConfig); + } + if (Array.isArray(transformed.dependencies)) { + transformed.dependencies.forEach(dep => { + this.addDependency(dep); + }); + } + + if (config.usingComponents) { + const usingComponents = {}; + Object.keys(config.usingComponents).forEach(key => { + const value = config.usingComponents[key]; + if (/^c-/.test(key)) { + const result = MINIAPP_PLUGIN_COMPONENTS_REG.test(value) ? value : removeExt(addRelativePathPrefix(relative(dirname(this.resourcePath), value))); + usingComponents[key] = normalizeOutputFilePath(result); + } else { + usingComponents[key] = normalizeOutputFilePath(value); + } + }); + config.usingComponents = usingComponents; + } + + // Only works when developing miniapp plugin, to declare the use of __app_css component + if (injectAppCssComponent) { + const appCssComponentPath = resolve(outputPath, '__app_css', 'index'); + const relativeAppCssComponentPath = addRelativePathPrefix(relative(pageDistDir, appCssComponentPath)); + config.usingComponents = { + '__app_css': relativeAppCssComponentPath, + ...config.usingComponents + }; + } + + const outputContent = { + code: transformed.code, + map: transformed.map, + css: transformed.style || '', + json: config, + template: transformed.template, + assets: transformed.assets, + importComponents: transformed.importComponents, + iconfontMap: transformed.iconfontMap, + imported: transformed.imported, + usingComponents: transformed.usingComponents + + }; + const outputOption = { + outputPath: { + code: distFileWithoutExt + '.js', + json: pageConfigPath, + css: distFileWithoutExt + platform.extension.css, + template: distFileWithoutExt + platform.extension.xml, + assets: outputPath + }, + cache, + mode, + platform, + isTypescriptFile: isTypescriptFile(this.resourcePath), + rootDir, + resourcePath: this.resourcePath + }; + + output(outputContent, content, outputOption); + + const dependencies = []; + Object.keys(transformed.imported).forEach(name => { + if (isCustomComponent(name, transformed.usingComponents)) { + const componentPath = resolve(dirname(resourcePath), name); + dependencies.push({ + name: isFromConstantDir(componentPath) ? name : `${name}?role=component`, // Native miniapp component js file will be loaded by script-loader + options: loaderOptions + }); + } else { + const importedArray = transformed.imported[name]; + let entirePush = false; + importedArray.forEach(importedContent => { + // Component library + if (importedContent.isFromComponentLibrary) { + dependencies.push({ + name, + loader: ScriptLoader, + options: Object.assign({}, loaderOptions, { + importedComponent: importedContent.local + }) + }); + } else { + if (!entirePush) { + dependencies.push({ name }); + entirePush = true; + } + } + }); + } + }); + return [ + `/* Generated by JSX2MP PageLoader, sourceFile: ${this.resourcePath}. */`, + generateDependencies(dependencies), + ].join('\n'); + } }; function createImportStatement(req) { diff --git a/packages/jsx2mp-loader/src/script-loader.js b/packages/jsx2mp-loader/src/script-loader.js index c20622d2..b131cfba 100644 --- a/packages/jsx2mp-loader/src/script-loader.js +++ b/packages/jsx2mp-loader/src/script-loader.js @@ -9,7 +9,8 @@ const { removeExt, doubleBackslash, normalizeOutputFilePath, addRelativePathPref const { isNpmModule, isJSONFile, isTypescriptFile } = require('./utils/judgeModule'); const isMiniappComponent = require('./utils/isMiniappComponent'); const parse = require('./utils/parseRequest'); -const { output, transformCode } = require('./output'); +const { getCache, getCacheDirName } = require('./utils/useCache'); +const { output, transformCode, writeFileWithDirCheck } = require('./output'); const ScriptLoader = __filename; @@ -28,7 +29,7 @@ module.exports = function scriptLoader(content) { } const loaderOptions = getOptions(this); - const { rootDir, disableCopyNpm, outputPath, mode, entryPath, platform, importedComponent = '', isRelativeMiniappComponent = false, aliasEntries, constantDir } = loaderOptions; + const { rootDir, disableCopyNpm, outputPath, mode, entryPath, platform, importedComponent = '', isRelativeMiniappComponent = false, aliasEntries, constantDir, cache } = loaderOptions; const rootContext = this.rootContext; const isJSON = isJSONFile(this.resourcePath); const isAppJSon = this.resourcePath === join(rootContext, 'src', 'app.json'); @@ -43,6 +44,7 @@ module.exports = function scriptLoader(content) { return path.indexOf(rootNodeModulePath) === 0; }); + // console.log('script cache', this.resourcePath); const isFromConstantDir = cached(isFromTargetDirs(constantDir)); const getNpmFolderName = cached(function getNpmName(relativeNpmPath) { @@ -70,10 +72,12 @@ module.exports = function scriptLoader(content) { let outputOption = {}; outputContent = { code: rawContent }; + const outputPathCode = removeExt(distSourcePath) + '.js'; outputOption = { outputPath: { - code: removeExt(distSourcePath) + '.js' + code: outputPathCode }, + cache, mode, externalPlugins: [ [ @@ -91,9 +95,17 @@ module.exports = function scriptLoader(content) { platform, isTypescriptFile: isTypescriptFile(this.resourcePath), rootDir, + resourcePath: this.resourcePath }; - output(outputContent, null, outputOption); + const cacheDirectory = getCacheDirName({ config: cache, mode }); + const cacheContent = getCache({ filePath: this.resourcePath, cacheDirectory: join(rootDir, cacheDirectory) }); + + if (cache && cacheContent && cacheContent.code) { + writeFileWithDirCheck( outputPathCode, cacheContent.code, { rootDir } ); + } else { + output(outputContent, null, outputOption); + } }; const outputDir = (source, target, { isThirdMiniappComponent = false, resourcePath } = {}) => { @@ -235,6 +247,7 @@ module.exports = function scriptLoader(content) { content ].join('\n'); } else { + // console.log('isFromNodeModule', isFromNodeModule); outputFile(rawContent); } } else if (isFromConstantDir(this.resourcePath) && isThirdMiniappComponent) { diff --git a/packages/jsx2mp-loader/src/utils/useCache.js b/packages/jsx2mp-loader/src/utils/useCache.js new file mode 100644 index 00000000..4b2d967d --- /dev/null +++ b/packages/jsx2mp-loader/src/utils/useCache.js @@ -0,0 +1,88 @@ +const fs = require('fs-extra'); +const BJSON = require('buffer-json'); +const path = require('path'); +const mkdirp = require('mkdirp'); +const crypto = require('crypto'); + +const saveCache = (content, { filePath, cacheDirectory }) => { + const stats = fs.statSync(filePath); + const mtime = stats.mtime.getTime(); + const cacheKey = cacheKeyFn({ + cacheDirectory, + filePath, + mtime, + }); + + write(cacheKey, content); +}; + +const getCache = ({ filePath, cacheDirectory }) => { + if (!fs.pathExistsSync(cacheDirectory)) return null; + + const stats = fs.statSync(filePath); + const mtime = stats.mtime.getTime(); + + const cacheKey = cacheKeyFn({ + cacheDirectory, + filePath, + mtime, + }); + + return read(cacheKey); +}; + +const getCacheDirName = ({ config, mode }) => { + return `${typeof config === 'object' && config.cacheDirectory || '.miniCache'}/${mode}`; +}; + +const directories = new Set(); + +function write(key, data, callback) { + const dirname = path.dirname(key); + const content = BJSON.stringify(data); + + if (directories.has(dirname)) { + // for performance skip creating directory + fs.writeFile(key, content, 'utf-8', callback); + } else { + mkdirp(dirname) + .catch((mkdirErr) => { + callback(mkdirErr); + }) + .then(() => { + directories.add(dirname); + fs.writeFile(key, content, 'utf-8', callback); + }); + } +} + +function read(key) { + try { + const content = fs.readFileSync(key, 'utf-8'); + const data = BJSON.parse(content); + return data; + } catch { + return null; + } +} + +function cacheKeyFn(options) { + const { cacheDirectory, filePath, mtime } = options; + const hash = digest(`${filePath}\n${mtime}`); + + return path.join(cacheDirectory, `${hash}.json`); +} + +function digest(str) { + return crypto.createHash('md5').update(str).digest('hex'); +} + +function compare(stats, dep) { + return stats.mtime.getTime() === dep.mtime; +} + +module.exports = { + saveCache, + getCache, + getCacheDirName +}; diff --git a/packages/miniapp-compile-config/src/setAppConfig.js b/packages/miniapp-compile-config/src/setAppConfig.js index d5bde916..5b404bc1 100644 --- a/packages/miniapp-compile-config/src/setAppConfig.js +++ b/packages/miniapp-compile-config/src/setAppConfig.js @@ -22,7 +22,8 @@ module.exports = ( disableCopyNpm = false, turnOffSourceMap = false, constantDir = [], - subPackages = false + subPackages = false, + cache, } = userConfig; const { rootDir, command } = context; const mode = command; @@ -44,6 +45,7 @@ module.exports = ( const originalConstantDir = isPublicFileExist ? ['src/public'].concat(constantDir) : constantDir; const loaderParams = { mode, + cache, entryPath, outputPath, disableCopyNpm, diff --git a/packages/miniapp-compile-config/src/setBaseConfig.js b/packages/miniapp-compile-config/src/setBaseConfig.js index 4a1e96a4..3332813d 100644 --- a/packages/miniapp-compile-config/src/setBaseConfig.js +++ b/packages/miniapp-compile-config/src/setBaseConfig.js @@ -95,7 +95,8 @@ module.exports = ( .options({ ...loaderParams, entryPath, - virtualHost + virtualHost, + rootDir }) .end() .use('platform') @@ -104,7 +105,10 @@ module.exports = ( .end() .use('script') .loader(ScriptLoader) - .options(loaderParams) + .options({ + ...loaderParams, + rootDir + }) .end(); chainConfig.module @@ -114,7 +118,10 @@ module.exports = ( .end() .use('script') .loader(ScriptLoader) - .options(loaderParams) + .options({ + ...loaderParams, + rootDir + }) .end(); chainConfig.module @@ -133,7 +140,10 @@ module.exports = ( .test(/\.json$/) .use('script-loader') .loader(ScriptLoader) - .options(loaderParams) + .options({ + ...loaderParams, + rootDir + }) .end() .use('json-loader') .loader(require.resolve('json-loader')); diff --git a/packages/miniapp-compile-config/src/setComponentConfig.js b/packages/miniapp-compile-config/src/setComponentConfig.js index a5eb68e2..d5b8056c 100644 --- a/packages/miniapp-compile-config/src/setComponentConfig.js +++ b/packages/miniapp-compile-config/src/setComponentConfig.js @@ -11,7 +11,8 @@ module.exports = ( const platformInfo = platformMap[target]; const { turnOffSourceMap = false, - constantDir = [] + constantDir = [], + cache } = userConfig; const { rootDir, command } = context; @@ -25,11 +26,13 @@ module.exports = ( const loaderParams = { mode: command, + cache, entryPath, outputPath, disableCopyNpm, turnOffSourceMap, platform: platformInfo, + rootDir }; config.entryPoints.clear();