@lwc/jest-transformer
Version:
Jest transformer for LWC and engine
207 lines (180 loc) • 7.44 kB
JavaScript
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const path = require('path');
const crypto = require('crypto');
const semver = require('semver');
const { isKnownScopedCssFile } = require('@lwc/jest-shared');
const MagicString = require('magic-string');
const babelCore = require('@babel/core');
const lwcCompiler = require('@lwc/compiler');
const { generateScopeTokens } = require('@lwc/template-compiler');
const jestPreset = require('babel-preset-jest');
const babelCommonJs = require('@babel/plugin-transform-modules-commonjs');
const babelDynamicImport = require('@babel/plugin-proposal-dynamic-import');
const babelClassProperties = require('@babel/plugin-syntax-class-properties');
const babelSyntaxDecorators = require('@babel/plugin-syntax-decorators');
const babelTsPreset = require.resolve('@babel/preset-typescript');
const compilerVersion = require('@lwc/compiler/package.json').version;
const apexScopedImport = require('./transforms/apex-scoped-import');
const apexContinuationScopedImport = require('./transforms/apex-continuation-scoped-import');
const customPermissionImport = require('./transforms/custom-permission-scoped-import');
const i18nScopedImport = require('./transforms/i18n-scoped-import');
const labelScopedImport = require('./transforms/label-scoped-import');
const resourceScopedImport = require('./transforms/resource-scoped-import');
const contentAssetUrlScopedImport = require('./transforms/content-asset-url-scoped-import');
const schemaScopedImport = require('./transforms/schema-scoped-import');
const userScopedImport = require('./transforms/user-scoped-import');
const userPermissionImport = require('./transforms/user-permission-scoped-import');
const clientScopedImport = require('./transforms/client-scoped-import');
const messageChannelScopedImport = require('./transforms/message-channel-scoped-import');
const accessCheck = require('./transforms/access-check-scoped-import');
const siteScopedImport = require('./transforms/site-scoped-import');
const importMeta = require('./transforms/import-meta');
const BABEL_TS_CONFIG = {
sourceMaps: 'inline',
babelrc: false,
configFile: false,
plugins: [
babelClassProperties,
[
babelSyntaxDecorators,
{
decoratorsBeforeExport: true,
},
],
],
presets: [[babelTsPreset, { allowDeclareFields: true }]],
};
const BABEL_CONFIG = {
sourceMaps: 'both',
presets: [jestPreset],
plugins: [
babelDynamicImport,
babelCommonJs,
apexScopedImport,
apexContinuationScopedImport,
customPermissionImport,
i18nScopedImport,
labelScopedImport,
contentAssetUrlScopedImport,
resourceScopedImport,
schemaScopedImport,
userScopedImport,
userPermissionImport,
clientScopedImport,
messageChannelScopedImport,
accessCheck,
siteScopedImport,
importMeta,
],
};
function isTypeScript(filePath) {
return path.extname(filePath) === '.ts';
}
function transformTypeScript(src, filePath) {
const { code } = babelCore.transform(src, {
...BABEL_TS_CONFIG,
filename: filePath,
});
return code;
}
function transformLWC(src, filePath, isSSR) {
if (isTypeScript(filePath)) {
src = transformTypeScript(src, filePath);
}
// Set default module name and namespace value for the namespace because it can't be properly guessed from the path
const compilerOptions = {
name: 'test',
namespace: 'x',
outputConfig: {
sourcemap: true,
},
experimentalDynamicComponent: {
strictSpecifier: false,
},
scopedStyles: isKnownScopedCssFile(filePath),
enableDynamicComponents: true,
enableLwcOn: true,
/**
* Prevent causing tons of warning log lines.
* @see {@link https://github.com/salesforce/lwc/pull/3544}
* @see {@link https://github.com/salesforce/lwc/releases/tag/v2.49.1}
*/
...(semver.lt(compilerVersion, '2.49.1') ? { enableLwcSpread: true } : {}),
};
const ssrMode = process.env.LWC_SSR_MODE || 'v2';
if (isSSR) {
if (ssrMode !== 'v1') {
compilerOptions.targetSSR = true;
compilerOptions.ssrMode = 'sync';
}
}
const { code, map, warnings } = lwcCompiler.transformSync(src, filePath, compilerOptions);
const cssScopeTokens = filePath.endsWith('.html')
? generateScopeTokens(filePath, 'x', 'test').cssScopeTokens
: undefined;
// Log compiler warnings, if any
if (warnings && warnings.length > 0) {
warnings.forEach((warning) => {
console.warn(`\x1b[33m[LWC Warn]\x1b[0m(${filePath}): ${warning?.message ?? warning}`);
});
}
// if is not .js, we add the .compiled extension in the sourcemap
const filename = path.extname(filePath) === '.js' ? filePath : filePath + '.compiled';
// **Note: .html and .css don't return valid sourcemaps cause they are used for rollup
const config = map && map.version ? { inputSourceMap: map } : {};
let result = babelCore.transform(code, { ...BABEL_CONFIG, ...config, filename });
if (cssScopeTokens) {
// Modify the code so that it calls into @lwc/jest-shared and adds the scope token as a
// known scope token so we can replace it later.
// Note we have to modify the code rather than use @lwc/jest-shared directly because
// the transformer does not run in the same Node process as the serializer.
const magicString = new MagicString(result.code);
// lwc-test may live in a different directory from the component module code, so
// we need to provide an absolute path
const jestSharedPath = require.resolve('@lwc/jest-shared');
magicString.append(
`\nconst { addKnownScopeToken } = require(${JSON.stringify(jestSharedPath)});`
);
for (const scopeToken of cssScopeTokens) {
magicString.append(`\naddKnownScopeToken(${JSON.stringify(scopeToken)});`);
}
const map = magicString.generateMap({
source: filePath,
includeContent: true,
});
const modifiedCode = magicString.toString() + `\n//# sourceMappingURL=${map.toUrl()}\n`;
result = {
...result,
code: modifiedCode,
map,
};
}
return result;
}
module.exports = {
process(src, filePath) {
return transformLWC(src, filePath, false);
},
getCacheKey(sourceText, sourcePath, ...rest) {
let configString;
let transformConfig;
if (rest.length === 1) {
transformConfig = rest[0];
configString = transformConfig.configString;
} else {
throw new Error('Unexpected transform arguments.');
}
const { NODE_ENV } = process.env;
return crypto
.createHash('md5')
.update(JSON.stringify(configString), 'utf8')
.update(sourceText + sourcePath + configString + NODE_ENV + compilerVersion, 'utf8')
.digest('hex');
},
};
module.exports.transformLwc = transformLWC;