Commit 47481cbd authored by Benjamin Franzke's avatar Benjamin Franzke
Browse files

[TASK] Migrate backend TypeScript to ECMAScript modules (ESM)

Use the JavaScript module and importmap Feature introduced
in #96510 to import JavaScript as native browser modules
(commonly referred to as ES6 modules, or short ESM).
To be specific: we actually use ES2020 (ES11) modules,
as we import modules dynamically via import() in
combination with an importmap (#96510).

Backwards compatibility for existing RequireJS imports
is provided by a shim as introduced in #96510.

How to (re)view this change
---------------------------

Exclude all automatic TypeScript/JavaScript/lockfile changes with:

git show --oneline -- . \
    ':(exclude)typo3/sysext/*.js' \
    ':(exclude)Build/Sources/TypeScript/*.ts' \
    ':(exclude)Build/yarn.lock'

git show --oneline -- $(find Build/ \
    -name DragUploader.ts -o \
    -name FormEngine.ts -o \
    -name Helper.ts -o \
    -name CKEditorLoader.ts -o \
    -name CodeMirrorElement.ts)

Gruntfile & tsconfig.json
-------------------------

Gruntfile is adapted to transform all our dependencies into ES6
modules.

Also tsconfig is adapted to emit es2020 modules (es2020 is required
for dynamic module imports with import()).
Also allowSyntheticDefaultImports is enabled while esModuleInterop
is now longer needed as we target ES modules and therefore removed.
allowSyntheticDefaultImports had been enabled implicitly by
esModuleInterop, we still need this enabled as the type declarations
of our dependencies still imply AMD/CJS exports
(which we fix in our build steps).

JSUnit
------

JSUnit configuration is moved from testing-framework into core for more
flexibility when adaptions are required. The testing is revamped
to use karma-rollup-preprocessor instead of karma-requirejs in
order to support the newly compiled ES6 modules.

TypeScript Exports and Imports
------------------------------

All export and import definitions have been auto-converted
from AMD pseudo import/exports to native ES6 import/exports.

Executed commands:

  find Build/Sources/TypeScript/ -name '*.ts' -exec sed -i \
    -e 's/export =/export default/' \
    -e 's/ = require(\(.*\))/ from \1/g' \
    -e "s/^import 'tablesort';/import Tablesort from 'tablesort';/" \
    '{}' +

Note that the switch from `export =` to `export default` is
not considered breaking because the require-adapter in
requirejs-loader wil transparently resolve the .default in moduls.
This is possible because AMD modules could not use `export =`
and `export {}` (for named exports) at the same time.

Explicitly use RequireJS for CodeMirror and ext:form for now
------------------------------------------------------------

CodeMirror can't be easily transformed to ES6 right now,
therefore this patch explicitly configures to use the
global RequireJS instance.

The ext:form JavaScript is currently not written in TypeScript
and will therefore not be automatically transformed from AND
to ES6 when updating tsconfig.json to emit ES6 modules.
Therefore requirejs is uses.

This partially reverts #96326 which changed some require()
calls to import() syntax too early.

New npm dependencies:
---------------------

 # For module-postprocessing in Gruntfile
 # This is the same parser as used for the
 # frontend polyfill es-module-shims
yarn add --dev es-module-lexer

 # A rollup bundle for unit testing in karma is created
 # usin a karma preprocessor
yarn remove karma-requirejs
yarn add --dev karma-rollup-preprocessor rollup-plugin-glob-import

 # Update grunt-terser to support es2020 syntax
 # (uses terser v5 which we already depended on, but grunt v1
 # used the older v4 version)
yarn add --dev grunt-terser@^2.0.0

Releases: main
Resolves: #96511
Related: #96510
Related: #96323
Change-Id: Id1a6d414c1e9b2e39227d0b6ce9e79333a372349
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72637


Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
parent 425f94f5
......@@ -14,6 +14,7 @@
module.exports = function (grunt) {
const sass = require('node-sass');
const esModuleLexer = require('es-module-lexer');
/**
* Grunt stylefmt task
......@@ -222,6 +223,20 @@ module.exports = function (grunt) {
punctuation: ''
},
ts_files: {
options: {
process: (source, srcpath) => {
const [imports, exports] = esModuleLexer.parse(source, srcpath);
source = require('./util/map-import.js').mapImports(source, srcpath, imports);
// Workaround for https://github.com/microsoft/TypeScript/issues/35802 to avoid
// rollup from complaining in karma/jsunit test setup:
// The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten
source = source.replace('__decorate=this&&this.__decorate||function', '__decorate=function');
return source;
}
},
files: [{
expand: true,
cwd: '<%= paths.root %>Build/JavaScript/',
......@@ -308,6 +323,43 @@ module.exports = function (grunt) {
}
]
},
lit: {
options: {
process: (content, srcpath) => content.replace(/\/\/# sourceMappingURL=[^ ]+/, '')
},
files: [{
expand: true,
cwd: '<%= paths.node_modules %>',
dest: '<%= paths.core %>Public/JavaScript/Contrib/',
rename: function (dest, src) {
let [packageName, ...paths] = src.split('/');
let packageJson = `${grunt.config.get('paths.node_modules')}/${packageName}/package.json`;
let scopedPackageJson = `${grunt.config.get('paths.node_modules')}/${packageName}/${paths[0]}/package.json`;
let version = '';
if (grunt.file.exists(packageJson)) {
version = '@' + grunt.file.readJSON(packageJson).version;
} else if (grunt.file.exists(scopedPackageJson)) {
version = '@' + grunt.file.readJSON(scopedPackageJson).version;
packageName += '/' + paths[0];
paths.shift();
}
return `${dest}${packageName}${version}/${paths.join('/')}`;
},
src: [
'lit/*.js',
'lit/decorators/*.js',
'lit/directives/*.js',
'lit-html/*.js',
'lit-html/directives/*.js',
'lit-element/*.js',
'lit-element/decorators/*.js',
'@lit/reactive-element/*.js',
'@lit/reactive-element/decorators/*.js',
],
}]
},
t3editor: {
files: [
{
......@@ -326,87 +378,57 @@ module.exports = function (grunt) {
},
rollup: {
options: {
format: 'amd'
format: 'esm',
entryFileNames: '[name].js'
},
'lit-html': {
'd3-selection': {
options: {
preserveModules: true,
preserveModules: false,
plugins: () => [
require('@rollup/plugin-replace')({ values: { 'globalThis': 'window' }, preventAssignment: false }),
{
name: 'terser',
renderChunk: code => require('terser').minify(code, {...grunt.config.get('terser.options'), ...{mangle: false}})
renderChunk: code => require('terser').minify(code, grunt.config.get('terser.options'))
}
]
},
files: {
'<%= paths.core %>Public/JavaScript/Contrib/lit-html': [
'node_modules/lit-html/*.js',
'node_modules/lit-html/directives/*.js',
]
}
},
'@lit/reactive-element': {
options: {
preserveModules: true,
plugins: () => [
require('@rollup/plugin-replace')({ values: { 'globalThis': 'window' }, preventAssignment: false }),
{
name: 'terser',
renderChunk: code => require('terser').minify(code, {...grunt.config.get('terser.options'), ...{mangle: false}})
},
]
},
files: {
'<%= paths.core %>Public/JavaScript/Contrib/@lit/reactive-element': [
'node_modules/@lit/reactive-element/*.js',
'node_modules/@lit/reactive-element/decorators/*.js',
// omitted, empty
'!node_modules/@lit/reactive-element/reactive-controller.js'
'<%= paths.core %>Public/JavaScript/Contrib/d3-selection.js': [
'node_modules/d3-selection/src/index.js'
]
}
},
'lit-element': {
'd3-dispatch': {
options: {
preserveModules: true,
preserveModules: false,
plugins: () => [
require('@rollup/plugin-replace')({ values: { 'globalThis': 'window' }, preventAssignment: false }),
{
name: 'terser',
renderChunk: code => require('terser').minify(code, {...grunt.config.get('terser.options'), ...{mangle: false}})
},
{
name: 'externals',
resolveId: (source) => {
if (source.startsWith('lit-html') || source.startsWith('@lit/reactive-element')) {
return {id: source.replace(/\.js$/, ''), external: true}
}
return null
}
renderChunk: code => require('terser').minify(code, grunt.config.get('terser.options'))
}
]
},
files: {
'<%= paths.core %>Public/JavaScript/Contrib/lit-element': [
'node_modules/lit-element/*.js',
'node_modules/lit-element/decorators/*.js',
'<%= paths.core %>Public/JavaScript/Contrib/d3-dispatch.js': [
'node_modules/d3-dispatch/src/index.js'
]
}
},
'lit': {
'd3-drag': {
options: {
preserveModules: true,
preserveModules: false,
plugins: () => [
require('@rollup/plugin-replace')({ values: { 'globalThis': 'window' }, preventAssignment: false }),
{
name: 'terser',
renderChunk: code => require('terser').minify(code, {...grunt.config.get('terser.options'), ...{mangle: false}})
renderChunk: code => require('terser').minify(code, grunt.config.get('terser.options'))
},
{
name: 'externals',
resolveId: (source) => {
if (source.startsWith('lit-html') || source.startsWith('lit-element') || source.startsWith('@lit/reactive-element')) {
return {id: source.replace(/\.js$/, ''), external: true}
if (source === 'd3-selection') {
return {id: 'd3-selection', external: true}
}
if (source === 'd3-dispatch') {
return {id: 'd3-dispatch', external: true}
}
return null
}
......@@ -414,10 +436,8 @@ module.exports = function (grunt) {
]
},
files: {
'<%= paths.core %>Public/JavaScript/Contrib/lit': [
'node_modules/lit/*.js',
'node_modules/lit/decorators/*.js',
'node_modules/lit/directives/*.js',
'<%= paths.core %>Public/JavaScript/Contrib/d3-drag.js': [
'node_modules/d3-drag/src/index.js'
]
}
},
......@@ -447,7 +467,7 @@ module.exports = function (grunt) {
]
},
files: {
'<%= paths.core %>Public/JavaScript/Contrib/bootstrap/bootstrap.js': [
'<%= paths.core %>Public/JavaScript/Contrib/bootstrap.js': [
'Sources/JavaScript/core/Resources/Public/JavaScript/Contrib/bootstrap.js'
]
}
......@@ -494,7 +514,21 @@ module.exports = function (grunt) {
},
dashboard: {
options: {
destPrefix: "<%= paths.dashboard %>Public"
destPrefix: "<%= paths.dashboard %>Public",
copyOptions: {
process: (source, srcpath) => {
if (srcpath.match(/.*\.js$/)) {
const imports = [];
if (srcpath === 'node_modules/chart.js/dist/Chart.min.js') {
imports.push('moment');
}
return require('./util/cjs-to-esm.js').cjsToEsm(source);
}
return source;
}
}
},
files: {
'JavaScript/Contrib/muuri.js': 'muuri/dist/muuri.min.js',
......@@ -503,39 +537,117 @@ module.exports = function (grunt) {
'Css/Contrib/chart.css': 'chart.js/dist/Chart.min.css'
}
},
all: {
umdToEs6: {
options: {
destPrefix: "<%= paths.core %>Public/JavaScript/Contrib"
destPrefix: "<%= paths.core %>Public/JavaScript/Contrib",
copyOptions: {
process: (source, srcpath) => {
let imports = [], prefix = '', suffix = '';
if (srcpath === 'node_modules/devbridge-autocomplete/dist/jquery.autocomplete.min.js') {
imports.push('jquery');
}
if (srcpath === 'node_modules/@claviska/jquery-minicolors/jquery.minicolors.min.js') {
imports.push('jquery');
}
if (srcpath === 'node_modules/imagesloaded/imagesloaded.js') {
imports.push('ev-emitter');
}
if (srcpath === 'node_modules/tablesort/dist/sorts/tablesort.dotsep.min.js') {
prefix = 'import Tablesort from "tablesort";';
}
if (srcpath === 'node_modules/jquery/dist/jquery.js') {
// Provide jQuery global "window.$" (and window.jQuery) for BC
// Still used by ./typo3/sysext/core/Tests/Acceptance/Application/FileList/FileStorageTreeFilterCest.php
// Also has been suggested to be used in #79221 and #82378
// @todo: deprecate $/jQuery global (note: PageRenderer->loadJQuery
// or maybe not and simply remove? Since PageRenderer->loadJQuery has long
// been deprecated with #86438
suffix = 'window.$ = window.jQuery = module.exports;';
}
return require('./util/cjs-to-esm.js').cjsToEsm(source, imports, prefix, suffix);
}
}
},
files: {
'autosize.js': 'autosize/dist/autosize.min.js',
'broadcastchannel.js': 'broadcastchannel-polyfill/index.js',
'ev-emitter.js': 'ev-emitter/ev-emitter.js',
'flatpickr/flatpickr.min.js': 'flatpickr/dist/flatpickr.js',
'flatpickr/locales.js': 'flatpickr/dist/l10n/index.js',
'imagesloaded.js': 'imagesloaded/imagesloaded.js',
'jquery.js': 'jquery/dist/jquery.js',
'jquery.autocomplete.js': 'devbridge-autocomplete/dist/jquery.autocomplete.min.js',
'jquery/minicolors.js': '../node_modules/@claviska/jquery-minicolors/jquery.minicolors.min.js',
'moment.js': 'moment/min/moment-with-locales.min.js',
'moment-timezone.js': 'moment-timezone/builds/moment-timezone-with-data.min.js',
'nprogress.js': 'nprogress/nprogress.js',
'sortablejs.js': 'sortablejs/dist/sortable.umd.js',
'tablesort.js': 'tablesort/dist/tablesort.min.js',
'tablesort.dotsep.js': 'tablesort/dist/sorts/tablesort.dotsep.min.js',
'require.js': 'requirejs/require.js',
'moment.js': 'moment/min/moment-with-locales.min.js',
'moment-timezone.js': 'moment-timezone/builds/moment-timezone-with-data.min.js',
'cropper.min.js': 'cropperjs/dist/cropper.min.js',
'imagesloaded.pkgd.min.js': 'imagesloaded/imagesloaded.pkgd.min.js',
'autosize.js': 'autosize/dist/autosize.min.js',
'taboverride.js': 'taboverride/build/output/taboverride.js',
'broadcastchannel-polyfill.js': 'broadcastchannel-polyfill/index.js',
'es-module-shims.js': 'es-module-shims/dist/es-module-shims.js',
'flatpickr/flatpickr.min.js': 'flatpickr/dist/flatpickr.min.js',
'flatpickr/locales.js': 'flatpickr/dist/l10n/index.js',
'jquery.minicolors.js': '../node_modules/@claviska/jquery-minicolors/jquery.minicolors.min.js',
'../../../../../backend/Resources/Public/Images/colorpicker/jquery.minicolors.png': '../node_modules/@claviska/jquery-minicolors/jquery.minicolors.png',
'jquery.autocomplete.js': '../node_modules/devbridge-autocomplete/dist/jquery.autocomplete.js',
'd3-dispatch.js': 'd3-dispatch/dist/d3-dispatch.min.js',
'd3-drag.js': 'd3-drag/dist/d3-drag.min.js',
'd3-selection.js': 'd3-selection/dist/d3-selection.min.js',
/**
* copy needed parts of jquery
*/
'jquery/jquery.js': 'jquery/dist/jquery.js',
'jquery/jquery.min.js': 'jquery/dist/jquery.min.js',
/**
* copy needed parts of jquery-ui
*/
}
},
install: {
options: {
destPrefix: "<%= paths.install %>Public/JavaScript",
copyOptions: {
process: (source, srcpath) => {
if (srcpath === 'node_modules/chosen-js/chosen.jquery.js') {
source = 'import jQuery from \'jquery\';\n' + source;
}
return source;
}
}
},
files: {
'chosen.jquery.min.js': 'chosen-js/chosen.jquery.js',
}
},
jqueryUi: {
options: {
destPrefix: "<%= paths.core %>Public/JavaScript/Contrib",
copyOptions: {
process: (source, srcpath) => {
const imports = {
core: [],
draggable: ['core', 'mouse', 'widget'],
droppable: ['core', 'widget', 'mouse', 'draggable'],
mouse: ['widget'],
position: [],
resizable: ['core', 'mouse', 'widget'],
selectable: ['core', 'mouse', 'widget'],
sortable: ['core', 'mouse', 'widget'],
widget: []
};
const moduleName = require('path').basename(srcpath, '.js');
const code = [
'import jQuery from "jquery";',
];
if (moduleName in imports) {
imports[moduleName].forEach(importName => {
code.push('import "jquery-ui/' + importName + '.js";');
});
}
code.push('let define = null;');
code.push(source);
return code.join('\n');
}
}
},
files: {
'jquery-ui/core.js': 'jquery-ui/ui/core.js',
'jquery-ui/draggable.js': 'jquery-ui/ui/draggable.js',
'jquery-ui/droppable.js': 'jquery-ui/ui/droppable.js',
......@@ -545,7 +657,17 @@ module.exports = function (grunt) {
'jquery-ui/selectable.js': 'jquery-ui/ui/selectable.js',
'jquery-ui/sortable.js': 'jquery-ui/ui/sortable.js',
'jquery-ui/widget.js': 'jquery-ui/ui/widget.js',
'Sortable.min.js': 'sortablejs/dist/sortable.umd.js'
}
},
all: {
options: {
destPrefix: "<%= paths.core %>Public/JavaScript/Contrib"
},
files: {
'require.js': 'requirejs/require.js',
'cropperjs.js': 'cropperjs/dist/cropper.esm.js',
'es-module-shims.js': 'es-module-shims/dist/es-module-shims.js',
'../../../../../backend/Resources/Public/Images/colorpicker/jquery.minicolors.png': '../node_modules/@claviska/jquery-minicolors/jquery.minicolors.png',
}
}
},
......@@ -558,7 +680,11 @@ module.exports = function (grunt) {
thirdparty: {
files: {
"<%= paths.core %>Public/JavaScript/Contrib/es-module-shims.js": ["<%= paths.core %>Public/JavaScript/Contrib/es-module-shims.js"],
"<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel-polyfill.js": ["<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel-polyfill.js"],
"<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel.js": ["<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel.js"],
"<%= paths.core %>Public/JavaScript/Contrib/cropperjs.js": ["<%= paths.core %>Public/JavaScript/Contrib/cropperjs.js"],
"<%= paths.core %>Public/JavaScript/Contrib/imagesloaded.js": ["<%= paths.core %>Public/JavaScript/Contrib/imagesloaded.js"],
"<%= paths.core %>Public/JavaScript/Contrib/ev-emitter.js": ["<%= paths.core %>Public/JavaScript/Contrib/ev-emitter.js"],
"<%= paths.core %>Public/JavaScript/Contrib/flatpickr/flatpickr.min.js": ["<%= paths.core %>Public/JavaScript/Contrib/flatpickr/flatpickr.min.js"],
"<%= paths.core %>Public/JavaScript/Contrib/flatpickr/locales.js": ["<%= paths.core %>Public/JavaScript/Contrib/flatpickr/locales.js"],
"<%= paths.core %>Public/JavaScript/Contrib/require.js": ["<%= paths.core %>Public/JavaScript/Contrib/require.js"],
"<%= paths.core %>Public/JavaScript/Contrib/nprogress.js": ["<%= paths.core %>Public/JavaScript/Contrib/nprogress.js"],
......@@ -572,7 +698,7 @@ module.exports = function (grunt) {
"<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/selectable.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/selectable.js"],
"<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/sortable.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/sortable.js"],
"<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/widget.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/widget.js"],
"<%= paths.install %>Public/JavaScript/chosen.jquery.min.js": ["<%= paths.node_modules %>chosen-js/chosen.jquery.js"]
"<%= paths.install %>Public/JavaScript/chosen.jquery.min.js": ["<%= paths.install %>Public/JavaScript/chosen.jquery.min.js"]
}
},
t3editor: {
......@@ -645,7 +771,7 @@ module.exports = function (grunt) {
}
},
concurrent: {
npmcopy: ['npmcopy:ckeditor', 'npmcopy:ckeditor_externalplugins', 'npmcopy:dashboard', 'npmcopy:all'],
npmcopy: ['npmcopy:ckeditor', 'npmcopy:ckeditor_externalplugins', 'npmcopy:dashboard', 'npmcopy:umdToEs6', 'npmcopy:jqueryUi', 'npmcopy:install', 'npmcopy:all'],
lint: ['eslint', 'stylelint', 'lintspaces'],
compile_assets: ['scripts', 'css'],
minify_assets: ['terser:thirdparty', 'terser:t3editor'],
......
if (typeof globalThis.TYPO3 === 'undefined') {
globalThis.TYPO3 = globalThis.TYPO3 || {};
globalThis.TYPO3.settings = {
'FormEngine': {
'formName': 'Test'
},
'DateTimePicker': {
'DateFormat': 'd.m.Y'
},
'ajaxUrls': {
}
};
globalThis.TYPO3.lang = {};
}
top.TYPO3 = globalThis.TYPO3;
globalThis.TBE_EDITOR = {};
'use strict';
/**
* Helper function to implement DataProvider
* @param {Function|Array|Object} values
* @param {Function} func
*/
function using(values, func) {
if (values instanceof Function) {
values = values();
}
if (values instanceof Array) {
values.forEach(function(value) {
if (!(value instanceof Array)) {
value = [value];
}
func.apply(this, value);
});
} else {
var objectKeys = Object.keys(values);
objectKeys.forEach(function(key) {
if (!(values[key] instanceof Array)) {
values[key] = [values[key]];
}
values[key].push(key);
func.apply(this, values[key]);
});
}
}
import jQuery from 'jquery';
globalThis.TYPO3 = globalThis.TYPO3 || {};
globalThis.TYPO3.jQuery = jQuery;
import './JQueryGlobal.js'
// glob-import interpolated by rollup-plugin-glob-import
import '../../typo3/sysext/**/Tests/JavaScript/**/*.js'
module.exports = function (config) {
require('./karma.conf.js')(config);
config.set({
browsers: ['ChromeHeadlessCustom'],
customLaunchers: {
ChromeHeadlessCustom: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
});
};
'use strict';
const fs = require('fs')
const globImport = require('rollup-plugin-glob-import');
/**
* Karma configuration
*/
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '../../',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
{ pattern: 'Build/JSUnit/Globals.js' },
{ pattern: 'Build/JSUnit/Helper.js' },
{ pattern: 'Build/JSUnit/TestSetup.js', watched: false },
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'Build/JSUnit/TestSetup.js': ['coverage', 'rollup'],
},
rollupPreprocessor: {
/**
* This is just a normal Rollup config object,
* except that `input` is handled for you.
*/
plugins: [
globImport({}),
{
name: 'resolve-modules',
resolveId: (source) => {
const importMap = {
'lit': 'lit@2.0.0/index',
'lit/': 'lit@2.0.0/',
'lit-html': 'lit-html@2.0.0/lit-html',
'lit-html/': 'lit-html@2.0.0/',
'lit-element': 'lit-element@3.0.0/lit-element',
'lit-element/': 'lit-element@3.0.0/',
'@lit/reactive-element': '@lit/reactive-element@1.0.0/reactive-element',
'@lit/reactive-element/': '@lit/reactive-element@1.0.0/',
};
if (source in importMap) {
source = importMap[source];
} else {
for (let prefix in importMap) {
if (prefix.endsWith('/') && source.startsWith(prefix)) {
source = importMap[prefix] + source.substring(prefix.length)
break;
}
}
}
if (source.startsWith('TYPO3/CMS')) {
const parts = source.substr(10).split('/');
const extension = parts.shift().split(/(?=[A-Z])/).join('_').toLowerCase();
const path = parts.join('/');
const fullPath = `typo3/sysext/${extension}/Resources/Public/JavaScript/${path}`;
return {id: fullPath}
}
let contribPath = `typo3/sysext/core/Resources/Public/JavaScript/Contrib/${source}.js`;
if (fs.existsSync(contribPath)) {
return {id: contribPath}
}
contribPath = `typo3/sysext/core/Resources/Public/JavaScript/Contrib/${source}`;
if (fs.existsSync(contribPath)) {
return {id: contribPath}
}
return null
}
}
],
output: {
format: 'iife',
name: 'TYPO3UnitTestBundle',
sourcemap: 'inline',
inlineDynamicImports: true,
},
},
// test results reporter to use
// possible values: 'dots', 'progress', 'coverage', 'junit'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'junit', 'coverage'],
junitReporter: {
outputDir: 'typo3temp/var/tests/',