diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..242e95ee03 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "plugins": ["jsdoc-closure"], + "parserOpts": { + "parser": "recast" + }, + "generatorOpts": { + "generator": "recast" + } +} diff --git a/.gitignore b/.gitignore index c38cba70da..8bce1f63bb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /coverage/ /dist/ /node_modules/ +src/index.js diff --git a/config/jsdoc/info/api-plugin.js b/config/jsdoc/info/api-plugin.js index af732d0761..afef0dc34e 100644 --- a/config/jsdoc/info/api-plugin.js +++ b/config/jsdoc/info/api-plugin.js @@ -8,7 +8,7 @@ exports.defineTags = function(dictionary) { dictionary.defineTag('api', { onTagged: function(doclet, tag) { - doclet.api = tag.text || 'experimental'; + doclet.api = true; } }); diff --git a/config/jsdoc/info/publish.js b/config/jsdoc/info/publish.js index 9d9bb1cb4a..9e42a6a6d3 100644 --- a/config/jsdoc/info/publish.js +++ b/config/jsdoc/info/publish.js @@ -30,12 +30,12 @@ exports.publish = function(data, opts) { {define: {isObject: true}}, function() { if (this.kind == 'class') { - if (!('extends' in this) || typeof this.api == 'string') { + if (!('extends' in this) || typeof this.api == 'boolean') { classes[this.longname] = this; return true; } } - return (typeof this.api == 'string' || + return (typeof this.api == 'boolean' || this.meta && (/[\\\/]externs$/).test(this.meta.path)); } ], @@ -53,7 +53,7 @@ exports.publish = function(data, opts) { docs.filter(function(doc) { var include = true; var constructor = doc.memberof; - if (constructor && constructor.substr(-1) === '_') { + if (constructor && constructor.substr(-1) === '_' && constructor.indexOf('module:') === -1) { assert.strictEqual(doc.inherited, true, 'Unexpected export on private class: ' + doc.longname); include = false; @@ -92,7 +92,6 @@ exports.publish = function(data, opts) { name: doc.longname, kind: doc.kind, description: doc.classdesc || doc.description, - stability: doc.api, path: path.join(doc.meta.path, doc.meta.filename) }; if (doc.augments) { diff --git a/config/webpack.js b/config/webpack.js new file mode 100644 index 0000000000..370128357f --- /dev/null +++ b/config/webpack.js @@ -0,0 +1,14 @@ +const webpack = require('webpack'); +const MinifyPlugin = require('babel-minify-webpack-plugin'); + +module.exports = { + entry: './src/index.js', + output: { + filename: 'build/ol.js' + }, + devtool: 'source-map', + plugins: [ + new webpack.optimize.ModuleConcatenationPlugin(), + new MinifyPlugin() + ] +}; diff --git a/package.json b/package.json index 8ee21836f1..2e0f817335 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,19 @@ ], "homepage": "https://openlayers.org/", "scripts": { - "lint": "eslint tasks test src examples transforms", - "pretest": "npm run lint", + "lint": "eslint tasks test src/ol examples transforms", + "pretest": "npm run lint && npm run typecheck", "test": "npm run karma -- --single-run", "karma": "karma start test/karma.config.js", "serve-examples": "mkdir -p build/examples && webpack --config examples/webpack/config.js --watch & serve build/examples", - "build-examples": "webpack --config examples/webpack/config.js --env=prod" + "build-examples": "webpack --config examples/webpack/config.js --env=prod", + "build-index": "node tasks/generate-index.js", + "prebuild": "npm run build-index", + "build": "webpack --config config/webpack.js", + "presrc-closure": "npm run prebuild", + "src-closure": "babel -q --out-dir build/src-closure src/", + "pretypecheck": "npm run src-closure", + "typecheck": "node tasks/typecheck" }, "main": "src/ol/index.js", "repository": { @@ -36,7 +43,9 @@ }, "devDependencies": { "async": "2.6.0", + "babel-cli": "6.26.0", "babel-minify-webpack-plugin": "^0.3.0", + "babel-plugin-jsdoc-closure": "1.0.2", "clean-css-cli": "4.1.10", "copy-webpack-plugin": "^4.0.1", "coveralls": "3.0.0", @@ -46,6 +55,7 @@ "front-matter": "^2.1.2", "fs-extra": "5.0.0", "glob": "7.1.1", + "google-closure-compiler": "20180101.0.0", "handlebars": "4.0.11", "html-webpack-plugin": "^2.30.1", "istanbul": "0.4.5", @@ -65,6 +75,7 @@ "nomnom": "1.8.1", "pixelmatch": "^4.0.2", "proj4": "2.4.4", + "recast": "0.13.0", "serve": "^6.0.6", "sinon": "4.2.2", "url-polyfill": "^1.0.7", diff --git a/src/ol.jsdoc b/src/ol.jsdoc index 47c6db565c..2c1bf53836 100644 --- a/src/ol.jsdoc +++ b/src/ol.jsdoc @@ -1,3 +1,4 @@ /** * @namespace ol */ + diff --git a/src/ol/View.js b/src/ol/View.js index bcca8b6a5e..22148dda1a 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -24,7 +24,8 @@ import Units from './proj/Units.js'; /** - * @type {number} Default min zoom level for the map view. + * Default min zoom level for the map view. + * @type {number} */ const DEFAULT_MIN_ZOOM = 0; diff --git a/src/ol/control/OverviewMap.js b/src/ol/control/OverviewMap.js index 7390d01fa0..9badaa2c65 100644 --- a/src/ol/control/OverviewMap.js +++ b/src/ol/control/OverviewMap.js @@ -21,15 +21,17 @@ import {containsExtent, getBottomLeft, getBottomRight, getTopLeft, getTopRight, /** - * @type {number} Maximum width and/or height extent ratio that determines - * when the overview map should be zoomed out. + * Maximum width and/or height extent ratio that determines when the overview + * map should be zoomed out. + * @type {number} */ const MAX_RATIO = 0.75; /** - * @type {number} Minimum width and/or height extent ratio that determines - * when the overview map should be zoomed in. + * Minimum width and/or height extent ratio that determines when the overview + * map should be zoomed in. + * @type {number} */ const MIN_RATIO = 0.1; diff --git a/src/ol/index.js b/src/ol/index.js index 05efbdbefd..eaa5c3d3b8 100644 --- a/src/ol/index.js +++ b/src/ol/index.js @@ -1,14 +1,15 @@ /** - * @module ol/index + * @module ol */ import webgl from './webgl.js'; /** + * Include debuggable shader sources. Default is `true`. This should be set to + * `false` for production builds. * TODO: move to a separate ol-webgl package - * @type {boolean} Include debuggable shader sources. Default is `true`. - * This should be set to `false` for production builds. + * @type {boolean} */ export const DEBUG_WEBGL = true; @@ -60,7 +61,8 @@ export {HAS_WEBGL, WEBGL_MAX_TEXTURE_SIZE, WEBGL_EXTENSIONS}; /** - * @type {string} OpenLayers version. + * OpenLayers version. + * @type {string} */ export const VERSION = 'v4.6.4'; diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index e023c41a9f..43f404f6b3 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -12,7 +12,8 @@ import {clamp} from '../math.js'; /** - * @type {number} Maximum mouse wheel delta. + * Maximum mouse wheel delta. + * @type {number} */ const MAX_DELTA = 1; diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 70a2d767ee..188889e506 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -9,7 +9,8 @@ const _ol_renderer_vector_ = {}; /** - * @type {number} Tolerance for geometry simplification in device pixels. + * Tolerance for geometry simplification in device pixels. + * @type {number} */ const SIMPLIFY_TOLERANCE = 0.5; diff --git a/src/ol/renderer/webgl/Map.js b/src/ol/renderer/webgl/Map.js index c3c75140b4..d3aefcb158 100644 --- a/src/ol/renderer/webgl/Map.js +++ b/src/ol/renderer/webgl/Map.js @@ -23,7 +23,8 @@ import ContextEventType from '../../webgl/ContextEventType.js'; /** - * @type {number} Texture cache high water mark. + * Texture cache high water mark. + * @type {number} */ const WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024; diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index 702c092c46..6216cd1689 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -8,23 +8,22 @@ import {getTransform} from '../proj.js'; /** - * @type {number} Maximum number of subdivision steps during raster - * reprojection triangulation. Prevents high memory usage and large - * number of proj4 calls (for certain transformations and areas). - * At most `2*(2^this)` triangles are created for each triangulated - * extent (tile/image). + * Maximum number of subdivision steps during raster reprojection triangulation. + * Prevents high memory usage and large number of proj4 calls (for certain + * transformations and areas). At most `2*(2^this)` triangles are created for + * each triangulated extent (tile/image). + * @type {number} */ const MAX_SUBDIVISION = 10; /** - * @type {number} Maximum allowed size of triangle relative to world width. - * When transforming corners of world extent between certain projections, - * the resulting triangulation seems to have zero error and no subdivision - * is performed. - * If the triangle width is more than this (relative to world width; 0-1), - * subdivison is forced (up to `MAX_SUBDIVISION`). - * Default is `0.25`. + * Maximum allowed size of triangle relative to world width. When transforming + * corners of world extent between certain projections, the resulting + * triangulation seems to have zero error and no subdivision is performed. If + * the triangle width is more than this (relative to world width; 0-1), + * subdivison is forced (up to `MAX_SUBDIVISION`). Default is `0.25`. + * @type {number} */ const MAX_TRIANGLE_WIDTH = 0.25; diff --git a/src/ol/reproj/common.js b/src/ol/reproj/common.js index 6b28ebfe6b..f74827bfe0 100644 --- a/src/ol/reproj/common.js +++ b/src/ol/reproj/common.js @@ -1,12 +1,13 @@ /** - * @type {number} Default maximum allowed threshold (in pixels) for - * reprojection triangulation. + * Default maximum allowed threshold (in pixels) for reprojection + * triangulation. + * @type {number} */ export const ERROR_THRESHOLD = 0.5; /** + * Enable automatic reprojection of raster sources. Default is `true`. * TODO: decide if we want to expose this as a build flag or remove it - * @type {boolean} Enable automatic reprojection of raster sources. Default is - * `true`. + * @type {boolean} */ export const ENABLE_RASTER_REPROJECTION = true; diff --git a/src/ol/source/Raster.js b/src/ol/source/Raster.js index bf28ff180f..b1133aaa39 100644 --- a/src/ol/source/Raster.js +++ b/src/ol/source/Raster.js @@ -8,7 +8,7 @@ import {createCanvasContext2D} from '../dom.js'; import {listen} from '../events.js'; import Event from '../events/Event.js'; import EventType from '../events/EventType.js'; -import {Processor} from 'pixelworks'; +import {Processor} from 'pixelworks/lib/index'; import {equals, getCenter, getHeight, getWidth} from '../extent.js'; import ImageLayer from '../layer/Image.js'; import TileLayer from '../layer/Tile.js'; diff --git a/src/ol/source/common.js b/src/ol/source/common.js index 373726a379..9e3f04c681 100644 --- a/src/ol/source/common.js +++ b/src/ol/source/common.js @@ -1,4 +1,5 @@ /** - * @type {string} Default WMS version. + * Default WMS version. + * @type {string} */ export const DEFAULT_WMS_VERSION = '1.3.0'; diff --git a/src/ol/sphere.js b/src/ol/sphere.js index fcb923bba2..48af84e903 100644 --- a/src/ol/sphere.js +++ b/src/ol/sphere.js @@ -6,7 +6,7 @@ */ /** - * @module ol/Sphere + * @module ol/sphere */ import {toRadians, toDegrees} from './math.js'; import GeometryType from './geom/GeometryType.js'; diff --git a/src/ol/style/AtlasManager.js b/src/ol/style/AtlasManager.js index 8bd2b80b08..81df116720 100644 --- a/src/ol/style/AtlasManager.js +++ b/src/ol/style/AtlasManager.js @@ -6,12 +6,14 @@ import Atlas from '../style/Atlas.js'; /** - * @type {number} The size in pixels of the first atlas image. + * The size in pixels of the first atlas image. + * @type {number} */ const INITIAL_ATLAS_SIZE = 256; /** - * @type {number} The maximum size in pixels of atlas images. + * The maximum size in pixels of atlas images. + * @type {number} */ const MAX_ATLAS_SIZE = -1; diff --git a/src/ol/tilegrid/common.js b/src/ol/tilegrid/common.js index b43e79676f..57a19b76ad 100644 --- a/src/ol/tilegrid/common.js +++ b/src/ol/tilegrid/common.js @@ -1,9 +1,11 @@ /** - * @type {number} Default maximum zoom for default tile grids. + * Default maximum zoom for default tile grids. + * @type {number} */ export const DEFAULT_MAX_ZOOM = 42; /** - * @type {number} Default tile size. + * Default tile size. + * @type {number} */ export const DEFAULT_TILE_SIZE = 256; diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js index 01115f37da..ae88f335a9 100644 --- a/src/ol/typedefs.js +++ b/src/ol/typedefs.js @@ -2,7 +2,8 @@ * @module ol/typedefs */ -/* global ol:false */ +//FIXME Remove when reworking typedefs, export typedefs as variables instead +const ol = {}; /** * File for all typedefs used by the compiler, and referenced by JSDoc. diff --git a/tasks/generate-index.js b/tasks/generate-index.js new file mode 100644 index 0000000000..dc02268697 --- /dev/null +++ b/tasks/generate-index.js @@ -0,0 +1,157 @@ +const fs = require('fs-extra'); +const path = require('path'); +const async = require('async'); +const generateInfo = require('./generate-info'); + + +/** + * Read the symbols from info file. + * @param {function(Error, Array., Array.)} callback Called + * with the patterns and symbols (or any error). + */ +function getSymbols(callback) { + generateInfo(function(err) { + if (err) { + callback(new Error('Trouble generating info: ' + err.message)); + return; + } + const symbols = require('../build/info.json').symbols; + callback(null, symbols.filter(symbol => symbol.kind != 'member')); + }); +} + +function getPath(name) { + const fullPath = require.resolve(path.resolve('src', name)); + return './' + path.posix.relative('src/', fullPath); +} + +/** + * Generate a list of symbol names. + * + * @param {Array.} symbols List of symbols. + * @param {function(Error, Array., Array.)} callback Called with + * the filtered list of symbols and a list of all provides (or any error). + */ +function addImports(symbols, callback) { + const imports = {}; + symbols.forEach(function(symbol) { + const defaultExport = symbol.name.split('~'); + const namedExport = symbol.name.split('.'); + if (defaultExport.length > 1) { + const from = defaultExport[0].replace(/^module\:/, './'); + const importName = from.replace(/[.\/]+/g, '$'); + const defaultImport = `import ${importName} from '${getPath(from)}';`; + imports[defaultImport] = true; + } else if (namedExport.length > 1) { + const from = namedExport[0].replace(/^module\:/, './'); + const importName = from.replace(/[.\/]+/g, '_'); + const namedImport = `import * as ${importName} from '${getPath(from)}';`; + imports[namedImport] = true; + } + }); + + callback(null, symbols, Object.keys(imports).sort()); +} + + +/** + * Generate code to export a named symbol. + * @param {string} name Symbol name. + * @param {Object.} namespaces Already defined namespaces. + * @return {string} Export code. + */ +function formatSymbolExport(name, namespaces) { + const parts = name.split('~'); + const isNamed = parts[0].indexOf('.') !== -1; + const nsParts = parts[0].replace(/^module\:/, '').split(/[\/\.]/); + const last = nsParts.length - 1; + const importName = isNamed ? + '_' + nsParts.slice(0, last).join('_') + '.' + nsParts[last] : + '$' + nsParts.join('$'); + let line = nsParts[0]; + for (let i = 1, ii = nsParts.length; i < ii; ++i) { + line += `.${nsParts[i]}`; + namespaces[line] = (line in namespaces ? namespaces[line] : true) && i < ii - 1; + } + line += ` = ${importName};`; + return line; +} + + +/** + * Generate export code given a list symbol names. + * @param {Array.} symbols List of symbols. + * @param {Object.} namespaces Already defined namespaces. + * @param {Array.} imports List of all imports. + * @return {string} Export code. + */ +function generateExports(symbols, namespaces, imports) { + let blocks = []; + symbols.forEach(function(symbol) { + const name = symbol.name; + if (name.indexOf('#') == -1) { + const block = formatSymbolExport(name, namespaces); + if (block !== blocks[blocks.length - 1]) { + blocks.push(block); + } + } + }); + const nsdefs = ['const ol = window[\'ol\'] = {};']; + const ns = Object.keys(namespaces).sort(); + for (let i = 0, ii = ns.length; i < ii; ++i) { + if (namespaces[ns[i]]) { + nsdefs.push(`${ns[i]} = {};`); + } + } + blocks = imports.concat(nsdefs).concat(blocks); + blocks.push(''); + return blocks.join('\n'); +} + + +/** + * Generate the exports code. + * + * @param {function(Error, string)} callback Called with the exports code or any + * error generating it. + */ +function main(callback) { + async.waterfall([ + getSymbols, + addImports, + function(symbols, imports, done) { + let code, err; + try { + code = generateExports(symbols, {}, imports); + } catch (e) { + err = e; + } + done(err, code); + } + ], callback); +} + + +/** + * If running this module directly, read the config file, call the main + * function, and write the output file. + */ +if (require.main === module) { + async.waterfall([ + main, + fs.outputFile.bind(fs, path.resolve('src', 'index.js')) + ], function(err) { + if (err) { + process.stderr.write(err.message + '\n'); + process.exit(1); + } else { + process.exit(0); + } + }); +} + + +/** + * Export main function. + */ +module.exports = main; diff --git a/tasks/generate-info.js b/tasks/generate-info.js index 089f203415..ba328d9b6d 100644 --- a/tasks/generate-info.js +++ b/tasks/generate-info.js @@ -208,62 +208,6 @@ function spawnJSDoc(paths, callback) { } -/** - * Given the path to a source file, get the list of provides. - * @param {string} srcPath Path to source file. - * @param {function(Error, Array.)} callback Called with a list of - * provides or any error. - */ -const getProvides = async.memoize(function(srcPath, callback) { - fs.readFile(srcPath, function(err, data) { - if (err) { - callback(err); - return; - } - const provides = []; - const matcher = /goog\.provide\('(.*)'\)/; - String(data).split('\n').forEach(function(line) { - const match = line.match(matcher); - if (match) { - provides.push(match[1]); - } - }); - callback(null, provides); - }); -}); - - -/** - * Add provides data to new symbols. - * @param {Object} info Symbols and defines metadata. - * @param {function(Error, Object)} callback Updated metadata. - */ -function addSymbolProvides(info, callback) { - if (!info) { - process.nextTick(function() { - callback(null, null); - }); - return; - } - - function addProvides(symbol, callback) { - getProvides(symbol.path, function(err, provides) { - if (err) { - callback(err); - return; - } - symbol.provides = provides; - callback(null, symbol); - }); - } - - async.map(info.symbols, addProvides, function(err, newSymbols) { - info.symbols = newSymbols; - callback(err, info); - }); -} - - /** * Write symbol and define metadata to the info file. * @param {Object} info Symbol and define metadata. @@ -294,7 +238,6 @@ function main(callback) { getNewerExterns, getNewer, spawnJSDoc, - addSymbolProvides, writeInfo ], callback); } diff --git a/tasks/typecheck.js b/tasks/typecheck.js new file mode 100644 index 0000000000..9c4632ec41 --- /dev/null +++ b/tasks/typecheck.js @@ -0,0 +1,28 @@ +const Compiler = require('google-closure-compiler').compiler; + +const compiler = new Compiler({ + js: [ + './build/src-closure/**.js', + './node_modules/pbf/package.json', './node_modules/pbf/**.js', './node_modules/ieee754/**.js', + './node_modules/pixelworks/package.json', './node_modules/pixelworks/**.js', + './node_modules/rbush/package.json', './node_modules/rbush/**.js', 'node_modules/quickselect/**.js' + ], + entry_point: './build/src-closure/index.js', + module_resolution: 'NODE', + dependency_mode: 'STRICT', + process_common_js_modules: true, + checks_only: true, + //FIXME Change newCheckTypes to jscomp_error when we have path types everywhere + jscomp_warning: ['newCheckTypes'], + // Options to make dependencies work + hide_warnings_for: 'node_modules' +}); + +compiler.run((exit, out, err) => { + if (exit) { + process.stderr.write(err, () => process.exit(exit)); + } else { + process.stderr.write(err); + process.stdout.write(out); + } +});