diff --git a/Makefile b/Makefile index bc4332d56a..d6742d2187 100644 --- a/Makefile +++ b/Makefile @@ -103,6 +103,7 @@ clean: rm -f build/test_rendering_requires.js rm -rf build/examples rm -rf build/compiled-examples + rm -rf build/package rm -rf $(BUILD_HOSTED) .PHONY: cleanall @@ -300,3 +301,11 @@ build/test_rendering_requires.js: $(SPEC_RENDERING_JS) %shader.js: %shader.glsl src/ol/webgl/shader.mustache bin/pyglslunit.py build/timestamps/node-modules-timestamp @python bin/pyglslunit.py --input $< | ./node_modules/.bin/mustache - src/ol/webgl/shader.mustache > $@ + +.PHONY: package +package: + @rm -rf build/package + @cp -r package build + @cd ./src && cp -r ol/* ../build/package + @rm build/package/typedefs.js + ./node_modules/.bin/jscodeshift --transform transforms/module.js build/package diff --git a/package.json b/package.json index 60dd547bc1..c96e010c20 100644 --- a/package.json +++ b/package.json @@ -56,10 +56,12 @@ "eslint": "3.15.0", "eslint-config-openlayers": "7.0.0", "eslint-plugin-openlayers-internal": "^3.1.0", + "esprima": "2.x", "expect.js": "0.3.1", "gaze": "^1.0.0", "istanbul": "0.4.5", "jquery": "3.1.1", + "jscodeshift": "^0.3.30", "mocha": "3.2.0", "mocha-phantomjs-core": "^2.1.0", "mustache": "2.3.0", diff --git a/package/package.json b/package/package.json new file mode 100644 index 0000000000..44ac1e5cb6 --- /dev/null +++ b/package/package.json @@ -0,0 +1,14 @@ +{ + "name": "ol", + "version": "3.21.0-beta.17", + "description": "OpenLayers as ES2015 modules", + "main": "index.js", + "module": "index.js", + "license": "BSD-2-Clause", + "dependencies": { + "pbf": "3.0.5", + "pixelworks": "1.1.0", + "rbush": "2.0.1", + "vector-tile": "1.3.0" + } +} diff --git a/package/readme.md b/package/readme.md new file mode 100644 index 0000000000..703e2cd25d --- /dev/null +++ b/package/readme.md @@ -0,0 +1,55 @@ +# ol + +OpenLayers as ES2015 modules. + +**Note: This package is in beta and the API is subject to change before a final stable release.** + +## Usage + +Add the `ol` package as a dependency to your project. + + npm install ol --save + +Import just what you need for your application: + +```js +import Map from 'ol/map'; +import View from 'ol/view'; +import TileLayer from 'ol/layer/tile'; +import XYZ from 'ol/source/xyz'; + +new Map({ + target: 'map', + layers: [ + new TileLayer({ + source: new XYZ({ + url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png' + }) + }) + ], + view: new View({ + center: [0, 0], + zoom: 2 + }) +}); +``` + +See the following examples for more detail on bundling OpenLayers with your application: + + * Using [Rollup & Uglify](https://gist.github.com/tschaub/8beb328ea72b36446fc2198d008287de) + * Using [Rollup & Closure Compiler](https://gist.github.com/tschaub/32a5692bedac5254da24fa3b12072f35) + * Using [Webpack & Uglify](https://gist.github.com/tschaub/79025aef325cd2837364400a105405b8) + * Using [Browserify & Uglify](https://gist.github.com/tschaub/4bfb209a8f809823f1495b2e4436018e) + +## Module Identifiers + +The module identifiers above (e.g. `ol/map`) are like the `ol.Map` names in the [API documentation](http://openlayers.org/en/latest/apidoc/) with `/` instead of `.` and all lowercase. Each module only has a `default` export (there are no other named exports). + +Constructors are exported from dedicated modules. For example, the `ol/layer/tile` module exports the `Tile` layer constructor. + +Utility functions are available as properties of the default export from utility modules. For example, the `getCenter` function is a property of the default export from the `ol/extent` utility module. + +## Caveats + + * Module identifiers and the structure of the exports are subject to change while this package is in beta. + * The WebGL renderer is not available in this package. diff --git a/transforms/.eslintrc b/transforms/.eslintrc new file mode 100644 index 0000000000..13e216f583 --- /dev/null +++ b/transforms/.eslintrc @@ -0,0 +1,6 @@ +{ + "env": { + "node": true, + "es6": true + } +} diff --git a/transforms/module.js b/transforms/module.js new file mode 100644 index 0000000000..ca913825bc --- /dev/null +++ b/transforms/module.js @@ -0,0 +1,215 @@ +const pkg = require('../package.json'); + +const defines = { + 'ol.DEBUG': false, + 'ol.ENABLE_WEBGL': false +}; + +function rename(name) { + const parts = name.split('.'); + return `_${parts.join('_')}_`; +} + +function resolve(fromName, toName) { + const fromParts = fromName.split('.'); + const toParts = toName.split('.'); + if (toParts[0] === 'ol' && toParts[1] === 'ext') { + let name = toParts[2]; + let packageName; + for (let i = 0, ii = pkg.ext.length; i < ii; ++i) { + const dependency = pkg.ext[i]; + if (dependency.module === name) { + packageName = name; + break; + } else if (dependency.name === name) { + packageName = dependency.module; + break; + } + } + if (!packageName) { + throw new Error(`Can't find package name for ${toName}`); + } + return packageName; + } + const fromLength = fromParts.length; + let commonDepth = 1; + while (commonDepth < fromLength - 2) { + if (fromParts[commonDepth] === toParts[commonDepth]) { + ++commonDepth; + } else { + break; + } + } + + const back = new Array(fromLength - commonDepth).join('../') || './'; + let relative = back + toParts.slice(commonDepth).join('/').toLowerCase(); + if (relative.endsWith('/')) { + relative += 'index'; + } + return relative; +} + +function getGoogExpressionStatement(identifier) { + return { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'goog' + }, + property: { + type: 'Identifier', + name: identifier + } + } + } + }; +} + +const defineMemberExpression = { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'ol' + }, + property: { + type: 'Identifier' + } +}; + +function getMemberExpression(name) { + function memberExpression(parts) { + const dotIndex = parts.lastIndexOf('.'); + if (dotIndex > 0) { + return { + type: 'MemberExpression', + object: memberExpression(parts.slice(0, dotIndex)), + property: { + type: 'Identifier', + name: parts.slice(dotIndex + 1) + } + }; + } else { + return { + type: 'Identifier', + name: parts + }; + } + } + return memberExpression(name); +} + +function getMemberExpressionAssignment(name) { + return { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + left: getMemberExpression(name) + } + }; +} + +module.exports = function(info, api) { + const j = api.jscodeshift; + const root = j(info.source); + + // store any initial comments + const {comments} = root.find(j.Program).get('body', 0).node; + + const replacements = {}; + + // replace all uses of defines + root.find(j.MemberExpression, defineMemberExpression) + .filter(path => { + const node = path.value; + const name = `${node.object.name}.${node.property.name}`; + return (name in defines) && path.parentPath.value.type !== 'AssignmentExpression'; + }) + .replaceWith(path => { + const name = `${path.value.object.name}.${path.value.property.name}`; + return j.literal(defines[name]); + }); + + // remove goog.provide() + let provide; + root.find(j.ExpressionStatement, getGoogExpressionStatement('provide')) + .forEach(path => { + if (provide) { + throw new Error(`Multiple provides in ${info.path}`); + } + provide = path.value.expression.arguments[0].value; + }).remove(); + + if (!provide) { + throw new Error(`No provide found in ${info.path}`); + } + replacements[provide] = rename(provide); + + // replace provide assignment with variable declarator + // e.g. `ol.foo.Bar = function() {}` -> `var _ol_foo_Bar_ = function() {}` + let declaredProvide = false; + root.find(j.ExpressionStatement, getMemberExpressionAssignment(provide)) + .replaceWith(path => { + declaredProvide = true; + const statement = j.variableDeclaration('var', [ + j.variableDeclarator(j.identifier(rename(provide)), path.value.expression.right) + ]); + statement.comments = path.value.comments; + return statement; + }); + + if (!declaredProvide) { + const body = root.find(j.Program).get('body'); + body.unshift( + j.variableDeclaration('var', [ + j.variableDeclarator(j.identifier(rename(provide)), j.objectExpression([])) + ]) + ); + } + + // replace `goog.require('foo')` with `import foo from 'foo'` + const imports = []; + root.find(j.ExpressionStatement, getGoogExpressionStatement('require')) + .forEach(path => { + const name = path.value.expression.arguments[0].value; + if (name in replacements) { + throw new Error(`Duplicate require found in ${info.path}: ${name}`); + } + const renamed = rename(name); + replacements[name] = renamed; + imports.push( + j.importDeclaration( + [j.importDefaultSpecifier(j.identifier(renamed))], + j.literal(resolve(provide, name)) + ) + ); + }) + .remove(); + + const body = root.find(j.Program).get('body'); + body.unshift.apply(body, imports); + + // replace all uses of required or provided names with renamed identifiers + Object.keys(replacements).sort().reverse().forEach(name => { + if (name.indexOf('.') > 0) { + root.find(j.MemberExpression, getMemberExpression(name)) + .replaceWith(j.identifier(replacements[name])); + } else { + root.find(j.Identifier, {name: name}) + .replaceWith(j.identifier(replacements[name])); + } + }); + + // add export declaration + root.find(j.Program).get('body').push( + j.exportDefaultDeclaration(j.identifier(rename(provide))) + ); + + // replace any initial comments + root.get().node.comments = comments; + + return root.toSource({quote: 'single'}); +};