diff --git a/.circleci/config.yml b/.circleci/config.yml index a1f9229b..80737742 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -61,7 +61,7 @@ jobs: at: . - run: name: Run tests in parallel - command: npx grunt execSpecsInParallel + command: npm run test:parallel test_browsers: &test_browsers executor: node18 diff --git a/.editorconfig b/.editorconfig index 0ba33458..9f7c7232 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,6 +3,6 @@ charset = utf-8 end_of_line = lf insert_final_newline = true -[*.{js, mjs, json, sh, yml}] +[*.{js,mjs,json,sh,yml}] indent_style = space indent_size = 2 diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a07ef6fa..00000000 --- a/.eslintrc +++ /dev/null @@ -1,47 +0,0 @@ -{ - "extends": [ - "plugin:compat/recommended" - ], - "env": { - "browser": true, - "node": true, - "es2017": true - }, - "parserOptions": { - "ecmaVersion": 2018 - }, - "rules": { - "curly": "error", - "quotes": [ - "error", - "single", - { - "avoidEscape": true - } - ], - "no-unused-vars": [ - "error", - { - "args": "none" - } - ], - "no-implicit-globals": "error", - "block-spacing": "error", - "func-call-spacing": [ - "error", - "never" - ], - "key-spacing": "error", - "no-tabs": "error", - "no-trailing-spaces": "error", - "no-whitespace-before-property": "error", - "semi": [ - "error", - "always" - ], - "space-before-blocks": "error", - "no-eval": "error", - "no-var": "error", - "no-debugger": "error" - } -} diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 33a7a2da..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,104 +0,0 @@ -module.exports = function(grunt) { - var pkg = require("./package.json"); - global.jasmineVersion = pkg.version; - - grunt.initConfig({ - pkg: pkg, - concat: require('./grunt/config/concat.js'), - sass: require('./grunt/config/sass.js'), - compress: require('./grunt/config/compress.js'), - cssUrlEmbed: require('./grunt/config/cssUrlEmbed.js') - }); - - require('load-grunt-tasks')(grunt); - - grunt.loadTasks('grunt/tasks'); - - grunt.registerTask('default', ['sass:dist', "cssUrlEmbed"]); - - grunt.registerTask('buildDistribution', - 'Builds and lints jasmine.js, jasmine-html.js, jasmine.css', - [ - 'sass:dist', - "cssUrlEmbed", - 'concat' - ] - ); - - grunt.registerTask("execSpecsInNode", - "Run Jasmine core specs in Node.js", - function() { - verifyNoGlobals(() => require('./lib/jasmine-core.js').noGlobals()); - const done = this.async(), - Jasmine = require('jasmine'), - jasmineCore = require('./lib/jasmine-core.js'), - jasmine = new Jasmine({jasmineCore: jasmineCore}); - - jasmine.loadConfigFile('./spec/support/jasmine.json'); - jasmine.exitOnCompletion = false; - jasmine.execute().then( - result => done(result.overallStatus === 'passed'), - err => { - console.error(err); - done(false); - } - ); - } - ); - - grunt.registerTask("execSpecsInParallel", - "Run Jasmine core specs in parallel in Node.js", - function() { - // Need to require this here rather than at the top of the file - // so that we don't break verifyNoGlobals above by loading jasmine-core - // too early - const ParallelRunner = require('jasmine/parallel'); - let numWorkers = require('os').cpus().length; - - if (process.env['CIRCLECI']) { - // On Circle CI, the above gives the number of CPU cores on the host - // computer, which is unrelated to the resources actually available - // to the container. 2 workers gives peak performance with our current - // configuration, but 4 might increase the odds of discovering any - // parallel-specific bugs. - numWorkers = 4; - } - - const done = this.async(); - const runner = new ParallelRunner({ - jasmineCore: require('./lib/jasmine-core.js'), - numWorkers - }); - - runner.loadConfigFile('./spec/support/jasmine.json') - .then(() => { - runner.exitOnCompletion = false; - return runner.execute(); - }).then( - jasmineDoneInfo => done(jasmineDoneInfo.overallStatus === 'passed'), - err => { - console.error(err); - done(false); - } - ); - } - ); - - grunt.registerTask("execSpecsInNode:performance", - "Run Jasmine performance specs in Node.js", - function() { - require("shelljs").exec("node_modules/.bin/jasmine JASMINE_CONFIG_PATH=spec/support/jasmine-performance.json"); - } - ); -}; - -function verifyNoGlobals(fn) { - const initialGlobals = Object.keys(global); - fn(); - - const extras = Object.keys(global).filter(k => !initialGlobals.includes(k)); - - if (extras.length !== 0) { - throw new Error('Globals were unexpectedly created: ' + extras.join(', ')); - } -} diff --git a/RELEASE.md b/RELEASE.md index 049b3570..632a75ee 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -41,7 +41,7 @@ When ready to release - specs are all green and the stories are done: ### Build standalone distribution -1. Build the standalone distribution with `grunt buildStandaloneDist` +1. Build the standalone distribution with `npm run buildStandaloneDist` 1. This will generate `dist/jasmine-standalone-.zip`, which you will upload later (see "Finally" below). ### Release the core NPM module diff --git a/grunt/config/compress.js b/grunt/config/compress.js deleted file mode 100644 index a732e147..00000000 --- a/grunt/config/compress.js +++ /dev/null @@ -1,57 +0,0 @@ -var standaloneLibDir = "lib/jasmine-" + jasmineVersion; - -function root(path) { return "./" + path; } -function libJasmineCore(path) { return root("lib/jasmine-core/" + path); } -function dist(path) { return root("dist/" + path); } - -module.exports = { - standalone: { - options: { - archive: root("dist/jasmine-standalone-" + global.jasmineVersion + ".zip") - }, - - files: [ - { src: [ root("LICENSE") ] }, - { - src: [ "jasmine_favicon.png"], - dest: standaloneLibDir, - expand: true, - cwd: root("images") - }, - { - src: [ - "jasmine.js", - "jasmine-html.js", - "jasmine.css" - ], - dest: standaloneLibDir, - expand: true, - cwd: libJasmineCore("") - }, - { - src: [ "boot0.js", "boot1.js" ], - dest: standaloneLibDir, - expand: true, - cwd: libJasmineCore("") - }, - { - src: [ "SpecRunner.html" ], - dest: root(""), - expand: true, - cwd: dist("tmp") - }, - { - src: [ "*.js" ], - dest: "src", - expand: true, - cwd: libJasmineCore("example/src/") - }, - { - src: [ "*.js" ], - dest: "spec", - expand: true, - cwd: libJasmineCore("example/spec/") - } - ] - } -}; diff --git a/grunt/config/concat.js b/grunt/config/concat.js deleted file mode 100644 index a9c6eedf..00000000 --- a/grunt/config/concat.js +++ /dev/null @@ -1,56 +0,0 @@ -var grunt = require('grunt'); - -function license() { - var currentYear = "" + new Date(Date.now()).getFullYear(); - - return grunt.template.process( - grunt.file.read("grunt/templates/licenseBanner.js.jst"), - { data: { currentYear: currentYear}}); -} - -module.exports = { - 'jasmine-html': { - src: [ - 'src/html/requireHtml.js', - 'src/html/HtmlReporter.js', - 'src/html/HtmlSpecFilter.js', - 'src/html/ResultsNode.js', - 'src/html/QueryString.js', - 'src/html/**/*.js' - ], - dest: 'lib/jasmine-core/jasmine-html.js' - }, - jasmine: { - src: [ - 'src/core/requireCore.js', - 'src/core/matchers/requireMatchers.js', - 'src/core/base.js', - 'src/core/util.js', - 'src/core/Spec.js', - 'src/core/Order.js', - 'src/core/Env.js', - 'src/core/JsApiReporter.js', - 'src/core/PrettyPrinter', - 'src/core/Suite', - 'src/core/**/*.js', - 'src/version.js' - ], - dest: 'lib/jasmine-core/jasmine.js' - }, - boot0: { - src: ['src/boot/boot0.js'], - dest: 'lib/jasmine-core/boot0.js' - }, - boot1: { - src: ['src/boot/boot1.js'], - dest: 'lib/jasmine-core/boot1.js' - }, - options: { - banner: license(), - process: { - data: { - version: global.jasmineVersion - } - } - } -}; diff --git a/grunt/config/cssUrlEmbed.js b/grunt/config/cssUrlEmbed.js deleted file mode 100644 index e883ffb3..00000000 --- a/grunt/config/cssUrlEmbed.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - encodeWithBaseDir: { - files: { - "lib/jasmine-core/jasmine.css": ["lib/jasmine-core/jasmine.css"] - } - } -}; diff --git a/grunt/config/sass.js b/grunt/config/sass.js deleted file mode 100644 index 3ce0ef61..00000000 --- a/grunt/config/sass.js +++ /dev/null @@ -1,13 +0,0 @@ -const sass = require('sass'); - -module.exports = { - options: { - implementation: sass, - sourceComments: false - }, - dist: { - files: { - "lib/jasmine-core/jasmine.css": "src/html/jasmine.scss" - } - } -}; diff --git a/grunt/tasks/build_standalone.js b/grunt/tasks/build_standalone.js deleted file mode 100644 index 1abccf28..00000000 --- a/grunt/tasks/build_standalone.js +++ /dev/null @@ -1,31 +0,0 @@ -var grunt = require("grunt"); - -function standaloneTmpDir(path) { return "dist/tmp/" + path; } - -grunt.registerTask("build:compileSpecRunner", - "Processes the spec runner template and writes to a tmp file", - function() { - var runnerHtml = grunt.template.process( - grunt.file.read("grunt/templates/SpecRunner.html.jst"), - { data: { jasmineVersion: global.jasmineVersion }}); - - grunt.file.write(standaloneTmpDir("SpecRunner.html"), runnerHtml); - } -); - -grunt.registerTask("build:cleanSpecRunner", - "Deletes the tmp spec runner file", - function() { - grunt.file.delete(standaloneTmpDir("")); - } -); - -grunt.registerTask("buildStandaloneDist", - "Builds a standalone distribution", - [ - "buildDistribution", - "build:compileSpecRunner", - "compress:standalone", - "build:cleanSpecRunner" - ] -); diff --git a/lib/jasmine-core/boot0.js b/lib/jasmine-core/boot0.js index 6f0f3739..5403bdf3 100644 --- a/lib/jasmine-core/boot0.js +++ b/lib/jasmine-core/boot0.js @@ -21,6 +21,7 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + /** This file starts the process of "booting" Jasmine. It initializes Jasmine, makes its globals available, and creates the env. This file should be loaded diff --git a/lib/jasmine-core/boot1.js b/lib/jasmine-core/boot1.js index 6368eceb..8c12f8c7 100644 --- a/lib/jasmine-core/boot1.js +++ b/lib/jasmine-core/boot1.js @@ -21,6 +21,7 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + /** This file finishes 'booting' Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 060604d5..102eaee0 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -21,6 +21,7 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + // eslint-disable-next-line no-var var jasmineRequire = window.jasmineRequire || require('./jasmine.js'); diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index c3ba71f5..c6665c2f 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -21,6 +21,7 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + // eslint-disable-next-line no-unused-vars,no-var var getJasmineRequireObj = (function(jasmineGlobal) { let jasmineRequire; diff --git a/package.json b/package.json index f961e464..c6547796 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ ], "scripts": { "posttest": "eslint \"src/**/*.js\" \"spec/**/*.js\" && prettier --check \"src/**/*.js\" \"spec/**/*.js\"", - "test": "grunt --stack execSpecsInNode", + "test": "node scripts/runSpecsInNode.js", + "test:parallel": "node scripts/runSpecsInParallel.js", "cleanup": "prettier --write \"src/**/*.js\" \"spec/**/*.js\"", - "build": "grunt buildDistribution", + "build": "node scripts/buildDistribution.js", + "buildStandaloneDist": "node scripts/buildStandaloneDist.js", "serve": "node spec/support/localJasmineBrowser.js", "serve:performance": "node spec/support/localJasmineBrowser.js jasmine-browser-performance.json", "ci": "node spec/support/ci.js", @@ -36,20 +38,16 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.24.0", + "archiver": "^7.0.1", + "css-url-embed": "^0.1.0", + "ejs": "^3.1.10", "eslint": "^9.24.0", "eslint-plugin-compat": "^6.0.2", "glob": "^10.2.3", "globals": "^16.0.0", - "grunt": "^1.0.4", - "grunt-cli": "^1.3.2", - "grunt-contrib-compress": "^2.0.0", - "grunt-contrib-concat": "^2.0.0", - "grunt-css-url-embed": "^1.11.1", - "grunt-sass": "^4.0.0", "jasmine": "^5.0.0", "jasmine-browser-runner": "github:jasmine/jasmine-browser-runner", "jsdom": "^26.0.0", - "load-grunt-tasks": "^5.1.0", "prettier": "1.17.1", "rimraf": "^5.0.10", "sass": "^1.58.3", diff --git a/scripts/buildDistribution.js b/scripts/buildDistribution.js new file mode 100644 index 00000000..286e2549 --- /dev/null +++ b/scripts/buildDistribution.js @@ -0,0 +1,3 @@ +const buildDistribution = require('./lib/buildDistribution'); + +buildDistribution(); diff --git a/scripts/buildStandaloneDist.js b/scripts/buildStandaloneDist.js new file mode 100644 index 00000000..7dab4a4d --- /dev/null +++ b/scripts/buildStandaloneDist.js @@ -0,0 +1,92 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const ejs = require('ejs'); +const archiver = require('archiver'); +const { rimrafSync } = require('rimraf'); +const buildDistribution = require('./lib/buildDistribution'); + +const tmpDir = 'dist/tmp' + +if (!fs.existsSync(tmpDir)) { + if (!fs.existsSync(path.dirname(tmpDir))) { + fs.mkdirSync(path.dirname(tmpDir)); + } + fs.mkdirSync(tmpDir); +} + +buildStandaloneDist().finally(function() { + rimrafSync(tmpDir); +}); + +async function buildStandaloneDist() { + buildDistribution(); + const pkg = JSON.parse(fs.readFileSync('package.json')); + compileSpecRunner(pkg.version); + await zipStandaloneDist(pkg.version); +} + +function compileSpecRunner(jasmineVersion) { + const template = fs.readFileSync('src/SpecRunner.html.ejs', + {encoding: 'utf8'}); + const runnerHtml = ejs.render(template, { jasmineVersion }); + fs.writeFileSync('dist/tmp/SpecRunner.html', runnerHtml, + {encoding: 'utf8'}); +} + +async function zipStandaloneDist(jasmineVersion) { + const fileGroups = [ + { + src: [ + 'LICENSE', + 'dist/tmp/SpecRunner.html', + ] + }, + { + src: [ + 'images/jasmine_favicon.png', + 'lib/jasmine-core/jasmine.js', + 'lib/jasmine-core/jasmine-html.js', + 'lib/jasmine-core/jasmine.css', + 'lib/jasmine-core/boot0.js', + 'lib/jasmine-core/boot1.js', + ], + destDir: 'lib/jasmine-' + jasmineVersion + }, + { + src: glob.sync('lib/jasmine-core/example/src/*.js'), + destDir: 'src' + }, + { + src: glob.sync('lib/jasmine-core/example/spec/*.js'), + destDir: 'spec' + } + ]; + + const destPath = `./dist/jasmine-standalone-${jasmineVersion}.zip`; + const output = fs.createWriteStream(destPath); + const archive = archiver('zip'); + + const done = new Promise(function(resolve, reject) { + output.on('close', resolve); + archive.on('warning', reject); + archive.on('error', reject); + }); + + archive.pipe(output); + + for (const group of fileGroups) { + for (const srcPath of group.src) { + let destPath = path.basename(srcPath); + + if (group.destDir) { + destPath = `${group.destDir}/${destPath}`; + } + + archive.file(srcPath, {name: destPath}); + } + } + + archive.finalize(); + await done; +} diff --git a/scripts/lib/buildDistribution.js b/scripts/lib/buildDistribution.js new file mode 100644 index 00000000..f92587ac --- /dev/null +++ b/scripts/lib/buildDistribution.js @@ -0,0 +1,129 @@ +const fs = require('fs'); +const sass = require('sass'); +const glob = require('glob'); +const ejs = require('ejs'); +const cssUrlEmbed = require('css-url-embed'); + +function buildDistribution() { + compileSass(); + embedCssAssets(); + concatFiles(); +} + +function embedCssAssets() { + const cssPath = 'lib/jasmine-core/jasmine.css'; + cssUrlEmbed.processFile(cssPath, cssPath, function(filePath) { + if (filePath.endsWith('.png')) { + return 'image/png'; + } else if (filePath.endsWith('.svg')) { + return 'image/svg+xml'; + } else { + throw new Error(`Don't know MIME type for file: ${filePath}`); + } + }); +} + +function compileSass() { + const output = sass.compile('src/html/jasmine.scss'); + fs.writeFileSync('lib/jasmine-core/jasmine.css', output.css, + {encoding: 'utf8'}); +} + +function concatFiles() { + const pkg = JSON.parse(fs.readFileSync('package.json')); + const configs = [ + { + src: [ + 'src/html/requireHtml.js', + 'src/html/HtmlReporter.js', + 'src/html/HtmlSpecFilter.js', + 'src/html/ResultsNode.js', + 'src/html/QueryString.js', + 'src/html/**/*.js' + ], + dest: 'lib/jasmine-core/jasmine-html.js', + }, + { + dest: 'lib/jasmine-core/jasmine.js', + src: [ + 'src/core/requireCore.js', + 'src/core/matchers/requireMatchers.js', + 'src/core/base.js', + 'src/core/util.js', + 'src/core/Spec.js', + 'src/core/Order.js', + 'src/core/Env.js', + 'src/core/JsApiReporter.js', + 'src/core/PrettyPrinter', + 'src/core/Suite', + 'src/core/**/*.js', + { + template: 'src/version.js', + data: {version: pkg.version} + }, + ], + }, + { + dest: 'lib/jasmine-core/boot0.js', + src: ['src/boot/boot0.js'], + }, + { + dest: 'lib/jasmine-core/boot1.js', + src: ['src/boot/boot1.js'], + } + ]; + const licenseBanner = { + template: 'src/licenseBanner.js.ejs', + data: {currentYear: new Date(Date.now()).getFullYear()} + }; + + for (const {src, dest} of configs) { + src.unshift(licenseBanner); + + function expand(srcListEntry) { + if (typeof srcListEntry === 'object') { + return srcListEntry; + } + + return glob.sync(srcListEntry) + .sort(function (a, b) { + // Match the sort order of previous build tools, so that the + // output is the same. + a = a.toLowerCase(); + b = b.toLowerCase(); + + if (a < b) { + return -1; + } else if (a === b) { + return 0; + } else { + return 1; + } + }); + } + + const srcs = src.flatMap(expand); + const seen = new Set(); + const chunks = []; + + for (const s of srcs) { + let content; + + if (!seen.has(s)) { + if (s.template) { + const template = fs.readFileSync(s.template, {encoding: 'utf8'}); + content = ejs.render(template, s.data); + } else { + content = fs.readFileSync(s, {encoding: 'utf8'}); + } + + chunks.push(content); + seen.add(s); + } + } + + fs.writeFileSync(dest, chunks.join('\n'), {encoding: 'utf8'}); + } +} + +module.exports = buildDistribution; diff --git a/scripts/runSpecsInNode.js b/scripts/runSpecsInNode.js new file mode 100644 index 00000000..08e772f3 --- /dev/null +++ b/scripts/runSpecsInNode.js @@ -0,0 +1,28 @@ +verifyNoGlobals(() => require('../lib/jasmine-core.js').noGlobals()); + +const Jasmine = require('jasmine'); +const jasmineCore = require('../lib/jasmine-core.js'); +const runner = new Jasmine({jasmineCore: jasmineCore}); + +runner.loadConfigFile('./spec/support/jasmine.json'); +runner.exitOnCompletion = false; +runner.execute() + .then( + result => result.overallStatus === 'passed', + err => { + console.error(err); + return false; + } + ) + .then(ok => process.exit(ok ? 0 : 1)); + +function verifyNoGlobals(fn) { + const initialGlobals = Object.keys(global); + fn(); + + const extras = Object.keys(global).filter(k => !initialGlobals.includes(k)); + + if (extras.length !== 0) { + throw new Error('Globals were unexpectedly created: ' + extras.join(', ')); + } +} diff --git a/scripts/runSpecsInParallel.js b/scripts/runSpecsInParallel.js new file mode 100644 index 00000000..9e15939b --- /dev/null +++ b/scripts/runSpecsInParallel.js @@ -0,0 +1,28 @@ +const ParallelRunner = require('jasmine/parallel'); +const jasmineCore = require('../lib/jasmine-core.js'); +let numWorkers = require('os').cpus().length; + +if (process.env['CIRCLECI']) { + // On Circle CI, the above gives the number of CPU cores on the host + // computer, which is unrelated to the resources actually available + // to the container. 2 workers gives peak performance with our current + // configuration, but 4 might increase the odds of discovering any + // parallel-specific bugs. + numWorkers = 4; +} + +const runner = new ParallelRunner({jasmineCore, numWorkers}); + +runner.loadConfigFile('./spec/support/jasmine.json') + .then(() => { + runner.exitOnCompletion = false; + return runner.execute(); + }) + .then( + jasmineDoneInfo => jasmineDoneInfo.overallStatus === 'passed', + err => { + console.error(err); + return false; + } + ) + .then(ok => process.exit(ok ? 0 : 1)); diff --git a/grunt/templates/SpecRunner.html.jst b/src/SpecRunner.html.ejs similarity index 100% rename from grunt/templates/SpecRunner.html.jst rename to src/SpecRunner.html.ejs diff --git a/grunt/templates/licenseBanner.js.jst b/src/licenseBanner.js.ejs similarity index 100% rename from grunt/templates/licenseBanner.js.jst rename to src/licenseBanner.js.ejs