mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 13:56:44 +00:00
CM6: Switch to using Rollup instead of Webpack; make RL-compatible
See https://w.wiki/9Twh for example usage with ResourceLoader. Webpack is retired in favor of Rollup, which allows us to convert the ECMAScript Modules into CommonJS modules for use by ResourceLoader. We now have a file in dist/ for each RL module that we want to offer, including the 'lib' module which includes the CM library itself. Because Rollup has no knowledge of the ResourceLoader module registry, the generated output requires other modules via relative path, when it needs to be the RL module name. To get around this, we do a crude find/replace after the files are generated. Hacky, but necessary to make CodeMirror usable by gadgets and scripts that don't also want WikiEditor. Add new RL modules 'ext.CodeMirror.v6.lib' (vendor code) and 'ext.CodeMirror.v6' (the main CodeMirror class, sans WikiEditor). Clean up extension.json, listing the v6 modules beneath the old ones. Bug: T214989 Change-Id: Ide716247e545cf2bdd977bea645729564ebbe6e2
This commit is contained in:
parent
00f947e97f
commit
ca02360228
|
@ -31,7 +31,6 @@ _NOTE: Consider using [Fresh](https://gerrit.wikimedia.org/g/fresh/) to run thes
|
|||
* `npm run test:i18n` for linting of i18n messages with banana-checker.
|
||||
* `npm run test:unit` for the new Jest unit tests.
|
||||
* `npm run selenium-test` for the Selenium tests.
|
||||
* `npm run test:bundlesize` to test if the gzip'd entrypoint is of acceptable size.
|
||||
|
||||
Older QUnit tests are in `resources/mode/mediawiki/tests/qunit/`. These will
|
||||
eventually be moved over to `tests/qunit` and rewritten for CodeMirror 6.
|
||||
|
|
|
@ -166,32 +166,24 @@
|
|||
"codemirror-toggle-label"
|
||||
]
|
||||
},
|
||||
"ext.CodeMirror.v6.WikiEditor": {
|
||||
"ext.CodeMirror.v6": {
|
||||
"dependencies": [
|
||||
"ext.wikiEditor",
|
||||
"web2017-polyfills",
|
||||
"mediawiki.api",
|
||||
"mediawiki.user",
|
||||
"user.options",
|
||||
"ext.CodeMirror.v6.messages"
|
||||
"ext.CodeMirror.v6.lib"
|
||||
],
|
||||
"packageFiles": [
|
||||
"dist/main.js",
|
||||
"dist/codemirror.js",
|
||||
{
|
||||
"name": "ext.CodeMirror.data.js",
|
||||
"callback": "MediaWiki\\Extension\\CodeMirror\\DataScript::makeScript"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"ext.CodeMirror.v6.less",
|
||||
"mode/mediawiki/mediawiki.less",
|
||||
"mode/mediawiki/colorblind-colors.less"
|
||||
"ext.CodeMirror.v6.less"
|
||||
],
|
||||
"messages": [
|
||||
"codemirror-toggle-label"
|
||||
]
|
||||
},
|
||||
"ext.CodeMirror.v6.messages": {
|
||||
"messages": [
|
||||
"codemirror-find",
|
||||
"codemirror-next",
|
||||
|
@ -231,6 +223,38 @@
|
|||
"codemirror-unfold",
|
||||
"codemirror-folded-code"
|
||||
]
|
||||
},
|
||||
"ext.CodeMirror.v6.lib": {
|
||||
"packageFiles": [
|
||||
"dist/vendor.js"
|
||||
],
|
||||
"dependencies": [
|
||||
"web2017-polyfills"
|
||||
]
|
||||
},
|
||||
"ext.CodeMirror.v6.mode.mediawiki": {
|
||||
"packageFiles": "dist/codemirror.mode.mediawiki.js",
|
||||
"styles": [
|
||||
"mode/mediawiki/mediawiki.less",
|
||||
"mode/mediawiki/colorblind-colors.less"
|
||||
],
|
||||
"dependencies": [
|
||||
"ext.CodeMirror.v6",
|
||||
"ext.CodeMirror.v6.lib"
|
||||
]
|
||||
},
|
||||
"ext.CodeMirror.v6.WikiEditor": {
|
||||
"dependencies": [
|
||||
"ext.wikiEditor",
|
||||
"ext.CodeMirror.v6.lib",
|
||||
"ext.CodeMirror.v6.mode.mediawiki"
|
||||
],
|
||||
"packageFiles": [
|
||||
"dist/codemirror.wikieditor.mediawiki.js"
|
||||
],
|
||||
"messages": [
|
||||
"codemirror-toggle-label"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ResourceFileModulePaths": {
|
||||
|
|
2955
package-lock.json
generated
2955
package-lock.json
generated
File diff suppressed because it is too large
Load diff
33
package.json
33
package.json
|
@ -2,15 +2,14 @@
|
|||
"name": "codemirror",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "webpack -w --mode=development",
|
||||
"build": "webpack --mode=production",
|
||||
"test": "npm run test:lint && npm run test:unit && npm run check-built-assets && bundlesize",
|
||||
"start": "rollup -c --watch",
|
||||
"build": "rollup -c",
|
||||
"test": "npm run test:lint && npm run test:unit && npm run check-built-assets",
|
||||
"test:lint": "npm run test:lint:styles && npm run test:lint:js && npm run test:lint:i18n",
|
||||
"test:lint:js": "eslint --cache .",
|
||||
"test:lint:styles": "stylelint \"resources/**/*.less\"",
|
||||
"test:lint:i18n": "banana-checker i18n/",
|
||||
"test:unit": "jest",
|
||||
"test:bundlesize": "bundlesize",
|
||||
"check-built-assets": "{ git status src/ | grep \"nothing to commit, working tree clean\"; } && { echo 'CHECKING BUILD SOURCES ARE COMMITTED' && npm run build && git status resources/dist/ | grep \"nothing to commit, working tree clean\" || { npm run node-debug; false; }; }",
|
||||
"node-debug": "node -v && npm -v && echo 'ERROR: Please ensure that production assets have been built with `npm run build` and commited, and that you are using the correct version of Node/NPM.'",
|
||||
"selenium-test": "wdio tests/selenium/wdio.conf.js"
|
||||
|
@ -19,40 +18,32 @@
|
|||
"node": "18.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.20",
|
||||
"@babel/plugin-transform-private-methods": "^7.22.5",
|
||||
"@babel/plugin-transform-runtime": "7.22.15",
|
||||
"@babel/preset-env": "7.3.0",
|
||||
"@babel/plugin-transform-private-methods": "7.23.3",
|
||||
"@babel/preset-env": "7.24.0",
|
||||
"@codemirror/commands": "6.2.5",
|
||||
"@codemirror/language": "6.9.3",
|
||||
"@codemirror/search": "6.5.4",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.22.2",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@wdio/cli": "7.30.1",
|
||||
"@wdio/junit-reporter": "7.29.1",
|
||||
"@wdio/local-runner": "7.30.1",
|
||||
"@wdio/mocha-framework": "7.26.0",
|
||||
"@wdio/spec-reporter": "7.29.1",
|
||||
"@wikimedia/mw-node-qunit": "7.2.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"bundlesize": "0.18.2",
|
||||
"clean-webpack-plugin": "3.0.0",
|
||||
"dotenv": "8.2.0",
|
||||
"eslint-config-wikimedia": "0.26.0",
|
||||
"grunt-banana-checker": "0.11.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jquery": "3.7.1",
|
||||
"rollup": "4.13.0",
|
||||
"rollup-plugin-copy": "3.5.0",
|
||||
"stylelint-config-wikimedia": "0.16.1",
|
||||
"wdio-mediawiki": "2.3.0",
|
||||
"webpack": "5.89.0",
|
||||
"webpack-cli": "5.1.4"
|
||||
},
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "resources/dist/main.js",
|
||||
"maxSize": "110.0kB"
|
||||
}
|
||||
]
|
||||
"wdio-mediawiki": "2.3.0"
|
||||
}
|
||||
}
|
||||
|
|
1
resources/dist/codemirror.js
vendored
Normal file
1
resources/dist/codemirror.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/dist/codemirror.mode.mediawiki.js
vendored
Normal file
1
resources/dist/codemirror.mode.mediawiki.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/dist/codemirror.wikieditor.mediawiki.js
vendored
Normal file
1
resources/dist/codemirror.wikieditor.mediawiki.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
"use strict";var e=require("ext.CodeMirror.v6.lib"),t=require("ext.CodeMirror.v6"),r=require("ext.CodeMirror.v6.mode.mediawiki"),i=function(t){function r(t,i){var o;return e._classCallCheck(this,r),(o=e._callSuper(this,r,[t])).langExtension=i,o.useCodeMirror=mw.user.options.get("usecodemirror")>0,o}return e._inherits(r,t),e._createClass(r,[{key:"setCodeMirrorPreference",value:function(t){this.useCodeMirror=t,e._get(e._getPrototypeOf(r.prototype),"setCodeMirrorPreference",this).call(this,t)}},{key:"enableCodeMirror",value:function(){var t=this;if(!this.view){var r=this.$textarea.prop("selectionStart"),i=this.$textarea.prop("selectionEnd"),o=this.$textarea.scrollTop(),s=this.$textarea.is(":focus"),a=[].concat(e._toConsumableArray(this.defaultExtensions),[this.langExtension,e.EditorView.domEventHandlers({blur:function(){return t.$textarea.triggerHandler("blur")},focus:function(){return t.$textarea.triggerHandler("focus")}}),e.EditorView.lineWrapping]);if(this.initialize(a),requestAnimationFrame((function(){t.view.scrollDOM.scrollTop=o})),0!==r||0!==i){var n=e.EditorSelection.range(r,i),d=e.EditorView.scrollIntoView(n);d.value.isSnapshot=!0,this.view.dispatch({selection:e.EditorSelection.create([n]),effects:d})}s&&this.view.focus(),mw.hook("ext.CodeMirror.switch").fire(!0,$(this.view.dom))}}},{key:"addCodeMirrorToWikiEditor",value:function(){var e=this,t=this.$textarea.data("wikiEditor-context"),r=t&&t.modules&&t.modules.toolbar;r&&(this.$textarea.wikiEditor("addToToolbar",{section:"main",groups:{codemirror:{tools:{CodeMirror:{label:mw.msg("codemirror-toggle-label"),type:"toggle",oouiIcon:"highlight",action:{type:"callback",execute:function(){return e.switchCodeMirror()}}}}}}}),r.$toolbar.find(".tool[rel=CodeMirror]").attr("id","mw-editbutton-codemirror"),this.readOnly&&this.$textarea.data("wikiEditor-context").$ui.addClass("ext-codemirror-readonly"),this.useCodeMirror&&this.enableCodeMirror(),this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!1,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0}))}},{key:"updateToolbarButton",value:function(){var e=$("#mw-editbutton-codemirror");e.toggleClass("mw-editbutton-codemirror-active",this.useCodeMirror),e.data("setActive")&&e.data("setActive")(this.useCodeMirror)}},{key:"switchCodeMirror",value:function(){if(this.view){this.setCodeMirrorPreference(!1);var e=this.view.scrollDOM.scrollTop,t=this.view.hasFocus,r=this.view.state.selection.ranges[0],i=r.from,o=r.to;$(this.view.dom).textSelection("unregister"),this.$textarea.textSelection("unregister"),this.$textarea.val(this.view.state.doc.toString()),this.view.destroy(),this.view=null,this.$textarea.show(),t&&this.$textarea.trigger("focus"),this.$textarea.prop("selectionStart",Math.min(i,o)).prop("selectionEnd",Math.max(o,i)),this.$textarea.scrollTop(e),mw.hook("ext.CodeMirror.switch").fire(!1,this.$textarea)}else this.enableCodeMirror(),this.setCodeMirrorPreference(!0);this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!0,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0})}}]),r}(t);mw.loader.getState("ext.wikiEditor")&&mw.hook("wikiEditor.toolbarReady").add((function(e){new i(e,r({bidiIsolation:"rtl"===e.attr("dir")})).addCodeMirrorToWikiEditor()}));
|
2
resources/dist/main.js
vendored
2
resources/dist/main.js
vendored
File diff suppressed because one or more lines are too long
1
resources/dist/main.js.map.json
vendored
1
resources/dist/main.js.map.json
vendored
File diff suppressed because one or more lines are too long
1
resources/dist/vendor.js
vendored
Normal file
1
resources/dist/vendor.js
vendored
Normal file
File diff suppressed because one or more lines are too long
87
rollup.config.js
Normal file
87
rollup.config.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
const nodeResolve = require( '@rollup/plugin-node-resolve' );
|
||||
const copy = require( 'rollup-plugin-copy' );
|
||||
const babel = require( '@rollup/plugin-babel' );
|
||||
const terser = require( '@rollup/plugin-terser' );
|
||||
|
||||
/**
|
||||
* Mapping of import paths to ResourceLoader module names.
|
||||
* See usage in 'plugins' below for explanation.
|
||||
* @type {Object}
|
||||
*/
|
||||
const importAliases = {
|
||||
'./vendor.js': 'ext.CodeMirror.v6.lib',
|
||||
'./codemirror.js': 'ext.CodeMirror.v6',
|
||||
'./codemirror.mode.mediawiki.js': 'ext.CodeMirror.v6.mode.mediawiki'
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
// One entry for each ResourceLoader module that we want to ship.
|
||||
input: [
|
||||
'src/codemirror.js',
|
||||
'src/codemirror.mode.mediawiki.js',
|
||||
'src/codemirror.wikieditor.mediawiki.js'
|
||||
],
|
||||
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
dir: 'resources/dist',
|
||||
|
||||
// Magically makes our ECMAScript Modules work with the
|
||||
// CommonJS-style preferred by ResourceLoader. Ta-da!
|
||||
format: 'cjs',
|
||||
|
||||
// Remove hash from chunked file name. We only want vendor code to be
|
||||
// chunked, and we need the file name to be stable for use by ResourceLoader.
|
||||
chunkFileNames: () => '[name].js',
|
||||
|
||||
// Bundle all vendor code into a single file called 'vendor.js'.
|
||||
// This includes the Babel helpers because they are used by all our modules.
|
||||
manualChunks: ( id ) => {
|
||||
if ( id.includes( 'node_modules' ) || id.includes( 'rollupPluginBabelHelpers' ) ) {
|
||||
return 'vendor';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
|
||||
// HACK: Rollup doesn't know about ResourceLoader and attempts to `require`
|
||||
// modules using a relative path, when they need to match the RL module name.
|
||||
// Here we do string replacements to fix that. This is nasty and brittle, but
|
||||
// otherwise we couldn't offer standalone CodeMirror functionality via RL,
|
||||
// which is necessary for usage in on-wiki scripts and gadgets (T214989).
|
||||
copy( {
|
||||
targets: [ {
|
||||
src: 'resources/dist/*',
|
||||
dest: 'resources/dist/',
|
||||
transform: ( contents ) => {
|
||||
Object.keys( importAliases ).forEach( ( alias ) => {
|
||||
contents = contents.toString().replace(
|
||||
`require("${ alias }")`,
|
||||
`require("${ importAliases[ alias ] }")`
|
||||
);
|
||||
} );
|
||||
return contents;
|
||||
}
|
||||
} ],
|
||||
hook: 'writeBundle'
|
||||
} ),
|
||||
|
||||
babel( { babelHelpers: 'bundled' } ),
|
||||
|
||||
terser()
|
||||
],
|
||||
|
||||
onwarn: ( warning, warn ) => {
|
||||
// Suppress "not exported" warnings. We import those for IDE support not for the build.
|
||||
if ( warning.code === 'MISSING_EXPORT' ) {
|
||||
return;
|
||||
}
|
||||
warn( warning );
|
||||
}
|
||||
}
|
||||
];
|
|
@ -14,7 +14,6 @@
|
|||
"commonjs": true
|
||||
},
|
||||
"globals": {
|
||||
"__non_webpack_require__": "readonly",
|
||||
"Tree": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
|
|
|
@ -5,10 +5,7 @@ import { searchKeymap } from '@codemirror/search';
|
|||
import { bracketMatching } from '@codemirror/language';
|
||||
import CodemirrorTextSelection from './codemirror.textSelection';
|
||||
|
||||
// Necessary so that `require` doesn't get mangled into `__webpack_require__`,
|
||||
// which ResourceLoader won't recognize and thus be unable to load the virtual file.
|
||||
// See https://webpack-v3.jsx.app/api/module-variables/#__non_webpack_require__-webpack-specific-
|
||||
__non_webpack_require__( '../ext.CodeMirror.data.js' );
|
||||
require( '../ext.CodeMirror.data.js' );
|
||||
|
||||
/**
|
||||
* @class CodeMirror
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const { EditorView } = require( '@codemirror/view' );
|
||||
const CodeMirror = require( '../../src/codemirror.js' ).default;
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import CodeMirror from '../../src/codemirror.js';
|
||||
const $textarea = $( '<textarea>' ),
|
||||
cm = new CodeMirror( $textarea );
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
jest.mock( '../../ext.CodeMirror.data.js', () => jest.fn(), { virtual: true } );
|
||||
global.mw = require( '@wikimedia/mw-node-qunit/src/mockMediaWiki.js' )();
|
||||
mw.user = Object.assign( mw.user, {
|
||||
options: {
|
||||
|
@ -12,8 +13,5 @@ mw.user = Object.assign( mw.user, {
|
|||
mw.config.get = jest.fn().mockReturnValue( '1000+ edits' );
|
||||
mw.track = jest.fn();
|
||||
mw.Api.prototype.saveOption = jest.fn();
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle, camelcase
|
||||
global.__non_webpack_require__ = jest.fn();
|
||||
global.$ = require( 'jquery' );
|
||||
$.fn.textSelection = () => {};
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/* eslint-env node */
|
||||
const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ),
|
||||
path = require( 'path' ),
|
||||
distDir = path.resolve( __dirname, 'resources/dist' ),
|
||||
srcMapExt = '.map.json',
|
||||
PUBLIC_PATH = '/w/extensions/CodeMirror';
|
||||
|
||||
module.exports = ( env, argv ) => ( {
|
||||
// Apply the rule of silence: https://wikipedia.org/wiki/Unix_philosophy.
|
||||
stats: {
|
||||
all: false,
|
||||
// Output a timestamp when a build completes. Useful when watching files.
|
||||
builtAt: true,
|
||||
errors: true,
|
||||
warnings: true
|
||||
},
|
||||
|
||||
// Fail on the first build error instead of tolerating it for prod builds. This seems to
|
||||
// correspond to optimization.emitOnErrors.
|
||||
bail: argv.mode === 'production',
|
||||
|
||||
// Specify that all paths are relative the Webpack configuration directory not the current
|
||||
// working directory.
|
||||
context: __dirname,
|
||||
|
||||
entry: './src/codemirror.wikieditor.mediawiki.js',
|
||||
|
||||
module: {
|
||||
rules: [ {
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
// Beware of https://github.com/babel/babel-loader/issues/690. Changes to browsers require
|
||||
// manual invalidation.
|
||||
cacheDirectory: true
|
||||
}
|
||||
}
|
||||
} ]
|
||||
},
|
||||
|
||||
optimization: {
|
||||
// Don't produce production output when a build error occurs.
|
||||
emitOnErrors: argv.mode !== 'production',
|
||||
|
||||
// Use filenames instead of unstable numerical identifiers for file references. This
|
||||
// increases the gzipped bundle size some but makes the build products easier to debug and
|
||||
// appear deterministic. I.e., code changes will only alter the bundle they're packed in
|
||||
// instead of shifting the identifiers in other bundles.
|
||||
// https://webpack.js.org/guides/caching/#deterministic-hashes (namedModules replaces NamedModulesPlugin.)
|
||||
moduleIds: 'named'
|
||||
},
|
||||
|
||||
output: {
|
||||
// Specify the destination of all build products.
|
||||
path: distDir,
|
||||
|
||||
// Store outputs per module in files named after the modules. For the JavaScript entry
|
||||
// itself, append .js to each ResourceLoader module entry name. This value is tightly
|
||||
// coupled to sourceMapFilename.
|
||||
filename: '[name].js',
|
||||
|
||||
// Rename source map extensions. Per T173491 files with a .map extension cannot be served
|
||||
// from prod.
|
||||
sourceMapFilename: `[file]${ srcMapExt }`,
|
||||
|
||||
devtoolModuleFilenameTemplate: `${ PUBLIC_PATH }/[resource-path]`
|
||||
},
|
||||
|
||||
// Accurate source maps at the expense of build time. The source map is intentionally exposed
|
||||
// to users via sourceMapFilename for prod debugging. This goes against convention as source
|
||||
// code is publicly distributed.
|
||||
devtool: 'source-map',
|
||||
|
||||
plugins: [
|
||||
// Delete the output directory on each build.
|
||||
new CleanWebpackPlugin( {
|
||||
cleanOnceBeforeBuildPatterns: [ '**/*', '!.eslintrc.json' ]
|
||||
} )
|
||||
],
|
||||
|
||||
performance: {
|
||||
// Size violations for prod builds fail; development builds are unchecked.
|
||||
hints: argv.mode === 'production' ? 'error' : false,
|
||||
|
||||
// Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers
|
||||
// up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are
|
||||
// well understood. Related to bundlesize minified, gzipped compressed file size tests.
|
||||
maxAssetSize: 352.0 * 1024,
|
||||
maxEntrypointSize: 352.0 * 1024,
|
||||
|
||||
// The default filter excludes map files, but we rename ours.
|
||||
assetFilter: ( filename ) => !filename.endsWith( srcMapExt )
|
||||
}
|
||||
} );
|
Loading…
Reference in a new issue