diff --git a/.circleci/config.yml b/.circleci/config.yml index e2c130225d..f5413f4d1c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,3 +50,11 @@ jobs: - store_artifacts: path: build/apidoc destination: apidoc + + - run: + name: Build Website + command: npm run build-site + + - store_artifacts: + path: public + destination: website diff --git a/.gitignore b/.gitignore index 6df267a88f..7f117a4ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /coverage/ /dist/ node_modules/ +/.cache/ +/public/ diff --git a/config/jsdoc/api-info/conf.json b/config/jsdoc/api-info/conf.json new file mode 100644 index 0000000000..7dec9899d4 --- /dev/null +++ b/config/jsdoc/api-info/conf.json @@ -0,0 +1,23 @@ +{ + "opts": { + "recurse": true, + "template": "node_modules/jsdoc-json" + }, + "tags": { + "allowUnknownTags": true + }, + "source": { + "includePattern": "\\.js$", + "include": [ + "src/ol" + ] + }, + "plugins": [ + "jsdoc-plugin-typescript", + "config/jsdoc/api-info/plugins/api", + "config/jsdoc/api-info/plugins/module" + ], + "typescript": { + "moduleRoot": "src" + } +} diff --git a/config/jsdoc/api-info/plugins/api.js b/config/jsdoc/api-info/plugins/api.js new file mode 100644 index 0000000000..fef1012aaa --- /dev/null +++ b/config/jsdoc/api-info/plugins/api.js @@ -0,0 +1,15 @@ + +/** + * Handle the api annotation. + * @param {Object} dictionary The tag dictionary. + */ +exports.defineTags = dictionary => { + + dictionary.defineTag('api', { + onTagged: (doclet, tag) => { + doclet.api = true; + } + }); + +}; + diff --git a/config/jsdoc/api-info/plugins/module.js b/config/jsdoc/api-info/plugins/module.js new file mode 100644 index 0000000000..e714c31a86 --- /dev/null +++ b/config/jsdoc/api-info/plugins/module.js @@ -0,0 +1,170 @@ +/** + * This plugin adds an `exportMap` property to @module doclets. Each export map + * is an object with properties named like the local identifier and values named + * like the exported identifier. + * + * For example, the code below + * + * export {foo as bar}; + * + * would be a map like `{foo: 'bar'}`. + * + * In the case of an export declaration with a source, the export identifier is + * prefixed by the source. For example, this code + * + * export {foo as bar} from 'ol/bam'; + * + * would be a map like `{'ol/bam foo': 'bar'}`. + * + * If a default export is a literal or object expression, the local name will be + * an empty string. For example + * + * export default {foo: 'bar'}; + * + * would be a map like `{'': 'default'}`. + */ +const assert = require('assert'); +const path = require('path'); + + +/** + * A lookup of export maps per source filepath. + */ +const exportMapLookup = {}; + +function loc(filepath, node) { + return `${filepath}:${node.loc.start.line}`; +} + +function nameFromChildIdentifier(filepath, node) { + assert.ok(node.id, `expected identifer in ${loc(filepath, node)}`); + assert.strictEqual(node.id.type, 'Identifier', `expected identifer in ${loc(filepath, node)}`); + return node.id.name; +} + +function handleExportNamedDeclaration(filepath, node) { + if (!(filepath in exportMapLookup)) { + exportMapLookup[filepath] = {}; + } + const exportMap = exportMapLookup[filepath]; + + const declaration = node.declaration; + if (declaration) { + // `export class Foo{}` or `export function foo() {}` + if (declaration.type === 'ClassDeclaration' || declaration.type === 'FunctionDeclaration') { + const name = nameFromChildIdentifier(filepath, declaration); + exportMap[name] = name; + return; + } + + // `export const foo = 'bar', bam = 42` + if (declaration.type === 'VariableDeclaration') { + const declarations = declaration.declarations; + assert.ok(declarations.length > 0, `expected variable declarations in ${loc(filepath, declaration)}`); + for (const declarator of declarations) { + assert.strictEqual(declarator.type, 'VariableDeclarator', `unexpected "${declarator.type}" in ${loc(filepath, declarator)}`); + const name = nameFromChildIdentifier(filepath, declarator); + exportMap[name] = name; + } + return; + } + + throw new Error(`Unexpected named export "${declaration.type}" in ${loc(filepath, declaration)}`); + } + + let prefix = ''; + const source = node.source; + if (source) { + // `export foo from 'bar'` + assert.strictEqual(source.type, 'Literal', `unexpected export source "${source.type}" in ${loc(filepath, source)}`); + prefix = `${source.value} `; + } + + const specifiers = node.specifiers; + assert.ok(specifiers.length > 0, `expected export specifiers in ${loc(filepath, node)}`); + // `export {foo, bar}` or `export {default as Foo} from 'bar'` + for (const specifier of specifiers) { + assert.strictEqual(specifier.type, 'ExportSpecifier', `unexpected export specifier in ${loc(filepath, specifier)}`); + + const local = specifier.local; + assert.strictEqual(local.type, 'Identifier', `unexpected local specifier "${local.type} in ${loc(filepath, local)}`); + + const exported = specifier.exported; + assert.strictEqual(local.type, 'Identifier', `unexpected exported specifier "${exported.type} in ${loc(filepath, exported)}`); + + exportMap[prefix + local.name] = exported.name; + } +} + +function handleDefaultDeclaration(filepath, node) { + if (!(filepath in exportMapLookup)) { + exportMapLookup[filepath] = {}; + } + const exportMap = exportMapLookup[filepath]; + + const declaration = node.declaration; + if (declaration) { + // `export default class Foo{}` or `export default function foo () {}` + if (declaration.type === 'ClassDeclaration' || declaration.type === 'FunctionDeclaration') { + const name = nameFromChildIdentifier(filepath, declaration); + exportMap[name] = 'default'; + return; + } + + // `export default foo` + if (declaration.type === 'Identifier') { + exportMap[declaration.name] = 'default'; + return; + } + + // `export default {foo: 'bar'}` or `export default 42` + if (declaration.type === 'ObjectExpression' || declaration.type === 'Literal') { + exportMap[''] = 'default'; + return; + } + } + + throw new Error(`Unexpected default export "${declaration.type}" in ${loc(filepath, declaration)}`); +} + +exports.astNodeVisitor = { + visitNode: (node, event, parser, filepath) => { + if (node.type === 'ExportNamedDeclaration') { + return handleExportNamedDeclaration(filepath, node); + } + + if (node.type === 'ExportDefaultDeclaration') { + return handleDefaultDeclaration(filepath, node); + } + } +}; + +const moduleLookup = {}; + +exports.handlers = { + + // create a lookup of @module doclets + newDoclet: event => { + const doclet = event.doclet; + if (doclet.kind === 'module') { + const filepath = path.join(doclet.meta.path, doclet.meta.filename); + + assert.ok(!(filepath in moduleLookup), `duplicate @module doc in ${filepath}`); + moduleLookup[filepath] = doclet; + } + }, + + // assign the `exportMap` property to @module doclets + parseComplete: event => { + for (const filepath in moduleLookup) { + assert.ok(filepath in exportMapLookup, `missing ${filepath} in export map lookup`); + moduleLookup[filepath].exportMap = exportMapLookup[filepath]; + } + + // make sure there was a @module doclet for each export map + for (const filepath in exportMapLookup) { + assert.ok(filepath in moduleLookup, `missing @module doclet in ${filepath}`); + } + } + +}; diff --git a/gatsby-config.js b/gatsby-config.js new file mode 100644 index 0000000000..4a3d0ed079 --- /dev/null +++ b/gatsby-config.js @@ -0,0 +1,11 @@ +module.exports = { + plugins: [ + 'gatsby-plugin-emotion', + { + resolve: 'gatsby-plugin-typography', + options: { + pathToConfigModule: 'site/util/typography' + } + } + ] +}; diff --git a/gatsby-node.js b/gatsby-node.js new file mode 100644 index 0000000000..70dbf1543f --- /dev/null +++ b/gatsby-node.js @@ -0,0 +1,23 @@ + +function getDocs() { + // TODO: build if not present + const info = require('./build/api-info.json'); + + return info.docs.filter(doc => !doc.ignore && (doc.api || doc.kind === 'module')); +} + +function createPages({actions: {createPage}}) { + createPage({ + path: '/api', + component: require.resolve('./site/pages/API.js'), + context: {docs: getDocs()} + }); + + createPage({ + path: '/info', + component: require.resolve('./site/pages/Info.js'), + context: {docs: getDocs()} + }); +} + +exports.createPages = createPages; diff --git a/package.json b/package.json index 332aafd0f7..93e8c7114c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ "copy-css": "shx cp src/ol/ol.css build/ol/ol.css", "transpile": "shx rm -rf build/ol && shx mkdir -p build/ol && shx cp -rf src/ol build/ol/src && node tasks/serialize-workers && tsc --project config/tsconfig-build.json", "typecheck": "tsc --pretty", - "apidoc": "jsdoc -R config/jsdoc/api/index.md -c config/jsdoc/api/conf.json -P package.json -d build/apidoc" + "apidoc": "jsdoc -R config/jsdoc/api/index.md -c config/jsdoc/api/conf.json -P package.json -d build/apidoc", + "api-info": "jsdoc --configure config/jsdoc/api-info/conf.json --destination build/api-info.json", + "serve-site": "gatsby develop", + "build-site": "npm run api-info && gatsby build" }, "main": "index.js", "repository": { @@ -43,6 +46,8 @@ "devDependencies": { "@babel/core": "^7.4.0", "@babel/preset-env": "^7.4.4", + "@emotion/core": "^10.0.10", + "@emotion/styled": "^10.0.11", "@openlayers/eslint-plugin": "^4.0.0-beta.2", "@types/arcgis-rest-api": "^10.4.4", "@types/geojson": "^7946.0.7", @@ -57,9 +62,14 @@ "coveralls": "3.0.3", "eslint": "^5.16.0", "eslint-config-openlayers": "^11.0.0", + "eslint-config-tschaub": "^13.1.0", + "eslint-plugin-react": "^7.13.0", "expect.js": "0.3.1", "front-matter": "^3.0.2", "fs-extra": "^8.0.0", + "gatsby": "^2.4.3", + "gatsby-plugin-emotion": "^4.0.6", + "gatsby-plugin-typography": "^2.2.13", "glob": "^7.1.4", "globby": "^9.2.0", "handlebars": "4.1.2", @@ -68,6 +78,7 @@ "istanbul-instrumenter-loader": "^3.0.1", "jquery": "3.4.1", "jsdoc": "3.6.2", + "jsdoc-json": "^2.0.2", "jsdoc-plugin-typescript": "^2.0.1", "karma": "^4.1.0", "karma-chrome-launcher": "2.2.0", @@ -84,7 +95,13 @@ "pixelmatch": "^4.0.2", "pngjs": "^3.4.0", "proj4": "2.5.0", + "prop-types": "^15.7.2", "puppeteer": "~1.16.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-markdown": "^4.0.8", + "react-syntax-highlighter": "^10.2.1", + "react-typography": "^0.16.19", "rollup": "^1.12.0", "rollup-plugin-babel": "^4.3.2", "rollup-plugin-commonjs": "^10.0.0", @@ -95,6 +112,8 @@ "sinon": "^7.3.2", "terser-webpack-plugin": "^1.2.3", "typescript": "^3.4.5", + "typography": "^0.16.19", + "typography-theme-alton": "^0.16.19", "url-polyfill": "^1.1.5", "walk": "^2.3.9", "webpack": "4.31.0", diff --git a/site/.eslintrc.json b/site/.eslintrc.json new file mode 100644 index 0000000000..2a8c7093a6 --- /dev/null +++ b/site/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "tschaub/react" +} diff --git a/site/components/Class.js b/site/components/Class.js new file mode 100644 index 0000000000..57640c4673 --- /dev/null +++ b/site/components/Class.js @@ -0,0 +1,29 @@ +import {object} from 'prop-types'; +import React from 'react'; +import Markdown from 'react-markdown'; +import Code from './Code'; + +function Class({cls, module}) { + const exportedName = module.getExportedName(cls.name); + let importCode; + if (exportedName === 'default') { + importCode = `import ${cls.name} from '${module.id}';`; + } else { + importCode = `import {${exportedName}} from '${module.id}';`; + } + + return ( +
+
+ | kind | +longname | +memberof | +
|---|---|---|
| {doc.kind} | +{doc.longname} | +{doc.memberof} | +