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/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 5554f3ab..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,248 +0,0 @@ -const fs = require('fs'); -const glob = require('glob'); -const ejs = require('ejs'); -const sass = require('sass'); - -module.exports = function(grunt) { - var pkg = require("./package.json"); - global.jasmineVersion = pkg.version; - - grunt.initConfig({ - pkg: pkg, - cssUrlEmbed: require('./grunt/config/cssUrlEmbed.js') - }); - - require('load-grunt-tasks')(grunt); - - grunt.registerMultiTask('cssUrlEmbed', "Embed URLs as base64 strings inside your stylesheets", function () { - var done = this.async(); - - import('css-url-embed').then(cssEmbed => { - for (const file of this.files) { - try { - grunt.log.subhead('Processing source file "' + file.src[0] + '"'); - const urls = cssEmbed.processFile(file.src[0], file.dest); - - for (const url of urls) { - grunt.log.ok('"' + url + '" embedded'); - } - - grunt.log.writeln('File "' + file.dest + '" created'); - } catch (e) { - grunt.log.error(e); - grunt.fail.warn('URL embedding failed\n'); - } - } - - done(); - }); - }); - - grunt.loadTasks('grunt/tasks'); - - grunt.registerTask('sass', - 'Compile sass to css', - function() { - try { - const output = sass.compile('src/html/jasmine.scss'); - fs.writeFileSync('lib/jasmine-core/jasmine.css', output.css, - {encoding: 'utf8'}); - } catch (e) { - console.error(e); - throw e; - } - } - ); - - grunt.registerTask('default', ['sass', "cssUrlEmbed"]); - - grunt.registerTask('concat', - 'Concatenate files', - function() { - try { - 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: jasmineVersion} - }, - ], - }, - { - 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: 'grunt/templates/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'}); - } - } catch (e) { - console.error(e); - throw e; - } - } - ); - - grunt.registerTask('buildDistribution', - 'Builds and lints jasmine.js, jasmine-html.js, jasmine.css', - [ - 'sass', - "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/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/tasks/build_standalone.js b/grunt/tasks/build_standalone.js deleted file mode 100644 index cb23618d..00000000 --- a/grunt/tasks/build_standalone.js +++ /dev/null @@ -1,105 +0,0 @@ -var grunt = require("grunt"); -const fs = require('fs'); -const path = require('path'); -const archiver = require('archiver'); -const glob = require('glob'); -const ejs = require('ejs'); - -function standaloneTmpDir(path) { return "dist/tmp/" + path; } - -grunt.registerTask("build:compileSpecRunner", - "Processes the spec runner template and writes to a tmp file", - function() { - const template = fs.readFileSync('grunt/templates/SpecRunner.html.ejs', - {encoding: 'utf8'}); - const runnerHtml = ejs.render(template, { jasmineVersion: global.jasmineVersion }); - - if (!fs.existsSync('dist/tmp')) { - fs.mkdirSync('dist/tmp'); - } - - fs.writeFileSync('dist/tmp/SpecRunner.html', runnerHtml, - {encoding: 'utf8'}); - } -); - -grunt.registerTask("build:cleanSpecRunner", - "Deletes the tmp spec runner file", - function() { - grunt.file.delete(standaloneTmpDir("")); - } -); - -const standaloneFileGroups = [ - { - 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' - } -]; - -grunt.registerTask("zipStandaloneDist", - "Creates a zip file for the standalone distribution", - function() { - const done = this.async(); - const destPath = `./dist/jasmine-standalone-${global.jasmineVersion}.zip`; - const output = fs.createWriteStream(destPath); - const archive = archiver('zip'); - - output.on('close', done); - - archive.on('warning', function (err) { - grunt.fail.warn(err) - }); - - archive.on('error', function (err) { - grunt.fail.warn(err) - }); - - archive.pipe(output); - - for (const group of standaloneFileGroups) { - 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(); - } -); - -grunt.registerTask("buildStandaloneDist", - "Builds a standalone distribution", - [ - "buildDistribution", - "build:compileSpecRunner", - "zipStandaloneDist", - "build:cleanSpecRunner" - ] -); diff --git a/package.json b/package.json index 72bb4db5..93b87683 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", @@ -43,12 +45,9 @@ "eslint-plugin-compat": "^6.0.2", "glob": "^10.2.3", "globals": "^16.0.0", - "grunt": "^1.0.4", - "grunt-cli": "^1.3.2", "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..a1f255f6 --- /dev/null +++ b/scripts/buildStandaloneDist.js @@ -0,0 +1,89 @@ +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)) { + 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.ejs b/src/SpecRunner.html.ejs similarity index 100% rename from grunt/templates/SpecRunner.html.ejs rename to src/SpecRunner.html.ejs diff --git a/grunt/templates/licenseBanner.js.ejs b/src/licenseBanner.js.ejs similarity index 100% rename from grunt/templates/licenseBanner.js.ejs rename to src/licenseBanner.js.ejs